Skip to content

Commit 8cf1fd0

Browse files
fix(spec)!: spell the seven multi-word taxonomy values as identifiers so a select can carry them (#31078)
Fixes #31066 Clause-②: yes (narrowing) — seven accepted facet values change spelling, inside the marketplace-taxonomy narrowing of the same release; no published version carried the hyphenated spelling Metadata-protocol: narrowed Skills-docs: none — no `skills/**` file names a facet value; the hand-written handbook example and the generated reference pages carry the identifier spellings in this diff ## 维护者速读 - **改了什么**:市场分类里 7 个带连字符的值改成下划线写法(customer-service → customer_service、supply-chain → supply_chain,行业 5 个同理),顺序和数量上限不变;另加一条守卫测试:分类里任何值都必须能当下拉选项的值。 - **为什么改**:按你的裁决「spec 改成下划线(推荐)」。下拉选项的值只允许小写字母、数字、下划线和点,带连字符的值进不了 select,cloud#3661 就没法把「职能」「行业」做成带选项的下拉,控制台 #31011 也按选项取标签。**下一版 CLI 发版要等这张卡和 objectstack-ai/cloud#3661 都完成。** - **风险与回滚**:低。这 7 个值是 #30980 引入的,还没有任何版本发布过(spec 18.3.0 在它合并前就已发布),所以没有已发布的数据或包会用到旧写法;回滚就是 revert 本 PR。 - **先看**:① `packages/spec/src/marketplace/taxonomy.zod.ts`(两个枚举里的 7 个值和 `LEGACY_CATEGORY_MAPPING` 的 erp 一行);② `packages/spec/src/marketplace/taxonomy.test.ts` 末尾新增的守卫测试;③ `.changeset/30904-marketplace-taxonomy.md` 里被改正的一句(未发布的发布说明,下面 Acceptance notes 有说明)。 - **两分钟验证**:`pnpm --filter @objectstack/spec exec vitest run src/marketplace/taxonomy.test.ts`,预期 41 个测试全绿;把任一值改回连字符再跑,守卫测试变红。 - **需要你做的**:无,席位落地。 ## What changes - `packages/spec/src/marketplace/taxonomy.zod.ts`: `MarketplaceBusinessFunctionSchema` respells `customer-service` → `customer_service` and `supply-chain` → `supply_chain`; `MarketplaceIndustrySchema` respells `trade-distribution`, `professional-services`, `financial-services`, `public-sector` and `real-estate-construction` with underscores. Order and arity (1-3 / 0-3) unchanged. `LEGACY_CATEGORY_MAPPING.erp` now maps to `['finance', 'supply_chain']`; the legacy category KEYS (`developer-tools`, `ui-theme`) are old values written in the wild and stay as they are. The module doc now states that every value is a `SystemIdentifierSchema` identifier. - Guard test (`taxonomy.test.ts`, block `id`): walks every export of `taxonomy.zod.ts` that has string `options` (so an enum added later is judged without being listed), floors the walk at the four known enums and 26 values, and parses every value as `SystemIdentifierSchema` and as a full `SelectOptionSchema` `{ label, value }`. - Carriers: `docs/qa/scenarios/service_desk.json` and `requisition_to_receipt.json`; `packages/cli/test/package-publish-taxonomy.test.ts` (the refusal that lists business functions, and the wire-name test); `content/docs/deployment/marketplace-handbook.mdx`; regenerated `content/docs/references/marketplace/*.mdx` (`check:generated --fix`). - Nothing to change, checked: the three connector manifests carry `collaboration`/`legal`, `it` and `finance`; the liveness rows (`packages/spec/liveness/template_manifest.json`) name no value; the scenario schema and the CLI publish path read the spec enums (`MarketplaceBusinessFunctionSchema.options`), so only their fixtures and tests carried spellings. `git grep` for the seven hyphenated spellings now hits only the new changeset's FROM/TO table, CHANGELOG history and an unrelated `supply-chain` comment in `release.yml`. - Changesets: new `.changeset/31066-taxonomy-identifiers.md` (`@objectstack/spec: minor`, both declaration lines, FROM/TO table). ADR-0087 disposition: `not-required (already-registered marketplace-category-retired)` — reasoning under Acceptance notes. ## Tests Readings in Verification; ablation on `b56f1fb013` (the guard block is byte-identical on the merged head). <details><summary>Suites</summary> - `pnpm --filter @objectstack/spec test`: see Verification. - `pnpm --filter @objectstack/cli exec vitest run --project unit` (integration tier declared to CI; the diff touches no integration-tier file or spawn entry). - `pnpm --filter @objectstack/scenarios test`, the three connector `test` scripts (each runs `objectstack validate` then vitest). </details> <details><summary>Ablation of the guard test</summary> Through `scripts/ablation-replace.mjs` under the verify lock, on committed head `b56f1fb013`: anchor `'real_estate_construction', //` x1 → x0, replacement `'real-estate-construction', //` x0 → x1, blob `c960cff330ba` → `8b9e00123016`. `vitest run src/marketplace/taxonomy.test.ts`: **1 failed | 40 passed**, the failure exactly the guard (`MarketplaceIndustrySchema: real-estate-construction: expected false to be true`). Restore: blob after restore == HEAD blob `c960cff330ba`, `git diff HEAD` empty. A value no other test names was chosen so only the guard could turn red. </details> ## Acceptance notes - **DELIBERATE CORRECTION of a pending release note.** `.changeset/30904-marketplace-taxonomy.md` (pending, from #30980, unreleased) listed the business functions with the old spelling; this PR made that sentence false, so it is corrected in place rather than restored. Corrected sentence: "`businessFunctions` (1-3 distinct values, the primary one first: `sales`, `marketing`, `customer_service`, `finance`, `hr`, `supply_chain`, `administration`, `project`, `legal`, `it`, `collaboration`)". `check-empty-changeset` stays red on that file by design until the dispatching seat confirms the correction by its own record. - **ADR-0087: no new ledger entry.** The seven values were introduced by #30980 (merged `468e0c16`, 2026-11-06T18:46Z); `@objectstack/spec@18.3.0`, the latest published version, was published 2026-11-03T15:15Z, before that merge, and #30980's changeset is still pending in `.changeset/`. So no published spec ever accepted the hyphenated spelling, and no stored row or authored source made against a release can hold it. `marketplace-category-retired` (present at the merge base) is the upgrade path from a released spec — `category` to the facets of `taxonomy.zod.ts` and `LEGACY_CATEGORY_MAPPING` — and names no value, so it stays complete. A value-rename entry would project into the upgrade guide a rename no released author can meet (ADR-0087's table calls that false data in the ledger); enum values also have no `retiredKey` surface. Disposition: `not-required (already-registered marketplace-category-retired)`; `check-adr-0087-registration` passes. - Consumers that follow: objectstack-ai/cloud#3661 (selects with options and translations, after its pin moves past this merge) and #31011 (console labels keyed by `.options`). objectstack-ai/cloud#3689 pins `90523379` and is unaffected until that follow-up. - Merged `origin/main` (`3f61e4f8fb`) before opening: main had moved `taxonomy.test.ts`, `publish.ts` and the template-manifest reference page; the deferred page was regenerated from the merged tree (`f7a05c8600`), and every reading below was retaken on that head. ## Verification Head `f7a05c8600` for every line. Exit codes captured before any pipe; suites under `scripts/pm/os-verify-lock.sh`. <details><summary>Suites</summary> - `pnpm --filter @objectstack/spec test` :: exit 0 — Test Files 820 passed (820), Tests 25920 passed, 1 todo - `pnpm --filter @objectstack/spec typecheck` :: exit 0 - `pnpm --filter @objectstack/spec check:generated` :: exit 0 — All 14 generated artifacts are up to date - `pnpm --filter @objectstack/cli exec vitest run --project unit` :: exit 0 — Test Files 396 passed (396), Tests 5574 passed - `pnpm --filter @objectstack/cli typecheck` :: exit 0 - `pnpm --filter @objectstack/scenarios test` :: exit 0 — 23 files, 341 tests; `typecheck` :: exit 0 - connector-app-docusign / -github / -stripe `test` (`objectstack validate` + vitest) :: exit 0 — 28 / 14 / 20 tests </details> <details><summary>Derived gate families (dispatch-gates --commands, 104)</summary> - 103 of 104 exit 0, among them `check-adr-0087-registration --base origin/main` (1 declared-breaking changeset, each carrying a disposition), `check-changeset-no-major --base origin/main`, `check:liveness`, `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:nul-bytes`, `check:dual-build-cjs-loads`. - `check-empty-changeset --base origin/main` :: exit 1 — the DELIBERATE CORRECTION above, red by design. - `dispatch-gates --ran`: 104 derived, 104 run, 0 NOT-MEASURED (derived from recorded exit codes). - Level axis driven offline with this body's declaration (`check-changeset-no-major --event`): exit 0, `yes (narrowing)` + `narrowed`, no moved package graded `patch`. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d6b88f1 commit 8cf1fd0

12 files changed

Lines changed: 113 additions & 44 deletions

File tree

‎.changeset/30904-marketplace-taxonomy.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Metadata-protocol: narrowed
1313

1414
**BREAKING** for a package source, publish request or listing that still writes `category`.
1515

16-
- **`@objectstack/spec`.** The marketplace taxonomy is defined once, in `@objectstack/spec/marketplace` (`taxonomy.zod.ts`): `businessFunctions` (1-3 distinct values, the primary one first: `sales`, `marketing`, `customer-service`, `finance`, `hr`, `supply-chain`, `administration`, `project`, `legal`, `it`, `collaboration`), `industries` (up to 3, optional; absent means cross-industry), `solutionType` (`app` | `template` | `connector`; absent means `app`) and `maturity` (`beta` | `stable`; absent means `stable`), on `PackageSchema`, `CreatePackageRequestSchema`, `UpdatePackageRequestSchema`, `TemplateManifestSchema` (`objectstack.manifest.json`) and `MarketplaceListingSchema`, where `businessFunctions` is required. A search filters by `businessFunctions` / `industries` / `solutionTypes` / `maturity` and reads one facet count list per facet. `explainSolutionTypeContradiction` is the publish cross-check (a `template` needs the `unmanaged` install mode, a `connector` a declared connector) and `LEGACY_CATEGORY_MAPPING` says what every old category value becomes.
16+
- **`@objectstack/spec`.** The marketplace taxonomy is defined once, in `@objectstack/spec/marketplace` (`taxonomy.zod.ts`): `businessFunctions` (1-3 distinct values, the primary one first: `sales`, `marketing`, `customer_service`, `finance`, `hr`, `supply_chain`, `administration`, `project`, `legal`, `it`, `collaboration`), `industries` (up to 3, optional; absent means cross-industry), `solutionType` (`app` | `template` | `connector`; absent means `app`) and `maturity` (`beta` | `stable`; absent means `stable`), on `PackageSchema`, `CreatePackageRequestSchema`, `UpdatePackageRequestSchema`, `TemplateManifestSchema` (`objectstack.manifest.json`) and `MarketplaceListingSchema`, where `businessFunctions` is required. A search filters by `businessFunctions` / `industries` / `solutionTypes` / `maturity` and reads one facet count list per facet. `explainSolutionTypeContradiction` is the publish cross-check (a `template` needs the `unmanaged` install mode, a `connector` a declared connector) and `LEGACY_CATEGORY_MAPPING` says what every old category value becomes.
1717
- **Retired.** `category` on all six schemas, and `MarketplaceSearchResponse.facets.categories`, are `retiredKey()` tombstones: `tsc` refuses them and a parse refuses them with the prescription. `MarketplaceCategorySchema` / `MarketplaceCategory` and `PackageCategorySchema` / `PackageCategory` are removed. `RETIRED_KEYS_BY_MAJOR[18]` lists the six keys, `RETIRED_DEFS_BY_MAJOR[18]` both defs, and the upgrade guide carries the semantic entry `marketplace-category-retired`. No ADR-0087 conversion: none of these shapes is a stack collection member, so `os migrate meta` has nothing to rewrite; a runtime boot never reads them.
1818
- **`@objectstack/cli`.** `os package publish` refuses a `category` in `objectstack.manifest.json`, or the now-hidden `--category` flag, before any network call, naming the facets for the value it held. It judges each facet with the spec's schema, checks a declared `solutionType` against the built artifact, and sends the facets as `business_functions`, `industries`, `solution_type` and `maturity`; an absent facet is not sent.
1919
- **`create-objectstack`.** The blank template no longer ships `category: "starter"`, which every project scaffolded from it inherited and which the hosted publish route refused.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: the seven multi-word marketplace taxonomy values are spelled as identifiers (`customer_service`, `supply_chain`, …), so a select field can carry the facets as its options
6+
7+
Clause-②: yes (narrowing) — seven accepted facet values change spelling, inside the marketplace-taxonomy narrowing of the same release; no published version carried the hyphenated spelling
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (already-registered marketplace-category-retired) The respelled values were introduced by the same unreleased change that registered this entry: @objectstack/spec 18.3.0 was published before it merged and its changeset is still pending, so no published spec ever accepted the hyphenated spelling and no stored row or authored source made against a release can hold it. The entry's prescription (category to the facets of marketplace/taxonomy.zod.ts and LEGACY_CATEGORY_MAPPING) names no value and stays the complete upgrade path; a value-rename entry would project a rename no released author can meet. -->
11+
12+
`businessFunctions` and `industries` used to accept seven values with hyphens. A select option's value is a `SystemIdentifierSchema` (lowercase letters, digits, `_` and `.`), so no select field — the cloud catalog's facet columns, a console filter — could carry them. They are now spelled with underscores; order and arity are unchanged, and a guard test refuses any future taxonomy value that is not an identifier.
13+
14+
| Facet | Wrote (unreleased builds only) | Write instead |
15+
|:--|:--|:--|
16+
| `businessFunctions` | `customer-service` | `customer_service` |
17+
| `businessFunctions` | `supply-chain` | `supply_chain` |
18+
| `industries` | `trade-distribution` | `trade_distribution` |
19+
| `industries` | `professional-services` | `professional_services` |
20+
| `industries` | `financial-services` | `financial_services` |
21+
| `industries` | `public-sector` | `public_sector` |
22+
| `industries` | `real-estate-construction` | `real_estate_construction` |
23+
24+
`LEGACY_CATEGORY_MAPPING` maps `erp` to `["finance", "supply_chain"]`, and `os package publish` lists the identifier spellings when it refuses a retired `category`.

‎content/docs/deployment/marketplace-handbook.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ that replace its retired `category`):
5757
{
5858
"manifestId": "app.objectstack.hotcrm",
5959
"displayName": "HotCRM",
60-
"businessFunctions": ["sales", "customer-service", "marketing"],
60+
"businessFunctions": ["sales", "customer_service", "marketing"],
6161
"license": "Apache-2.0",
6262
"iconUrl": "https://raw.githubusercontent.com/objectstack-ai/hotcrm/main/assets/icon.svg",
6363
"homepageUrl": "https://github.com/objectstack-ai/hotcrm",

‎content/docs/references/marketplace/marketplace.mdx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ Public-facing package listing on the marketplace
180180
| **tagline** | `string` | optional | Short tagline (max 120 chars) |
181181
| **description** | `string` | optional | Full description (Markdown) |
182182
| **category** | `never` | optional | [REMOVED] `MarketplaceListing.category` was removed in @objectstack/spec 18 (ADR-0087) — one category had to say both who a package is for and what it is, and the platform and the cloud catalog spelled it from lists that disagreed. Delete the key. Declare `businessFunctions` (1-3 business functions, the primary one first), optionally `industries` (up to 3; absent means cross-industry) and `solutionType` (`app`, `template` or `connector`; absent means `app`). `LEGACY_CATEGORY_MAPPING` in @objectstack/spec/marketplace gives the facets for every old category value. |
183-
| **businessFunctions** | `Enum<'sales' \| 'marketing' \| 'customer-service' \| 'finance' \| 'hr' \| 'supply-chain' \| 'administration' \| 'project' \| 'legal' \| 'it' \| 'collaboration'>[]` | ✅ | Business functions the package serves: 1-3 distinct values, the first is primary |
184-
| **industries** | `Enum<'manufacturing' \| 'trade-distribution' \| 'retail' \| 'logistics' \| 'professional-services' \| 'financial-services' \| 'healthcare' \| 'education' \| 'public-sector' \| 'real-estate-construction'>[]` | optional | Industries the package is built for: up to 3 distinct values; absent means cross-industry |
183+
| **businessFunctions** | `Enum<'sales' \| 'marketing' \| 'customer_service' \| 'finance' \| 'hr' \| 'supply_chain' \| 'administration' \| 'project' \| 'legal' \| 'it' \| 'collaboration'>[]` | ✅ | Business functions the package serves: 1-3 distinct values, the first is primary |
184+
| **industries** | `Enum<'manufacturing' \| 'trade_distribution' \| 'retail' \| 'logistics' \| 'professional_services' \| 'financial_services' \| 'healthcare' \| 'education' \| 'public_sector' \| 'real_estate_construction'>[]` | optional | Industries the package is built for: up to 3 distinct values; absent means cross-industry |
185185
| **solutionType** | `Enum<'app' \| 'template' \| 'connector'>` | optional (default: `"app"`) | What a marketplace package is: a managed app, an unmanaged template, or a connector |
186186
| **maturity** | `Enum<'beta' \| 'stable'>` | optional (default: `"stable"`) | Package maturity: beta shows a badge on the listing; absent means stable |
187187
| **tags** | `string[]` | optional | Search tags |
@@ -246,8 +246,8 @@ Marketplace search request
246246
| :--- | :--- | :--- | :--- |
247247
| **query** | `string` | optional | Full-text search query |
248248
| **category** | `never` | optional | [REMOVED] `MarketplaceSearchRequest.category` was removed in @objectstack/spec 18 (ADR-0087) — one category had to say both who a package is for and what it is, and the platform and the cloud catalog spelled it from lists that disagreed. Delete the key. Declare `businessFunctions` (1-3 business functions, the primary one first), optionally `industries` (up to 3; absent means cross-industry) and `solutionType` (`app`, `template` or `connector`; absent means `app`). `LEGACY_CATEGORY_MAPPING` in @objectstack/spec/marketplace gives the facets for every old category value. |
249-
| **businessFunctions** | `Enum<'sales' \| 'marketing' \| 'customer-service' \| 'finance' \| 'hr' \| 'supply-chain' \| 'administration' \| 'project' \| 'legal' \| 'it' \| 'collaboration'>[]` | optional | Filter: any of these business functions |
250-
| **industries** | `Enum<'manufacturing' \| 'trade-distribution' \| 'retail' \| 'logistics' \| 'professional-services' \| 'financial-services' \| 'healthcare' \| 'education' \| 'public-sector' \| 'real-estate-construction'>[]` | optional | Filter: any of these industries |
249+
| **businessFunctions** | `Enum<'sales' \| 'marketing' \| 'customer_service' \| 'finance' \| 'hr' \| 'supply_chain' \| 'administration' \| 'project' \| 'legal' \| 'it' \| 'collaboration'>[]` | optional | Filter: any of these business functions |
250+
| **industries** | `Enum<'manufacturing' \| 'trade_distribution' \| 'retail' \| 'logistics' \| 'professional_services' \| 'financial_services' \| 'healthcare' \| 'education' \| 'public_sector' \| 'real_estate_construction'>[]` | optional | Filter: any of these industries |
251251
| **solutionTypes** | `Enum<'app' \| 'template' \| 'connector'>[]` | optional | Filter: any of these solution types |
252252
| **maturity** | `Enum<'beta' \| 'stable'>` | optional | Filter by maturity |
253253
| **tags** | `string[]` | optional | Filter by tags |
@@ -291,8 +291,8 @@ Public-facing package listing on the marketplace
291291
| **tagline** | `string` | optional | Short tagline (max 120 chars) |
292292
| **description** | `string` | optional | Full description (Markdown) |
293293
| **category** | `never` | optional | [REMOVED] `MarketplaceListing.category` was removed in @objectstack/spec 18 (ADR-0087) — one category had to say both who a package is for and what it is, and the platform and the cloud catalog spelled it from lists that disagreed. Delete the key. Declare `businessFunctions` (1-3 business functions, the primary one first), optionally `industries` (up to 3; absent means cross-industry) and `solutionType` (`app`, `template` or `connector`; absent means `app`). `LEGACY_CATEGORY_MAPPING` in @objectstack/spec/marketplace gives the facets for every old category value. |
294-
| **businessFunctions** | `Enum<'sales' \| 'marketing' \| 'customer-service' \| 'finance' \| 'hr' \| 'supply-chain' \| …>[]` | ✅ | Business functions the package serves: 1-3 distinct values, the first is primary |
295-
| **industries** | `Enum<'manufacturing' \| 'trade-distribution' \| 'retail' \| 'logistics' \| …>[]` | optional | Industries the package is built for: up to 3 distinct values; absent means cross-industry |
294+
| **businessFunctions** | `Enum<'sales' \| 'marketing' \| 'customer_service' \| 'finance' \| 'hr' \| 'supply_chain' \| …>[]` | ✅ | Business functions the package serves: 1-3 distinct values, the first is primary |
295+
| **industries** | `Enum<'manufacturing' \| 'trade_distribution' \| 'retail' \| 'logistics' \| …>[]` | optional | Industries the package is built for: up to 3 distinct values; absent means cross-industry |
296296
| **solutionType** | `Enum<'app' \| 'template' \| 'connector'>` | optional (default: `"app"`) | What a marketplace package is: a managed app, an unmanaged template, or a connector |
297297
| **maturity** | `Enum<'beta' \| 'stable'>` | optional (default: `"stable"`) | Package maturity: beta shows a badge on the listing; absent means stable |
298298
| **tags** | `string[]` | optional | Search tags |
@@ -316,8 +316,8 @@ Public-facing package listing on the marketplace
316316
| Property | Type | Required | Description |
317317
| :--- | :--- | :--- | :--- |
318318
| **categories** | `never` | optional | [REMOVED] `MarketplaceSearchResponse.facets.categories` was removed in @objectstack/spec 18 (ADR-0087) — one category had to say both who a package is for and what it is, and the platform and the cloud catalog spelled it from lists that disagreed. Delete the key. Declare `businessFunctions` (1-3 business functions, the primary one first), optionally `industries` (up to 3; absent means cross-industry) and `solutionType` (`app`, `template` or `connector`; absent means `app`). `LEGACY_CATEGORY_MAPPING` in @objectstack/spec/marketplace gives the facets for every old category value. |
319-
| **businessFunctions** | `{ value: Enum<'sales' \| 'marketing' \| 'customer-service' \| 'finance' \| 'hr' \| 'supply-chain' \| …>; count: integer }[]` | optional | |
320-
| **industries** | `{ value: Enum<'manufacturing' \| 'trade-distribution' \| 'retail' \| 'logistics' \| …>; count: integer }[]` | optional | |
319+
| **businessFunctions** | `{ value: Enum<'sales' \| 'marketing' \| 'customer_service' \| 'finance' \| 'hr' \| 'supply_chain' \| …>; count: integer }[]` | optional | |
320+
| **industries** | `{ value: Enum<'manufacturing' \| 'trade_distribution' \| 'retail' \| 'logistics' \| …>; count: integer }[]` | optional | |
321321
| **solutionTypes** | `{ value: Enum<'app' \| 'template' \| 'connector'>; count: integer }[]` | optional | |
322322
| **pricing** | `{ model: Enum<'free' \| 'freemium' \| 'paid' \| 'subscription' \| 'usage-based' \| 'contact-sales'>; count: integer }[]` | optional | |
323323

0 commit comments

Comments
 (0)