Skip to content

feat(spec)!: one marketplace taxonomy — business functions, industries, a solution type and maturity replace the single category - #30980

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-30904-marketplace-taxonomy
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/issue-30904-marketplace-taxonomy

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

spec — the taxonomy, defined once
  • 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.
spec — the ADR-0087 retirement of both category schemas
  • 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).
CLI, first-party manifests, scenario library, docs
  • 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).

Tests

Measured on head 7cd7fe941c (the commits after it are the changeset declaration and nothing a test reads).

Suites
  • 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).
Ablations — one per rule, each through scripts/ablation-replace.mjs (mutation proven on disk, restore proven against HEAD)
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.

Acceptance notes

Every caller class that newly sees a refusal
  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.
Sequencing, declarations and what this PR deliberately does not do
  • 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 Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #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).

Verification

Gates — dispatch-gates.mjs derivation, reconciled
  • 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).
Dogfood battery

Owed on the final head (title !, (narrowing) arm). Its reading is in the report comment on the card.


Generated by Claude Code

claude added 11 commits November 6, 2026 11:08
…ario cards re-tagged

Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2
Co-authored-by: Claude <noreply@anthropic.com>
…into claude/issue-30904-marketplace-taxonomy
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 8 package(s): @objectstack/connector-app-docusign, @objectstack/connector-app-github, @objectstack/connector-app-stripe, @objectstack/cli, create-objectstack, objectstack-blank, @objectstack/scenarios, @objectstack/spec, touching 51 documentable anchor(s). ⚠️ 29 changed file(s) yielded no anchor (packages/apps/connector-docusign/objectstack.manifest.json, packages/apps/connector-docusign/vitest.config.ts, packages/apps/connector-github/objectstack.manifest.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/declarative-endpoints.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/concepts/metadata-lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/cli.mdx (via PackageSchema (symbol, a top-level const), businessFunctions (symbol, a field of const object ScenarioCardSchema; a field of const object TAXONOMY_WIRE_KEYS; a field of type LegacyCategoryMapping), solutionType (symbol, a field of const object TAXONOMY_WIRE_KEYS; a field of type LegacyCategoryMapping), businessFunctions (literal, a string literal in MarketplaceBusinessFunctionsSchema; a string literal in retiredCategoryRefusal), solutionType (literal, a string literal in retiredCategoryRefusal), os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/install-modes.mdx (via installModes (symbol, a field of interface SolutionTypeEvidence))
  • content/docs/deployment/marketplace-handbook.mdx (via businessFunctions (symbol, a field of const object ScenarioCardSchema; a field of const object TAXONOMY_WIRE_KEYS; a field of type LegacyCategoryMapping), solutionType (symbol, a field of const object TAXONOMY_WIRE_KEYS; a field of type LegacyCategoryMapping), businessFunctions (literal, a string literal in MarketplaceBusinessFunctionsSchema; a string literal in retiredCategoryRefusal), solutionType (literal, a string literal in retiredCategoryRefusal), os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/publish-and-preview.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/metadata-service.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/plugin-spec.mdx (via businessFunctions (symbol, a field of const object ScenarioCardSchema; a field of const object TAXONOMY_WIRE_KEYS; a field of type LegacyCategoryMapping), businessFunctions (literal, a string literal in MarketplaceBusinessFunctionsSchema; a string literal in retiredCategoryRefusal))
  • content/docs/protocol/objectql/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/objectql/schema.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-5.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/releases/v17/17-6.mdx (via PackageSchema (symbol, a top-level const), os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/releases/v9.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 29 changed file(s) yielded no anchor (packages/apps/connector-docusign/objectstack.manifest.json, packages/apps/connector-docusign/vitest.config.ts, packages/apps/connector-github/objectstack.manifest.json, …) — pages documenting those are invisible to this run
  • 39 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 181 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 69e133ea0d9734dd0447d08875bbd8e3af0e1517 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e2e24b15b6ea88b503300b66f70205a9e40eaee0 — the merge of head 95b6e070942530efdf3f947b7f90e8d089fc9ea7 into base 69e133ea0d9734dd0447d08875bbd8e3af0e1517, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e2e24b15b6ea88b503300b66f70205a9e40eaee0 && git checkout e2e24b15b6ea88b503300b66f70205a9e40eaee0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 69e133ea0d9734dd0447d08875bbd8e3af0e1517 95b6e070942530efdf3f947b7f90e8d089fc9ea7 && git checkout -B drift-repro 69e133ea0d9734dd0447d08875bbd8e3af0e1517 && git merge --no-ff 95b6e070942530efdf3f947b7f90e8d089fc9ea7

node scripts/docs-audit/affected-docs.mjs --json 69e133ea0d9734dd0447d08875bbd8e3af0e1517

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 69e133ea0d9734dd0447d08875bbd8e3af0e1517 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…into claude/issue-30904-marketplace-taxonomy
…ess function; the taxonomy module header opens below level 1

Claude-Session: https://claude.ai/code/session_01Q4EjjRhUZjdy8VXs6Kkur2
Co-authored-by: Claude <noreply@anthropic.com>
…into claude/issue-30904-marketplace-taxonomy

# Conflicts:
#	docs/qa/scenarios/crm_account_with_contacts.json
#	packages/qa/scenarios/src/schema.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

Contract-review-claim: session_01Q4EjjRhUZjdy8VXs6Kkur2 · head 95b6e07 · 2026-11-06T18:30Z

Face: declared Clause-②: yes (narrowing) + Metadata-protocol: narrowed. A new spec taxonomy (taxonomy.zod.ts); category tombstoned under ADR-0087 on Package, CreatePackageRequest, UpdatePackageRequest, TemplateManifest, MarketplaceListing and MarketplaceSearchRequest; MarketplaceCategorySchema and PackageCategorySchema deleted; os package publish refuses category → CONTRACT_REVIEW_TIER, run by an isolated reviewer the seat starts now. ⛔ Not queued before a PASS on this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 95b6e070942530efdf3f947b7f90e8d089fc9ea7
Local-runs: none
Scope: packages/spec/src/marketplace/index.ts, packages/spec/src/marketplace/marketplace.zod.ts, packages/spec/src/marketplace/package.zod.ts, packages/spec/src/marketplace/taxonomy.zod.ts, packages/spec/src/marketplace/template-manifest.zod.ts, packages/spec/src/migrations/entries/retired-defs/18.marketplace__MarketplaceCategory.ts, packages/spec/src/migrations/entries/retired-defs/18.marketplace__PackageCategory.ts, packages/spec/src/migrations/entries/retired-keys/18.marketplace__CreatePackageRequest__category.ts, packages/spec/src/migrations/entries/retired-keys/18.marketplace__MarketplaceListing__category.ts, packages/spec/src/migrations/entries/retired-keys/18.marketplace__MarketplaceSearchRequest__category.ts, packages/spec/src/migrations/entries/retired-keys/18.marketplace__Package__category.ts, packages/spec/src/migrations/entries/retired-keys/18.marketplace__TemplateManifest__category.ts, packages/spec/src/migrations/entries/retired-keys/18.marketplace__UpdatePackageRequest__category.ts, packages/spec/src/migrations/entries/semantic/18.marketplace-category-retired.ts, packages/spec/src/migrations/registry.ts.template + packages/cli/src/commands/package/publish.ts, packages/cli/test/package-publish-taxonomy.test.ts, packages/spec/src/marketplace/taxonomy.test.ts, packages/spec/src/kernel/manifest.zod.ts (resolveInstallModes, DEFAULT_INSTALL_MODES), packages/spec/src/shared/retired-key.ts, packages/spec/liveness/template_manifest.json, packages/spec/scripts/liveness/check-liveness.mts, packages/spec/scripts/check-template-manifests.ts, packages/spec/authorable-surface/marketplace.json, packages/spec/dropped-refinements.baseline.json, packages/create-objectstack/src/templates/blank/objectstack.manifest.json, packages/create-objectstack/src/templates/blank/objectstack.config.ts, packages/create-objectstack/src/rewrite-identity.ts, packages/apps/connector-{stripe,github,docusign}/objectstack.manifest.json and test/listing.test.ts, packages/qa/scenarios/src/schema.ts, packages/qa/scenarios/src/export/{manifest,exporter}.ts, docs/qa/scenarios/*.json (nine cards), packages/cloud-connection/src/marketplace-proxy-plugin.ts, ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx (read for the API call it makes), .changeset/30904-marketplace-taxonomy.md, content/docs/deployment/{cli,marketplace-handbook}.mdx, scripts/check-changeset-no-major.mjs (header), docs/adr/0087 (the 2026-09-13 level amendment), AGENTS.md Post-Task Checklist 3-4, .claude/skills/spec-property-retirement/SKILL.md

Reviewed read-only at 2026-11-06T18:41Z against the card's rulings (comments 6683195046 and 6691021157), the net diff origin/main...95b6e07 (76 files), the cited code at the head, and the head's check-runs read once: 67 runs, 58 success, 9 skipped (Lint UI, Dogfood Verify CLI, UI Published Artifacts, the opt-in gates), 0 pending, 0 failure — nothing NOT MEASURED. Check Changeset, Governed Surface Queue Guard, Spec property liveness, Type Check UI, Test Core and Build Core all green on this head. The dev's reports and the seat's conclusions were not used as evidence.

① Derived judgments

  1. Enums and cardinalities — right. MarketplaceBusinessFunctionSchema carries the ruling's F1 values in the ruling's order (sales, marketing, customer-service, finance, hr, supply-chain, administration, project, legal, it, collaboration); MarketplaceIndustrySchema the ten F2 values in order with no general/cross-industry; MarketplaceSolutionTypeSchema app | template | connector, no theme; PackageMaturitySchema beta | stable. businessFunctions is 1-3 distinct and order-preserving (taxonomy.zod.ts distinct + .min(1) + .max(MAX_BUSINESS_FUNCTIONS)), optional on Package / the two requests / TemplateManifest, required on MarketplaceListing — consistent with the ruling's "schema 层面字段可以不填 … 上架市场时至少 1 个". industries 0-3 distinct, optional everywhere. Row shapes default solutionType: 'app' and maturity: 'stable'; requests and the manifest carry no default, so a re-publish never rewrites a stored value (the visibility lesson, pinned in taxonomy.test.ts and the CLI test "sends no facet the manifest does not declare").
  2. LEGACY_CATEGORY_MAPPING — right against 3.1 and 3.2. Every row of 3.1 (crm→[sales]+tag crm, erp→[finance, supply-chain]+tag erp, hr/finance/project/collaboration mapped, analytics/automation/ai/other manual with the ruled tags, integration manual+connector, security and developer-tools→[it] with tags, ui-theme and storage unlistable under ADR-0019) and of 3.2 (app/application/apps manual app; sales, marketing, productivity→[collaboration]; operations/business/ops→[administration] with review: true and tag operations; data manual + tag analytics; integrations/connector/connectors manual connector; devtools/tools/developer→[it]+tag devtools; utility and travel manual with their tag; starter manual template with isStarter untouched). No other bucket anywhere; a manual value is the author's choice (pinned: "folds nothing into a catch-all"). Two parts of 3.2 are migration-process rules a static table cannot hold and belong to cloud#3661: the integrations row's "read the manifest's original category first, then remap", and "unknown value → manual + the original value as a tag" (the table returns undefined for a value outside it — the right signal, but the migration must read it as manual, never as a skip). Should-fix, carried by cloud#3661, not this PR — see ③(d).
  3. Tombstones follow ADR-0087's registration — right. Six retiredKey() sites on the authorable shapes plus facets.categories on the search response; six exact RETIRED_KEYS_BY_MAJOR[18] entries (marketplace/{Package,CreatePackageRequest,UpdatePackageRequest,TemplateManifest,MarketplaceListing,MarketplaceSearchRequest}:category), two RETIRED_DEFS_BY_MAJOR[18] entries, one D3 semantic entry marketplace-category-retired with non-empty reason and acceptance criteria, one STEP18_RATIONALE fragment, the [RETIRED] rows in authorable-surface/marketplace.json, the deliberate json-schema.manifest deletion, the renamed-defs.ts note, the ADR-0122 pin count moved 837→841 (+6 −2), and the liveness row with the REMOVED note. The prescription (retiredCategoryGuidance) opens with the fully qualified key, names businessFunctions, industries, solutionType and LEGACY_CATEGORY_MAPPING, and carries no os migrate meta sentence — correct, since no conversion covers the surface. facets.categories is a nested response key, not an authorable-surface row, so no registry entry is owed for it; its parse refusal is pinned. The changeset carries the BREAKING banner, the FROM→TO remedy and the adr-0087: registered marker.
  4. "No D2 conversion" — true. os migrate meta replays the chain over the loaded stack (normalizeStackInput); Package, the two requests, MarketplaceListing and MarketplaceSearchRequest are catalog rows and HTTP bodies, and objectstack.manifest.json is a file beside objectstack.config.ts that no stack loader reads. The precedents cited (marketplace/PackageDependency:optional, kernel/PackageDependencyResolutionResult:resolvedIn) are the same class. Pinned: ALL_CONVERSIONS holds no entry touching the family.
  5. Nothing at boot reads the retired keys — right; one runtime reader to route. Readers of objectstack.manifest.json at the head: os package publish, os deploy (no category read), create-objectstack, the scenario exporter, check:template-manifests, sync-template-versions.mjs — none at boot. No runtime package imports the six schemas except the CLI. One runtime, non-boot reader of a category wire field exists in this repo and is not disclosed: packages/cloud-connection/src/marketplace-proxy-plugin.ts:437,466-480 filters the public R2 list snapshot by ?category= against r.category. It parses no spec schema and no in-repo caller sends the parameter (MarketplacePage.tsx:407 calls listMarketplacePackages({ limit: 100 }) and filters client-side), so no boot-time tolerance is owed and nothing breaks at this head; but when cloud#3661 drops category from the snapshot the filter goes silently dead and the proxy needs the facet parameters. Should-fix (disclosure and routing): name it on cloud#3661's R2/Worker item or on console marketplace: facet filters (business function, industry, solution type) and a Beta badge replace the single category chip row — reads the spec taxonomy (objectstack#30904) #31011; no change to this PR.
  6. CLI refusal before any network call — right. run() reads the artifact and the manifest from disk, then refuses at publish.ts:655-668 (flags.category, then m.category, then readPublishTaxonomy); resolveCloudCredential (local config) is at :731, the first network call resolveInstallTarget at :752 and the publish fetch at :1066. The message names the facets for the held value through LEGACY_CATEGORY_MAPPING (mapped → the functions and type; manual → the type and "yours to choose" with the 11 functions; unlistable → the reason; unknown → the 11 functions; starter → "never classified your package"). Pinned in package-publish-taxonomy.test.ts with fetch stubbed to count calls.
  7. Cross-check — correct and not over-strict. explainSolutionTypeContradiction: template needs unmanaged in resolveInstallModes(artifact.manifest) (ADR-0131 D6; absent → ['managed']), connector needs at least one entry in artifact.connectors, app or absent asks nothing. A package permitting both modes passes as a template; a connector that also ships objects passes; nothing forces a function onto a connector (the ruling leaves it to the author). The CLI reads the modes through ManifestSchema.shape.installModes.safeParse, so an unreadable value counts as the default rather than crashing (pinned). The three connector listing tests run the same rule against stack.manifest / stack.connectors.
  8. Blank template declares no facet — right. create-objectstack copies objectstack.manifest.json into every scaffolded project (rewrite-identity.ts), so a facet there would classify apps it does not describe; solutionType: template would be refused by the cross-check, since the blank config declares no installModes (→ ['managed']). §3.3 is labelled 方案/供参考, not a ruled decision. The consequence — a project scaffolded from blank is refused at marketplace submission until its author declares a function — is the ruling's own "never a catch-all". (The same file still ships isStarter: true, copied the same way; recorded dead in the ledger, out of this card's scope.)

② Semver level

  • A narrowing at minor is what this spec line requires, not merely allows: AGENTS.md Post-Task Checklist 3 ("a kind takes at least minor on a package the diff moves"; (narrowing) is BREAKING and carries the banner and the ADR-0087 disposition), ADR-0087's 2026-09-13 level amendment ([Decision] 两条裁决援引同一个 launch-window convention,却给出相反的 changeset 等级(minor vs major)—— 退役一个可写键到底发哪一级? #18003: pre-GA a metadata-facing retirement ships minor; an npm major is a planned act, never a side effect of one retirement), and scripts/check-changeset-no-major.mjs (refuses major in the launch window). Check Changeset is green on this head.
  • Metadata-protocol: narrowed is right: a tombstoned authorable key is narrowed; the new keys are added; several ⇒ narrowed over added. Clause-②: yes (narrowing) is the matching arm. The changeset grades @objectstack/spec and @objectstack/cli minor and create-objectstack patch (it moves a template file, no protocol surface) — consistent with the gate's level axis.
  • The guidance dates the removal "@objectstack/spec 18": the npm and protocol majors coincide at 18.x here (spec at 18.3.0; the retirement ships in the next minor), and the spelling matches the other 18 tombstones at the head, so ADR-0087's "names the npm release, never the protocol major" is not violated.

③ Boundary flags

  • (a) Release order (「等 cloud 上线再发 CLI(推荐)」) — purely the maintainer's release hold; nothing in this PR needs to carry it. The ruling's step 1 says landing is not held; the hold sits on the chore: version packages PR. The changeset is the CHANGELOG an upgrading agent greps and should describe the contract, which it does; a deployment sequence written into it would be stale the day the hold lifts. The handbook already tells an author that a declared facet "is accepted and not yet shown" until each side adopts. No change.
  • (b) hotcrm's category: crm — disclosed, routed by the ruling, not yet filed. The PR body ("Not touched: HotCRM's manifest (hotcrm lane)") and both os-dev reports ("carrier: hotcrm lane · noted, not filed") disclose it; ruling §6 item 8 assigns it to the hotcrm lane. No hotcrm card exists. Escalate to the seat (should-fix, outside this PR): file the hotcrm card before the CLI release the maintainer holds — the hold is the window. Not verified at hotcrm (not an input of this review).
  • (c) template_manifest keys recorded dead, not retired — right for this PR; the worklist is owed a card. The ledger's discipline (spec-property-retirement §2) is that a dead row with verifiedAt and a note IS the ADR-0049 enforce-or-remove worklist; enrolling a new type measures its whole shape, and retiring seven unrelated keys (name, specVersion, skills, scaffold + 2 children, visibility, publisher, isStarter) in a taxonomy PR would be scope creep. Spec property liveness is green. Escalate (should-fix, outside this PR): the seat files one ADR-0049 card for the seven non-category dead rows; the dev's "carrier: none · noted, not filed" leaves them with no owner.
  • (d) cloud#3661 carry-overs found while judging ① (should-fix, carried there): (i) a value outside LEGACY_CATEGORY_MAPPING reads undefined — treat as manual + original value as tag, never skip; (ii) the integrations rows' "read the manifest's original category first" is a migration step, not a table row; (iii) MarketplaceListing.businessFunctions is required, so the catalog must say what it serves for an already-approved row whose functions are still manual after the dry-run (complete it or withhold it from listings) — the cloud body's "Expected" does not yet name this.

④ Design judgment

  • Duplicates / out-of-order. Duplicates are refused on every schema by the shared distinct refinement (spec, the CLI through CreatePackageRequestSchema.shape, scenario cards through BusinessFunctionsSchema), pinned in three suites. Order is the author's statement — the first value is the primary one by definition, and the gallery exporter reads [0] — so "out of order" is not a schema-detectable state; the handbook and the module doc say which comes first. One honest gap, recorded by the PR itself: distinct is a .refine() and does not project to JSON Schema (eight new dropped-refinements.baseline.json rows), so an author validating against the published $schema learns of a duplicate only at parse. A uniqueItems arm in shared/refinement-projection.ts would close it; the baseline makes that a public-contract decision, so it is a follow-up, not this PR. Should-fix (follow-up).
  • A connector re-tag that drops its function. Not reachable through the contract: solutionType and businessFunctions are independent keys, the mapping writes manual (no function) for every integrations spelling instead of inventing one, UpdatePackageRequest carries no default so an omitted key leaves the stored functions alone, and the three connector manifests keep theirs ([finance], [it], [collaboration, legal]). The place a function was lost before — cloud's publish-connector-apps.mjs rewriting category — is cloud#3661 item 5, as the ruling assigns.
  • Search facet semantics. The request declares array filters per facet (businessFunctions, industries, solutionTypes) and a scalar maturity, and states OR-inside / AND-across in the schema doc and the module header — the same sentence as ruling §2 总则. The spec can state it, not apply it; the catalog (cloud#3661 item 4) and the console (console marketplace: facet filters (business function, industry, solution type) and a Beta badge replace the single category chip row — reads the spec taxonomy (objectstack#30904) #31011) are named as the appliers. Consistent; no finding.
  • i18n keyed from .options. MarketplaceBusinessFunctionSchema.options resolves through the lazySchema proxy (the CLI already lists the functions from it, tested). Keys are the kebab-case values (marketplace.businessFunction.customer-service), which i18next accepts; console marketplace: facet filters (business function, industry, solution type) and a Beta badge replace the single category chip row — reads the spec taxonomy (objectstack#30904) #31011's parity test iterating .options is the right guard, and maturity needs only beta. No finding.
  • A future enum value. Appending a value is Metadata-protocol: added ⇒ Clause-②: yes (widening) at minor (AGENTS.md); renaming or removing one is an ADR-0087 narrowing. The module header states append-only, demand-pull (a real listed package or a ruled card) and "the console's labels in the same change" — and since Type Check UI / Test UI compile ui/ against the PR's spec, a value without its nine labels reds its own PR once console marketplace: facet filters (business function, industry, solution type) and a Beta badge replace the single category chip row — reads the spec taxonomy (objectstack#30904) #31011's parity test exists. Right.

Findings, by weight: must-fix: none. Should-fix: ①5 route the marketplace-proxy-plugin.ts ?category= filter; ③(b) file the hotcrm card; ③(c) file the ADR-0049 card for the seven dead rows; ③(d) three cloud#3661 carry-overs; ④ the uniqueItems projection follow-up. None changes this diff.

Implemented-by: claude/issue-30904-marketplace-taxonomy
Reviewed-by: session_01Q4EjjRhUZjdy8VXs6Kkur2

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:cloud#3 · hotlong · session_01Q4EjjRhUZjdy8VXs6Kkur2

席位核对 · ACCEPT · 2026-11-06T18:44Z

Head-sha: 95b6e070942530efdf3f947b7f90e8d089fc9ea7
Implemented-by: os-dev (session session_01Q4EjjRhUZjdy8VXs6Kkur2, branch claude/issue-30904-marketplace-taxonomy) · Reviewed-by: repo:cloud#3

  • 形态: 市场分类在 spec 里只定义一次(taxonomy.zod.ts):

    • 职能 11 个,取 1–3 个;
    • 行业 10 个,取 0–3 个,没有兜底值;
    • 类型:app / template / connector;
    • 成熟度:beta / stable。

    另外:

    • 旧值映射表 LEGACY_CATEGORY_MAPPING,没把握的标「需人工」;
    • 6 处 category 按 ADR-0087 退役,两个旧分类 schema 删除;
    • os package publish 在发任何网络请求之前就拒收 category,并在提示里给出对应的新维度;
    • 场景库改用职能;三个连接器重新打标;空白模板不声明维度。
  • 契约复核: PASS 6692597068(head 95b6e070)。Clause-②: yes (narrowing) + Metadata-protocol: narrowed,spec 和 cli 用 minor,复核确认这是 AGENTS.md 和 ADR-0087 正式发布前的退役规则要求的级别。没有 must-fix。should-fix 的去向:

  • 门禁: dispatch-gates 119 个推导出的检查里 116 个 exit 0,3 个因缺前置构建没测;spec、cli、create-objectstack、scenarios、连接器的全量测试全部通过;10 组消融各自变红。check-runs 58 个 success、9 个跳过、0 个失败。

  • 发布顺序(维护者裁决「等 cloud 上线再发 CLI(推荐)」): 本 PR 照常落地。cloud pin 随后跟进并派发 cloud#3661;cloud#3661 部署上线之前,请维护者暂不合并 chore: version packages。

  • 落地条款: 非受治理面,复核 PASS 且 CI 绿 ⇒ 席位翻 ready、挂 auto-merge,经合并队列落地。

Merged via the queue into main with commit 468e0c1 Nov 6, 2026
68 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30904-marketplace-taxonomy branch November 6, 2026 19:17
os-tesla pushed a commit that referenced this pull request Nov 6, 2026
…lding, not only name

rewriteProjectIdentity deleted only `name`, so a template authored before
spec 18 would still hand a new project `specVersion`, `skills`, `scaffold`,
`visibility` or `category`, which the schema refuses (and publish refuses
`visibility` and `category`). One list, RETIRED_TEMPLATE_MANIFEST_KEYS in
rewrite-identity.ts, now drives the strip; a test pins it equal to the
spec's RETIRED_KEYS_BY_MAJOR rows for TemplateManifest (which is how it
picked up `category`, retired in #30980), proves a pre-18 manifest parses
after the strip, and pins the call site.

check:template-manifests also walks packages/apps, so the three connector
manifests are parsed by the gate too.

Claude-Session: https://claude.ai/code/session_01L9qibz5AiZzgut2WFyXVqW
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant