feat(spec)!: plugins / devPlugins are artifact envelope keys — excluded from the assembled package body, refused inside packages[] (#15219) - #15289
Conversation
…uded from the assembled package body Maintainer ruling A for both keys (2026-09-04): the two keys are runtime assembly instructions, not metadata. They stay `concat` for in-memory composition and at the artifact top level; `assembledPackageBodyShape()` and the `AssembledPackageBodyKey` derivation now exclude them through one `ASSEMBLED_PACKAGE_BODY_ENVELOPE_KEYS` constant beside `packages`, so a body carrying either is refused at the manifest's strict close. Pins, an ADR-0087 semantic entry under protocol 18, and a BREAKING minor changeset ride along. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
…ugins-envelope-keys
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3347fdc06ce9c226c22d8392bb3b7c4c6a7d9763 && git checkout 3347fdc06ce9c226c22d8392bb3b7c4c6a7d9763
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1226f2e2ec1a46b26ab8dc0abe138f87cdfbfafa 7497291853d1d01b26ad641b0588f672e2a68c0a && git checkout -B drift-repro 1226f2e2ec1a46b26ab8dc0abe138f87cdfbfafa && git merge --no-ff 7497291853d1d01b26ad641b0588f672e2a68c0a
node scripts/docs-audit/affected-docs.mjs --json 1226f2e2ec1a46b26ab8dc0abe138f87cdfbfafa
|
…ugins-envelope-keys
…ceptance pin and zoo prose Patch round on the seat's ruling: the literal envelope list at the pin's shape assertion gains `devPlugins` and `plugins` (nine keys, in the file's own sort order), and the zoo header/const prose that said "seven" says "nine". Mechanical restatement of the count where it is printed; nothing else in either file moves. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
|
Provenance (seat landing stroke, 2026-09-04T11:33Z): Clause ② PASS · ACCEPT recorded on #15219 (comment 5539806437) by the Generated by Claude Code |
Fixes #15219
needs:contract-review(Clause ② yes — the published artifact format's accept set narrows:packages[i].manifest.plugins/.devPluginsgo from accepted to refused). Draft; thedomain:specseat reviews at tier and flips ready. Head pinned in the measurements table:749729185(patch round on top of mergeb6ada92c1oforigin/main; earlier headsd5ff0b3e6/703acb0af; worktree base460134af). Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXkPatch round (seat ruling, option A on the report's open question 1)
The claim surface was extended by exactly two mechanical edits in
packages/cli/test/:option-b-reader-acceptance.pin.test.ts:188— the literal envelope list gainsdevPluginsandplugins(nine keys, in the file's own sort order);fixtures/option-b-collection-zoo.ts:26and:67— the prose "seven" artifact-envelope keys becomes "nine". Nothing else in either file moves. The cli pin is now 6/6 (was 1 failed / 5 passed).Ruling executed (not re-decidable)
Maintainer ruling A for both keys (director relay 5537162356, 2026-09-04, verbatim 「同意」):
pluginsanddevPluginsare artifact envelope keys — top level only, never insidepackages[]; the emitter half never touches them. Operative sentences, quoted from 5537162356:plugins/devPluginsatconcatfor in-memory composition (live stacks still concatenate their plugins to the top level) but exclude both from the assembled package body — an explicit envelope exclusion beside the existingpackagesskip inassembledPackageBodyShape(), with the collections-shape docblock's envelope list updated to name them and say why (runtime assembly instructions, not metadata)"assembled-package-body.test.tsasserts the two keys are absent from the body key set and that a package body carryingpluginsis refused (strict object); theAssembledPackageBodyKeyderivation and the#1521037-key list move to 35 — restate the count where it is printed"devPluginson any artifact path (expect none; if one exists, stop and report — that is the fallback the maintainer was offered)"minorwith a BREAKING banner per the launch-window convention, stating the migration: an existing multi-package artifact that carriespackages[i].manifest.plugins(ifos buildever wrote one — not directly measured) is refused on load after this change and must be rebuilt"serve.ts/schema-migration-plugins.ts(they stay top-level readers, now correct by construction); the#15004probe row forpluginsbelongs to the emitter half; A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512'sBlocked-by: #15219clears when this card's PR merges."Options B (readers resolve JSON descriptions into live plugins) and C (emitter carve-out) are not in this diff: no resolver, no emitter special case, no CLI source edit.
What changed
packages/spec/src/stack.zod.tsASSEMBLED_PACKAGE_BODY_ENVELOPE_KEYS = ['packages', 'plugins', 'devPlugins'], declaredas const satisfies readonly StackDefinitionKey[], now feeds both halves: theAssembledPackageBodyKeytype excludes its member union, andassembledPackageBodyShape()skips its members in the loop where thepackagesskip used to be a bare string compare. This is the "explicit envelope exclusion beside the existingpackagesskip" of ruling (1), written so the static and runtime halves cannot name different sets (the file's own derive-never-transcribe principle).packages: ADR-0130 D1;plugins/devPlugins: runtime assembly instructions a host hands tokernel.use()/ theos devload list — inert JSON inside a body that no loader could construct). The collections-shape docblock gains the same sentence, since the two keys live in that shape (they compose byconcat) while being envelope keys for the body.plugins(:718) anddevPlugins(:781) declarations unchanged; both disposition rows stayconcat;assemblePackageBody()needed no edit — it iterates the derived shape, somanifest: 'preserve'stops folding the two keys into bodies by derivation.packages/spec/src/assembled-package-body.test.ts—ARTIFACT_ENVELOPE_KEYSgains the two keys; a new#15219block pins: both keys absent from the body key set while still on the stack schema; a body carryingpluginsrefused withunrecognized_keysat path[]keys['plugins']; same fordevPlugins; throughArtifactPackageSchemathe refusal sits at path['manifest'](theartifact-packages.tsseam); and a two-stackmanifest: 'preserve'composition keepsplugins/devPluginsconcatenated at the top level in stack order, puts neither into anypackages[i].manifest, and still parses throughObjectStackDefinitionSchema.packages/spec/src/compose-stacks-manifest-preserve.test.ts/compose-stacks-key-loss.test.ts— read-and-report: neither enumerates the body key set (:273names thepackagesdisposition only; the key-loss structural pin walks the stack schema's shape, not the body's). Untouched, both green.packages/spec/src/migrations/entries/semantic/18.assembled-package-body-plugins-envelope.ts+ regeneratedregistry.ts(151 semantic after merging #15227's entry) — ADR-0087 D3 semantic entry. Notretired-keys: nothing is retired — both keys stay declared on the stack schema at the top level, andretired-keysids aredef:keytombstones for keys that left a schema. Not D2: no lossless target —pluginsmay hold live instances that never survived JSON, and hoisting a body-level entry to the envelope changes who loads it..changeset/assembled-package-body-plugins-envelope.md—@objectstack/specminor, BREAKING banner, FROM → TO block, the migration sentence of ruling (4) verbatim in substance, and theadr-0087: registered assembled-package-body-plugins-envelopedisposition marker (an HTML comment, as the gate requires).packages/cli/test/option-b-reader-acceptance.pin.test.ts,packages/cli/test/fixtures/option-b-collection-zoo.ts— the patch round above (the printed envelope count restated, per ruling 2).Not touched:
packages/cli/src/commands/serve.ts,packages/cli/src/utils/schema-migration-plugins.ts,packages/core/src/artifact-packages.ts.Premise re-verified at the worktree base
460134af(and again at head)stack.zod.ts:718plugins: z.array(z.unknown());:781devPlugins: z.array(z.union([ManifestSchema, z.string()]));:930/:933bothconcat;:961-976the body docblock;:977-980theExcludederivation over'packages'only;:999-1006assembledPackageBodyShape()withif (key === 'packages') continue;;assembled-package-body.test.ts:64-72the seven-keyARTIFACT_ENVELOPE_KEYS,:125the set-difference pin,:190-201theunrecognized_keyspin. Behaviour at base, measured by the reverse run below (the mutated tree is the base behaviour): a wrapper carryingmanifest.pluginsparses green, the body acceptsdevPlugins, package-owned keys = 37, envelope = 7. Premise holds.Ruling (3): readers of
devPluginson any artifact path — nonegit grep -n devPlugins HEAD -- packages ':!packages/spec'(identical at460134af,d5ff0b3e6,749729185): 23 hits in 4 files —packages/cli/README.md:196(the--devflag text);packages/cli/src/commands/serve.ts:792, 2625-2627(the live config under--dev:if (flags.dev && config.devPlugins) plugins = [...plugins, ...config.devPlugins]— top level of the in-memory config);packages/cli/src/utils/format.ts:492, 530, 1040-1131(a stats counter overconfig.devPlugins, top level);packages/cli/test/print-metadata-stats-zero-row.test.ts(that counter's test, 10 hits). A second, broader grep forpackages[…].devPlugins,manifest.devPluginsorartifact … devPluginsacross all ofpackages/: 4 hits, all four being this PR's own migration-entry prose (18.assembled-package-body-plugins-envelope.tsand its regenerated copy inregistry.ts); 0 outsidepackages/spec. No reader resolvesdevPluginsfrompackages[]or from an artifact, so the fallback offered to the maintainer is not triggered.Ruling (2): the count — 35, printed here because no code or doc site prints it
No
37appears instack.zod.ts, the two spec test files, or the docs for this surface; the 37-key list lives in #15210's body. The one printed site of the complement — the cli pin's seven-key envelope list — is restated to nine by the patch round. Measured with atsxscript oversrc/stack.zod.tsthat sortsObject.keys(schema.shape)forObjectStackDefinitionSchemaandAssembledPackageBodySchemaand intersects them (OS_EAGER_SCHEMAS=1 pnpm --filter @objectstack/spec exec tsx ...; specsrcis byte-identical betweend5ff0b3e6and749729185, build input hash2fa354834bab37c2):The refusal text a body-level
pluginsnow gets, through the wrapper (pathmanifest): "Unrecognized key(s) on this package manifest:plugins. Until this surface was closed, an unknown key insidemanifest:parsed green and its value was silently dropped …" — the manifest's strict-close message, inherited through.extend().Reverse verification (on the committed tree, restore proven by blob hash)
Mutation: the runtime exclusion only — the
includes(key)line inassembledPackageBodyShape()replaced by the oldif (key === 'packages') continue;, type untouched. On disk: injected marker count 1, removed line count 0, mutated blobb0cb9bcb…differs from HEAD blobf600508f…. The test file resolves./stack.zodfromsrc, so nodistis on this path and no rebuild is needed for either leg. Observed: 6 tests red — the #14242 set-difference pin (lacks exactly the artifact-envelope keys) plus all five #15219 pins; the count returned to 37 and the envelope to 7; both refusals flipped tosuccess= true. Restore viagit checkout HEAD --on the absolute path, under an EXIT/INT/TERM trap: restored blobf600508fcd481af2d4b53e055eb063a12a7794d8==HEAD:packages/spec/src/stack.zod.ts,git diff HEAD --statempty,git status --porcelainempty, marker count 0. Direction observed: red (as expected), not a reversal. (stack.zod.tsis unchanged since; the same blob is at749729185.)Consumers (direction: DOWNSTREAM of
@objectstack/spec; closure built)Built:
@objectstack/specwith declarations, then each consumer's own dependency closure through turbo —'@objectstack/metadata...'+'@objectstack/objectql...'(15 tasks) and'@objectstack/cli...'(56 tasks) — withOS_SKIP_DTS=1(listed inturbo.jsonglobalEnv, so the cache key differs from a full build). ⚠ Measured:--filter='!@objectstack/spec'does not stop turbo 2.10.10 from executing@objectstack/spec:buildas a^builddependency, so a closure run underOS_SKIP_DTS=1replaces spec'sdistwith a declarations-less build; everydist-reading gate below was run after a freshpnpm --filter @objectstack/spec build(34/34 declarations) that followed the closure runs. Tests reachingresolveArtifactPackageOrder/ArtifactPackageSchema(no test file inpackages/coreimportsartifact-packages.tsby path; its readers are the three below):@objectstack/metadatasrc/plugin-artifact-packages-attribution.test.tsd5ff0b3e6)@objectstack/objectqlsrc/artifact-load-path.test.tsd5ff0b3e6)@objectstack/clitest/option-b-reader-acceptance.pin.test.ts(+ its zoo importerfixtures/option-b-reader-probe.ts)749729185(1 failed / 5 passed atd5ff0b3e6, before the patch round)The metadata and objectql rows were not re-run at
749729185: the patch-round diff touches only the two cli test files, and specsrcis byte-identical between the two heads (build input hash2fa354834bab37c2).Mechanism assumptions — which held
ManifestSchema's strict close carried through.extend(); removing a key fromassembledPackageBodyShape()is exactly what makes the body refuse it (under the mutation both refusals became accepts). Pins assert issue code + path + keys.Pickreturn type turns a derived key missing from the collections shape into a compile error, but it cannot see a runtime-only mismatch — the shape is cast to that type. What went red in reverse was the pin (6 tests), nottypecheck(not re-measured in the mutated state; the cast hides it by construction). Closed structurally instead: both halves now read one constant.registeredaccepted bycheck-adr-0087-registration("new here");gen:migration-registry+check:migration-registrygreen; feat(spec)!: ExecutionStepLog.iteration is single-valued (the enclosing loop iteration); the parallel branch index moves to a new optionalbranchkey (#14414) #15227 merged and regenerated (the text merge was already the byte-identical union).check:generated --fixregenerated nothing before and after the merges (all 15 artifacts current, clean second run) — the body is structurally annotated and sits insidepackages[], so no authorable-surface shard, docs shard,spec-changes.jsonor upgrade-guide line changed.artifact-packages.tsneeds no edit and refuses atmanifest.plugins; the cli pin transcribed the seven keys and went red until the seat extended the claim.pluginsconcatenation intact (pinned).check:livenessandcheck:strictness-ledgergreen; no newz.object; both keys still on the stack schema.Measurements at head
749729185(exit captured before any pipe; verdict lines are the gates' own)Rows marked † were measured at
d5ff0b3e6; between the two heads the tree differs only by main's mergeb6ada92c1(no specsrcchange: same build input hash2fa354834bab37c2) and the two cli test files, which none of the † gates read.pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/option-b-reader-acceptance.pin.test.tspnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/assembled-package-body.test.ts src/compose-stacks-key-loss.test.ts src/compose-stacks-manifest-preserve.test.tspnpm --filter @objectstack/spec build(with declarations).build-input-hash2fa354834bab37c2…check:generated(after--fix+ clean second run atd5ff0b3e6, nothing regenerated)check:migration-registrynode scripts/check-adr-0087-registration.mjs --base origin/mainnode scripts/check-changeset-no-major.mjs --base origin/mainmajorbump.check:api-surface(insidecheck:generated)check:browser-reachable-entries/check:dual-source-exports/check:entry-nameability/check:exported-anyany: 2457 types + 1524 schemaspnpm check:nul-bytes--no-inline-configon the two cli filespnpm --filter @objectstack/spec typecheckerror TScheck:authorable-surface/check:docs/check:spec-changes/check:upgrade-guidecheck:liveness/check:strictness-ledgerpnpm check:doc-authoring/node scripts/check-system-context-census.mjspnpm lint(repo-wideeslint . --no-inline-config, full run, not narrowed)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 5 paths vs merge base)pnpm check:dual-build-cjs-loadsdist/) — CI's farmpnpm check:type-check-debt@objectstack/metadata/@objectstack/objectql(table above)Lock discipline: every build/test ran through
scripts/pm/os-verify-lock.sh(slotissue-15219); wall-clock figures are shared-box readings.Related, not in this diff
ManifestSchemaan open object after #14192 closed it —stack.zod.ts,compile.ts, coreartifact-packages.ts, the strictness ledger's own prose, and themaindescribe's loose ADR-0025 citation #14721 (open) already records the stale "NOTstrictObject" paragraph onAssembledPackageBodySchema's docblock (stack.zod.ts), which now sits a few lines under pins asserting exactly that refusal — left for its card.#15004probe row forpluginsbelongs to the emitter half (ruling 5); A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512 remains open here.🤖 Generated with Claude Code
https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
Generated by Claude Code