Repository navigation
fix(runtime): a seed dataset declared once registers once on an additive multi-package artifact - #19981
Conversation
…taset on an additive artifact `resolveArtifactCollections` claims a nameless item (a seed dataset, a `datasourceMapping` rule) by whole value. Boot registration stamps the ADR-0010 envelope (`_packageId` / `_provenance`) onto the package body's copy in place, and AppPlugin resolves its collections after that, so on a compiled additive artifact the stamped body copy and the unstamped top-level copy stopped comparing equal: a dataset declared once reached the shared seed registry twice and a `mode: 'insert'` row was written twice per boot. The value spelling of the identity claim now leaves the registration envelope out, with the key set derived from `MetadataProtectionFields` rather than restated. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 26 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 ec08434aff553b8cc464d7418b046636c4bfcef9 && git checkout ec08434aff553b8cc464d7418b046636c4bfcef9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1011b01bc071c545f72f2761647b8d9ab56375 26a5faabafb313970586ca816e186362d9d7051e && git checkout -B drift-repro 2c1011b01bc071c545f72f2761647b8d9ab56375 && git merge --no-ff 26a5faabafb313970586ca816e186362d9d7051e
node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375 |
Face review (post-hoc, released)Served-tier: Scope: the changeset (and named docs) of PR #19981, released; this review cannot block anything and exists to find false released prose. Sentence verdicts(
Still true on origin/main?Yes. FindingNONE. Reviewed-by: VERDICT: CLEAN Generated by Claude Code |
Fixes #19672
Clause-②: no
On an additive multi-package artifact, a seed dataset declared once now reaches the shared
seed-datasetsregistry once. An additive artifact carries its collections both flattened at the top level and underpackages[]. Before this change the dataset registered twice, so amode: 'insert'dataset wrote its rows twice per boot and again on every per-org replay.The grading question: is the additive shape reachable? Yes. It is still produced today, not only by upgrades
Triage comment 5782064125 made this the dispatch's first act: reachable on an upgrade path means p0; not reachable means p1 stands. Measured on
origin/main2c1011b01b, which already contains the emitter change (6057357d):seed-datasetsentries after a real boot6057357dcomposeStacks([...], { manifest: 'preserve' })then JSON: top-leveldata1,packages2manifestdata1,packages1packagesonlypreservepackageskey"Real boot" means
LiteKernel+ the realObjectQLPlugin+AppPlugin, with registration actually stamping the body copy (_packageId: com.probe.core,_provenance: package). The temporary probe was not committed.So the answer is reachable, on two routes:
os devwithout--compileboots whateverdist/objectstack.jsonholds (packages/cli/src/commands/dev.ts:376). Its staleness check compares source mtimes, not the CLI or runtime version.os starttakes--artifact, then$OS_ARTIFACT_PATH, then./dist/objectstack.json, and compiles only when no artifact exists (packages/cli/src/commands/start.ts:188).composeStacks(…, { manifest: 'preserve' })keeps the additive shape whenever its three strip conditions fail (packages/spec/src/stack.zod.ts:4611,:5140). That happens when a package's standalone action binds to a sibling package's object, or when an input carries nomanifest. Both are legal compositions that currentmainemits.By triage's own rule that makes this p0. The seat carries the re-grade; this PR writes no label.
Mechanism, confirmed on
2c1011b01bAppPlugin.inithands the artifact to themanifestservice (app-plugin.ts:394), which registerspackages[]only.registerAppstep 6 callsregisterItem('data', dataset, 'object', id)(packages/objectql/src/engine.ts:5708), andapplyProtectionstamps_packageId/_provenanceonto the body's dataset in place (registry.ts:3484).AppPlugin.startthen readsthis.collections, andresolveArtifactCollectionsclaims a nameless item by whole value (itemIdentity,artifact-collections.ts:246). In a compiled artifact the two halves are different objects, so the reference claim cannot help, and the stamped copy no longer equals the top-level one.#15262guard compares by reference only (app-plugin.ts:1355), andmergeSeedDatasetsis a plain push (seed-datasets.ts:76).PM mechanism assumption 2 holds: the reader still accepts the legacy additive shape. Nothing converts it or refuses it, and the real boot above reads 2.
The fix: at the identity claim that owns the de-duplication
The value spelling of
itemIdentitynow leaves out the ADR-0010 registration envelope. The key set is derived fromMetadataProtectionFields(@objectstack/spec/kernel), the declaration the metadata schemas spread, and is never restated here.flow-clone.tsin this package derives its drop set the same way.app-plugin.ts:1355andmergeSeedDatasetsare unchanged.Class sweep: other nameless kinds stamped before the claim?
Measured by reading every registrar that mutates
AppPlugin's own bundle beforethis.collectionsis first read:registerApp. Named items are stamped in place, which is harmless under name identity.registerMetadataCollectionsregisters a nameless view container as a spread copy (engine.ts:5967).datais the only nameless kind it stamps in place.MetadataPluginartifact door stamps its ownObjectStackDefinitionSchema.parseoutput (packages/metadata/src/plugin.ts:915), not this bundle.translationsbundle is not even admissible.ArtifactPackageSchemarefuses_packageIdinside it; this surfaced in the first run of the new resolver test.The fix lives in the identity rule, so any nameless kind is covered. The resolver pin exercises
datasourceMappingbesidedatato show the rule is not special-cased for seeds.Tests
artifact-collections.test.ts:dataanddatasourceMappingcopies are stamped with the realapplyProtectionresolves each to 1, and hands on the unstamped top-level array by reference.app-plugin.seed-stamped-additive.test.ts(new), with aninsert-mode dataset declared once:LiteKernel+ObjectQLPlugin+AppPlugin) first asserts that registration stamped the body copy, then assertsseed-datasetshas exactly 1 entry.AppPlugin.startcallsobjectql.insertexactly once.Ablation (committed fix, then
node scripts/ablation-replace.mjsin WRAP mode plus an outertraprestore):stableIdentity(withoutRegistrationEnvelope(item))replaced withstableIdentity(item). Anchor count 1 to 0, replacement 0 to 1, blob5351f76dto2686c9b1.insertcalled 2 times5351f76dequals the HEAD blob, andgit diff HEADis empty.Runs, all at
26a5faab:pnpm --filter @objectstack/runtime test:Test Files 274 passed (274),Tests 3821 passed | 1 skipped (3822).pnpm --filter @objectstack/runtime typecheck: exit 0.check:test-typecheck: OK.tsc -p tsconfig.test.json --listFilesincludes both test files.node scripts/pm/dispatch-gates.mjs --commandsderived 58; 56 exit 0. Reconciliation with--ran:58 derived, 56 run, 2 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loads, reason: exit 3PREREQUISITE NOT MET, 37 packages have nodist/in this worktree and a fullpnpm builddoes not fit the foreground cap. Supplemental reading:require('./dist/index.cjs')on the rebuilt runtime loads 260 exports.node scripts/check-plugin-teardown-shape.mjs --self-test, reason: exit 3, its positive control is pinned to a commit this shallow clone cannot reach. It grades the checker, not this diff; the non-self-test run of the same gate exited 0.pnpm lint, as a proven narrowing,eslint --no-inline-config --format jsonon the changed files: files=3, errors=0, warnings=0.isPathIgnored/calculateConfigForFileon all 4 changed paths. The 3 TS files are linted; the changeset.mdis ignored.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules; stated at lines 326-328). Its only file reads are its own two baselines, which this diff does not touch, so no untouched file's verdict can move.Changeset:
.changeset/19672-seed-dataset-stamped-duplicate.md,@objectstack/runtimepatch.Acceptance notes
applyProtectionalso translates an author-facingprotectionblock into_lock*and deletes the block. Identity does not normalise that pair. No nameless item stamped in place can carryprotection:SeedSchemais strict and declares noprotectionkey, and the one nameless in-place stamp isdata.composeStacks(…, { manifest: 'preserve', objectConflict: 'merge' })over two packages that each own one object name emits an artifact that a real boot refuses withArtifactObjectNameConflictError(ADR-0130 D3), naming theextendfix. The refusal is loud and correct. Whetheros compilerefuses the same composition earlier was not measured, so this is recorded here and not filed.packageswhile refusing a malformed entry, which leaves the cli outside ruling #15293-A #19925 readers (non-arraypackages) are out of scope and untouched. So arepackages/runtime/src/domains/mcp.tsandpackages/cli/src/commands/serve.ts.Durable attribution: authored by the os-dev agent dispatched by the
domain:cliPM seat, on branchclaude/issue-19672-seed-dataset-stamped-dup.Generated by Claude Code