Repository navigation
feat(spec,metadata-protocol): the publish receipt names the rows a seed created and the rows it only matched - #30427
Conversation
…d names the rows a seed created and the rows it only matched seedApplied gains an optional `landed` key on both publish responses, one shared SeedAppliedLandedSchema: per object, `inserted` (rows this publish created) and `matched` (pre-existing rows a seed key matched), each capped at SEED_APPLIED_LANDED_IDS_CAP (200) ids with `total` and `truncated`. The loader publishes SeedLoadResult.insertedIds beside landedIds so the receipt can tell a created row from a customer row the seed only matched. Both receipt producers fold through one exported projection. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzi6PCniByMTFP6RQ6P
📓 Docs Drift CheckThis PR changes 3 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
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 eed2f12ceb5527c9484871b0504ca576a8ea6673 && git checkout eed2f12ceb5527c9484871b0504ca576a8ea6673
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8fa738fbe370c6cd8742fa14857e1b82d9b5d2a4 8dea3777a4a25877370951d5c2c7a70a0276a6aa && git checkout -B drift-repro 8fa738fbe370c6cd8742fa14857e1b82d9b5d2a4 && git merge --no-ff 8dea3777a4a25877370951d5c2c7a70a0276a6aa
node scripts/docs-audit/affected-docs.mjs --json 8fa738fbe370c6cd8742fa14857e1b82d9b5d2a4
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head c438aed · 2026-11-05T10:52Z 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 split into Implemented-by: VERDICT: PASS |
…on directly and the empty-seed receipt; state the ids' record order - seed-applied-landed.test.ts pins seedAppliedLanded without an engine: junk entries skipped, [] for no results, exactly 200 vs 201 through the cap (per list), and the cross-dataset inserted-then-matched merge. - A seed with `records: []` publishes one landed entry with two empty lists, as the description promises. - SeedAppliedIdListSchema.ids says the loader's record order (datasets in load order, records as authored), not "first-landed first". Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzi6PCniByMTFP6RQ6P
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审 PASS,没有必须修的。前三条建议已在一次推送里采纳:投影函数的 9 个直接钉(含 200/201 边界)、空种子的入口钉、id 顺序的措辞改为加载器的记录顺序。第四条(中途抛错时丢失已落地 id)另开 #30429。 席位核对 — ACCEPT · domain:spec#3 · head
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
Requested by Yibo Wang · project thread
Fixes #30348
Ruling-ref: objectstack-ai/cloud#3598 (6651768430)
Clause-②: yes (widening: an optional, capped per-object
landedkey on bothseedAppliedreceipts, andinsertedIdson the seed loader's per-dataset result)Metadata-protocol: added
Skills-docs: none owed — no skill, eval or doc teaches
seedAppliedorlandedIds(git grep -n "seedApplied\|landedIds" -- skills content/docs ':!content/docs/references' ':!content/docs/releases': 0 hits; controlgit grep -c "publish" -- skills/objectstack-ai/SKILL.md: 5); the generated references (content/docs/references/api/protocol.mdx,data/seed-loader.mdx) are regenerated here. The skills lane is paused; nothing is owed to it from this PR.Prior rulings read: seedapplied,landedids,publish receipt → 1 hits; ADR-0038 Decision §4; thread: none; repo: objectstack-ai/objectstack
维护者速读
seedApplied新增可选字段landed,按对象分两张 id 列表:inserted(本次发布新建的行)和matched(种子按自然键匹配到的已有行),每张最多 200 个 id,超出时truncated: true并给出未截断的total;种子加载器结果新增insertedIds。ignore模式下,种子会"落"在键相同的客户已有行上而不写它;只给一张表会让 cloud 删掉客户自己的行。inserted里的行,且要跨多次发布累积,因为再次发布时早先插入的行会出现在matched。packages/metadata-protocol/src/seed-loader.ts的insertedRecordIndexes(两个插入点);packages/metadata-protocol/src/seed-applied-landed.ts(合并、去重、截断);packages/spec/src/api/protocol.zod.ts的SeedAppliedLandedSchema与上限常量。pnpm --filter @objectstack/objectql exec vitest run src/publish-seed-receipt-landed-ids.test.ts,预期 9 例全绿(含客户已有行只进matched、超限截断、空种子给两张空表、无种子无字段)。What changes
@objectstack/spec(packages/spec/src/api/protocol.zod.ts):seedApplied.landed, one shared schema on both publish responses.SeedAppliedLandedSchema:{ object, inserted, matched }, each list aSeedAppliedIdListSchema{ ids, total, truncated }with all three required.ids.max(SEED_APPLIED_LANDED_IDS_CAP), in the loader's record order (datasets in load order, records as authored), not the order the writes completed (the loader buffers inserts and flushes them after later skips).SEED_APPLIED_LANDED_IDS_CAP = 200(source below).PublishMetaItemResponseSchema.seedApplied.landedandPublishPackageDraftsResponseSchema.seedApplied.landedboth point at it, with one description text. Optional: absent means "not reported", never "nothing landed".PublishMetaItemResponseSchemadocblock (the three conditional receipts) names the key, the two lists, the cap, the truncation shape and the accepted cost.@objectstack/spec(packages/spec/src/data/seed-loader.zod.ts):SeedLoadResult.insertedIds, the ids amonglandedIdswhose row the load created.landedIds' TSDoc now states what each mode puts in it (below).@objectstack/metadata-protocol:seed-loader.ts: the two insert sites (the batched insert flush and the self-referencing sequentialwriteRecordwithaction === 'inserted') mark the record index besidenoteLanded; the result carriesinsertedIdsin record order, deduped.seed-applied-landed.ts(new, exported asseedAppliedLanded): folds the loader's per-dataset results into one entry per object, in first-loaded order; ids keep the loader's record order (datasets in load order, records as authored, first occurrence kept), not the order the writes completed. Several datasets of one object merge; ids dedupe; an id any dataset of the object inserted is never alsomatched; each list is capped. An object whose datasets landed nothing still gets an entry with two empty lists.applySeedBodiesreturnslanded: seedAppliedLanded(r.results)on the path where the loader returned.publishPackageDraftsandpublishMetaItem(throughrunPublishSideEffects) pass it through; their inline return types gain the key.@objectstack/runtime(domains/packages.ts): the route-level fallback apply (applyPublishedSeeds, for protocols that do not self-apply) returns the samelandedthrough the same exported projection.authorable-surface/{api,data}.json,authorable-defaults/data.json,json-schema.manifest/api.json,api-surface,export-origins,declaration-map, the strictness-ledger counts, andcontent/docs/references/{api/protocol,data/seed-loader,index}.mdx..changeset/30348-seed-receipt-landed-ids.md: spec minor, metadata-protocol minor, runtime patch.What "landed" means, per seed mode (measured at
seed-loader.ts, everynoteLandedsite)insertedmatchedinsert/replaceignore(default since #30081)upsertupdateA dry run lists nothing. A matched row whose update was refused is listed because the loader notes it before the write (the row exists and its key resolved).
Consequence for cloud (cloud#3598): only
insertednames rows the seed created. Amatchedrow may be a customer's own record that shares the seed's natural key; underignoreit is matched without being written. ⛔ Never clear amatchedrow on this receipt's word.insertedis per publish: a row an earlier publish inserted shows asmatchedon a re-publish, so the ledger must accumulateinsertedids across publishes rather than read the latest receipt alone.The cap and its source
SEED_APPLIED_LANDED_IDS_CAP = 200ids per list, per object. The card asked for the cloud ledger's cap onseeded_record_ids; objectstack-ai/cloud could not be read from this session (no repository access; an org-wide code search forseeded_record_idsreturned 0 hits). There is no platform precedent for a bounded id list on a response, so the number follows the platform's existing bounds on a record-id list:EXPLAIN_BATCH_MAX_RECORD_IDS = 200(packages/spec/src/security/explain.zod.ts:257, the cap on record ids crossing the public API) and the seed loader's 200-row insert batch (BULK_BATCH_SIZE). The truncation shape followsSearchAllResponseSchema.truncated: declared, never silent. Chosen with spec seat 3 before building. If cloud's ledger cap differs, aligning it is a one-constant change.Accepted cost (ruling A): row ids appear on the public
POST /packages/:id/publish-draftsandPOST /meta/:type/:name/publishresponses, bounded by the cap.Why
AI builds land their sample rows when the seed draft is published. The loader computes which rows it landed on (
landedIds), butapplySeedBodiesreturned onlysuccess,insertedandupdated, so cloud's "clear sample data" could not find the rows (measured on a hosted kernel at 1a3ea0c, cloud#3598 report 6650239213). Ruling A puts the ids on the receipt. Splitting them intoinsertedandmatchedis what keeps the clear safe under the create-only default.Verification
Head
8dea3777(c438aed4plus the review round's test-and-wording commit8dea3777; branch merged withorigin/mainfe835b49), underos-verify-lock,NODE_OPTIONS=--max-old-space-size=4096:pnpm --filter @objectstack/spec test: Test Files 813 passed (813), Tests 25580 passed, 1 todo (atc438aed4).typecheckgreen for spec, metadata-protocol and runtime (runtime'scheck:test-typecheckincluded, after a fullturbo run build, 119/119).pnpm --filter @objectstack/metadata-protocol test: Test Files 313 passed, 4 skipped (317), Tests 22398 passed;pnpm --filter @objectstack/objectql test: Test Files 551 passed (551), Tests 10975 passed;pnpm --filter @objectstack/runtime test: Test Files 542 passed (542), Tests 8644 passed, 19 skipped (all at11b7c671, the implementation commit; the merge brought no change to these files). The new pins re-ran green atc438aed4: objectql 3 files 26 passed,seed-loader-landed-ids.test.ts14 passed. At8dea3777(review round):seed-applied-landed.test.ts+seed-loader-landed-ids.test.ts23 passed,publish-seed-receipt-landed-ids.test.ts9 passed, specprotocol.test.ts242 passed after a fullturbo run build(119/119);check:generatedgreen aftergen:docs(the rewordedidsdescription).check-changeset-no-majorwith this body as the event: exit 0 onMetadata-protocol: added; the control withnoneexits 1 (authorable-surface/authorable-defaultsbaselines moved).New pins:
packages/objectql/src/publish-seed-receipt-landed-ids.test.ts(9, real protocol + real ObjectQL engine + real loader over an in-memory driver; every id read back from the engine): a fresh seed lists exactly the engine's rows underinserted; underignorea customer's pre-existing row matched by key ismatchedand the fresh recordinserted; a re-publish lists the first publish's rows asmatchedand nothinginserted; 205 records give the first 200 ids in record order,truncated: true,total: 205, and the schema refuses 205 ids; two seeds of one object fold into one entry; a seed withrecords: []reports one entry with two empty lists (reported-and-empty, not absent); a publish with no seed carries noseedApplied; a load that threw reports counts and nolanded; the single-itempublishMetaItemdoor carries the samelandedandPublishMetaItemResponseSchemakeeps it. Both response schemas parse the real responses.packages/metadata-protocol/src/seed-applied-landed.test.ts(9, the projection without an engine): junk entries (null, missing or non-stringobject) skipped; no results give[]; a dataset that landed nothing (or predatesinsertedIds) gives two empty lists; one dataset's split; exactly 200 ids carried whole withtruncated: false, 201 cut to 200 withtotal: 201,truncated: true, and each list cut on its own; across datasets of one object a row one inserted and another matched staysinsertedonly, whichever order; objects in first-loaded order and ids deduped, first occurrence kept. Its output parses throughSeedAppliedLandedSchema.packages/metadata-protocol/src/seed-loader-landed-ids.test.ts(+6):insertedIdson a fresh load equalslandedIds; a replay (skip and update) inserts nothing;ignoreover a pre-existing row; a refused update is landed, not inserted; the self-referencing sequential path; a refused insert and a dry run.packages/spec/src/api/protocol.test.ts(+4): both receipts carrylanded; existing receipts parse unchanged and gain no key; the cap (200 parses, 201 refused);total,truncatedand both lists are required.Existing pins updated:
publish-meta-response-conformance.test.ts(the real seed publish now asserts thelandedentry) andpublish-package-drafts-seed-receipt-counts.test.ts(twotoEquals on the success receipt admitlanded).Reverse verification (three mutations, each restored by scripts/ablation-replace.mjs to the HEAD blob)
if (internalId) insertedRecordIndexes.add(recordIndex);deleted fromseed-loader.ts, anchor 1 → 0, blob4140f681fe20→d5fc404362b8):seed-loader-landed-ids.test.ts3 failed | 11 passed (14) (fresh load,ignoreover a pre-existing row, refused insert). The metadata-protocol tests import the loader fromsrc. Restored: blob == HEAD4140f681fe20,git diff HEADempty.[...landed].filter((id) => !inserted.has(id))→[...landed].filter(() => true), blob76e34e96d4fe→e694847b8e36),@objectstack/metadata-protocolrebuilt andablation-dist-preflight.mjsconfirming the marker in 2 built files:publish-seed-receipt-landed-ids.test.ts6 failed | 2 passed (8) (the two green are the no-seed and threw-load pins, which hold either way). Restored, rebuilt,--absentpreflight green, the file back to 8 passed. A first attempt at this mutation was a no-op (the replacement text already occurred once; the tool refused and restored), so its run read the unmutated dist and is discarded.truncated: ids.length > SEED_APPLIED_LANDED_IDS_CAP→>=inseed-applied-landed.ts, blob1884875a7f21→47cf0511a4af):seed-applied-landed.test.ts1 failed | 8 passed (9), the exactly-200 pin. The test imports the projection fromsrc. Restored: blob == HEAD1884875a7f21,git diff HEADempty.Gates: dispatch-gates --commands, 107 derived at 8dea377, 107 run, 0 NOT-MEASURED
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran gates-ran.listat8dea3777(re-run in full after the review round; the same 107 also ran all-green atc438aed4): "107 derived famil(ies) accounted for — 107 run, 0 NOT-MEASURED", exit 0. Every lineexit 0;check-changeset-no-major,check:generated,check:liveness,check:spec-parsed-aliasandcheck:nul-bytesincluded.11b7c671hadcheck:livenessandcheck:generatedred on origin/main's unanchoredquery/searchFieldscitation (fixed on main by fix(spec): repoint the query.searchFields liveness row at engine-find.ts#expandSearchOnAst #30368, merged in here), and three exit-3 prerequisites (check-plugin-teardown-shape --self-test: pinned fixture fetched;check:skill-examples,check:dual-build-cjs-loads: no dists). All green onc438aed4after the merge and a fullturbo run build(119/119).Prior rulings readis fromnode scripts/pm/check-prior-rulings.mjs --card 30348 --terms seedApplied,landedIds,"publish receipt"; ADR-0038 Decision §4 (L3 probes attach to the publish response "likeseedApplied") does not rule on the receipt's ids.!in the title and no(narrowing)arm: no full dogfood run. CI owns the rest of the farm.Acceptance notes
inserted/updatedcounts fromseedLoadPartialWritesbut no ids, solandedis absent there. Carrying ids on that path would mean wideningSeedLoadPartialWrites; not done here, and cloud must read an absentlandedbesidesuccess: falseas "rows may have landed, ids unknown".applySeedBodies(protocol-publish-package-drafts.test.ts), so theirtoEqualreceipts are unchanged.Human Minimum
packages/specholds no runtime logic (one constant and schemas; the projection lives in metadata-protocol).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_01WGufzi6PCniByMTFP6RQ6P