Repository navigation
Commit 468e0c1
feat(spec)!: one marketplace taxonomy — business functions, industries, a solution type and maturity replace the single category (#30980)
Fixes #30904
Clause-②: yes (narrowing) — new facet keys and `maturity` widen;
`category` retired and refused by name narrows; a contract review runs
before the queue
Metadata-protocol: narrowed
> The claim (card comment 6683208761) and the dispatch declared
`Clause-②: yes (widening)` / `Metadata-protocol: none`. `Check
Changeset` refuses that pair on this diff, measured locally:
`metadata-protocol declares none while an authorable-surface or
authorable-defaults baseline moved`
(`packages/spec/authorable-surface/marketplace.json`). AGENTS.md defines
a tombstoned key as `narrowed`, and `narrowed` needs the `(narrowing)`
arm and a `minor`, so the two lines above and the changeset carry that
pair. The seat can still rule otherwise; see Acceptance notes.
## 维护者速读
- **改了什么:** 市场分类在 spec 里只定义一处:职能 `businessFunctions`(1–3 个,第一个是主职能)、行业
`industries`(0–3 个,可选)、类型 `solutionType`(app / template /
connector),另加独立的 `maturity`(beta / stable)。两份旧的 `category` 按 ADR-0087
退役,带 `category` 的发布被点名拒绝并给出改法。
- **为什么改:** 一个 `category` 同时表示“给谁用”和“是什么”,平台和 cloud 各有一套取值且互相对不上;按维护者裁决
D1–D3 合并为一套。空白模板的 `"starter"` 会让每个新项目第一次发布被 cloud 拒绝,这里一并删掉。
- **风险与回滚:** 破坏性:还写着 `category` 的清单在发布时被拒(附映射)。运行时启动不读这些
schema,不受影响。cloud 在 cloud#3661 落地前会忽略新字段,且其提交审核仍要求 `category`,需要先落 cloud
再发 CLI(见 Acceptance notes)。回滚即 revert 本 PR。
- **先看:**
`packages/spec/src/marketplace/taxonomy.zod.ts`(取值、上限、交叉校验、旧值映射);`packages/cli/src/commands/package/publish.ts`
的 `retiredCategoryRefusal` /
`readPublishTaxonomy`;`packages/spec/liveness/template_manifest.json`(新纳入的台账与消费方)。
- **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run
src/marketplace/taxonomy.test.ts`,预期 39 个测试全部通过;在任一
`objectstack.manifest.json` 里加 `"category": "finance"` 后跑 `os package
publish`,预期在联网前被拒,提示 `"businessFunctions": ["finance"], "solutionType":
"app"`。
- **需要你做的:** 无,席位落地(契约复核后由席位决定声明行与发布顺序)。
## What changes
<details><summary>spec — the taxonomy, defined once</summary>
- New `packages/spec/src/marketplace/taxonomy.zod.ts`, exported from
`@objectstack/spec/marketplace`:
- `MarketplaceBusinessFunctionSchema` (11 values, ruling §2 F1) and
`MarketplaceBusinessFunctionsSchema` (1–3 distinct, ordered, the first
is primary).
- `MarketplaceIndustrySchema` (10 values, §2 F2 / D1) and
`MarketplaceIndustriesSchema` (0–3 distinct; no catch-all value —
absence means cross-industry).
- `MarketplaceSolutionTypeSchema` (`app` | `template` | `connector`; no
`theme`, ADR-0019) and `PackageMaturitySchema` (`beta` | `stable`, D3 —
a flag, not a facet).
- `explainSolutionTypeContradiction(solutionType, { installModes,
connectorCount })` — the publish cross-check: `template` needs
`unmanaged` among the install modes (ADR-0131 D6), `connector` needs a
declared connector. One rule and one sentence for the CLI and, through
cloud#3661, the cloud publish gate.
- `LEGACY_CATEGORY_MAPPING` — ruling §3.1 + §3.2: every value of the
retired spec enum, the cloud list and its folded aliases → `mapped` /
`manual` (author chooses the functions; never a catch-all) /
`unlistable` (ADR-0019). `operations` is mapped and flagged `review`.
- `PackageSchema` / `CreatePackageRequestSchema` /
`UpdatePackageRequestSchema` / `TemplateManifestSchema` /
`MarketplaceListingSchema` carry the four keys. Row shapes default
`solutionType: 'app'` and `maturity: 'stable'`; requests and the
manifest carry no default (absent stays absent, so a re-publish never
rewrites a stored value). `businessFunctions` is required on the
listing.
- `MarketplaceSearchRequestSchema` filters by `businessFunctions` /
`industries` / `solutionTypes` (OR inside a facet, AND across) and
`maturity`; `MarketplaceSearchResponseSchema.facets` has one count list
per facet.
</details>
<details><summary>spec — the ADR-0087 retirement of both category
schemas</summary>
- `category` is a `retiredKey()` tombstone on all six schemas (and
`facets.categories` on the search response). The schemas are not strict,
so a bare deletion would strip silently; the tombstone refuses with the
prescription in both channels (`tsc`, parse). `TemplateManifestSchema`
re-declares its own so the refusal names the file an author writes.
- `MarketplaceCategorySchema` / `MarketplaceCategory` and
`PackageCategorySchema` / `PackageCategory` are deleted:
`RETIRED_DEFS_BY_MAJOR[18]` (two entries), the deliberate
`json-schema.manifest/marketplace.json` deletion, and the two
`scripts/lib/renamed-defs.ts` rows retired with a note.
- Registration: six `RETIRED_KEYS_BY_MAJOR[18]` entries, one D3 semantic
entry `marketplace-category-retired`, one `STEP18_RATIONALE` fragment.
**No D2 conversion**: none of these shapes is a stack collection member
(the `marketplace/PackageDependency:optional` precedent), so a
conversion would be a transform no seam runs.
- **Publish-time and boot-time consequence** for a manifest that still
carries `category`: publish refuses it (the CLI before any network call;
a parse of any of the schemas, with the prescription). Boot: nothing —
no runtime boot reads `objectstack.manifest.json`, a package row or a
listing (measured: the only readers of the file are `os package
publish`, create-objectstack and `check:template-manifests`). So there
is nothing for the boot to tolerate or refuse; ADR-0087's load-path
conversion has no seam here, and the decision recorded in the D3 entry
is refuse-at-publish, migrate-stored-rows-once (cloud#3661, reading
`LEGACY_CATEGORY_MAPPING`).
- Generated: `authorable-surface`, `authorable-defaults`,
`json-schema.manifest`, `api-surface`, `export-origins`,
`declaration-map`, `content/docs/references/marketplace/**`, the
strictness-ledger counts; `dropped-refinements.baseline.json` declares
the uniqueness refinement on the two facet lists (no projectable arm for
"distinct items" exists); six ADR-0122 isomorphic pins (837 → 841).
</details>
<details><summary>CLI, first-party manifests, scenario library,
docs</summary>
- `os package publish` (`packages/cli/src/commands/package/publish.ts`):
- refuses `category` in `objectstack.manifest.json` and the now-hidden
`--category` flag before any network call, naming the facets for the
value it held (from `LEGACY_CATEGORY_MAPPING`); a `starter` copied from
the blank template is told it never classified the package;
- judges each facet with the spec's own schema, cross-checks a declared
`solutionType` against the built artifact (`resolveInstallModes` + the
artifact's `connectors`), and sends them as `business_functions` /
`industries` / `solution_type` / `maturity`; an absent facet is not
sent.
- Manifests: connector-stripe `[finance]`, connector-github `[it]`,
connector-docusign `[collaboration, legal]`, each `solutionType:
connector` (their listing tests now assert the stack declares the
connector that makes them one). The create-objectstack blank template
drops `category: "starter"` and declares **no** facet:
create-objectstack copies this file into every scaffolded project, so a
facet here would be inherited by projects it does not describe, and a
`template` there would be refused by the new cross-check on the first
publish (the blank config permits only `managed`).
- `packages/qa/scenarios`: the hand copy `SCENARIO_CATEGORIES` is gone;
a card's key is `businessFunctions`, typed by the spec schema. The nine
cards are re-tagged per ruling §3.3.
- Docs: `content/docs/deployment/marketplace-handbook.mdx`,
`content/docs/deployment/cli.mdx`,
`content/docs/protocol/kernel/plugin-spec.mdx`,
`docs/qa/scenarios/README.md` — no hand copy of the value lists (the
handbook links the generated taxonomy reference).
- Liveness: `objectstack.manifest.json` (`TemplateManifestSchema`)
enters the ledger as `template_manifest` via `SPEC_ONLY_SCHEMAS`.
`businessFunctions` / `industries` / `maturity` are `planned`, naming
their consumers — the cloud catalog (objectstack-ai/cloud#3661) and the
console marketplace card the seat files; `solutionType` is `live`
through the publish cross-check; `category` is the tombstone row. The
platform checklist carries a measured waiver for it (no checklist item
can drive a hosted publish).
</details>
## Tests
Measured on head `7cd7fe941c` (the commits after it are the changeset
declaration and nothing a test reads).
<details><summary>Suites</summary>
- `pnpm --filter @objectstack/spec test` — exit 0 (818 + 1 files; the
one earlier red, the dropped-refinements header totals, fixed and re-run
green: 27/27).
- `pnpm --filter @objectstack/spec typecheck` — exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2` — exit 0, `Test Files 390 passed (390)`, `Tests 5496
passed (5496)`. Integration tier declared to CI (no spawn entry or
driver path touched).
- `pnpm --filter @objectstack/cli typecheck` — exit 0.
- `pnpm --filter create-objectstack test` / `typecheck` — exit 0 / 0.
- `pnpm --filter @objectstack/scenarios test` / `typecheck` — exit 0 /
0.
- connector-stripe / connector-github / connector-docusign `test` — exit
0 each.
- New: `packages/spec/src/marketplace/taxonomy.test.ts` (39 cases: one
block per rule — F1, F2, F3, D3, the cross-check, the refusal on every
schema, the registration, the mapping) and
`packages/cli/test/package-publish-taxonomy.test.ts` (14 cases).
</details>
<details><summary>Ablations — one per rule, each through
scripts/ablation-replace.mjs (mutation proven on disk, restore proven
against HEAD)</summary>
| Rule | Mutation | Result |
|:--|:--|:--|
| F1 at most 3 | `.max(MAX_BUSINESS_FUNCTIONS,` → `.max(99,` | 1 failed
/ 39 — "refuses a fourth value" |
| F1 at least 1 | `.min(1,` → `.min(0,` | 1 failed / 39 |
| F1/F2 distinct | the refine predicate → always true | 2 failed / 39 |
| F2 at most 3 | `.max(MAX_INDUSTRIES,` → `.max(99,` | 1 failed / 39 |
| template needs unmanaged | the template branch → `if (false)` | 1
failed / 39 |
| connector needs a connector | the connector branch → `if (false)` | 1
failed / 39 |
| category tombstone | delete `CreatePackageRequest.category` | 2 failed
/ 39 (the refusal and the tsc pin) |
| CLI refuses manifest `category` | the guard → `if (false)` | 2 failed
/ 14 |
| CLI refuses `--category` | the guard → `if (false)` | 1 failed / 14 |
| CLI wires the cross-check | delete the refusal return | 3 failed / 14
|
Every leg ends `git diff HEAD` empty for its file; the final tree
carried 0 modified source paths.
</details>
## Acceptance notes
<details><summary>Every caller class that newly sees a refusal</summary>
1. A TypeScript author writing `category` on `Package` /
`CreatePackageRequest` / `UpdatePackageRequest` / `TemplateManifest` /
`MarketplaceListing` / `MarketplaceSearchRequest` — `tsc` error (the
retired mark).
2. Any code parsing those schemas with `category` present — parse
refusal with the prescription. Measured at cloud `8f574b9`: cloud reads
only `PackageSchema.shape.manifestId` and
`CreatePackageRequestSchema.shape.tags`, so its pin bump is not refused
by this.
3. A TypeScript importer of `MarketplaceCategorySchema` /
`PackageCategorySchema` / their types — compile error (zero importers
here and in cloud at `8f574b9`).
4. `os package publish` with `category` in `objectstack.manifest.json` —
refused before any network call. This includes every project already
scaffolded from the blank template (`"starter"`).
5. `os package publish --category …` — refused.
6. A manifest declaring `solutionType: template` without the `unmanaged`
install mode, or `connector` with no connector, or a malformed facet —
refused (new keys, no existing caller).
7. A producer of `MarketplaceListingSchema` without `businessFunctions`
— refused (new requirement; no producer in this repo).
8. A scenario card with `category`, or a business function outside the
facet — loader refusal.
9. `check:template-manifests` — any shipped manifest with `category`
reds.
</details>
<details><summary>Sequencing, declarations and what this PR deliberately
does not do</summary>
- **Cloud sequencing (needs the seat's call):** at cloud `8f574b9` the
publish route picks named body keys, so the four facets are dropped
until objectstack-ai/cloud#3661 lands, and `marketplace-policy.ts` still
requires `category` at marketplace submission. A CLI released from this
PR before cloud#3661 deploys can no longer satisfy that submission rule
for a package that has no stored category. Order: cloud#3661 (accept the
facets, require a business function instead of `category`) before the
next CLI release reaches authors.
- **Declarations:** see the quote under the declaration lines. The pair
here is what `Check Changeset` accepts and what AGENTS.md defines; if
the seat keeps `none`, the changeset must go back to `patch` and the
level gate stays red on the moved authorable-surface shard.
- **Blank template reproduction:** reproduced in two halves, not end to
end. Cloud half executed: cloud `8f574b9`'s
`isValidPackageCategory("starter")` returns `false` (the route answers
400 on it at `package-publish.ts:896`). CLI half read: at the base,
publish sent `m.category` verbatim and create-objectstack copied the
template's `category` into every project.
- **Kebab-case values** (`customer-service`, `trade-distribution`, …)
are the ruling's verbatim values and match the marketplace enums'
existing spelling (`usage-based`, `in-review`); Prime Directive #3's
snake_case is the general rule for machine names.
- **`explainSolutionTypeContradiction` lives in the spec** as a pure
function beside `resolveInstallModes`, so the CLI and the cloud gate
share one rule and one sentence instead of two copies.
- **Liveness enrolment found more than the taxonomy:** `name`,
`specVersion`, `skills`, `scaffold`, and the warned-and-unsent
`visibility` / `publisher` / `isStarter` have no reader of the file's
value. Recorded `dead` in the new ledger, not retired here.
- Not touched: HotCRM's manifest (hotcrm lane); cloud (cloud#3661); the
console `ui/` (the seat files that card);
`docs/design/marketplace-publishing.md` (a design record whose
`--category` search example describes a command this repo does not
ship).
</details>
## Verification
<details><summary>Gates — dispatch-gates.mjs derivation,
reconciled</summary>
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran …` → `✓ 119 derived famil(ies) accounted for — 115 run, 4
NOT-MEASURED (4 DERIVED from a recorded exit 3).`
- NOT MEASURED (exit 3, the gate's own prerequisite verdict — each needs
a whole-workspace build outside this diff's closure; CI runs them):
`check:skill-examples` (client-react has no `.d.ts`),
`check:i18n-coverage` (`@objectstack/connector-slack` unbuilt),
`check:dual-build-cjs-loads` (packages outside the closure unbuilt),
`check:type-check-debt` (its own locked closure build never got the
shared lock).
- Fixed on the way: `check:llms-txt` (the marketplace row and the
inventory total), `check:platform-checklist` (the `template_manifest`
waiver), `check:test-source-alias` (the connector configs alias
`@objectstack/spec/kernel` to source).
- `pnpm --filter @objectstack/spec check:generated` — all artifacts
current after `--fix`; `check:liveness`, `check:spec-parsed-alias`,
`check-adr-0087-registration`, `check-changeset-no-major` (level axis,
against this body) — exit 0.
- `node scripts/check-platform-checklist.mjs --changed-from origin/main`
— exit 0 (no anchored file in the change set).
</details>
<details><summary>Dogfood battery</summary>
Owed on the final head (title `!`, `(narrowing)` arm). Its reading is in
the report comment on the card.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 41dc302 commit 468e0c1
76 files changed
Lines changed: 2081 additions & 227 deletions
File tree
- .changeset
- content/docs
- deployment
- protocol/kernel
- references
- marketplace
- docs
- audits/2026-07-unknown-key-strictness-ledger.counts
- qa
- platform-checklist
- scenarios
- packages
- apps
- connector-docusign
- test
- connector-github
- test
- connector-stripe
- test
- cli
- src/commands/package
- test
- create-objectstack/src
- templates/blank
- qa/scenarios
- src
- export
- test
- build
- export
- spec
- api-surface
- authorable-defaults
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- state-counts
- scripts
- lib
- liveness
- src
- marketplace
- migrations
- entries
- retired-defs
- retired-keys
- semantic
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3264 | 3264 | | |
3265 | 3265 | | |
3266 | 3266 | | |
3267 | | - | |
3268 | 3267 | | |
3269 | 3268 | | |
3270 | 3269 | | |
| |||
3300 | 3299 | | |
3301 | 3300 | | |
3302 | 3301 | | |
3303 | | - | |
3304 | | - | |
3305 | | - | |
| 3302 | + | |
| 3303 | + | |
| 3304 | + | |
| 3305 | + | |
| 3306 | + | |
| 3307 | + | |
| 3308 | + | |
| 3309 | + | |
| 3310 | + | |
3306 | 3311 | | |
3307 | 3312 | | |
3308 | 3313 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
38 | 41 | | |
39 | 42 | | |
40 | 43 | | |
41 | 44 | | |
42 | 45 | | |
43 | 46 | | |
44 | 47 | | |
| 48 | + | |
| 49 | + | |
45 | 50 | | |
46 | 51 | | |
47 | 52 | | |
48 | | - | |
| 53 | + | |
| 54 | + | |
49 | 55 | | |
50 | 56 | | |
51 | 57 | | |
52 | 58 | | |
53 | 59 | | |
54 | | - | |
| 60 | + | |
55 | 61 | | |
56 | 62 | | |
57 | 63 | | |
| |||
189 | 195 | | |
190 | 196 | | |
191 | 197 | | |
192 | | - | |
| 198 | + | |
193 | 199 | | |
194 | 200 | | |
195 | 201 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
139 | | - | |
140 | | - | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
141 | 142 | | |
142 | 143 | | |
143 | 144 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
| 252 | + | |
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | | - | |
259 | | - | |
| 258 | + | |
| 259 | + | |
260 | 260 | | |
| 261 | + | |
261 | 262 | | |
262 | 263 | | |
263 | 264 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
0 commit comments