Repository navigation
fix(metadata-protocol,runtime): a seed load that throws part-way still reports landed ids for the datasets that finished - #30519
Conversation
…l reports landed ids for the datasets that finished SeedLoadPartialWrites carries the finished datasets' landedIds/insertedIds, and both receipt producers (applySeedBodies' catch and the runtime fallback) fold them through seedAppliedLanded. The dataset that threw stays out of landed: its ids are only partly known when it throws. Co-Authored-By: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV
Co-Authored-By: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV
📓 Docs Drift CheckThis PR changes 3 package(s): 1 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 — 175 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 03e546fa3e9379289110d2a05e09f2c62b3bebfa && git checkout 03e546fa3e9379289110d2a05e09f2c62b3bebfa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa9c0aa2a0a8c2752f83b57765f3aae978ae196a 7f2442b0338fb295c8ffd047fececd535ea75938 && git checkout -B drift-repro fa9c0aa2a0a8c2752f83b57765f3aae978ae196a && git merge --no-ff 7f2442b0338fb295c8ffd047fececd535ea75938
node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 4c02bdd · 2026-11-05T14:03Z Full review of the first head at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe fix is at the right layer (the loader's own partial-writes record, which already held the finished results) and keeps the shape #30348 set: one projection, two lists, one cap, two producers importing the same two symbols. The failing-dataset decision is argued from the loader's own retry rule rather than asserted, and every claim the order makes is pinned at a real door with real reds. Must-fix: none. Should-fix, in order: (1) in the runtime fallback's catch, spread Implemented-by: VERDICT: PASS |
…its counts; pin a pass-2 throw Contract-review round 1: the runtime fallback's thrown receipt carries inserted/updated and names the stopped object; the landed describe says it is empty when no dataset finished; a pass-2 throw lists every dataset as finished; the older catch comment agrees with the new one. Co-Authored-By: Claude <[email protected]> Claude-Session: https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审一轮 PASS,无必改;四条建议全部采纳并核对了增量。抛错的那个数据集不列入 席位核对 — ACCEPT · domain:spec#3 · head
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
Requested by Yibo Wang · project thread
Fixes #30429
Clause-②: yes (widening: a thrown load's receipt carries
landedfor the datasets that finished, where it carried none)Metadata-protocol: semantic
Skills-docs: none owed — no skill, eval or doc teaches
seedApplied,landedIdsorSeedLoadPartialWrites(git grep -n "seedApplied\|landedIds\|SeedLoadPartialWrites" -- skills content/docs ':!content/docs/references' ':!content/docs/releases': 0 hits; controlgit grep -c "publish" -- skills/objectstack-ai/SKILL.md: 5); the generatedcontent/docs/references/api/protocol.mdxis regenerated here. The skills lane is paused; nothing is owed to it from this PR.Prior rulings read: seedapplied,seedloadpartialwrites,landed → 16 hits; ADR-0038 Decision §4, ADR-0056 D10, ADR-0058 D7, ADR-0060 D5, ADR-0061 D2, ADR-0076 D10, ADR-0090 D10, ADR-0093 D9, ADR-0094 D5; thread: none; repo: objectstack-ai/objectstack
维护者速读
seedApplied现在也带landed,列出抛错前已经跑完的数据集落下的行 id(同一个投影、同样分inserted/matched两张表、同样 200 上限);抛错的那个数据集不列出。运行时的兜底种子应用走同一读取与投影,失败回执也补上了 schema 要求的inserted/updated条数,并在error里点名停在哪个对象。landed缺席的含义收窄为"没有运行加载",抛错前一个数据集都没跑完时给[]。回滚即还原本 PR。packages/metadata-protocol/src/seed-loader.ts的SeedLoadPartialWrites.finished与notePartialWrites;packages/metadata-protocol/src/protocol-publish-batch.ts的 catch;packages/runtime/src/domains/packages.ts兜底 catch。pnpm --filter @objectstack/objectql exec vitest run src/publish-seed-receipt-landed-ids.test.ts,预期 10 例全绿,其中"load that threw"一例的landed只含先跑完的对象。What changes
@objectstack/metadata-protocol(seed-loader.ts):SeedLoadPartialWrites.finished.notePartialWritessnapshots the finished datasets'{ object, landedIds, insertedIds }, in load order, off the results it already keeps (loadProgress.finished). Nothing new is tracked; the ids are the ones each finished dataset's returned result carries. Empty when the throw came before any dataset finished.@objectstack/metadata-protocol(protocol-publish-batch.ts):applySeedBodies' catch addslanded: seedAppliedLanded(written.finished)whenever a partial-writes reading exists. Same projection as the returned path (seedAppliedLanded), so the same two lists, the same per-object fold and the same 200 cap. No reading (the request parse refused, or the loader module never loaded) leaveslandedabsent.@objectstack/metadata-protocol(index.ts):seedLoadPartialWritesandSeedLoadPartialWritesare exported from the package root, so the runtime fallback reads the same record.@objectstack/runtime(domains/packages.ts): the route-level fallback seed apply's catch readsseedLoadPartialWrites(e)and reports the samelanded. It had the same thrown path (applyPublishedSeeds→loader.loadthrows → catch withsuccess: false,error). It now also reportsinserted/updatedfrom the same reading (the response schema requires both; this catch used to send neither) and names the object whose seed stopped the load inerror(seed apply stopped at the seed for object 'X': …), exactly asapplySeedBodies' catch does; the caught sentence itself is still withheld unless declared. No reading: zero counts, the bare sentence, nolanded.@objectstack/spec(api/protocol.zod.ts): the sharedseedApplied.landeddescription (both publish responses) and thePublishMetaItemResponseSchemadocblock now say whatlandedcovers on a thrown load (the datasets that finished, empty when none had; not the one that threw) and when it is still absent (no load ran, or a producer that reports no ids). Schema shape unchanged.content/docs/references/api/protocol.mdxregenerated.protocol-publish-batch.ts: the older comment above the catch's counters now agrees with the new one besidelanded(no reading means no load ran; a reading with zero counts is a load that threw before writing a row)..changeset/30429-seed-receipt-landed-on-throw.md: spec minor, metadata-protocol minor, runtime patch, with a "Supersedes, in the same release" paragraph for the sentence in the unreleased.changeset/30348-seed-receipt-landed-ids.mdthat this makes false.The dataset that threw: omitted, and why
Measured at
seed-loader.ts: the dataset that throws can have landed rows before the throw. Its batched inserts flush everyinsertBatchSizerecords (flushPendingInserts), so earlier batches are written, and a throw can come after a row was written (the seed-file settle, pinned inseed-loader-seed-files.test.ts:pw_doc's D1 is in the store when the storage service throws). Those rows are ininserted/updatedand the object is named inerror, but they are not listed inlanded:A reader that wants to know whether unlisted rows exist compares
insertedwith the listedinserted.totals;errornames the object.What
landedanswers nowlanded[](the loader ran; nothing finished)Why
#30348 (PR #30427) put the ids on the receipt, but only on the returned-load path; its contract review named the gap at
applySeedBodies' catch. After a partial publish cloud's "clear sample data" had no ids for the datasets that did land, so those sample rows could never be cleared by id.Verification
Head
4c02bddf8, underos-verify-lock,NODE_OPTIONS=--max-old-space-size=4096:pnpm --filter @objectstack/metadata-protocol test: Test Files 316 passed, 4 skipped (320), Tests 22425 passed, 26 skipped.typecheckexit 0.pnpm --filter @objectstack/objectql test: Test Files 551 passed (551), Tests 10989 passed.typecheckexit 0.pnpm --filter @objectstack/runtime test: Test Files 543 passed (543), Tests 8647 passed, 19 skipped.typecheckexit 0.pnpm --filter @objectstack/spec test: Test Files 815 passed (815), Tests 25622 passed, 1 todo.typecheckexit 0.packages/objectql/src/publish-seed-receipt-landed-ids.test.ts(real protocol, real ObjectQL engine, real loader over an in-memory driver; ids read back from the engine). The old pin "a load that threw reports no ids" is replaced by: a good seed (with a customer's pre-existing row its key matches) lands, a later seed naming a missing object throws; the receipt lists the good seed's created id underinsertedand the customer's row undermatched, has no entry for the object that threw, andPublishPackageDraftsResponseSchemakeeps it. Plus: a load that throws before any seed finished reportslanded: [], not absent.packages/runtime/src/domains/packages-seed-apply-landed-thrown.test.ts(new, the fallback door with the realSeedLoaderService): positive control on a returned load; a second dataset's existing-row read throws after the first dataset wrote, and the receipt lists the first, reportsinserted: 1,updated: 0, anderrornames the stopped object with the driver sentence withheld; a failure before any load ran leaveslandedabsent.packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts(+1): a throw in pass 2, after both datasets of a circular pair wrote: the reading has nofailedObject,finishedlists both datasets with their store ids, andseedAppliedLanded(finished)names every row. Pass 2 guards its own driver traffic (a failed back-fill write is counted, a failed lookup probe is a miss; both measured), so the throw is staged at its one unguarded call, thelogger.errorits back-fill failure reports through.packages/metadata-protocol/src/seed-loader-seed-files.test.ts:seedLoadPartialWritesnow carriesfinishedwith the finished dataset's store ids, and leaves out the dataset that threw although its row D1 was written; a throw before any dataset readsfinished: [].Reverse verification (two mutations, each restored by scripts/ablation-replace.mjs to the HEAD blob)
The objectql and runtime pins resolve
@objectstack/metadata-protocolfromdist, so each mutation was rebuilt andablation-dist-preflight.mjsconfirmed the marker in 2 built files before reading a result. Directions predicted before running.The loader carries no finished ids (
finished: finished.map(...)→finished: (globalThis as any).ABL30429A ?? [], bloba7986c958ece→c3a35b1b3aa0): objectql landed pins 1 failed | 9 passed (the thrown-after-finish case; the before-any-finished[]case holds either way), runtime fallback 1 failed | 2 passed,seedLoadPartialWritesunit 1 failed | 2 passed. Restored: blob == HEAD.The catch drops
landed(...(written ? { landed: ... } : {})→...((globalThis as any).ABL30429B ?? {}), blobeccb3c495621→cb59f9935201): objectql landed pins 2 failed | 8 passed (both thrown cases). Restored: blob == HEAD.Round 1, the runtime catch's counts (
inserted: written?.inserted ?? 0,→inserted: 0,inpackages.ts, blob39d41e8df5cb→8a37f8d6e007; runtime resolves its ownsrc):packages-seed-apply-landed-thrown.test.ts1 failed | 2 passed (the thrown case). Restored: blob == HEAD,git diff HEADempty.Restore leg for 1 and 2: rebuilt,
--absentpreflight green for both markers,git diff HEADempty, the three files back to 10/10, 3/3, 3/3. A first attempt planted the markers as comments, which the build strips; the preflight refused it (marker absent from dist) before any test ran, and it is discarded.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran gates-ran.list: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero — all 105 recorded an exit code and none of them is 3)", exit 0. Every lineexit 0.be4d35b5(the implementation commit);4c02bddf8adds only the changeset's "Supersedes" paragraph, and the changeset gates (check-empty-changeset,check-changeset-no-major) pluscheck:generatedandcheck:nul-byteswere re-run on4c02bddf8, exit 0.check-plugin-teardown-shape --self-test(its pinned fixture commit fetched),check:skill-examplesandcheck:dual-build-cjs-loads(after a fullturbo run build, 119/119).Prior rulings readis fromnode scripts/pm/check-prior-rulings.mjs --card 30429 --terms seedApplied,SeedLoadPartialWrites,landed. ADR-0038 Decision §4 (L3 probes attach to the publish response "likeseedApplied") does not rule on the receipt's ids; the other hits match the word "landed" in unrelated decisions. The card's thread holds no ruling.check-changeset-no-majorwith this body as the event: exit 0 onMetadata-protocol: semantic(spec and metadata-protocolminor). Controls:noneexits 1 (theminorentries are unaccounted for);addedalso exits 0, so the gate checks the level, not the kind. The kind issemanticby reading: no schema key is added or removed (the schema shape is byte-identical; only the description moves), and an existing key's absence changes meaning on a thrown load, from "not reported" to "no load ran".!in the title and no(narrowing)arm: no full dogfood run. CI owns the rest of the farm.Acceptance notes
claim-seed-ownership.ts,seed-settlement.ts) still readslandedIds: out of scope per the card.Human Minimum
packages/specholds no runtime logic (description text only).pnpm lintpasses (CI-owned run).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzht5n1PKh96TqTfrvV