Repository navigation
fix(spec,cli): os migrate meta lists and writes load-path conversions inside composed package bodies - #22393
Conversation
… skipped defineStack gains one internal parameter, a Symbol.for key read off its options and honoured only with strict: false, that skips the load-time ADR-0087 D2 conversion pass. Neither declared on DefineStackOptions nor exported. The CLI's authored-source composeStacks wrap now produces every wrapped input again from what the author wrote (refused and recorded, or accepted and kept) in that mode, so composition's own rule assembles each package body from the authored source and the chain lists and --write writes a conversion the load still applies inside a package body. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
… migrate meta half Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…own-limit bullets it falsifies Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 145 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 08ebfd94f93c96c37ae63cbdc985ee10fe572931 && git checkout 08ebfd94f93c96c37ae63cbdc985ee10fe572931
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e030d436bf0a3244502e021fe53035fc6b7b28da 9abe8861b07522100017ce37e255a466f25f1f8d && git checkout -B drift-repro e030d436bf0a3244502e021fe53035fc6b7b28da && git merge --no-ff 9abe8861b07522100017ce37e255a466f25f1f8d
node scripts/docs-audit/affected-docs.mjs --json e030d436bf0a3244502e021fe53035fc6b7b28da
|
…t still said always The stack-provenance header now names the one non-strict call that skips the D2 pass, and the pending composed-project changeset says a refused input is produced again with the load-time conversions skipped. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
|
Generated by Claude Code |
Contract reviewServed-tier: Record written 2026-10-09T02:56Z by the isolated contract reviewer for PR #22393 (card #22256). Inputs, and nothing else: the card body with its 13 comments (rulings and earlier records included), the PR body, its 9-file list, the net diff against Check-runs on this head, read at 2026-10-09T02:49Z: 35, all ① Derived judgmentsEach accept-set or public-surface change the diff implies, with my verdict on it.
Not implied by the diff: no REST route, no schema key, no retired key, no ADR-governed decision reversed ( ② Semver level
③ Boundary flagsEvery dev flag and
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22256
Clause-②: no (a seam off the public entry: an internal
Symbol.for-keyed parameter, undeclared onDefineStackOptionsand not exported; the seat's answer A in its review on #22256)The composed half of #22256 (the one-package half landed in PR #22351). Inside a
composeStacks([…], { manifest: 'preserve' })project,os migrate metanow lists a conversion the load already applies inside a package body (for exampledatasources[].driver: 'mongo'),--writewrites it into the file that authored that input, and the next load converts nothing. This holds whether the input'sdefineStackcall accepted its argument or was refused and produced again instrict: falsemode.The seam (H2), and why it is off the public entry
composeStacksassembles eachpackages[i].manifestbody from the stack its input's producer RETURNED, anddefineStackruns the load-time ADR-0087 D2 conversion pass in both modes. No existing switch skips that pass for adefineStackcall (no option, env or global;applyConversionshas none either), so apackages/specchange is required.Candidates measured:
exportsDefineStackOptionstypeSymbol.forkey read offdefineStack's options, honoured only withstrict: false, that skips the D2 passcomposeStacksoption assembling bodies from per-input authored sourcesDefineStackOptionsmember (aconvert: falsetwin ofNormalizeStackInputOptions.convert)name (kind)only)Clause-②: yespreservePackageEntries/assemblePackageBody, or acomposeAuthoredStacks)Clause-②: yescomposeStacks([])returns with authored contentpackages/clidomain:cliseat's cutC1 is the conversion-free twin of the
strict: falsere-produce the shim already performed for refused inputs (PR #22326), so the semantic delta is exactly "the D2 pass is skipped". A strict call ignores the key, so it can never let an old spelling reach the strict parse unconverted; the output is marked as every non-strict output is. The key is documented besidedefineStack(AUTHORED_INPUT_OPTION,packages/spec/src/stack.zod.ts) and is neither declared onDefineStackOptionsnor exported.check:api-surfaceandcheck:export-originsstay green with no regenerated artifact.The consumer (H3)
packages/cli/src/utils/config.ts, the authored-source shim'scomposeStackswrap: every input whosedefineStackcall the shim saw is produced again from what the author wrote, through the realdefineStackinstrict: falsemode with the key. A refused input is its recorded hand-through (as before); an accepted one is the argument PR #22351 keeps underAUTHORED_ARGUMENT_KEY, followed to the innermost literal. Inputs the shim never saw adefineStackcall build (a nested composition, a plain object) reach the realcomposeStacksuntouched, soSTACK_PROVENANCE_MISSINGfor an unwrapped input is unchanged. Composition then runs its own rule over the authored inputs. No composition logic is copied intopackages/cli.meta.tsand the--writeplanner are unchanged in code (comment only): the planner already tracespackages[i].manifest.KEYto input i's literal (PR #22326). Measured: the H4 sitepackages[0].manifest.datasources[0].driveris written intosrc/service.stack.ts, the input's own module, and nothing else moves.Measurements
H1 on
origin/main16096e8d7(BASE), built CLI, composed fixture (composeStacks([ServiceStack, AppStack], { manifest: 'preserve' }), the service body carryingdriver: 'mongo'):appliedwrite.filesos validatereload[][]'mongo'converted at load … datasource-driver-mongo-to-mongodb'10:00Z'), re-producedstrict: falsetime-default-utc-suffix-droppedsrc/service.stack.ts1 site'mongo'converted at load[][]After the fix (same fixtures,
--from 16):appliedwrite.filesconverted at loadlines on reloaddatasource-driver-mongo-to-mongodb @ packages[0].manifest.datasources[0].driversrc/service.stack.ts1 siteapplied []packages[0].manifest…src/service.stack.ts2 sitesapplied [][][][]Corpus control, BASE vs fix (both trees built, the four example apps,
app-multi-packagebeing the composed one;--from 16and--from 17;--jsondry and--writeon same-depth copies;durationremoved): 16 of 16 documents byte-identical by md5, the written-file sets identical, and the written bytes identical (app-showcasesrc/automation/flows/index.ts2aec23aad9d3,app-todosrc/flows/task.flow.ts4ffb6d9aa6ce, in both trees).The
--outsnapshot of a composed project moves, as ruling A (REWORK6068295985) already accepted for one-package stacks: onapp-multi-packageat--from 17, 53 differences, every one a key the schema's parse fills in that exists only on BASE (externalId,hidden,multiple,readonly,searchable,sortable,unique,required,scope,defaultDatasource,datasource,isSystem,priority,deleteBehavior,active,isDefault,expanded). No value changed and nothing was added. The changeset states it.Pins
packages/spec/src/stack-authored-input.test.ts(3): the key keeps the authored spelling, the output is marked, and no conversion is recorded, while the control without it converts; a strict call ignores it;composeStacksover such inputs assembles the body from the authored spelling, and over plain non-strict inputs from the converted one.packages/cli/test/migrate-meta-composed-load-conversions.test.ts(5,unittier, in-process): the H4 accepted case listed, written into the input's file and nothing else, a clean re-run, and a strict reload with an emptystackConversions; the authored-source load's body carries'mongo'while every other load's carries'mongodb'; an input defined twice is listed once; the refused input re-produced by the shim lists and writes both conversions; the canonical control writes nothing and its--jsonsummary equals the one the built composition gives.os migrate metaruns on acomposeStacksproject and migrates its package bodies #22326's 8 composed pins and PR fix(cli):os migrate metalists and writes a conversion the load already applies, on a stack the schema accepts #22351's 5 one-package pins stay green beside them.Ablations
Both committed first, mutated through
scripts/ablation-replace.mjs(anchor hit once, mutation proven on disk, restore proven: blob equal to HEAD andgit diff HEADempty, whole-treegit status --porcelainempty).@objectstack/specthrough itsexports): deletedconvert: strict || !asksForAuthoredInput(options),frombuildDefinedStack. Presence reading on the pristine build: the marker in 4 built files. After the spec rebuild,ablation-dist-preflight --absent: absent from all 232 built files, tree reading "mutate leg". Predicted spec 2 red / 1 green and the new CLI file 4 red / 1 green (control green), the 13 neighbouring pins green; got exactly that (Tests 2 failed | 1 passed (3);Tests 4 failed | 14 passed (18)). Restore leg: rebuild, marker present in 4 built files, tree clean,3 passed (3)and18 passed (18).src): made__composablehand an accepted input through as built. Predicted 3 red (the three accepted-input pins) / 15 green; gotTests 3 failed | 15 passed (18).Deliberate correction of two pending changesets
This change makes the "Known limit" bullet of two pending, unreleased changesets false, and all three ship in the same
@objectstack/clirelease:.changeset/22289-migrate-meta-composed-project.md(PR #22326) and.changeset/22256-migrate-meta-load-path-conversions.md(PR #22351). Each loses exactly that one bullet; nothing else in either file moves.check-empty-changeset's foreign-changeset rule is therefore red by design, in its DELIBERATE CORRECTION class (precedent PR #21683 and PR #22351); it asks for the correction to be confirmed on the PR, not restored.Tests and gates
All at
a0d0a6253(this branch merged withorigin/main117d34de3), after rebuilding the CLI closure:pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2(the whole unit tier):Test Files 271 passed (271),Tests 3973 passed (3973). The integration tier is declared to CI: the diff touches no integration-tier file and no spawn entry.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2(the package'stestscript):Test Files 627 passed (627),Tests 18745 passed | 1 todo (18746).pnpm --filter @objectstack/spec --filter @objectstack/cli run typecheck: exit 0. Bothcheck:test-typecheckledgers hold unchanged (spec 52 files / 246 errors / 135 signatures; cli 3 / 28 / 6), so both new test files compile clean..tsfiles, all inside eslint's linted set (no "file ignored" notice in the JSON output). Count from--format json: 5 files, 0 errors, 0 warnings. Invariance:eslint.config.mjsenables no type-aware linting (noparserOptions.project, noprojectService), so this diff cannot move the verdict of any untouched file. The fullpnpm lintis CI's.pnpm --filter @objectstack/spec check:generated: all 15 generated artifacts up to date, tree clean.node scripts/pm/dispatch-gates.mjs --commands(no paths) ata0d0a6253derives 90 commands from the change set (8 paths against merge base117d34de3). All 90 ran with their exit codes recorded, and--rananswers90 derived famil(ies) accounted for — 90 run, 0 NOT-MEASURED.check:api-surface("public API surface + factory signatures unchanged"),check:export-origins("5363 exports across 19 entry points resolve exactly as recorded"),check:dual-source-exports,check:exported-any,check:docs,check:nul-bytes,check:cross-package-test-inputs,check-changeset-no-majorandcheck-adr-0087-registration.check-empty-changeset --base origin/main, the foreign-changeset rule on the two corrected changesets above.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: 8 packages withoutdist/). After those 8 were built it exited 0, and that is the code recorded.Acceptance notes
stack-provenance.tsmodule header described the conversion as applied in both modes. The text-only round9abe8861b(seat review6072731732) made both exact. The bullet now says a refused input "is produced again bydefineStack(input, { strict: false })with the load-time conversions skipped ([finding] cli(migrate meta):--writenever writes a conversion the authored-source load already applied (e.g.driver: 'mongo') — the chain sees a pre-converted stack and reportsapplied: []#22256) before it is handed tocomposeStacks", and the header names the one exception.composeStacks([x])underos migrate metanow hands the chainxproduced again as authored (normalised, bound actions merged) instead of the raw argument. Only the--outsnapshot of such a project can differ, and only when it has bound standalone actions. This edge's--outnow differs from a one-package stack's snapshot shape, which keeps bound actions unmerged (contract review6073322979, finding 6). Carrier: thedomain:cliseat, at the next change to--out.composeStacks([])is one instance of that.content/docsdescribes the composed limit (searched), and noskills/**text states it.Generated by Claude Code