fix(cli): resolve packages[] at the four config-load boundaries and the gates that key off config.objects - #15228
Conversation
… the gates that key off `config.objects` The CLI holds four independent config-load boundaries, and every read of a package-owned collection behind them was an inline expression against the flattened top level. A stack carrying each definition once under `packages[]` reached those expressions with the key absent, and nothing threw: `os serve` booted with no query engine and no storage driver, its i18n plugin never registered, `os dev`'s recompile inventory went permanently empty, and `os build`'s union author-time rule run judged an empty stack and published green. One seam (`utils/stack-collections.ts`) now answers all of them, in `resolveArtifactPackageOrder`'s dependency order. Each predicate starts from the expression it replaced, so every stack that boots or builds today takes the identical branch — including one declaring an empty `objects: []` — and `packages[]` is consulted only where the old read returned nothing. Being callable is the other half of the point: the #15004 acceptance probe could not reach an expression inside an oclif command body, which is why these four sites sat outside its ledger. The probe now carries a row per site that calls the decision each command makes. No command emits anything different. `OPTION_B_LOSSES` is unchanged at 24 rows: none of them names a cli reader. Part of #15006 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…eaves alone `plugins` / `devPlugins` are package-owned collections that `serve.ts` and `schema-migration-plugins.ts` read off the top level only, and the mechanical repair is wrong there: `packages[i].manifest.plugins` in a JSON artifact is inert data where `kernel.use()` needs a live instance. Whether a live-object collection belongs in the package-owned key set at all is a `packages/spec` question upstream of every reader in this program, so the pin's header points at #15219 instead of at this card's report. Part of #15006 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
…lection key `resolveArtifactPackageOrder` parses every `packages[]` entry whole, and the `os build` union fold asks about all 37 package-owned collections — so the first cut ran that parse 38 times per build, and the `AppPlugin` wrap gate ran it four times per boot. The bodies are resolved once and handed to `collectFrom`, which is the only shape that changed; every answer is identical. Also corrects the probe's note: six rows enter, not four. The two beyond the card's named sites are the reads sitting beside them on the same boundaries — the `AppPlugin` wrap gate, which measured does NOT lose, and the i18n auto-registration gate, which does. Part of #15006 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
📓 Docs Drift CheckThis PR changes 1 package(s): 40 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 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 f8afbeab6b408f9ea843cdc7765b2d87595d167a && git checkout f8afbeab6b408f9ea843cdc7765b2d87595d167a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c1bcda382067e75e2d69f11086d6c986ccb987a dbe2fa58a6093e75fd5767451c502833710df770 && git checkout -B drift-repro 9c1bcda382067e75e2d69f11086d6c986ccb987a && git merge --no-ff dbe2fa58a6093e75fd5767451c502833710df770
node scripts/docs-audit/affected-docs.mjs --json 9c1bcda382067e75e2d69f11086d6c986ccb987a
|
复审 PASS —— 三个 open question 全裁给实施席;另有两处它自己发现的东西,比卡面要求的更进一步PM 席(epic #14122,session 三条硬约束逐条核过(读 diff,不是只看 pin 绿)
两处比卡面要求更进一步的东西,记下来一、空数组的陷阱。 每个谓词先答调用方的原始表达式,只在它 falsy 时才查 二、它补上了探针设计里我没想到的一个洞。 新加的
对。集合相等保护「还在丢的」和「刚修好的」,但一个绿着的行被悄悄移除测量,集合依然相等,pin 不会红。这是我在定探针验收时漏掉的失效模式,它自己发现并用 它也把「⛔ 不得用 Open question 1 —— 零删台账行:裁 A,零是正确答案24 行里没有一行是 cli 读取方,全是 ⛔ 选项 B(删 Open question 2 —— 多做的两处(i18n 门 +
|
Fixes #15006
Reader program 3/4 of the ADR-0130 D4 option-B ruling (#14512 comment 5528589044, maintainer 2026-09-03, batch #23). The artifact stays additive through this card: no command emits anything different,
composeStacksandpackages/spec/src/stack.zod.tsare untouched.What was broken
@objectstack/cliholds four independent config-load boundaries, and every read of a package-owned collection behind them was an inline expression against the FLATTENED top level. A stack that carries each definition once underpackages[]reached those expressions with the key simply absent — and nothing threw.serve.ts:2632if (config.objects && !hasObjectQL)serve.ts:2669if (!hasDriver && config.objects)serve.ts:2976-2995readArtifactObjects()dev.ts:653-666compile.ts:344os buildpublishes greenconfigHasMetadata, twice —serve.ts:2802andos migrate's own secondloadConfigatschema-migration-plugins.ts:1094manifestis an envelope keyNothing between the artifact and the two
serve.tsgates could notice the loss:standalone-stack.ts:785omits theobjectskey entirely when the array is absent (not[]), andmergeBootConfigis a plain spread.The seam
packages/cli/src/utils/stack-collections.ts— one module, one resolution rule, seven exports. Two properties make it safe to land:if (stack.objects) return true;is byte-identical to the old gate;packages[]is consulted only where the old read already returned nothing. So no stack that boots or builds today can take a different branch. That ordering is load-bearing, not stylistic:config.objectsis truthy for an EMPTY array, so a stack declaringobjects: []gets an engine today. Re-expressing the gate asresolve(...).length > 0would have quietly stopped doing that — reintroducing "boots with no query engine" in a different case. Pinned instack-collections.test.ts.@objectstack/cli: the four config-load boundaries and the gates that key offconfig.objects#15006 comment 5530257178). The probe now calls the DECISION each command makes — the whole gate, plugin duck-typing included, because what silently fails is the decision and not the count.Package order comes from
resolveArtifactPackageOrder(@objectstack/core, #14643) — never re-derived. Its ADR-0112 refusal for a malformedpackagesis deliberately not swallowed: it is reachable only once the caller's own expression has come back falsy, i.e. only on the shape that boots silently broken today.The package-owned collection key set the
os buildfold needs is derived —ObjectStackDefinitionSchema∩AssembledPackageBodySchema, the same reconciliationpackages/spec/src/assembled-package-body.test.tsperforms — never hand-listed, so a collection family added to the stack schema next month joins the fold without anyone remembering to. (#14877 would let it read that set as an export instead.)The ledger: 24 rows, unchanged. Zero deleted, zero added.
No
OPTION_B_LOSSESrow was deleted or added. That is the correct outcome and worth stating plainly, because "which rows did you delete" is the usual question for this program:@objectstack/runtimereader (collectBundle*,AppPlugin,createStandaloneStack,resolve-project-database) or a@objectstack/plugin-securityone — cards reader program 2/4 —@objectstack/runtime: every top-level collection read gains apackages[]path #15005 and reader program 4/4 —@objectstack/plugin-security: the app default permission set readsconfig.permissions#15007. The cli's own reads were never in the ledger because the probe could not reach them.serve.tsgates stays ledgered, and stays reader program 2/4 —@objectstack/runtime: every top-level collection read gains apackages[]path #15005's.B1 · createStandaloneStack surfaced objects (CLI tier resolution + engine/driver auto-registration) · objectsis that row, and its label already names these gates. Measured on the probe:createStandaloneStacksurfaces neitherobjectsnorpackagesfor an option-B artifact (its result keys are["api","manifest","plugins"]), so no cli-side seam can resolve what the runtime never surfaced. This PR does not touch that row and does not add a duplicate beside it.Six new rows enter the probe. Five were RED before this change and are GREEN after, so none enters the ledger; the sixth was green in both shapes and stays green.
Reverse verification — the new rows are a measurement, not decoration
Committed first (
38d97fb90), then ablated with a trap-restored script using absolute paths. The mutation removes the option-B leg at one anchor (packageBodiesreturns[]), and was confirmed on disk before the run: injected marker occurrences1, bloba85e9f16…different from the HEAD blob2577e5d5…. No rebuild is in the path — the pin reaches this module by RELATIVE path from inside@objectstack/cli(test/fixturesup to../../src/utils), so vitest transforms the source and nodist/copy participates.Ablated pin: 1 failed | 6 passed. The one failure is THE PIN, naming exactly the five rows this change fixes:
In that SAME run the additive BASELINE, the
packages[]anti-vacuity CONTROL, the shape-derivation test, the phantom-ledger test, the five-boundary test and the new #15006 representation test all passed — which is what makes the red a discrimination rather than a broken fixture. Restore proved by an emptygit diff HEADand a blob hash byte-identical to HEAD.One site deliberately NOT fixed
pluginsanddevPluginsare package-owned collections by the same derivation every row here uses, andserve.ts:2622/schema-migration-plugins.ts:1082read them off the top level only. The mechanical repair is wrong there:packages[i].manifest.pluginsin a JSON artifact is inert data wherekernel.use()needs a live instance, and whether a live-object collection belongs in the package-owned key set at all is apackages/specquestion upstream of every reader in this program. Filed as #15219, named in the pin's header, and left alone here. The zoo declares no plugins, so no probe row covers it — adding one would pin a shape that may be about to change.Docs drift — hand-read, not anchor-listed
The drift bot reports 1 package, 22 documentable anchors, 40 hand-written pages, 5 release-owned pages. Re-derived on this worktree: 45 pages, the same split.
⛔ The 5 release-owned pages (
releases/v12,v13,v14,v16,v17) are untouched and stay untouched. Release notes are written centrally at release time; this PR's input to them is its changeset.The bot's own note says an anchor list cannot reach a page that states a rule by its inputs, because such a page shares no identifier with the emitter. This diff changes exactly that kind of rule — "whether
config.objectsexists decides if the engine and driver auto-register" — so the rule-restating pages were read by hand, searched by the rule's INPUTS across all ofcontent/docsminusreleases/:auto-register/automatically registers,config.objects,top-level objects,I18nServicePlugin+translations,author-time rules,packages[]/multi-package,recompil.plugins/index.mdx:238os devon a minimal config auto-registers ObjectQL, the default datasource, AppPlugin, HonoServer and the REST APIobjects, and the first clause of both gates is byte-identical to the expression it replaceddeployment/cli.mdx:206-208config.pluginsalways wins"data-modeling/drivers.mdx:55resolveDriverTypeis not in this diff; what moved is the gate on whether to build one at all, which no page stateskernel/services-checklist.mdx:428DevPluginauto-wiresI18nServicePluginwhen the stack declares translations@objectstack/plugin-dev's own reader, already filed as a site of #15210getting-started/examples.mdx:352-432packages[]; registration order is topological fromdependencies, never array orderresolveArtifactPackageOrder, the same topological order this page describesdeployment/cli.mdx:456,deployment/validating-metadata.mdx:558,ui/react-pages.mdx:380,getting-started/build-with-claude-code.mdx:266→ Running author-time rules (41)...authoringRulesFor('build').length, untouched, and the fold changes the rules' INPUT, not the table or the step lineprotocol/kernel/plugin-spec.mdx:187,652No page is falsified, and the reason is a property rather than an inspection: this change is strictly additive on every shape the platform emits today. Each predicate's first clause is the expression it replaced, and
authoringRuleUnionStackreturns the stack BY IDENTITY whenever the top level carries its collections — measured on the acceptance fixture and on the realbuild-multi-package-artifact/compile-artifact-packagese2e runs. Behaviour moves only on the option-B shape, which nothing emits yet and which no page documents.Verification
All at
dbe2fa58a(final commit). Gate family re-derived there withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— identical to the earlier derivation: 9 paths, 46 runnable commands — and the union re-run whole on that head.PREREQUISITE NOT METon an earlier pass and are MEASURED here:check:i18n→OK (9 package(s) — all bundles in sync, no undeclared authoring keys);check:i18n-coverage→OK (13 config(s), 691 baselined untranslated string(s), none new);check:dual-build-cjs-loads→OK — 102 published require entry point(s) across 66 package(s) loadcheck:type-check-debt→OK — 16 ledger entr(ies) re-measured in 223.4s, 207 raw tsc error(s) total, none above its recorded number,surplus: none.check:type-check-coverage→OK — 73/79 workspace packages type-checked.check:type-source-resolution→OK — 120 tsc program(s) across 78 packages scanned.check:test-source-alias→OK — 72 packages with tests scannedpnpm --filter @objectstack/cli typecheckcheck:test-typecheckreports3 file(s) / 28 error(s) / 6 pinned signature(s)— byte-identical to the committedtest-typecheck-debt.json, so zero new diagnostics.tsc --noEmit --listFilesconfirms both new files are in the program (1 hit each), so "typecheck is clean" is a statement ABOUT themTest Files 8 passed (8) / Tests 75 passed (75)— the seam's contract,merge-boot-config, the acceptance pin,authoring-rule-command-parity,compile-artifact-packages.e2e,build-multi-package-artifact.e2e,format-zod-union,hook-body-build-reach.e2e. Plus a second targeted run over the units for the edited command files —Test Files 8 passed (8) / Tests 83 passed (83):serve-automation-shadowing,serve-i18n-load-diagnostic,serve-driver-banner,serve-host-config-security-registrar.pin,serve-settings-ordering.pin,database-driver-flag-derivation,dev-default-db,graft-runtime-hookscheck-stack-collection-maps(8 enumerations reconciled against the schema),check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check:swallow-census-controls— all exit 0. Named becausedispatch-gatesscores them "silent" on a roster, which is a fact about a list rather than a clearanceEvery exit code above was captured before any pipe.
Declared narrowing. The whole 224-file
@objectstack/clisuite was started and deliberately terminated at 34 minutes (SUITE_EXIT=130), because it was holding the shared verification lock with a sibling agent queued behind it. Its partial output named two failures —format-zod-union.test.ts(1) andhook-body-build-reach.e2e.test.ts(2) — with file durations of 666s and 747s against 120s per-test timeouts, i.e. timeout signatures rather than assertions. Both files pass in the targeted runs above, so the classification is contention, not this diff. CI runs the suite whole regardless.typecheckreturned TS7016 for@objectstack/client, whosedist/index.d.tswas being rewritten at that instant bycheck:type-check-debt's own build closure running unlocked beside it. The file was present, and re-measured, typecheck is clean. Recorded because it is the same shape as #15042 (a package's.d.ts"vanishing" after a green build) and offers that card a concrete cause: a concurrent rebuild in the same worktree, read through tsup's non-atomic write window.The B2/B3/B4 shared-helper question, measured rather than assumed
The card left open whether the three config loads can collapse into one helper (
serve.tscallsbundleRequire;compile.ts:211andschema-migration-plugins.ts:1083callloadConfig). Measured: the loaders are not the problem and were not touched. All three load an ordinary module and hand its export on unchanged — the collection loss is in the READERS each of them then drives, which is what this PR moved. That refactor stays unmade and out of this card.🤖 Generated with Claude Code
https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m