Part of #14122 · blocks #14512 · Blocked-by: #15004 (the acceptance probe).
Reader half of the program ruled on #14512 (comment 5528589044, maintainer 2026-09-03, batch #23). The artifact stays additive through this card.
The sites
From the boundary measurement in 5523741937. Note this package holds four independent config-load boundaries, not one — a fold at any single point reaches none of the others.
| Boundary / reader | file:line | Note |
|---|
B2 os serve / os dev config-module load | packages/cli/src/commands/serve.ts:2170 (bundleRequire) → :2172 (let config = mod.default || mod;) | The from-source entry. The compiled artifact never passes through it |
configHasMetadata + new AppPlugin(config) | serve.ts:2802, :2923 | Everything AppPlugin then reads — see card 2/4 |
| i18n auto-registration | serve.ts:2976-2995 | |
| ObjectQL engine auto-registration | serve.ts:2632 — if (config.objects && !hasObjectQL) | ⚠️ Silent: with no top-level objects this reads undefined and the engine is simply not registered |
| Storage-driver auto-registration | serve.ts:2669 — if (!hasDriver && config.objects) | ⚠️ Same mechanism, same silence |
appSecurityPluginOptions(config) | serve.ts:3710 | Feeds the ADR-0056 D7 permission surface from the from-source side — the same surface card 2/4 feeds from the artifact side |
B3 os build config-module load | packages/cli/src/commands/compile.ts:211 (loadConfig) | Then normalizeStackInput:231, safeParse :318, authoring rules :344, per-package run :401 |
B4 os migrate — its own second, independentloadConfig | packages/cli/src/utils/schema-migration-plugins.ts:1083 → configHasMetadata:1094, new AppPlugin(config, …):1100 | Not behind B2; missed by both earlier enumerations |
dev.tsreadArtifactObjects() | packages/cli/src/commands/dev.ts:653-666 | Opens the artifact with its ownJSON.parse(readFileSync(...)) at :655 and its own envelope unwrap at :656, reading top-level meta.objects at :657 to diff the inventory across recompiles. Non-fatal — it just goes permanently empty — but it is a sixth place the artifact is opened |
resolveDevDatabase / os start / os db clean | dev.ts:56, start.ts:557, db/clean.ts:46 | These call into resolve-project-database (card 2/4) before any stack exists |
The trap in this card
The two auto-registration gates at serve.ts:2632 and :2669 are the sharpest silent failures in the whole program, and they interact with a runtime-side detail: standalone-stack.ts:785omits the objects key entirely when the array is absent rather than setting [], and mergeBootConfig (packages/cli/src/utils/merge-boot-config.ts:42) is a plain spread. So an option-B artifact reaches these gates with config.objects === undefined and the app boots with no query engine and no storage driver, having thrown nothing. Verify this pair against card 2/4's behaviour on the probe before calling either done.
Acceptance
Sibling cards: #15004 (probe, blocks this), #15005@objectstack/runtime, 4/4 @objectstack/plugin-security. Emitter half: #14512.
Part of #14122 · blocks #14512 · Blocked-by: #15004 (the acceptance probe).
Reader half of the program ruled on #14512 (comment 5528589044, maintainer 2026-09-03, batch #23). The artifact stays additive through this card.
The sites
From the boundary measurement in 5523741937. Note this package holds four independent config-load boundaries, not one — a fold at any single point reaches none of the others.
file:lineos serve/os devconfig-module loadpackages/cli/src/commands/serve.ts:2170(bundleRequire) →:2172(let config = mod.default || mod;)configHasMetadata+new AppPlugin(config)serve.ts:2802,:2923AppPluginthen reads — see card 2/4serve.ts:2976-2995serve.ts:2632—if (config.objects && !hasObjectQL)objectsthis readsundefinedand the engine is simply not registeredserve.ts:2669—if (!hasDriver && config.objects)appSecurityPluginOptions(config)serve.ts:3710os buildconfig-module loadpackages/cli/src/commands/compile.ts:211(loadConfig)normalizeStackInput:231, safeParse:318, authoring rules:344, per-package run:401os migrate— its own second, independentloadConfigpackages/cli/src/utils/schema-migration-plugins.ts:1083→configHasMetadata:1094,new AppPlugin(config, …):1100dev.tsreadArtifactObjects()packages/cli/src/commands/dev.ts:653-666JSON.parse(readFileSync(...))at:655and its own envelope unwrap at:656, reading top-levelmeta.objectsat:657to diff the inventory across recompiles. Non-fatal — it just goes permanently empty — but it is a sixth place the artifact is openedresolveDevDatabase/os start/os db cleandev.ts:56,start.ts:557,db/clean.ts:46resolve-project-database(card 2/4) before any stack existsThe trap in this card
The two auto-registration gates at
serve.ts:2632and:2669are the sharpest silent failures in the whole program, and they interact with a runtime-side detail:standalone-stack.ts:785omits theobjectskey entirely when the array is absent rather than setting[], andmergeBootConfig(packages/cli/src/utils/merge-boot-config.ts:42) is a plain spread. So an option-B artifact reaches these gates withconfig.objects === undefinedand the app boots with no query engine and no storage driver, having thrown nothing. Verify this pair against card 2/4's behaviour on the probe before calling either done.Acceptance
resolveArtifactPackageOrder(@objectstack/core, since fix(metadata): register a packages[] artifact per package at the metadata door so every object has one owner across every door (#14599) #14643) — not re-derived here.composeStacks,packages/spec/src/stack.zod.ts, or what any command emits. Artifact stays additive.serve.tscallsbundleRequiredirectly whilecompile.tsandschema-migration-plugins.tscallloadConfig. If they turn out to be behaviourally interchangeable, one helper is better than three call sites; measure before assuming, and do not let that refactor grow this card.Sibling cards: #15004 (probe, blocks this), #15005
@objectstack/runtime, 4/4@objectstack/plugin-security. Emitter half: #14512.