Skip to content

feat(spec, cli)!: bundled is the one plugin packaging strategy — manifest-deps is retired, and os plugin build refuses a native addon (#27123, slice 3 of 3, server half) - #27679

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-c3umeu
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-c3umeu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Part of #27123 (slice 3)

Ruling-ref: 6467932167
Clause-②: no (narrowing) — PluginPackagingSchema drops manifest-deps, so every door that parses a plugin manifest now refuses a value it accepted, and os plugin build refuses a native .node addon it used to pack (or fail on, unnamed)
Metadata-protocol: narrowed
Skills-docs: none — no skill under skills/** and no docs page outside the generated references teaches manifest-deps or a native addon. content/docs/protocol/kernel/plugin-spec.mdx lists packaging among the optional manifest keys (still true) and its .osplugin format list already names no package.json; the five generated reference pages are regenerated by gen:docs

维护者速读

  • 改了什么: 插件清单的 packaging 只剩 bundled 一个值。manifest-deps 改为退役值:所有解析插件清单的入口(os plugin build 读的 objectstack.plugin.json、defineStack 的 manifest、制品的 packages[].manifest、POST /packages 安装请求体)都按名拒绝它并给出迁移说明,TypeScript 也不再接受。os plugin build 删掉了 manifest-deps 的两个分叉(依赖外置、打包 package.json 和锁文件),并在写制品之前拒绝原生 .node 插件:入口导入的 .node 文件、带 node-gyp 构建或自带 .node 的依赖包、assets/ 下的 .node 文件,逐个点名,不留制品。配套 D2 转换、D3 条目、liveness 行(改为 planned)、平台清单条目和参考页。
  • 为什么改: 这是 ADR-0025 M1 — local T0 install loop for first-party, self-signed plugins: os plugin install (signature + integrity, materialize, record), boot-time loader with the tier dispatch point, disable/uninstall, Stripe sample as acceptance #27123 裁决 B′ 第 3 片的 server 半(ADR-0025 v2 §3.3、Q6)。manifest-deps 构建出的制品把依赖留在包外,但从来没有任何环节去安装它们,插件加载必然缺依赖;原生插件既不能打进 bundle,也不是每个宿主都能运行。
  • 风险与回滚: 写了 packaging: 'manifest-deps' 的清单会被拒绝;仓库内没有任何来源这样写(树级缺席扫描为零)。stack 源码可用 os migrate meta --from 17 --write 自动删掉这个键,objectstack.plugin.json 需手工删。带原生插件的插件不能再构建。已声明 Clause-②: no (narrowing)、Metadata-protocol: narrowed,changeset 带 BREAKING 横幅和 ADR-0087 registered 处置,spec 为 minor、cli 为 patch。回滚 = revert 本 PR。
  • 先看: packages/cli/src/commands/plugin/build.ts 的 findNativeAddons 和拒绝块;packages/spec/src/kernel/manifest.zod.ts 的 PACKAGING_RETIRED / PluginPackagingSchema;packages/spec/src/conversions/registry.ts 的 manifestPackagingManifestDepsRemoved;packages/spec/liveness/manifest.json 的 packaging 行。
  • 两分钟验证: 运行 pnpm --filter @objectstack/cli exec vitest run --project unit test/plugin-build-bundled-only.test.ts,预期 9/9 通过(它从 spec 的 dist/ 读 ManifestSchema,先 pnpm --filter @objectstack/spec build);再运行 pnpm --filter @objectstack/spec exec vitest run --project repo src/kernel/manifest-packaging-retirement.test.ts,预期 15/15 通过。
  • 需要你做的: 现在无需操作。按分诊 6498767357 的跨域例外,批准前要先完成契约审查(pm-dispatch(contract-review): a seat sends a PR to the maintainer for approval only after a contract PASS on the current head, and a PR that landed still owing one is re-reviewed after landing, with each finding filed as a new card #27345),由席位安排;审查通过后由席位落地。

What changes

@objectstack/spec

  • PluginPackagingSchema is enumWithRetiredValues(['bundled'], PACKAGING_RETIRED). manifest-deps is refused at packaging with its prescription (removed in 18.3.0; delete the key; os migrate meta --from 17 for a stack source; an objectstack.plugin.json is edited by hand) at every door that parses a manifest: ManifestSchema, a stack's manifest through defineStack (STACK_SCHEMA_INVALID, path manifest.packaging), ArtifactPackageEntrySchema (packages[].manifest), and the install body (PackageInstallManifestSchema extends ManifestSchema). The value is out of the input type, so authoring it is a compile error.
  • ADR-0087 D2 conversion manifest-packaging-manifest-deps-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', order 80): deletes packaging: 'manifest-deps' from a stack's manifest and from each packages[].manifest, one notice each (to: '(removed)'), copy-on-write, idempotent, with its fixture.
  • D3 entry manifest-packaging-manifest-deps-retired naming the conversion, and its STEP18_RATIONALE fragment (order 114) in registry.ts.template.
  • retired-key-removal-version.pin.test.ts gains the conversion's JOIN row.
  • Liveness: the packaging row is planned on the one-member precedent (sharing_rule.type, connector.authentication): nothing dispatches on the value any more, and the refusal is real, so neither live nor dead. The note keeps the history of the 2026-08-28 live grade, which rested on the two forks this PR deletes, and the flip condition. The manifest state-count shard is regenerated (gen:liveness-counts: live 29 → 28, planned 0 → 1).
  • vitest.repo-tests.json lists the new pin: its tree-scoped absence walk reads outside the package, over globs already declared in scripts/cross-package-test-inputs.mjs and turbo.json.

@objectstack/cli

  • os plugin build: the esbuild call externalizes @objectstack/* only; the packaging === 'manifest-deps' externalize fork and the package.json / pnpm-lock.yaml packing fork are gone. The Loaded line still echoes packaging (default bundled).
  • Native-addon refusal, before the integrity map and before anything is written. An esbuild plugin loads every .node module as empty so the graph completes, and the metafile is read for three signals: a .node module the bundle reaches (named with its importers); a bundled package (the directory after the last node_modules in an input path) that has binding.gyp, gypfile: true or a shipped .node file; and a .node file staged from assets/. Every hit is listed under ✗ Native addon refused: …, exit 1, no .osplugin.
  • osplugin.ts: the archive-layout docblock no longer lists the manifest-deps rows.

Ledgers and generated pages

  • Platform checklist cli.plugin-manifest-build-contract, revision 4: the fork clause becomes the bundled-only clause (with the manifest-deps refusal), a native-addon clause and probes 6f–6h are added, and steps, fixtures, knownGaps, negatives and sources follow. The integrity and trust-tier negatives are untouched and were not re-verified here.
  • gen:docs regenerated content/docs/references/{kernel/manifest, kernel/package-registry, api/package-api, api/package-api-assembled, api/protocol}.mdx: Enum<'bundled'> and the new describe text. No other generated artifact moved (check:generated names only these).

Why

ADR-0025 v2 §3.3 (maintainer decision Q6, ruling B′) makes bundled the only packaging strategy and disallows native addons, with M1 owing the build refusal. manifest-deps produced an artifact that kept its dependencies outside the bundle and carried package.json for an install step that nothing ever ran (os plugin install and the boot-time loader import dist/index.mjs as packed), so such a plugin could not load its dependencies.

Tests

Red first on df778ab4 (tests only, through the real doors), then green on the fix.

  • packages/cli/test/plugin-build-bundled-only.test.ts (new, 9 cases) drives PluginBuild.run([dir]) over real on-disk projects with their own node_modules. Every refusal asserts exit 1, the subject named, and no artifact on disk.
    • bundled and unset: the dependency is inlined, the archive is exactly SIGNATURE, dist/index.mjs and the manifest, with no package.json or lockfile and no surviving import.
    • packaging: 'manifest-deps': refused under objectstack.plugin.json is invalid, the ✗ line names packaging.
    • A .node file the entry imports (file and importer named); a dependency whose main is addon.node; a bundled dependency with binding.gyp, with gypfile: true, and with prebuilds/…/fake.node, each locating its binary at run time (the package is named, the pure dependency beside it is not); assets/bin/addon.node (named; assets/icon.svg is not).
  • packages/spec/src/kernel/manifest-packaging-retirement.test.ts (new, 15 cases, repo project): the vocabulary is exactly bundled; each door refuses with the prescription (ManifestSchema, ArtifactPackageEntrySchema, defineStack's 422 envelope at manifest.packaging), with a control that a near miss (bundle) keeps zod's own message; bundled parses and absence stays absent; @ts-expect-error at the authoring site; the D2 conversion (strips both places, leaves bundled, notices, copy-on-write, idempotent, not replayed by normalizeStackInput, wired into MIGRATIONS_BY_MAJOR[18] with retiredAfter: '18.2.0'); the D3 entry names it; and a tree-scoped absence walk over packages, examples, skills, content, scripts (anti-vacuity: more than 1000 files visited, more than 3 packaging: 'bundled' mentions seen), with each structural exclusion's reason stated.
  • retired-key-removal-version.pin.test.ts: the JOIN row for the new conversion.

Acceptance notes

  • Disposition registered. The changeset carries <!-- adr-0087: registered manifest-packaging-manifest-deps-removed, manifest-packaging-manifest-deps-retired -->. The narrowing has a mechanical migration for a stack source, so not-required would contradict the BREAKING FROM→TO. The conversion, the D3 entry, the STEP18 fragment, the pin row and the checklist edit were added under the seat's claim increment 6499637007, and the vitest.repo-tests.json line under increment 6503048893.
  • Levels. @objectstack/spec is minor (Metadata-protocol: narrowed must grade at least one package minor); @objectstack/cli is patch with the BREAKING banner, the slice-2 precedent for a non-protocol narrowing.
  • Liveness planned, not live or dead. Argued in the row's note: no fork reads the value, the refusal is enforcement, and the ADR keeps the key with one live value. The liveness README's manifest notes cell names no packaging set, so it was left alone (it is outside this PR's claim).
  • The native-addon refusal is conservative. A bundled package that ships a .node file it never loads on the plugin's code path is still refused; the remedy in the message (replace or drop it) applies.
  • os migrate meta and objectstack.plugin.json. The D2 conversion runs over stack sources. An objectstack.plugin.json is not a source os migrate meta reads, so the prescription tells the author to delete the key there by hand.
  • Branch is the card thread's harness-pinned claude/project-thread-c3umeu, which claim 6499525158 names.

Known limitations

  • An addon loaded at run time from an absolute or computed path outside every bundled package is invisible to the build. The checklist item records it as a knownGap.
  • ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts still types cloud listing data as 'bundled' | 'manifest-deps', and docs/design/plugin-distribution.md and ADR-0025 mention manifest-deps. All three are outside this slice (the ADR edit is the Tier H half), and the absence walk does not reach ui/, docs/ or .tsx.

Verification

Head b0bfccdd = fix c349e372 + one merge of origin/main (b3b4af13, merge base efa2915e) + the roster line. Heavy runs under scripts/pm/os-verify-lock.sh (slot issue-27123, NODE_OPTIONS=--max-old-space-size=4096).

Red, then green
  • Red on df778ab4 (fix reverted in the tree, spec dist/ from before the fix): spec pin Tests 8 failed | 7 passed (15) (expected [ 'bundled', 'manifest-deps' ] to deeply equal [ 'bundled' ], each door accepting the value, the conversion and the D3 entry absent); cli pin Tests 7 failed | 2 passed (9) (manifest-deps built an artifact; the .node import failed as a bare Bundle failed; the run-time-located and assets/ addons built an artifact). The two bundled/unset controls were green on df778ab4 too.
  • Green: spec pin 15 passed (15) (repo project, final head); cli pin 9 passed (9) after the spec rebuild.
Suites and typecheck
  • @objectstack/spec --project local, full, on the fix: 751 files passed; the 24 failures were all in scripts/liveness/check-liveness.test.ts, the stale manifest count shard. After gen:liveness-counts: check:liveness exit 0 and scripts/liveness/ 12 passed (12), 359 passed (359).
  • On the merged head: spec src/kernel/manifest-packaging-retirement.test.ts, src/shared/, src/conversions/, src/migrations/, scripts/conversions-major18-merge.test.ts, scripts/step18-rationale-merge.test.ts, scripts/liveness/: 81 passed (81), 2321 passed (2321). cli plugin-build-bundled-only, osplugin, plugin-sign, plugin-publish: 25 passed (25).
  • @objectstack/cli --project unit, full: 350 passed (350), 5063 passed (5063). --project integration on serve-installed-plugins, plugin/install and schema-migrate.one-shot-family: 153 passed (153). (A whole-package run of both projects exceeded the 30-minute background limit and was stopped, so it is not counted.)
  • typecheck of spec and cli, each with check:test-typecheck: exit 0.
  • @objectstack/spec test:repo on the final head (the new pin now runs here): 87 passed (87), 1448 passed (1448).
Ablations (scripts/ablation-replace.mjs, restored blob == HEAD)
  • A, the native-addon refusal (if (nativeAddons.length > 0) { → if (false && …), build.ts on c349e372): mutation landed (anchor 1 → 0) → cli pin 6 failed | 3 passed (9), exactly the six addon cases → restored, blob f5cb8ec5 == HEAD, git diff HEAD empty.
  • B, the narrowing (enumWithRetiredValues(['bundled'], PACKAGING_RETIRED) → enumWithRetiredValues(['bundled', 'manifest-deps'], { 'ablation-27123-marker': … })): spec pins 6 failed | 68 passed (74) (the vocabulary, the three doors, the authoring-site case and the removal-version JOIN row); spec rebuilt, ablation-dist-preflight.mjs @objectstack/spec ablation-27123-marker → marker present in 2 built files, exit 0; cli pin 1 failed | 8 passed (9), exactly the manifest-deps case → restored, blob ed95910f == HEAD, git diff HEAD empty; after the rebuild, --absent → marker absent from all 580 built files, tree clean, exit 0.
Derived gates and the --ran reconciliation
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 111 commands, the same list on b3b4af13 and on the final head; each run with its exit captured before any pipe.
  • Three were not green at first, and each was rerun green on the final head: check:cross-package-test-inputs exit 1 (the new pin was missing from vitest.repo-tests.json; fixed in b0bfccdd under increment 6503048893), check-plugin-teardown-shape --self-test exit 3 (its pinned fixture commit was not in the shallow clone; fetched), check:dual-build-cjs-loads exit 3 (six packages unbuilt; built). check-changeset-no-major, check-adr-0087-registration and check-empty-changeset were also rerun on the final head.
  • --ran: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.
  • check-adr-0087-registration --base origin/main: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed] registered manifest-packaging-manifest-deps-removed, manifest-packaging-manifest-deps-retired".
  • check-changeset-no-major --base origin/main --event with this body as the pull_request payload: no major; LEVEL AXIS ✓ ("declares clause-② no (narrowing) and metadata-protocol narrowed, and it grades a package whose published source … it moves at minor or above": @objectstack/spec: minor; @objectstack/cli: patch not refused), exit 0.
Dogfood, lint, checklist, merge-tree

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

…on refusal (red)

Claude-Session: https://claude.ai/code/session_01L6bhgWLCWkh3MaLj1qTLCe
Co-authored-by: Claude <noreply@anthropic.com>
… plugin build` refuses a native addon

ADR-0025 v2 §3.3 (ruling B′, slice 3, server half):

- spec: PluginPackagingSchema narrows to `bundled`; `manifest-deps` is a
  retired value refused at every manifest door with its prescription. D2
  conversion manifest-packaging-manifest-deps-removed strips it from a
  stack's manifest and each packages[].manifest; D3 entry
  manifest-packaging-manifest-deps-retired and its STEP18 fragment carry it
  to the upgrade guide. The removal-version pin gains its JOIN row. The
  `packaging` liveness row is `planned` (one-member enum, no fork to read);
  the manifest state-count shard is regenerated.
- cli: os plugin build drops both manifest-deps forks (externalize + pack
  package.json / lockfile) and refuses a native addon before writing the
  artifact: a `.node` module in the graph, a bundled package that declares
  a node-gyp build or ships a `.node` file, a `.node` file under assets/.
- The archive layout docblock, the platform checklist item
  cli.plugin-manifest-build-contract (revision 4) and the generated
  reference pages follow.

Claude-Session: https://claude.ai/code/session_01L6bhgWLCWkh3MaLj1qTLCe
Co-authored-by: Claude <noreply@anthropic.com>
Its tree-scoped absence walk reads outside the package, so it runs under
`@objectstack/spec#test:repo`, whose hash moves with the globs it walks
(already declared in cross-package-test-inputs and turbo.json).

Claude-Session: https://claude.ai/code/session_01L6bhgWLCWkh3MaLj1qTLCe
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 25 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/cli/src/utils/osplugin.ts, packages/spec/liveness/manifest.json, packages/spec/liveness/state-counts/manifest.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e16a7af2e0fe3ab02d610733f76d68325b730e4f.

⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/cli/src/utils/osplugin.ts, packages/spec/liveness/manifest.json, packages/spec/liveness/state-counts/manifest.md, …) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 176 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 e16a7af2e0fe3ab02d610733f76d68325b730e4f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 90e591f4893601fb57f29506683f6d3cb15af57c — the merge of head b0bfccdd74d06f63a04a92bfc6350d26c8e42218 into base e16a7af2e0fe3ab02d610733f76d68325b730e4f, 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 90e591f4893601fb57f29506683f6d3cb15af57c && git checkout 90e591f4893601fb57f29506683f6d3cb15af57c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e16a7af2e0fe3ab02d610733f76d68325b730e4f b0bfccdd74d06f63a04a92bfc6350d26c8e42218 && git checkout -B drift-repro e16a7af2e0fe3ab02d610733f76d68325b730e4f && git merge --no-ff b0bfccdd74d06f63a04a92bfc6350d26c8e42218

node scripts/docs-audit/affected-docs.mjs --json e16a7af2e0fe3ab02d610733f76d68325b730e4f

⚠️ 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 e16a7af2e0fe3ab02d610733f76d68325b730e4f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head b0bfccd · 2026-10-29T17:30Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b0bfccdd74d06f63a04a92bfc6350d26c8e42218
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/kernel/manifest.zod.ts, packages/spec/src/migrations/entries/semantic/18.manifest-packaging-manifest-deps-retired.ts, packages/spec/src/migrations/registry.ts.template + packages/spec/src/kernel/manifest-packaging-retirement.test.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/vitest.repo-tests.json, packages/spec/liveness/manifest.json, packages/spec/liveness/state-counts/manifest.md, packages/cli/src/commands/plugin/build.ts, packages/cli/src/utils/osplugin.ts, packages/cli/test/plugin-build-bundled-only.test.ts, docs/qa/platform-checklist/areas/cli.json, .changeset/27123-plugin-packaging-bundled-only.md, content/docs/references/{api/package-api-assembled, api/package-api, api/protocol, kernel/manifest, kernel/package-registry}.mdx (generated; read against the describe text); cited and read, not reviewed: packages/spec/src/shared/retired-key.ts, packages/spec/src/api/package-api.zod.ts, packages/spec/src/kernel/package-registry.zod.ts, packages/spec/src/stack.zod.ts, packages/spec/scripts/build-migration-registry.ts, packages/cli/src/commands/plugin/install.ts, packages/cli/src/commands/plugin/publish.ts, packages/runtime/src/installed-plugin-loader.ts, packages/spec/liveness/README.md, packages/spec/liveness/connector.json, docs/adr/0025-plugin-package-distribution.md, docs/design/plugin-distribution.md, ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts, scripts/pm/check-governed-merges.mjs, scripts/check-changeset-no-major.mjs, scripts/cross-package-test-inputs.mjs, docs/qa/platform-checklist/README.md, .claude/skills/spec-property-retirement/SKILL.md.

Inputs: card #27123 body and all 21 comments (triage 6463804227 and 6498767357, ruling 6467932167, claim 6499525158 with amendments 6499637007 and 6503048893, the slice-1 and slice-2 records and hand-over 6496079064, os-dev-report 6503774579); PR #27679 body and file list (19 files, +929/−77); the net diff origin/main...pr27679 (merge base efa2915e); the check-runs on the head, read in this act (2026-10-29T17:46Z). Reviewed adversarially: the tree, never the body's account of it.

File surface. Sixteen of the nineteen files sit inside the claim plus its two amendments by name. Three do not, in letter: packages/cli/test/plugin-build-bundled-only.test.ts (the build door's own pin — the claim names build.ts and the amendment names only a spec pin), .changeset/27123-plugin-packaging-bundled-only.md (the changeset the Post-Task Checklist mandates; the slice-1 and slice-2 claims declared changesets explicitly, this one did not), and packages/spec/liveness/state-counts/manifest.md (the generated twin of the declared packaging row). Each is the declared work's own test, mandatory input or generated shard, so this is a declaration gap, not a scope breach — should-fix (record): the seat posts a third claim amendment naming the three.

Tier. GOVERNED_SURFACES (13 rows) read against the file list: no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md, .github/workflows/** or any ui/ governed row. docs/qa/platform-checklist/** and packages/spec/liveness/** are not register rows. Changed lines 1006, under 3000. The tier is the schema and narrowing face alone. Draft, no auto-merge, Governed Surface Queue Guard success.

Check-runs at this reading. 32 success — among them Lint & Repo Gates, Build Core, Governed Surface Queue Guard, Check Changeset, Spec property liveness, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Test UI, Build & E2E UI, Live E2E UI, Test Core (4/8), (5/8), (8/8), Dogfood Regression Gate, Check PR Size. 7 still in progress — Test Core (1/8), (2/8), (3/8), (6/8), (7/8), Type Check UI, Type Check · workspace (the lane that carries check:api-surface, check:authorable-surface, check:docs, check:spec-changes, check:upgrade-guide). 9 skipped (UI shards, opt-in harnesses, Lint UI with no ui/ path). 0 failed. A PASS here is one of the two conditions; the aggregate TypeScript Type Check and Test Core conclusions are the other, and they are not in yet.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. PluginPackagingSchema accept set {bundled, manifest-deps} → {bundled}; manifest-deps answers invalid_value at packaging with the prescription (enumWithRetiredValues's error map fires on the exact string only; a near miss keeps zod's message, pinned). Right. .options is ['bundled'], so JSON Schema and the five reference pages render the one-member enum (spelled Enum of 'bundled' on every row) — the regenerated pages match the two new .describe() strings word for word (schema-level text on the PluginPackaging section, field-level text on every packaging row of the six carriers). Right.
  2. PluginPackaging (z.input) loses the literal; authoring it is a compile error, pinned with @ts-expect-error in a file the repo project compiles. Right. api-surface/, authorable-surface/, json-schema.manifest/ and api-surface-signatures/ are byte-identical to main — a value retirement moves none of them (kernel/Manifest:packaging and api/PackageInstallManifest:packaging record the key, not its members), and PACKAGING_RETIRED is unexported. Right.
  3. Every carrier of ManifestSchema inherits the refusal: ManifestSchema itself (os plugin build, the ObjectQL registry), defineStack's manifest (STACK_SCHEMA_INVALID 422 at manifest.packaging, pinned), ArtifactPackageEntrySchema.manifest (pinned), PackageInstallManifestSchema (an .extend() of ManifestSchema, package-api.zod.ts:229) and through it the install body, the upgrade body, the resolve-dependencies body and InstalledPackageSchema.manifest, and marketplace-install-local-plugin.ts in cloud-connection. Right, and the PR's door list is accurate — it names these and claims no more.
  4. os plugin install and loadInstalledPlugins are not ManifestSchema doors: both JSON.parse the compiled manifest and read id/version (install.ts, loader step 4), a slice-1/slice-2 design the earlier records passed. A pre-retirement manifest-deps artifact therefore installs and is refused at boot as import_failed on its externalized imports — named, non-fatal, but not the prescription. Not wrong for this PR (no regression, nothing silent, and ADR-0025 §3.3 records zero consumers of the strategy), should-fix as a follow-up: parse the compiled manifest through ManifestSchema at os plugin install, which also catches every other compiled-manifest drift.
  5. D2 manifest-packaging-manifest-deps-removed (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', order 80 in MAJOR_18_CONVERSIONS): strips the exact string from manifest and each packages[].manifest, one notice each, copy-on-write, idempotent; not replayed by normalizeStackInput (author meets the refusal), replayed with includeRetired (stored artifacts land clean). Reach identical to the sibling manifest-permissions-string-list-removed (also manifest + packages[], no devPlugins[]). retiredAfter equals the current stable label 18.2.0; the prescription's "removed in @objectstack/spec 18.3.0" is what the removal-version pin derives (PROTOCOL_MAJOR, next release off main) and what the thirteen sibling prescriptions on this label already say; --from 17 is toMajor − 1. Right.
  6. D3 manifest-packaging-manifest-deps-retired in entries/semantic/18.*.ts (filename ↔ id, as loadEntries requires; surface without backticks; conversionIds names the D2) and the STEP18_RATIONALE fragment (order 114, unique). Precedent value retirements (page-kind-jsx, storage-scope-public, manifest-permissions-string-list) carry exactly this kit — no retired-keys entry, since no key leaves. Right.
  7. os plugin build: externals are @objectstack/* only; the artifact never carries package.json or pnpm-lock.yaml; Loaded still echoes packaging (default bundled). Right against ADR-0025 v2 §3.3 ("Install = unpack + verify; no npm/toolchain at install time").
  8. os plugin build refuses a native addon before computeIntegrity and before any write — esbuild runs with write: false and metafile: true, so nothing lands on disk before the refusal. Three signals: a .node module in the graph (named with importers), a bundled package with binding.gyp / gypfile: true / a shipped .node (nested node_modules and symlinks skipped — a nested package the bundle reaches is its own metafile input and gets its own check), a .node under assets/. Right, conservative by design: a package that ships an optional prebuilt beside a JS fallback is refused too, which the ADR's "disallowed by rule" covers and the PR discloses. The known gap (an addon located by a computed or absolute path outside every bundled package) is recorded in cli.json. Scope: card item 4 names the refusal; claim 6499525158 names it in build.ts's surface. In scope.
  9. Liveness packaging live → planned (shard 28 live / 1 planned, regenerated). The row's field shape (status, verifiedAt, evidence, evidenceScope, note; no authorWarn) is the shape of the cited precedent connector.authentication (planned: the one accepted value does nothing, every other value loudly refused), and the README's sharing_rule.type prose records the same one-member rule. dead would tell authors to remove a key the ADR keeps; live would claim a fork that no longer exists. Right.
  10. Changeset: spec minor, cli patch, one adr-0087: registered marker naming both ids, Clause-②: no (narrowing), Metadata-protocol: narrowed, BREAKING with before / now / fix, ! in the title. Right (② below).

② Semver level

  • Clause-②: no (narrowing) — correct arm: an accepted authorable value is refused, which the Post-Task Checklist defines as narrowed and BREAKING; the PR body and the changeset carry both lines line-initial, and the triage-required Skills-docs: line is present and true (no skill or hand-written page teaches the value; plugin-spec.mdx lists the key, which stays).
  • Level: check-changeset-no-major's rule is "added / narrowed / semantic → at least one moved package minor+" (ruling A). @objectstack/spec is the package whose authorable surface moved and is minor — right. @objectstack/cli's change is behavioural, not a metadata-protocol change, so patch with the BREAKING banner is the repo's rule for a non-protocol narrowing (the slice-2 precedent). Check Changeset is green on this head. The changeset publishes what the diff publishes.
  • Disposition registered — right: a BREAKING FROM → TO with a mechanical edit for the stack-source case cannot be not-required, and both ids resolve in the tree (the D2 in ALL_CONVERSIONS / MIGRATIONS_BY_MAJOR[18], the D3 file under entries/semantic/).

③ Boundary flags

os-dev-report 6503774579 open_questions: none. Every dev flag, answered:

  • cli.json: the integrity and runtime negatives are false since slices 1–2, left untouched in revision 4 (dev: ledger drift, noted). Judged should-fix, not must-fix and not out of scope. The checklist README's lifecycle rule for a change is "edit the fields, bump revision, append history"; it does not require a revision to re-verify the whole item, the scored clauses stay true, and the history row discloses the gap honestly. But the two lines are now wrong in the direction that makes a runner under-score a guarantee os plugin install and the boot loader do deliver (install.ts step 3 re-verifies integrity at unpack; the loader's dispatchPluginRuntime loads node and refuses sandbox / worker by name), and both cite issue numbers (manifest.integrity declares per-file artifact digests the spec says the runtime re-verifies at unpack — nothing computes them and nothing checks them #11331, [text correction · ruled B] manifest.runtime tombstone/describe reads truthfully: publish-gate enforced (cloud), load-side NOT enforced #11330) that do not resolve in this repository. Since the PR already revises this item, the cheapest correct act is to rewrite the two negatives under the same revision 4 (keep the still-true half: a printed Integrity entries count or runtime: line is a log line, not the check) and extend the history sentence; a follow-up card is acceptable if this PR does not respin.
  • ui marketplaceApi.ts, docs/design/plugin-distribution.md, ADR-0025 still mention manifest-deps. None rides this PR: ui/ is another lane's tree and the mention is a comment on a string-typed field for cloud listing data (packaging?: string, no type to break); docs/design/plugin-distribution.md is a hand-written design note outside the claim (five lines describe the strategy as current: 114–115, 122, 144, 168, 195); ADR-0025 is Tier H. Follow-ups listed under ④.
  • README manifest cell says Dead 19 while the shard says 13; names no packaging set (dev: pre-existing, outside the claim). The count drift is pre-existing and generated counts are the authority, but the retirement skill's surface list asks for the per-type README row with every ledger change, and this PR gives manifest its first planned row without a sentence in that cell. Should-fix (prose; claim amendment or follow-up).
  • PR body Known limitations — runtime-path addon invisible to the build: accepted; recorded as a knownGap on the checklist item and in the changeset text. Acceptance notes — disposition, levels, planned, conservative refusal, os migrate meta scope (the prescription says the stack-manifest case and tells an objectstack.plugin.json author to edit by hand — honest), branch claude/project-thread-c3umeu matches claim 6499525158: each checked above, each holds.
  • Cross-domain exception 6498767357: the one PR carries both halves, the claim declares the spec files, and the contract review it required is this record.

④ Design judgment

Each in-scope file against the code it cites and the scenario it is accepted against:

  • packages/spec/src/kernel/manifest.zod.ts — enumWithRetiredValues(['bundled'], PACKAGING_RETIRED) is the kit's own wrapper (thirteen precedent sites; it throws at module load if the retired member were still listed or the prescription empty). The prescription states what replaced the member, the removing release and the one-line fix, carries no tracker number, and distinguishes the stack-source case (os migrate meta --from 17) from objectstack.plugin.json (by hand). PACKAGING_RETIRED is declared with // as the precedents do. The field keeps .optional() with no default, so an absent key stays absent (pinned). Holds. No finding.
  • packages/spec/src/conversions/registry.ts — the entry mirrors manifestPermissionsStringListRemoved in shape and reach; dispatches on the retired string, never on the key, so bundled rides through (fixture + pin); expectedNotices: 2 equals the stripped values. Order 80 does not collide with a live order. Holds. No finding.
  • 18.manifest-packaging-manifest-deps-retired.ts + registry.ts.template — field conventions identical to the precedent entry; the rationale fragment keeps the sorted-by-key, one-key-each shape the merge test pins. Holds. No finding.
  • manifest-packaging-retirement.test.ts — drives the real doors (ManifestSchema, ArtifactPackageEntrySchema, defineStack), pins the issue code, path and the prescription's first sentence, the control for a misspelling, the @ts-expect-error, the D2 behaviour on both carriers, idempotence, non-replay on the authoring funnel, the chain wiring and the D3 link. The absence walk is tree-scoped over the five roots the spec package already declares (packages/**, examples/**, skills/**, content/**, scripts/** held by the precedent witnesses in cross-package-test-inputs.mjs), judges the authoring shape only, states each exclusion's reason, and carries anti-vacuity floors. Holds. No finding.
  • retired-key-removal-version.pin.test.ts, vitest.repo-tests.json — the JOIN row is a refusal row on the enum itself (the right shape for a value retirement), and the repo-project listing is what check:cross-package-test-inputs demanded (green on this head). Holds. No finding.
  • packages/spec/liveness/manifest.json, state-counts/manifest.md — grade and evidence re-read against the tree: PluginPackagingSchema has one member and build.ts:230 reads the key only to echo it. The note keeps the history of the earlier live grade and states the flip condition. Holds. Should-fix: the README manifest cell (③).
  • packages/cli/src/commands/plugin/build.ts — flow re-read end to end: manifest parse → entry → esbuild (write: false, absWorkingDir: cwd, metafile) → findNativeAddons → refuse or computeIntegrity → pack. The .node loader plugin keeps the graph whole so every addon is named at once. packageRootOf takes the last node_modules segment (scoped names width 2), so pnpm store realpaths answer the right package and a nested package the bundle reaches is judged on its own directory. Tried to break it: (a) a dependency that merely ships an optional prebuilt (bufferutil reached through ws) is refused — a conservative true positive under the ADR, disclosed; (b) nested node_modules — covered when reached, invisible when not, which is the recorded gap; (c) a project whose dependencies resolve above its own node_modules (../ inputs with no node_modules segment) escapes the package scan but not the .node-module scan — edge case, within the recorded gap. Should-fix (diagnostic, follow-up acceptable): a package-level hit names the package and its directory but not the importer chain, so for a transitive hit the author cannot see which direct dependency pulled it in; the .node-module branch already names importers, the package branch should too. Holds otherwise.
  • packages/cli/src/utils/osplugin.ts — docblock now matches what build.ts packs. Holds. No finding.
  • packages/cli/test/plugin-build-bundled-only.test.ts — drives PluginBuild.run over on-disk projects with their own node_modules; every refusal asserts exit 1, the subject named and the artifact absent; the package cases assert the pure sibling is not named. Holds. No finding.
  • docs/qa/platform-checklist/areas/cli.json — revision 4: the fork clause becomes the bundled-only clause with the refusal half, a native-addon clause with probes 6f–6h scored on both halves, steps, fixtures, knownGaps, negative and sources re-pointed; the history row names what it did not re-verify. Holds for the scored clauses; should-fix the two stale ⛔ negatives (③).
  • .changeset/27123-plugin-packaging-bundled-only.md — levels, both declaration lines, one disposition marker, BREAKING with before / now / fix, no model identifier. Holds. No finding.
  • content/docs/references/** — five pages, each hunk equals the new describe strings and the one-member enum rendering; nothing hand-edited. Holds.

Follow-ups for the seat to file (none blocks this head):

  1. Tier H docs follow-up on ADR-0025, joining the four edits already owed: line 159's // bundled | manifest-deps comment; lines 185–186 "the build does not yet refuse them mechanically (M1 adds the refusal)" — now false; line 191 "the build-side half is removed in M1" — done; line 267 "For manifest-deps, run …" in the install steps; lines 595–596 (risk "mitigated by the opt-in manifest-deps strategy").
  2. docs/design/plugin-distribution.md lines 114–115, 122, 144, 168, 195 — docs-only PR in this lane.
  3. ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts:171 comment — the ui lane, one line, with the next ui PR.
  4. os plugin install: parse the compiled manifest through ManifestSchema so a stale artifact meets the prescription instead of import_failed at boot (①.4).
  5. build.ts package-level refusal lines name the importer chain (④).
  6. If this PR does not respin: cli.json negatives (③) and the liveness README manifest cell (③); and a claim amendment naming the three undeclared files (File surface, above).

Implemented-by: claude/project-thread-c3umeu
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

席位意见: ACCEPT。收窄覆盖每个 ManifestSchema 入口,处置套件与先例一致;审查的 should-fix 均为散文或诊断,不 respin,全部转卡。

席位核对

ACCEPT · head b0bfccdd74d06f63a04a92bfc6350d26c8e42218 · 2026-10-29T17:53Z

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant