Skip to content

docs(skills): teach the seed modes the loader runs (ignore default, replace deletes nothing) and that a deleted seed row stays deleted - #31097

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-ba8x76
Nov 7, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-ba8x76

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Irene · project thread

Fixes #31090

Item 3 of the card (the seed mode text in packages/spec/src/data/seed.zod.ts, comment 6702892720) is routed to the spec lane as #31099 (with the stale replace string in packages/cli/src/commands/diff.ts:81). This PR does not touch packages/spec; the wording owed is listed below.

Clause-②: no
Metadata-protocol: none

dispatch-gates --tier reads a MANDATORY model tier for skills/** (clause ① of the model-tiering ruling); it is waived by the standing order of the maintainer in this project (2026-11-03), and this card ran on the project's default model. The dispatching seat runs the contract review.

维护者速读

  • 改了什么:objectstack-data 技能的 references/seeds.md 改正种子模式:默认是 ignore(只建不改),replace 不删任何行,insert 每次重放都会重复插入;并补一句「AppPlugin 的启动和按组织重放不会把人删掉的种子行插回(发布、装包不记账,会插回),seed-replayer 带 { reseed: true } 才恢复」。content/docs/data-modeling/seed-data.mdx 新增一小节讲同一规则,并列出不记账的加载入口。
  • 为什么改:技能表格仍写 upsert(默认)和 replace =「先删光再插」,自 [Decision] A restart puts seeded records back: a customer's edit to a seeded row is overwritten by the next boot #30081 起已不成立;PR fix(metadata-protocol,runtime): a seed row a person deleted is no longer inserted again by the next seed replay (per-organization record of landed seed keys) #31083 落地的「已落地键」规则文档里没有,客户想找回示例数据没有文档可查。
  • 风险与回滚:只改文字和一条评测的期望描述;seeds.md token 1566 → 1560(上限不动);发 @objectstack/skills patch;回滚即 revert。
  • 先看:skills/objectstack-data/references/seeds.md 的「Import modes」表及其下一段,seed-data.mdx 新小节「A Deleted Seed Row Stays Deleted」,对照下文「Code sites」。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期退出 0,skills/objectstack-data/references/seeds.md is 1560 tokens (ceiling 1566; headroom 6)。
  • 需要你做的:skills/** 属 Tier H(人合),需要 os-zhuang 或 hotlong 的一次 approval,之后席位落地。

What changes

skills/objectstack-data/references/seeds.md (whole-file sweep):

-| `externalId` | `'name'` | Stable business key used for upsert / update lookup |
-| `mode` | `'upsert'` | Import strategy (see below) |
+| `externalId` | `'name'` | Stable business key every keyed mode matches on |
+| `mode` | `'ignore'` | Import strategy (see below) |
@@ Import modes
-| `upsert` (default) | Update by `externalId`, insert if missing. Idempotent. | Reference data, bootstrap rows |
-| `insert` | Insert all; fail on duplicate `externalId`. | Append-only / audit tables |
+| `ignore` (default) | Create-only: insert a missing row, leave a matched row as is. | Data the customer owns |
+| `upsert` | Insert if missing; every replay writes declared columns back. | Reference data kept current |
+| `insert` | Insert every record, no match: duplicates on each replay. | Rows loaded once |
 | `update` | Update only existing rows; never create. | Patching existing config |
-| `ignore` | Insert; silently skip duplicates. | Additive bootstrap |
-| `replace` ⚠️ | Delete everything, then insert. **Data loss.** | Cache / lookup tables only — never user data |
+| `replace` ⚠️ | Insert with no match; deletes nothing, the caller clears the table. | Cache / lookup tables only |
+
+A seed replays on each boot, install and re-publish. AppPlugin's boot and organization
+replays (not a publish or package install) do not re-insert a keyed row a
+person deleted; `seed-replayer` with `{ reseed: true }` restores it.
@@ Seed best practices
-| Default to `upsert` | Idempotent re-runs |
+| Leave `mode` unset; `upsert` only for reference data | Replays keep customer edits |
-| Use `replace` only on cache/lookup tables, with comments | Data-loss footgun |
+| `replace` only on cache tables the caller clears | It never deletes: duplicates otherwise |

Also in that file:

  • Quick start: the demo categorySeed (≈L36) drops mode: 'upsert', so demo data takes the create-only default (as seed-data.mdx teaches). The reference statusSeed (≈L26) keeps mode: 'upsert': reference data is the case upsert is for.
  • applyOn section: its example defineSeed(Invoice, …) gains mode: 'upsert'. Without it the example ran under ignore, where applyOn: 'create' changes nothing, so it silently relied on the old default.

skills/objectstack-data/evals/hooks-security-seeds-datasources.json, case 4. The demo Opportunity seed had no mode, yet the case required applyOn: 'create' to keep a replay off stage, which only matters under the old upsert default. The prompt now asks for demo rows refreshed from the seed on every replay except stage, so the right answer is mode: 'upsert' plus columns: { stage: { applyOn: 'create' } }, and expected_output says so. The assertions are unchanged and every one of them is now live (review S3: the dataset declares upsert, rather than dropping the must_contain).

content/docs/data-modeling/seed-data.mdx: a new subsection, "A Deleted Seed Row Stays Deleted", after the insert subsection: the landed-keys rule; the loads that keep and honour it (AppPlugin's single-tenant boot seed, per-organization replay and dev hot reload, for a known organization); the loads that keep no record, where a deleted row comes back (a seed publish, a package install or upgrade including the marketplace install, heal and sample-data reseed, a load with no organization, a composition without sys_seed_landed_key, the marketplace's fallback seed-replayer); the once-more case for a row deleted before the upgrade; and the seed-replayer { reseed: true } door. The ignore and upsert rows of the modes table link to it, scoped to "a boot or organization replay". The rest of the page (:74-80) already matched the loader.

Plus .changeset/skills-data-seed-modes-landed-keys.md: '@objectstack/skills': patch.

Why

An AI that follows the published skill writes seeds expecting upsert replays by default and a replace that clears the table. Neither is what the loader does since #30081. After #31021 (PR #31083), a seed row a person deleted stays deleted across replays, and nothing documented that or how to restore it.

Code sites (each taught fact, measured on main b631573)

Taught fact Code site
The default mode is ignore, and the loader's defaultMode is that same default packages/spec/src/data/seed.zod.ts:193 mode: SeedMode.default('ignore'); packages/spec/src/data/seed-loader.zod.ts:253; packages/metadata-protocol/src/seed-loader.ts:1303 dataset.mode || config.defaultMode
ignore: matched row skipped, missing row inserted packages/metadata-protocol/src/seed-loader.ts:3298-3304 (writeRecord), :3355-3356 (decideWriteAction)
upsert: matched row updated with the declared columns (skipped only on a no-op), missing row inserted seed-loader.ts:3284-3296, :3350-3354
insert: inserts every record, no match seed-loader.ts:3267-3270, :3357-3360
update: matched row updated, missing row skipped seed-loader.ts:3272-3282, :3345-3349
replace deletes nothing: it inserts, "caller should have cleared the table" seed-loader.ts:3306-3310, :3358-3360; no 'replace' branch anywhere in packages/metadata-protocol/src or packages/runtime/src clears a table
The record of landed keys is the organization-owned sys_seed_landed_key (cascades with the organization) packages/platform-objects/src/system/sys-seed-landed-key.object.ts:47, :88
Only upsert / ignore datasets read and write it seed-loader.ts:1406-1408
A recorded key whose row is gone is not inserted again; a never-recorded key still lands seed-loader.ts:1946-1951 isWithdrawnRow; option doc :671-690
A reference to the removed row is left unwritten, not reported unresolved seed-loader.ts:2701 (resolveReferenceItem answers withdrawn via isWithdrawnSeedTarget, :4119-4122), handled at :2066 and :2851
Loads that honour it: boot seed, per-organization replay, dev hot reload packages/runtime/src/app-plugin.ts:2098 (boot), :1996 (replayer), :2526 (hot reload), all through seedLoaderOptionsFor :84-98
The boot seed is single-tenant; a multi-tenant boot skips it for the per-organization replay app-plugin.ts:2069 (organizationWallActive), :2084 (multi-tenant mode — skipping inline seed)
Loads that keep no record (no landedKeys option) packages/metadata-protocol/src/protocol-publish-batch.ts:103 (publish apply); packages/runtime/src/domains/packages.ts:4729 (package install); packages/cloud-connection/src/marketplace-install-local-plugin.ts:2982, :4768, :4880 (install-local loads); its fallback seed-replayer :1689 (registerSeedReplayerIntoKernel, only when none is registered)
No organization or no sys_seed_landed_key: no record, said once seed-loader.ts:4051-4070
The deliberate re-seed: kernel service seed-replayer, (organizationId, { reseed: true }) app-plugin.ts:1963 (signature), :1996 (reseed === true → 'reseed'), :2042 → packages/runtime/src/seed-datasets.ts:41 service name; callable by server code holding a plugin context (ctx.getService), e.g. packages/plugins/organizations/src/organizations-plugin.ts:408; it has no HTTP route
A row deleted before this release comes back once, then is remembered PR #31083 body, "Boundaries, by design"; the first load records every row still present (seed-loader.ts:2491 recordLandedKeys)

Acceptance notes

Token budget (node scripts/check-skills-token-ratchet.mjs, exit 0)

File main head Ceiling
skills/objectstack-data/references/seeds.md 1566 1560 1566 (unchanged)
skills/objectstack-data/SKILL.md 6078 6078 (not touched) 6078
skills/objectstack-data/evals/hooks-security-seeds-datasources.json 1753 1786, measured, not ratcheted (eval fixture) —

No ceiling moves; the ratchet script is not touched. Every added line in seeds.md is at most 120 bytes (longest: the replace row, 119 B; ignore row 111 B, upsert row 106 B). The corrected rows plus the landed-keys lines were paid by deleting restatement in the same file:

Deleted Surviving home
Intro: "defineSeed() is type-safe: TypeScript checks every record's field keys against the object definition." Same file, Seed best practices row "Always use defineSeed(), never SeedSchema.parse() — Lose compile-time field checking otherwise"
applyOn intro: "A seed is replayed (every dev-server boot, every re-publish)" Same file, the line under the modes table: "A seed replays on each boot, install and re-publish."
applyOn intro: "right for master data," The modes table, upsert row: "Reference data kept current"
applyOn parenthesis: the mode-by-mode restatement, shortened to "update never writes it; no other mode updates a matched row" The modes table rows for ignore / insert / replace
Determinism gate: "CEL + pinned now is what guarantees that — using Date.now() will fail CI." Same file, Dynamic values (CEL): "This is the only correct way … new Date() ships the package author's clock to every customer and breaks build determinism."
Quick start categorySeed: mode: 'upsert', Not restatement: removed as a correction (demo data takes the default).

Review fixes (contract review 6703924081)

  • M1: seed-data.mdx names the loads that keep no record, so a deleted row comes back on a publish or a package install or upgrade, and says the boot seed is the single-tenant boot. The ignore row's "(unless a person deleted it)" now reads "(a boot or organization replay skips one a person deleted)".
  • S1: the mdx names the marketplace fallback seed-replayer, a load with no organization, and a composition without sys_seed_landed_key. seeds.md scopes its sentence to "AppPlugin's boot and organization replays (not a publish or package install)".
  • S2: seeds.md says again when a seed replays: "each boot, install and re-publish".
  • S3: eval case 4's demo dataset now declares upsert (prompt and expected_output); the must_contain list is unchanged and live.
  • S4: done by the seat (spec + cli: seed mode text still says replace deletes every row and omits the landed-keys rule (seed.zod.ts, os diff note) #31099); the body links it.
  • S5: the ignore and upsert rows are 111 B and 106 B.
  • N1: the mdx upsert row links the rule too. N2: the externalId row says "every keyed mode matches on".

Sweep hits

Grep for upsert (default), default.*upsert and replace.*delete (plus the seed mode claims by hand) over skills/objectstack-data/SKILL.md, rules/*.md, evals/*.json and references/:

  • references/seeds.md:51 (externalId "upsert / update lookup", N2), :52 (mode default), :63 (upsert (default)), :64 (insert "fail on duplicate"), :66 (ignore "silently skip duplicates"), :67 (replace "Delete everything"), :160 ("Default to upsert"), :163 (replace "Data-loss footgun"): fixed.
  • references/seeds.md:36 (demo seed mode: 'upsert') and :82-86 (applyOn example without mode): fixed, as above. :26 (reference data mode: 'upsert'): correct, left.
  • evals/hooks-security-seeds-datasources.json case 4: implicit old default (it required applyOn: 'create' on a dataset with no mode), fixed as above.
  • SKILL.md, rules/*.md, evals/objects-fields-relationships.json: no hit. (SKILL.md:104 "derived verbs (search/export/upsert/…)" is the API verb, not a seed mode.)

Other content/docs pages that restate seed modes: the generated reference pages (references/data/seed.mdx:33, references/data/seed-loader.mdx:198, references/api/package-api.mdx:246, references/kernel/manifest.mdx:100) already say ignore is the default and replace inserts into a table the caller cleared; they pick up the spec wording below when it lands. No hand-written page outside seed-data.mdx restates them wrongly. Releases pages were not read for edits (release-owned).

Outside both lanes, found while measuring: packages/cli/src/commands/diff.ts:81 still says replace means "a replay deletes every row of the object and inserts these". It is CLI output text, carried on #31099; not touched here.

Spec wording owed (spec lane, item 3, filed as #31099; not edited here)

packages/spec/src/data/seed.zod.ts:

  • :27 enum comment 'replace', // Delete ALL records in object then insert (Dangerous - use for cache tables) → e.g. // Insert with no match; the loader deletes nothing, the caller clears the table (cache tables).
  • :24 enum comment 'insert', // Try to insert, fail on duplicate → e.g. // Insert every record with no match: a replay duplicates rows unless a unique index refuses them.
  • :171-172 docstring "a load inserts the records whose row is missing" and :195-196 .describe() "a missing row is inserted and an existing row is left as it is" → add: a missing row whose key already landed in the organization (a person removed it) is not inserted again by AppPlugin's loads (boot, per-organization replay, dev hot reload); the seed-replayer service with { reseed: true } inserts it.
  • :116-117 the strict-object history string "on the defaults (mode: upsert, …)" describes the default at the time; accurate as history, optional to annotate.

Tests

None: the diff is teaching text and one eval expectation, with no runtime behaviour to test. The gates below are the checks.

Verification

Local gates on b739914: 68 derived, 68 run with exit 0, 0 NOT MEASURED (`dispatch-gates --ran`)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 68 commands (43 pnpm, 25 direct node) for the 4 changed paths. Each ran under a 900 s timeout with NODE_OPTIONS=--max-old-space-size=8192, its exit code captured before any pipe, over the full pnpm build (exit 0) made at 6ed361c (the review commit touches no built package). All 68 exited 0 at 6ed361c and again at b739914.
  • They include check-skills-token-ratchet.mjs, check:skill-examples, check:skill-refs, check:skill-compatibility, check:skill-frame-sync, check:skill-identifier-liveness, check:doc-authoring, check:doc-anchors, check:docs, check:dts-closure, check:dual-build-cjs-loads, check-changeset-no-major.mjs, check-empty-changeset.mjs and check-comment-mask-corpus.mjs.
  • --ran printed: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3).
  • Also run, outside the derived list: pnpm check:changeset-fixed (exit 0), the one artifact-roster family whose roster sits under .changeset.
  • node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-ba8x76: GOVERNED, Tier H (人合): skills/**; 86 changed lines (+63 / −23), under 3000.
  • node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0 before each push.
  • main moved after the branch point; none of its commits touches these files or the ratchet.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema touched.)
  • 2. Every name I added uses the casing for its kind. (No names added.)
  • 3. No formatter ran. (Markdown, MDX and one JSON string only.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. skills/** publishes @objectstack/skills, so the PR carries a '@objectstack/skills': patch changeset. Both declaration lines are filled in above.
  • 6. packages/spec not touched; nothing to regenerate.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is Chinese, per the lane template.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8VVAkA4qny8vRgP2NzRgs

… seed row stays deleted

The objectstack-data seeds reference called upsert the default and said
replace deletes every row first. The loader defaults to ignore
(create-only) and replace deletes nothing. The reference, its seed eval
and the data-modeling seed guide now say so, and teach that a boot or
organization replay does not re-insert a seed row a person deleted,
with seed-replayer { reseed: true } as the way to restore it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAkA4qny8vRgP2NzRgs
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 7, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 6ed361c · 2026-11-07T07:22Z

Full contract review of PR #31097 (#31090, claim 6703601270). The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6ed361c83cabbb5b93fe8d3ee59e3c60dac3c844
Local-runs: probe — node scripts/check-skills-token-ratchet.mjs in a throwaway worktree at the head (exit 0, removed after), to read the token budget the PR claims; check-governed-merges --pr 31097 is a platform read (GOVERNED, Tier H)
Scope: skills/objectstack-data/references/seeds.md, skills/objectstack-data/evals/hooks-security-seeds-datasources.json, content/docs/data-modeling/seed-data.mdx, .changeset/skills-data-seed-modes-landed-keys.md; cited code, read on origin/main 9c1edbcd2 (none of these paths moved since merge base b63157335): packages/metadata-protocol/src/seed-loader.ts (writeRecord, decideWriteAction, SeedLoaderServiceOptions.landedKeys, openLandedKeyLedger, isWithdrawnRow, the reference withdrawn outcome), packages/runtime/src/app-plugin.ts (seedLoaderOptionsFor, the boot seed, the seed-replayer closure, the hot-reload seeder), packages/runtime/src/seed-datasets.ts, packages/runtime/src/domains/packages.ts, packages/metadata-protocol/src/protocol-publish-batch.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts (its own loads and its fallback seed-replayer), packages/spec/src/data/seed.zod.ts, packages/spec/src/data/seed-loader.zod.ts, packages/plugins/organizations/src/organizations-plugin.ts, packages/cli/src/commands/diff.ts, scripts/check-skills-token-ratchet.mjs, scripts/pm/check-governed-merges.mjs

① Derived judgments

Inputs: card #31090 (body; item 3 comment 6702892720; the triage; the unlock 6703573368; the claim 6703601270), PR #31083 (its body, "Boundaries, by design"), the PR body, its 4-file list and the net diff against merge base b63157335. Each taught fact was checked against the code on main:

  • The default. TRUE. SeedMode.default('ignore') in seed.zod.ts, and the loader's defaultMode is the same schema default. seeds.md's field table and modes table now say ignore.
  • ignore. TRUE. A matched row is skipped. A missing row is inserted (writeRecord and decideWriteAction agree).
  • upsert. TRUE. A matched row is updated with the declared columns, or skipped on a no-op replay. A missing row is inserted.
  • insert. TRUE. Every record is inserted with no match, so a replay duplicates rows (unless a unique index refuses them).
  • update. TRUE. A matched row is updated. A missing row is skipped.
  • replace deletes nothing. TRUE. Its branch only inserts ("caller should have cleared the table"). No 'replace' branch in the loader or runtime clears a table. The only text that still says it deletes is CLI output in diff.ts:81, which the PR body names and leaves out of lane.
  • applyOn sentence "no other mode updates a matched row". TRUE. ignore skips, and insert / replace never match.
  • Landed keys: where they are kept. TRUE as far as it goes. The record is sys_seed_landed_key, per organization, read and written only by upsert / ignore datasets. A recorded key whose row is gone is not inserted. A never-recorded key still lands. A reference to a removed row is left unwritten, not reported. A row deleted before the upgrade comes back once (the PR fix(metadata-protocol,runtime): a seed row a person deleted is no longer inserted again by the next seed replay (per-organization record of landed seed keys) #31083 body says so).
  • Landed keys: which loads honour it. Only the three loads AppPlugin builds pass landedKeys: the boot seed, the seed-replayer closure, and the dev hot-reload seeder. Both pages name exactly these. The loads that keep no record are: the package publish apply (protocol-publish-batch.ts), the runtime package install (domains/packages.ts), and the marketplace install-local install, heal and reseed. A load with no organization, or a composition without sys_seed_landed_key, keeps none either.
    ⛔ must-fix M1, seed-data.mdx, new subsection and the ignore row. The same page (L66-69) defines a replay as "every boot …, every package install or upgrade, and every publish". The new section then says "the next replay does not insert it again", and the ignore row says it inserts a missing row "(unless a person deleted it)". Neither is true for a package install, upgrade or publish: those loads keep no record, so a deleted seed row comes back there. The card asks which replays honour the rule and which do not, and the page only names the first set. Add one sentence, for example: "A package install or upgrade (os package install, the marketplace install) and a seed publish keep no record, so a deleted row comes back on those." Also say the boot seed is the single-tenant boot (a multi-tenant boot does not seed). The page has no token budget, so this costs nothing against the ratchet.
  • The reseed door. TRUE. The kernel service is seed-replayer (seed-datasets.ts:41), with signature (organizationId, { userId?, reseed? }). reseed === true maps to 'reseed', which inserts every missing row and records it again. Server code with a plugin context calls it (ctx.getService, as organizations-plugin.ts:408 does). Neither page invents an HTTP route.
  • should-fix S1, the organization replay that is not AppPlugin's. marketplace-install-local-plugin.ts registers its own fallback seed-replayer when no AppPlugin app has seeds. That one takes no reseed and keeps no record. So in that composition, seeds.md's "an organization replay does not re-insert" is false. ({ reseed: true } still has the right effect there, since it never withholds a row.) The no-organization and no-sys_seed_landed_key cases are missing too. One clause in the mdx is enough, for example "only where AppPlugin runs the load and the organization is known". seeds.md can stay as it is if the mdx carries this.
  • Deleted restatements in seeds.md. Four of five homes carry the fact:
    • "defineSeed() is type-safe" is carried by the best-practices row ("Lose compile-time field checking otherwise").
    • "right for master data" is carried by the upsert row ("Reference data the package keeps current").
    • The mode-by-mode parenthesis is carried by the modes table.
    • "CEL + pinned now … Date.now() will fail CI" is carried by Dynamic values (CEL) ("single per-load pinned now", "new Date() … breaks build determinism") plus the gate sentence that stays.
    • should-fix S2: "A seed is replayed (every dev-server boot, every re-publish)" has no surviving home. The named home, the modes table, says "every replay" but not when a replay happens. The re-publish half appears nowhere else in the skill (SKILL.md and rules/*.md were grepped). It matters here because a re-publish is one of the loads that does not honour landed keys. Either keep a short "(each boot and re-publish)" or let the landed-keys line say "a boot or organization replay (not a publish or package install)". That costs about 6 tokens, and there are 11 of headroom.
  • Eval case 4. It is inside the claim's evals clause: the old expectation leaned on an upsert default for a demo dataset with no mode. Only expected_output changed. The assertions are byte-identical. The new text is true: the ignore default never writes a matched row, and applyOn: 'create' is what protects stage on an upsert dataset.
    • should-fix S3: must_contain still requires applyOn: 'create'. For the demo Opportunity seed it is now inert, so an answer that follows the corrected skill (no mode, no applyOn) fails the case. Either reword the prompt so stage sits on an upsert dataset, or accept it and track it on the card. Changing the assertion is inside the claim's evals clause, but it is not wording-only.
  • The applyOn example gains mode: 'upsert'. CORRECT. Under ignore, applyOn: 'create' changes nothing, so the old example relied on the old default.
  • The demo categorySeed drops mode: 'upsert'. CORRECT. Demo data takes the create-only default, which matches the mdx and the new best-practices row. The reference statusSeed keeps upsert, which is right.

② Semver level

'@objectstack/skills': patch is right. Only skill prose and an eval fixture ship. content/docs does not publish. The behaviour itself shipped in #30081 and #31083. Clause-②: no and Metadata-protocol: none are right, because no schema, contract or metadata meaning moves here. Both lines are line-initial in the changeset and in the PR body. No skip-changeset label is set (labels: documentation, size/s, tooling).

③ Boundary flags

  • Diff against the claim. The diff stays inside the claim: seeds.md, one eval in evals/*.json, seed-data.mdx and one changeset. SKILL.md and rules/*.md are untouched, and the sweep found no hit there. The ratchet script is unchanged against main. packages/spec is untouched, so item 3 is not in this PR.
  • Tier. check-governed-merges --pr 31097: GOVERNED, Tier H, skills/** ×2 (seeds.md and the eval). The size is 70 lines (+50 / −20), under 3000. Landing needs the maintainer's merge or an APPROVED review from GOVERNED_APPROVERS. The PR body says so. The PR is a draft, and mergeable_state is blocked.
  • PR body. It says Fixes #31090. It routes item 3 and gives the spec wording owed for seed.zod.ts :24, :27, :171-172, :195-196 and :116-117. That wording is accurate against main. The 维护者速读 section has six lines. The body also gives the code-site table, the token readings (which match mine), the sweep hits and the deleted-restatement table. The model waiver is stated without naming a model. No model identifier appears in the body, the diff or the commit (the trailer is model-free).
  • should-fix S4, item 3 has no card. The body says item 3 "is routed to the spec lane on a separate card". No such card is linked on docs + skills: teach that a deleted seed row stays deleted (#31021), and fix the stale seed-mode table in the objectstack-data skill #31090, and a search finds none. File it and link it from the card before merge. Put the diff.ts:81 CLI string on the same card or a sibling, so neither stale replace text gets lost.

④ Design judgment

  • One home per rule. Yes. The modes have one table in each surface. The landed-keys rule has its home in the mdx subsection, and seeds.md carries one sentence plus the reseed call. M1 and S1 only ask that home to name the doors that keep no record.
  • Token budget. Counted as ceil(bytes/4) with the ceilings from check-skills-token-ratchet.mjs on main. No row is raised.
File main head ceiling
references/seeds.md 1566 1555 1566
objectstack-data/SKILL.md 6078 6078 6078
evals/hooks-security-seeds-datasources.json 1753 1777 not ratcheted

The script exits 0, and the readings match the PR body.

  • should-fix S5, line length. seeds.md adds two lines over 120 B, the ignore and upsert table rows, at 123 B each. The longest line on main was 118 B. Trim each by a few words, for example "every replay writes declared columns back". That also pays toward S2. (The changeset paragraph and the mdx table row are also long, but those files are not skill files.)
  • nit N1. The mdx modes table links the landed-keys rule from the ignore row only. upsert withholds a removed row too, so link it there as well.
  • nit N2. seeds.md L48 says externalId is "used for upsert / update lookup". It is also the ignore default's match key, and the landed key. Optional to add.
  • Check-runs on this head, read 2026-11-07T07:26Z: 42 runs, 25 success, 12 skipped, 5 in progress, 0 failed.

Must-fix: M1. Should-fix: S1, S2, S3, S4, S5. Nit: N1, N2.

Implemented-by: claude/project-thread-ba8x76
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: FAIL

…the record

The landed-keys rule holds only where AppPlugin runs the load for a
known organization. The seed guide now names the loads that keep no
record (a publish, a package install or upgrade, the marketplace
fallback replayer, a load with no organization or no
sys_seed_landed_key), and the seeds reference scopes its sentence the
same way and says again when a seed replays. Its two long table rows
are trimmed under 120 bytes, the externalId row names every keyed
mode, and the seed eval's demo dataset declares upsert so applyOn
'create' is live.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8VVAkA4qny8vRgP2NzRgs
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head b739914 · 2026-11-07T07:43Z

Delta review against the FAIL at head 6ed361c (comment 6703924081: must-fix M1, should-fixes S1–S5, nits N1–N2). The delta is one commit answering M1, S1–S3, S5, N1 and N2, plus the PR body (patched by the seat). S4 is filed as #31099. The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b7399145351eb7d9fe35d157fde4127a403f12c2
Local-runs: none
Scope: delta review 1. git diff 6ed361c83 b73991453: one commit, no rebase, merge base still b63157335. It touches seeds.md, eval case 4, seed-data.mdx and the changeset. The patched PR body and #31099 were read too. Cited code was read on origin/main 9c1edbcd2: packages/runtime/src/app-plugin.ts (the boot seed's multi-tenant skip, the seed-replayer closure, registerSeedReplayerOnce), packages/cloud-connection/src/marketplace-install-local-plugin.ts (its four SeedLoaderService constructions, buildSeedReplayer, the kernel:ready rehydrate), packages/plugins/organizations/src/organizations-plugin.ts (the replay on sys_organization insert), packages/metadata-protocol/src/protocol-publish-batch.ts, packages/runtime/src/domains/packages.ts, packages/metadata-protocol/src/seed-loader.ts (openLandedKeyLedger), scripts/check-skills-token-ratchet.mjs (the ceiling row)

① Derived judgments

Each fix from the first review was checked against the code on main:

  • M1, resolved. The mdx now names the loads that keep no record: a seed publish, a package install or upgrade, and the marketplace install, its heal and its sample-data reseed. It says a deleted row comes back on those. This is TRUE.
    • protocol-publish-batch.ts and domains/packages.ts pass no landedKeys.
    • The marketplace builds its four loaders with (ql, metadata, ctx.logger), so no options at all.
    • The boot seed is the single-tenant one. A walled boot suppresses it (multi-tenant-replay).
    • The ignore row reads "(a boot or organization replay skips one a person deleted)". The new section opens with "the next boot or organization replay". Both are scoped by the paragraph that follows ("only where AppPlugin runs the load for a known organization").
  • S1, resolved.
    • A load with no organization and a composition without sys_seed_landed_key keep no record (openLandedKeyLedger). TRUE.
    • The marketplace's fallback seed-replayer keeps no record either (buildSeedReplayer → runInlineSeed, with no landedKeys and no reseed). TRUE.
    • "Which it registers when no app has seed data" is TRUE. AppPlugin registers its replayer during start(). The marketplace registers through register-once, on install or on its kernel:ready rehydrate, which is after AppPlugin. So the fallback wins only when no AppPlugin app registered seeds.
    • seeds.md's "AppPlugin's boot and organization replays (not a publish or package install)" is accurate.
    • "Organization replay" is a fair name for seed-replayer. The code calls it "the per-organization replay" and logs [seed-replayer] organization …. seeds.md names the service in the same sentence, so the two connect.
  • S2, resolved. "A seed replays on each boot, install and re-publish" restores the deleted fact. Naming install here is consistent: an install is a replay, and the next sentence says it does not keep the record. The PR body's homes table now points at this line.
  • S3, resolved. The case 4 prompt now asks for demo rows refreshed on every replay except stage. expected_output gives mode: 'upsert' plus columns: { stage: { applyOn: 'create' } }. "The ignore default would refresh nothing" is TRUE. The assertions are byte-identical (no +/- line touches them), and every must_contain entry is now live. The prompt edit is inside the claim's evals clause. That clause is the file hit, and the case taught the old default.
  • S4, resolved by the seat. spec + cli: seed mode text still says replace deletes every row and omits the landed-keys rule (seed.zod.ts, os diff note) #31099 is filed (open). It carries the seed.zod.ts wording and the diff.ts:81 CLI string, and the body links it.
  • S5, resolved. The longest added seeds.md line is 119 B (the replace row, unchanged here). The ignore row is 111 B and the upsert row is 106 B.
  • N1, resolved. The mdx upsert row links the rule.
  • N2, resolved. "Stable business key every keyed mode matches on" is TRUE: upsert, update and ignore match on it. insert and replace match nothing.
  • No new false claim. Two loose phrasings remain. Neither is false about the rule, and neither blocks:
    • nit N3, seed-data.mdx: "a multi-tenant boot seeds through the per-organization replay instead". A walled boot does not seed at all. Each organization is seeded by the replay when it is created (OrganizationsPlugin's middleware on sys_organization insert) or when a host calls the service. Suggested: "a multi-tenant runtime seeds each organization through the per-organization replay when it is created".
    • nit N4, seeds.md: "on each boot" holds for a single-tenant boot only. The deleted text had the same reach ("every dev-server boot"). "each single-tenant boot" costs about 2 tokens, and there are 6 of headroom.
  • nit N5, eval case 4 against the best-practices row. The row says "upsert only for reference data", and case 4 now expects upsert on demo rows. The prompt asks for refresh, so the answer is right for the prompt. This is just worth knowing if the case is read as a model answer for demo data.

② Semver level

Unchanged: '@objectstack/skills': patch. The changeset text now says AppPlugin's boot and organization replays keep the record, and a publish or package install keeps none. That matches the pages. Clause-②: no and Metadata-protocol: none are still line-initial in the changeset and the body.

③ Boundary flags

  • Diff against the claim. It is still inside the claim. The three-dot diff from main is the same 4 files: seeds.md, one eval, seed-data.mdx and the changeset. The ratchet row is still 1566. Neither the ratchet script nor packages/** is in the PR diff. (A two-dot diff shows packages/qa/scenarios only because main moved.)
  • Tier. Unchanged: GOVERNED Tier H on skills/** ×2 (the first review's check-governed-merges reading). The size is +63 / −23 against main, under 3000. The PR is a draft, and mergeable_state is blocked.
  • PR body. It has Fixes #31090 and item 3 routed as spec + cli: seed mode text still says replace deletes every row and omits the landed-keys rule (seed.zod.ts, os diff note) #31099. The 维护者速读 section has six lines, with tokens 1566 → 1560 and headroom 6. It has the review-fixes list and the updated homes table. The model waiver still names no model. No model identifier appears in the body, the diff or the commit trailer.
  • nit N6, PR body. 维护者速读 says "只改文字和一条评测的期望描述", and Tests says "one eval expectation". Case 4's prompt changed too. Say "一条评测的提示与期望" / "one eval case (prompt and expectation)".

④ Design judgment

  • One home per rule. It still holds. The mdx subsection is the full home, with both the loads that keep the record and the ones that do not. seeds.md carries one scoped sentence and the reseed call.
  • Token budget. Counted as ceil(bytes/4), ceiling from the script on the head:
File main head ceiling
references/seeds.md 1566 1560 1566
objectstack-data/SKILL.md 6078 6078 (untouched) 6078
evals/hooks-security-seeds-datasources.json 1753 1786 not ratcheted

These match the PR body.

  • Line length. No added seeds.md line is over 120 B. The two long added lines are JSON strings in the eval fixture (prompt and expected_output), which cannot wrap, as on main.
  • Check-runs on this head, read 2026-11-07T07:45Z: 45 runs, 21 success, 14 skipped, 10 in progress, 0 failed.

Must-fix: none. Should-fix: none. Nit: N3, N4, N5, N6.

Implemented-by: claude/project-thread-ba8x76
Reviewed-by: session_01J8VVAj7CvjkGXvgXzd6RZ7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7

维护者速读

席位核对 — ACCEPT · domain:skills#3 · head b7399145351eb7d9fe35d157fde4127a403f12c2

  1. 形态: draft → main,Fixes #31090;'@objectstack/skills': patch,Clause-②: no,Metadata-protocol: none;作者 objectstack-fleet[bot];4 个文件,在认领面之内(seeds.md、seed-data.mdx、evals/hooks-security-seeds-datasources.json、changeset)。
  2. 前提实测: 复核逐项对照 main 代码:seed-loader.ts 五种模式与默认、replace 只插入;app-plugin.ts 的启动种子、seed-replayer 闭包与热重载传 landedKeys;protocol-publish-batch.ts、domains/packages.ts、marketplace-install-local-plugin.ts 不记账。
  3. 门禁(钉 head b7399145): dev 派生门禁 68 条全部 exit 0;check-runs 48 条:34 success、14 skip、0 失败;棘轮 seeds.md 1560/1566,无行变动;新增行均 ≤ 120 B。check-governed-merges --pr 31097:Tier H(skills/**)。
  4. 接受的偏差: 评测 case 4 的提示改为「每次重放都刷新、stage 除外」,断言逐字未变,在认领的 evals 条款之内。增量复核的四个 nit(N3 多租户启动不播种的措辞、N4「每次启动」仅指单租户、N5、N6)不阻塞,留给下一位改这两页的人。
  5. 范围外去向: packages/spec 的 seed.zod.ts 文案(item 3)与 os diff 的 replace 提示 → spec + cli: seed mode text still says replace deletes every row and omits the landed-keys rule (seed.zod.ts, os diff note) #31099。
  6. 落地条款: Tier H。保持 draft,挂 pm:awaiting-merge,请 os-zhuang、hotlong 审批;获批后本席过落地前检、清标、ready、auto-merge;维护者直合亦可。

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 7, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 7, 2026 07:58
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit 2178d80 Nov 7, 2026
54 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-ba8x76 branch November 7, 2026 08:19
@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs + skills: teach that a deleted seed row stays deleted (#31021), and fix the stale seed-mode table in the objectstack-data skill

2 participants