You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a7b8876
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,metadata-protocol): the publish receipt names the rows a seed created and the rows it only matched (#30427)
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBY35nWXYXKyBvnYcvZDpPht)_Fixes#30348
Ruling-ref: objectstack-ai/cloud#3598 (6651768430)
Clause-②: yes (widening: an optional, capped per-object `landed` key on
both `seedApplied` receipts, and `insertedIds` on the seed loader's
per-dataset result)
Metadata-protocol: added
Skills-docs: none owed — no skill, eval or doc teaches `seedApplied` or
`landedIds` (`git grep -n "seedApplied\|landedIds" -- skills
content/docs ':!content/docs/references' ':!content/docs/releases'`: 0
hits; control `git 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`。
- 为什么改:执行 cloud#3598 裁决 A。cloud 的「清理示例数据」需要知道发布写了哪些行,回执原来只给条数。拆成两张表是因为默认
`ignore` 模式下,种子会"落"在键相同的客户已有行上而不写它;只给一张表会让 cloud 删掉客户自己的行。
- 风险与回滚:纯增字段、可选,旧回执原样解析;代价是公开发布响应里出现行 id(受 200 上限约束,裁决已接受)。回滚即还原本
PR。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
a `SeedAppliedIdListSchema` `{ 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.landed` and
`PublishPackageDraftsResponseSchema.seedApplied.landed` both point at
it, with one description text. Optional: absent means "not reported",
never "nothing landed".
- The `PublishMetaItemResponseSchema` docblock (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 among `landedIds` whose 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 sequential `writeRecord` with `action ===
'inserted'`) mark the record index beside `noteLanded`; the result
carries `insertedIds` in record order, deduped.
- `seed-applied-landed.ts` (new, exported as `seedAppliedLanded`): 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 also `matched`; each list
is capped. An object whose datasets landed nothing still gets an entry
with two empty lists.
- `applySeedBodies` returns `landed: seedAppliedLanded(r.results)` on
the path where the loader returned. `publishPackageDrafts` and
`publishMetaItem` (through `runPublishSideEffects`) 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 same `landed` through the same exported
projection.
- Generated artifacts regenerated: `authorable-surface/{api,data}.json`,
`authorable-defaults/data.json`, `json-schema.manifest/api.json`,
`api-surface`, `export-origins`, `declaration-map`, the
strictness-ledger counts, and
`content/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`,
every `noteLanded` site)
| Mode | `inserted` | `matched` | Not listed |
| --- | --- | --- | --- |
| `insert` / `replace` | every row an insert created | none | a record
whose insert failed |
| `ignore` (default since #30081) | rows created for records no existing
row matched | existing rows a record's key matched, left as they were |
a failed insert |
| `upsert` | rows created | rows updated, rows matched and left
unchanged (no-op replay), rows matched whose update the engine refused |
a failed insert |
| `update` | none | rows updated, matched and unchanged, or refused | a
record that matched no row |
A 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 `inserted` names rows the
seed created. A `matched` row may be a customer's own record that shares
the seed's natural key; under `ignore` it is matched without being
written. ⛔ Never clear a `matched` row on this receipt's word.
`inserted` is per publish: a row an earlier publish inserted shows as
`matched` on a re-publish, so the ledger must accumulate `inserted` ids
across publishes rather than read the latest receipt alone.
### The cap and its source
`SEED_APPLIED_LANDED_IDS_CAP = 200` ids per list, per object. The card
asked for the cloud ledger's cap on `seeded_record_ids`;
objectstack-ai/cloud could not be read from this session (no repository
access; an org-wide code search for `seeded_record_ids` returned 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 follows
`SearchAllResponseSchema.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-drafts` and `POST /meta/:type/:name/publish`
responses, 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`), but
`applySeedBodies` returned only `success`, `inserted` and `updated`, 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 into `inserted` and `matched` is
what keeps the clear safe under the create-only default.
## Verification
Head `8dea3777` (`c438aed4` plus the review round's test-and-wording
commit `8dea3777`; branch merged with `origin/main` `fe835b49`), under
`os-verify-lock`, `NODE_OPTIONS=--max-old-space-size=4096`:
- `pnpm --filter @objectstack/spec test`: Test Files 813 passed (813),
Tests 25580 passed, 1 todo (at `c438aed4`). `typecheck` green for spec,
metadata-protocol and runtime (runtime's `check:test-typecheck`
included, after a full `turbo 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 at `11b7c671`, the
implementation commit; the merge brought no change to these files). The
new pins re-ran green at `c438aed4`: objectql 3 files 26 passed,
`seed-loader-landed-ids.test.ts` 14 passed. At `8dea3777` (review
round): `seed-applied-landed.test.ts` + `seed-loader-landed-ids.test.ts`
23 passed, `publish-seed-receipt-landed-ids.test.ts` 9 passed, spec
`protocol.test.ts` 242 passed after a full `turbo run build` (119/119);
`check:generated` green after `gen:docs` (the reworded `ids`
description).
- `check-changeset-no-major` with this body as the event: exit 0 on
`Metadata-protocol: added`; the control with `none` exits 1
(`authorable-surface`/`authorable-defaults` baselines 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 under `inserted`; under `ignore` a customer's
pre-existing row matched by key is `matched` and the fresh record
`inserted`; a re-publish lists the first publish's rows as `matched` and
nothing `inserted`; 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 with `records: []`
reports one entry with two empty lists (reported-and-empty, not absent);
a publish with no seed carries no `seedApplied`; a load that threw
reports counts and no `landed`; the single-item `publishMetaItem` door
carries the same `landed` and `PublishMetaItemResponseSchema` keeps 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-string
`object`) skipped; no results give `[]`; a dataset that landed nothing
(or predates `insertedIds`) gives two empty lists; one dataset's split;
exactly 200 ids carried whole with `truncated: false`, 201 cut to 200
with `total: 201`, `truncated: true`, and each list cut on its own;
across datasets of one object a row one inserted and another matched
stays `inserted` only, whichever order; objects in first-loaded order
and ids deduped, first occurrence kept. Its output parses through
`SeedAppliedLandedSchema`.
- `packages/metadata-protocol/src/seed-loader-landed-ids.test.ts` (+6):
`insertedIds` on a fresh load equals `landedIds`; a replay (skip and
update) inserts nothing; `ignore` over 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 carry
`landed`; existing receipts parse unchanged and gain no key; the cap
(200 parses, 201 refused); `total`, `truncated` and both lists are
required.
- Existing pins updated: `publish-meta-response-conformance.test.ts`
(the real seed publish now asserts the `landed` entry) and
`publish-package-drafts-seed-receipt-counts.test.ts` (two `toEqual`s on
the success receipt admit `landed`).
<details><summary>Reverse verification (three mutations, each restored
by scripts/ablation-replace.mjs to the HEAD blob)</summary>
1. The batched insert site's mark removed (`if (internalId)
insertedRecordIndexes.add(recordIndex);` deleted from `seed-loader.ts`,
anchor 1 → 0, blob `4140f681fe20` → `d5fc404362b8`):
`seed-loader-landed-ids.test.ts` **3 failed | 11 passed (14)** (fresh
load, `ignore` over a pre-existing row, refused insert). The
metadata-protocol tests import the loader from `src`. Restored: blob ==
HEAD `4140f681fe20`, `git diff HEAD` empty.
2. The matched/inserted split removed in the projection
(`[...landed].filter((id) => !inserted.has(id))` →
`[...landed].filter(() => true)`, blob `76e34e96d4fe` → `e694847b8e36`),
`@objectstack/metadata-protocol` rebuilt and
`ablation-dist-preflight.mjs` confirming the marker in 2 built files:
`publish-seed-receipt-landed-ids.test.ts` **6 failed | 2 passed (8)**
(the two green are the no-seed and threw-load pins, which hold either
way). Restored, rebuilt, `--absent` preflight 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.
3. The cap boundary moved (`truncated: ids.length >
SEED_APPLIED_LANDED_IDS_CAP` → `>=` in `seed-applied-landed.ts`, blob
`1884875a7f21` → `47cf0511a4af`): `seed-applied-landed.test.ts` **1
failed | 8 passed (9)**, the exactly-200 pin. The test imports the
projection from `src`. Restored: blob == HEAD `1884875a7f21`, `git diff
HEAD` empty.
</details>
<details><summary>Gates: dispatch-gates --commands, 107 derived at
8dea377, 107 run, 0 NOT-MEASURED</summary>
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran gates-ran.list` at `8dea3777` (re-run in full after the review
round; the same 107 also ran all-green at `c438aed4`): "107 derived
famil(ies) accounted for — 107 run, 0 NOT-MEASURED", exit 0. Every line
`exit 0`; `check-changeset-no-major`, `check:generated`,
`check:liveness`, `check:spec-parsed-alias` and `check:nul-bytes`
included.
- The first pass at `11b7c671` had `check:liveness` and
`check:generated` red on origin/main's unanchored `query/searchFields`
citation (fixed on main by #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 on `c438aed4` after the merge and a full `turbo run
build` (119/119).
- `Prior rulings read` is from `node scripts/pm/check-prior-rulings.mjs
--card 30348 --terms seedApplied,landedIds,"publish receipt"`; ADR-0038
Decision §4 (L3 probes attach to the publish response "like
`seedApplied`") does not rule on the receipt's ids.
- No `!` in the title and no `(narrowing)` arm: no full dogfood run. CI
owns the rest of the farm.
</details>
## Acceptance notes
- A load that THROWS (a seed naming a missing object, a storage outage
mid-dataset) reports `inserted`/`updated` counts from
`seedLoadPartialWrites` but no ids, so `landed` is absent there.
Carrying ids on that path would mean widening `SeedLoadPartialWrites`;
not done here, and cloud must read an absent `landed` beside `success:
false` as "rows may have landed, ids unknown".
- Contract review (spec seat 3, PASS) should-fix 4, the ids of datasets
that landed before a mid-batch throw being dropped with the rest, is the
case in the note above; the seat files it as a follow-up, so it stays
out of this PR.
- Two producer tests use a mocked `applySeedBodies`
(`protocol-publish-package-drafts.test.ts`), so their `toEqual` receipts
are unchanged.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic
(one constant and schemas; the projection lives in metadata-protocol).
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (CI-owned run).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block
excepted, per project convention).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzi6PCniByMTFP6RQ6P
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec,metadata-protocol): the publish receipt's `seedApplied.landed` names the rows a seed created and the rows it only matched, per object, capped at 200 ids per list
8
+
9
+
Clause-②: yes (widening)
10
+
Metadata-protocol: added
11
+
12
+
-**`@objectstack/spec`: `seedApplied.landed`** on both publish responses (`PublishMetaItemResponseSchema`, `PublishPackageDraftsResponseSchema`), one shared schema `SeedAppliedLandedSchema`: per object, `inserted` (the rows this publish created) and `matched` (pre-existing rows a seed record's key matched: updated, left as they were, or refused an update), each a `SeedAppliedIdListSchema``{ ids, total, truncated }`. `ids` carries at most `SEED_APPLIED_LANDED_IDS_CAP` (200) ids, in the loader's record order (datasets in load order, records as authored); past it `truncated` is `true` and `total` is the uncut count. Absent `landed` means "not reported" (a load that threw, or a producer that reports no ids), never "nothing landed". Only `inserted` names rows the seed created: a `matched` row may be a customer's own record that shares the seed's natural key, and under `ignore` (the default) such a row is matched without being written.
13
+
-**`@objectstack/spec`: `SeedLoadResult.insertedIds`**, the ids among `landedIds` whose row the load created. Per load: a row an earlier load inserted is matched on a replay, so a reader tracking a seed's rows across publishes accumulates `insertedIds` over every load. `landedIds`' TSDoc now says what each seed mode puts in it.
14
+
-**`@objectstack/metadata-protocol`**: `applySeedBodies` returns `landed`, so `publishPackageDrafts` and `publishMetaItem` both carry it; the loader records which landed rows it inserted. `seedAppliedLanded(results)` is exported, the one projection both receipt producers fold the loader's ids through.
15
+
-**`@objectstack/runtime`**: the package-publish door's fallback seed apply (for protocols that do not self-apply) reports the same `landed`.
16
+
- Accepted cost (objectstack-ai/cloud#3598 ruling A): row ids appear on the public publish responses, bounded by the cap.
Copy file name to clipboardExpand all lines: content/docs/references/data/seed-loader.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -164,6 +164,7 @@ Result of loading a single dataset
164
164
|**referencesDropped**|`integer`| optional (default: `0`) | Reference fields dropped from records that were still written |
165
165
|**summariesStale**|`integer`| optional (default: `0`) | Roll-up summary values left stale by writes for this dataset |
166
166
|**landedIds**|`string[]`| optional (default: `[]`) | Ids of the stored rows this dataset landed on: inserted, updated, or matched on replay |
167
+
|**insertedIds**|`string[]`| optional (default: `[]`) | Ids among landedIds whose row this load created (a successful insert); every other landed id was a pre-existing row a record key matched |
@@ -302,6 +303,7 @@ Result of loading a single dataset
302
303
|**referencesDropped**|`integer`| optional (default: `0`) | Reference fields dropped from records that were still written |
303
304
|**summariesStale**|`integer`| optional (default: `0`) | Roll-up summary values left stale by writes for this dataset |
304
305
|**landedIds**|`string[]`| optional (default: `[]`) | Ids of the stored rows this dataset landed on: inserted, updated, or matched on replay |
306
+
|**insertedIds**|`string[]`| optional (default: `[]`) | Ids among landedIds whose row this load created (a successful insert); every other landed id was a pre-existing row a record key matched |
|[Automation Protocol](/docs/references/automation)| 16 | 81 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
26
26
|[Data Protocol](/docs/references/data)| 31 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
0 commit comments