Skip to content

feat(spec)!: a seed is create-only by default — an omitted mode is ignore - #30152

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-2k4081
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-2k4081

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Requested by Yibo Wang · project thread

Fixes #30081

Ruling-ref: 6631016988
Clause-②: yes (narrowing)
Metadata-protocol: semantic
ADR-0087: D3 semantic entry seed-mode-default-create-only (registered in the changeset). No D2 conversion: a compiled artifact's written "mode": "upsert" cannot be told from an authored one, so a package built before this change keeps today's replay until it is rebuilt, and nothing converts it. An os package install keeps the installed body in its install ledger and its sample-data reseed replays that body, so such an install keeps overwriting on reseed until the rebuilt artifact is re-installed. A stored seed metadata item with no mode is not rewritten: its next publish parses it to ignore, with no signal.
Skills-docs: owed — the skills lane is paused and skills/** is Tier H, so this PR does not edit it. Owed lines: skills/objectstack-data/references/seeds.md:36 (a demo dataset taught as mode: 'upsert'), :52 and :63 (upsert named the default), :160 ("Default to upsert"); skills/objectstack-data/evals/hooks-security-seeds-datasources.json:37 (an applyOn: 'create' column on a dataset with no mode, which now changes nothing); skills/objectstack-platform/SKILL.md:377 (upsert default). seeds.md:26 (reference data declaring upsert) stays correct.
Prior rulings read: seed,seedmode,applyon → 18 ADR hits (check-prior-rulings --card 30081); none rules on a seed's mode default. ADR-0057 D6 and ADR-0066 D5 cover the boot reconcile of stack-declared roles, units and policies, a separate path from SeedSchema datasets; ADR-0131 retired per-organization platform seeding and leaves the per-organization replay of an app's datasets as it was, which now follows the same default. Governing: maintainer ruling 6631016988 on this card (measure production first, then option A) and the seat's contract look 6633227800. Also read: #25761 (columns.FIELD.applyOn), kept as the escape for an upsert dataset. Thread: 6 comments, 0 ruling-shaped by first line; ruling 6631016988 read by hand. Repo: objectstack-ai/objectstack.

维护者速读

  • 改了什么:种子数据集不写 mode 时,默认值从 upsert 改为 ignore(只插入缺失的行,不改已有行);SeedLoaderConfig.defaultMode 直接就是这个字段,六处重放调用点不再各自写死 'upsert';CRM、showcase、todo 三个示例共 24 个业务数据集去掉了显式 upsert。
  • 为什么改:生产实测(评论 6633189577)证明,os start/os serve 每次重启、启动产物升级、os package install 升级,都会把客户对种子行的修改静默改回出厂值;按裁决 6631016988 选 A。
  • 风险与回滚:这是破坏性改动。依赖重放刷新的数据集需要显式写 mode: 'upsert';改动前构建的包产物里已写死 "mode":"upsert",重新构建前仍按旧行为重放(不做猜测式转换);用 os package install 装过的包还要用新产物重新安装,否则其 reseed 仍会覆盖。回滚就是 revert 本 PR。
  • 先看:packages/spec/src/data/seed.zod.ts 的 mode 字段、packages/runtime/src/seed-default-create-only-replay.integration.test.ts,以及下面的普查表。
  • 两分钟验证:cd packages/runtime && npx vitest run --project local src/seed-default-create-only-replay.integration.test.ts(真实 SQLite 驱动:修改保留、缺行补插、未动行 updated_at 不变;显式 upsert 对照组仍回写)。
  • 需要你做的:Skills-docs 行列出的 skills/** 几行待技能通道恢复后补改;其余无需操作。

What changes

  • @objectstack/spec. SeedSchema.mode defaults to ignore. The loader already treats ignore as create-only, so no mode value is added: a replay inserts a record whose row is missing and leaves an existing row as it is. The mode describe and TSDoc say so, name mode: 'upsert' (with columns: { FIELD: { applyOn: 'create' } }) as the way to keep reference data current, and state the compiled-artifact disposition. SeedLoaderConfig.defaultMode is now SeedSchema.shape.mode itself, so the default is stated once. Three DEFAULT_CHANGES_BY_MAJOR[18] rows (Seed:mode, SeedLoaderConfig:defaultMode, SeedLoaderRequest:config) and the D3 semantic entry seed-mode-default-create-only declare the move. The authorable-defaults projection and the generated reference pages follow from the build.
  • The six replay sites stop restating defaultMode: 'upsert': packages/runtime/src/app-plugin.ts (the per-organization replayer, the inline boot seed and the dev hot reload), packages/runtime/src/domains/packages.ts (applyPublishedSeeds), packages/metadata-protocol/src/protocol-publish-batch.ts, and packages/cloud-connection/src/marketplace-install-local-plugin.ts (runInlineSeed: install, reseed and the rehydrate healer). The save door (protocol-save-door-refusals.ts) reads the schema's default for a seed item that declares no mode, instead of its own 'upsert' fallback.
  • Census (below): the reference apps' business datasets drop their explicit mode: 'upsert'. Each file says why it declares no mode, and that os dev --fresh is how a seed edit is seen again.
  • Docs: the seed-data guide leads with the create-only default, moves upsert to reference data, and carries a callout on packages built before the change. The getting-started example and the stack's data docblock follow.
  • Changeset: minor for @objectstack/spec, @objectstack/runtime, @objectstack/metadata-protocol and @objectstack/cloud-connection. It carries BREAKING; before and now; compiled artifacts, installed packages and stored seed items; and the remedy (rebuild, re-install what os package install installed, purge then reseed to get the shipped values back).
  • Install-local reseed docblock: it no longer promises "a clean baseline back after editing demo rows". A reseed puts back missing rows in each dataset's stored mode; purge first to reset edited rows.

Phase 1 measurement (comment 6633189577, f68b47ca, before any change)

A seed dataset with no mode; a customer PATCH to a seeded row; then a replay. Every boot ran the built CLI with NODE_ENV unset, which the CLI defaults to production.

Path After the replay
os start --artifact, restart on the same home edit overwritten (updated: 1)
os serve, restart edit overwritten
boot artifact 1.0.0 → 1.1.0 edit overwritten
os package install 1.0.0, edit, then 1.1.0 edit overwritten (inline seed … updated=1)
that runtime restarted (rehydrate) edit kept: no seed runs

No signal reached the customer: the replay logs at info under a warn default, the banner reads Seeds: … 2 rows, and the install prints nothing about rewritten rows.

Census: in-tree seeds that relied on the overwrite

Seed Datasets Before Now Reason
examples/app-crm/src/data/index.ts accounts, contacts, opportunities, leads, activities (5) mode: 'upsert' no mode (create-only) The customer's business data: the card's own reproduction, and the seat's contract look names it.
examples/app-showcase/src/data/seed/index.ts accounts, contacts, inquiries, products, projects, tasks, categories, businessUnits, orgUnits (sys_business_unit), teams, fieldZoo, invoices, invoiceLines, expenseReports, expenseLines, preferences, announcements, clientBriefs (18) mode: 'upsert' no mode The demo workspace's business data, which a user edits once it is seeded. orgUnits is environment-owned admin data by its own comment; its fixed ids, which metadata references, are still inserted when missing. memberships already declared ignore and is unchanged.
examples/app-todo/src/data/index.ts tasks (1) mode: 'upsert' no mode The user's own tasks.
Kept as upsert none No in-tree dataset is reference data the package must keep refreshed.
Platform packages, plugins, services, create-objectstack templates none No defineSeed / SeedSchema.parse dataset ships outside packages/spec itself and the three examples.
Test fixtures, unchanged packages/cli/test/fixtures/option-b-collection-zoo.ts:246, packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.ts:72 mode: 'upsert' unchanged They declare the mode they test. Loader tests that pass a raw defaultMode: 'upsert' with unparsed datasets pin upsert replay and are unchanged; their comments that claimed to mirror the AppPlugin request now say what that request is.

Tests

  • Real driver (packages/runtime/src/seed-default-create-only-replay.integration.test.ts, new). SqlDriver (better-sqlite3) under ObjectQL. The request is built as every replay site now builds it: parsed, with no mode of its own. A first version is loaded, a customer edits one row, then a second version replays (it changes the edited row's value and an untouched row's value, and adds a record). With no mode: the edit survives, the added record is inserted, and the untouched row keeps its value and its updated_at (Date faked per step, so the stamps differ). Control, mode: 'upsert' declared: the shipped value is written back over the edit and onto the untouched row, whose updated_at moves.
    • Ablation: with the default put back to upsert, the default arm fails (expected 2 to be 0 updates) and the parse case fails (expected 'upsert' to be 'ignore'); the control still passes.
  • Install-local reseed (packages/cloud-connection/src/marketplace-install-local-reseed.test.ts). It captures the request the reseed hands the loader and parses it with the real SeedLoaderRequestSchema. A mode-less dataset arrives as ignore, a declared upsert stays upsert, and the config carries no defaultMode. The heal test stops expecting the restated defaultMode: 'upsert'.
    • Ablation: with defaultMode: 'upsert' restored in runInlineSeed, both the reseed pin and the heal test fail.
  • Boot inline seed (packages/runtime/src/seed-datasets.test.ts, round 1). A single-tenant boot's inline seed hands the loader a multi-pass request with no defaultMode. The per-organization replayer and the install-local doors are pinned the same way.
    • Ablation: with defaultMode: 'upsert' restored at app-plugin.ts's inline config, it fails (expected { multiPass: true, …(2) } to not have property "defaultMode").
  • Spec: an omitted mode parses to ignore; every authored mode parses as written; SeedLoaderConfig.defaultMode equals the seed default; a mode-less dataset in a parsed SeedLoaderRequest reaches the loader as ignore.

Acceptance notes

  • Compiled artifacts (finding, as the contract look asked). os build writes the parsed seed into dist/objectstack.json, default included, so every package built before this PR ships "mode": "upsert" and keeps overwriting until it is rebuilt. The loader cannot tell that value from an authored one, so nothing converts it (contract look item 3). This is why the changeset's remedy leads with "rebuild". A follow-up could stop os build from writing a seed's defaulted mode into the artifact, so a future default move reaches a package without a rebuild; that is outside this card's scope.
  • Installed packages (round 1). The install-local ledger keeps the body as installed (marketplace-install-local-plugin.ts, rehydrate and seedDatasetsOf(entry.manifest)). Only the sample-data reseed replays it against existing rows, in the body's stored mode. A pre-change install keeps overwriting there until the rebuilt artifact is re-installed; a post-change install reseeds create-only. The rehydrate seeds nothing, the heal seeds only when every seeded object is empty, and the per-organization replay runs only for a new organization, so none of them overwrites. To get the shipped values back over edits: purge, then reseed.
  • Stored seed items (round 1). A sys_metadata seed item with no mode is replayed on its next publish (protocol-publish-batch.ts). It now parses to ignore, so that publish inserts only missing rows. Nothing is rewritten and nothing signals it; the changeset and the D3 reason say so.
  • What create-only gives up. A value changed in a later seed version reaches only a row the load creates. A seed that relied on the overwrite to keep relative dates fresh (the CRM's daysFromNow/daysAgo) now resolves them once, at first insert. Each example file says so and points at os dev --fresh.
  • Per-organization replay follows the same default with no change of its own (it already ran a parsed request), as the contract look scoped it.
  • Not in scope: the env default.

Verification

Round 1 (585143c7, contract review 6636509452): pnpm --filter @objectstack/spec build, then check:generated: green. Spec seed, seed-loader, migrations and scripts/lib tests: 276 passed. The cloud-connection reseed and heal tests: 15 passed. seed-datasets.test.ts: 8 passed. dispatch-gates --commands at 585143c7: 114 derived, run one by one with this body as the event, all 114 exit 0; --ran reconciles 114 run, 0 NOT-MEASURED, 0 UNRUN. The round 1 diff is text and one test, so the round 0 dogfood battery below stands for the code.

Round 0 (6353695e):

  • pnpm --filter @objectstack/spec build, then check:generated (after gen:docs): green.
  • GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event F (F carries this body's declarations): exit 0. It measured minor on every package the diff moves.
  • node scripts/check-adr-0087-registration.mjs: 1 declared-breaking changeset, registered seed-mode-default-create-only.
  • Affected suites under os-verify-lock, at the changeset commit: @objectstack/spec (810 files, 24,395 tests), @objectstack/metadata-protocol (312 files, 4 skipped), @objectstack/cloud-connection (68 files, 824 tests), @objectstack/organizations (18 files), objectql's seed files (11), the CLI's seed files (6), and the three examples (crm 7, showcase 46, todo 8 files): all green. @objectstack/runtime: 540 of 541 files green; the one red, seed-datasets.test.ts, asserted the per-organization replayer's restated defaultMode: 'upsert'. It now asserts that the request carries no mode of its own (7 of 7 green), in 6353695e.
  • node scripts/pm/dispatch-gates.mjs --commands: 114 derived commands, run one by one at 6353695e with this body as the event. All 114 exit 0. check-plugin-teardown-shape --self-test first exited 3 on the shallow clone, and exited 0 once its fixture commit was fetched. --ran reconciles: 114 run, 0 NOT-MEASURED, 0 UNRUN.
  • One locked run on the final head 6353695e: full build (119 of 119 tasks), then @objectstack/spec test:repo (90 files, 1,539 tests), then the full @objectstack/dogfood battery (417 files passed, 3 skipped; 3,462 tests passed, 28 skipped). All green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6

…gnore`

A replay (every single-tenant boot, a package install or upgrade, a
publish of a `seed` item) now inserts the rows a dataset is missing and
leaves existing rows alone, so a customer's edit to a seeded row is no
longer written back on the next restart. `SeedLoaderConfig.defaultMode`
is `SeedSchema.mode` itself, and the six loader call sites (boot inline
seed, per-organization replayer, dev hot-reload, package publish apply,
protocol publish batch, install-local) plus the seed save door stop
restating `'upsert'`.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
The spec build refuses a moved authorable default until it is declared.
Three rows join DEFAULT_CHANGES_BY_MAJOR[18] (Seed.mode,
SeedLoaderConfig.defaultMode and the SeedLoaderRequest.config default
object), each carrying the ruling, the measurement, the remedy and the
built-artifact disposition: a package built before the change keeps
replaying with upsert until it is rebuilt, and nothing converts it.

The ADR-0087 D3 semantic entry `seed-mode-default-create-only` states the
same disposition and why no conversion can guess the author's intent.
The authorable-defaults projection and the generated reference pages
follow from the build.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
Spec: an omitted mode parses to `ignore`, every authored mode parses as
written, SeedLoaderConfig.defaultMode equals the seed default, and a
mode-less dataset in a parsed SeedLoaderRequest reaches the loader as
`ignore`.

Runtime, on a real SqlDriver under ObjectQL with the request built the way
every replay site now builds it (parsed, no mode of its own): across a
replay of a new seed version, a customer edit survives, a record the new
version adds is inserted, and a row nobody touched keeps its value and its
`updated_at`. Control: the same steps with `mode: 'upsert'` declared write
the shipped value back and move the untouched row's `updated_at`.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
…ault

The reseed endpoint and the rehydrate healer set no mode of their own. The
reseed pin captures the request the handler hands the loader and parses it
with the real SeedLoaderRequestSchema: a mode-less dataset arrives as
`ignore` and a dataset that declares `mode: 'upsert'` keeps it. The heal
test stops expecting the restated `defaultMode: 'upsert'`.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
…ly default

Census of the in-tree seeds that relied on the overwrite. Every dataset
the CRM (5), the showcase (18) and the todo app (1) declared as
`mode: 'upsert'` is business or starter data the user edits once it is
seeded, so none keeps it: a replay now inserts a missing row and leaves an
existing one as the user left it. No in-tree dataset is reference data the
package must keep refreshed, and no platform package ships a seed. Each
file says why it declares no mode and that `os dev --fresh` is how a seed
edit, or a relative date, is seen again.

The seed-data guide leads with the create-only default, moves `upsert` to
reference data with `applyOn: 'create'` as the escape for owned columns,
and states that a package built before the change keeps replaying with
upsert until it is rebuilt. The getting-started example and the
`data` docblock on the stack follow. Test comments that claimed to mirror
the AppPlugin's `defaultMode: 'upsert'` request now say what the request
is, and the per-organization request a real-driver test rebuilds drops it.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
Minor for spec, runtime, metadata-protocol and cloud-connection, with the
Clause-② (narrowing) and Metadata-protocol: semantic declarations, the
ADR-0087 registration marker, the before/now/compiled-artifacts account
and the remedy.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
The replayer's request is tenant-scoped and multi-pass, and no longer
restates `defaultMode: 'upsert'`: each dataset replays in the mode it
declares, the create-only spec default when it declares none.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:data tests tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cloud-connection, @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/runtime/src/domains/packages.ts, packages/spec/authorable-defaults/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/seed-data.mdx (via SeedSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx (via MarketplaceInstallLocalPlugin (symbol, a top-level class))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/runtime/src/domains/packages.ts, packages/spec/authorable-defaults/data.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 178 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json dcb5fb3ef30b373aae040476e84e1df84cec77d2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9b68d046506a0299979c47cb25be7eefad1eafd2 — the merge of head 585143c72d6aae36af0119a8e18e4228d4562175 into base dcb5fb3ef30b373aae040476e84e1df84cec77d2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9b68d046506a0299979c47cb25be7eefad1eafd2 && git checkout 9b68d046506a0299979c47cb25be7eefad1eafd2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dcb5fb3ef30b373aae040476e84e1df84cec77d2 585143c72d6aae36af0119a8e18e4228d4562175 && git checkout -B drift-repro dcb5fb3ef30b373aae040476e84e1df84cec77d2 && git merge --no-ff 585143c72d6aae36af0119a8e18e4228d4562175

node scripts/docs-audit/affected-docs.mjs --json dcb5fb3ef30b373aae040476e84e1df84cec77d2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs dcb5fb3ef30b373aae040476e84e1df84cec77d2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 6353695 · 2026-11-04T16:40Z

Full review of the first head at CONTRACT_REVIEW_TIER (a narrowing: the seed mode default).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6353695e8fc8d49c15c3299b9ec2cfb4642fd85e
Local-runs: none
Scope: full diff f68b47c..6353695 (33 files); packages/spec/src/data/seed.zod.ts, seed-loader.zod.ts, seed.test.ts, seed-loader.test.ts; packages/spec/scripts/lib/default-changes.ts; packages/spec/authorable-defaults/data.json; packages/spec/src/migrations/entries/semantic/18.seed-mode-default-create-only.ts; packages/spec/src/stack.zod.ts; packages/metadata-protocol/src/seed-loader.ts (head, :1173-1196, :2990-3090), protocol-publish-batch.ts (:80-125), protocol-save-door-refusals.ts (:860-887); packages/runtime/src/app-plugin.ts (:1812-1836, :1913-1931, :2339-2354), domains/packages.ts (:3952-3973), seed-default-create-only-replay.integration.test.ts, seed-datasets.test.ts; packages/cloud-connection/src/marketplace-install-local-plugin.ts (:1193-1237, :1300-1326, :2613, :4618-4654), the reseed and heal tests; examples/app-crm, app-showcase, app-todo seed files; content/docs diff; skills/objectstack-data/references/seeds.md, evals/hooks-security-seeds-datasources.json, skills/objectstack-platform/SKILL.md; docs/adr/0087 addendum; .changeset/30081-seed-mode-default-create-only.md; card #30081 and all 7 comments; PR #30152 body and comments; check-runs on the head; the two memory files.

① Derived judgments

  • Loader: ignore is create-only on every replay path. seed-loader.ts:1196 takes dataset.mode || config.defaultMode; :3023-3030 and :3080 insert a missing row and skip a matched one without comparing, so no updated_at moves. All six sites parse through SeedLoaderRequestSchema, which fills each dataset's mode from SeedSchema (seed-loader.zod.ts:526), and none restates defaultMode any more: app-plugin.ts:1816, :1920, :2349; packages.ts:3967; protocol-publish-batch.ts:115; marketplace-install-local-plugin.ts:4646 (install, reseed, heal all go through runInlineSeed). A head-wide grep finds no remaining defaultMode: 'upsert' outside tests that declare the mode they test. The save door reads SeedSchema.shape.mode.parse(undefined) (protocol-save-door-refusals.ts:884); ignore is in KEYED_SEED_MODES (:860), so the externalId refusal still fires for a mode-less seed. SeedLoaderConfig.defaultMode is SeedSchema.shape.mode (seed-loader.zod.ts:253) and is read only on the validate() cast path (seed-loader.ts:1173-1181), as its TSDoc says. Honest.
  • Stored data. The compiled-artifact disposition ("mode":"upsert" baked by os build, keeps replaying until rebuilt, no guessing conversion) is stated consistently in the changeset (:26), the D3 entry (:35-38), the shared ledger-row reason (default-changes.ts:77-79), the mode TSDoc (seed.zod.ts:185-189) and the seed-data guide callout. Three DEFAULT_CHANGES_BY_MAJOR[18] rows are right: the authorable-defaults projection lists all three keys (data.json:100, :107, :112) and the build refuses each moved key without its row. ⛔ Two stores are not stated at all (see ③): the install-local ledger and a stored sys_metadata seed item.
  • Census. CRM 5, showcase 18, todo 1 counted against the diff: all business or starter data the customer owns once loaded; each file says why and points at os dev --fresh. memberships already declared ignore. Showcase orgUnits (sys_business_unit, fixed ids referenced by metadata) is environment-owned admin data by its own comment (seed/index.ts:232-249); create-only still inserts the missing ids, so metadata references resolve on a fresh boot. No platform package, plugin, service or template ships a defineSeed/SeedSchema.parse dataset. option-b-collection-zoo.ts:246 and the dogfood seed-ownership-claim-dispatch fixture declare upsert and test it. No test or dogfood flow found that re-seeds and expects an existing row reset: install-local-listing-sample-data.dogfood reseeds into an empty organization (inserted: SEED_TOTAL), and the showcase seed.test.ts checks static seeded states.
  • Pins. seed-default-create-only-replay.integration.test.ts on a real SqlDriver: edit survives (:155), missing row inserted (:157), untouched row's value and updated_at unchanged under faked Date (:159-162), explicit upsert control writes back and moves updated_at (:165-176), parse pin mode === 'ignore' (:82). The reseed pin parses the captured request with the real SeedLoaderRequestSchema: mode-less dataset arrives as ignore, declared upsert kept, no config.defaultMode (marketplace-install-local-reseed.test.ts:252-272); the heal test and seed-datasets.test.ts:222-227 pin the per-organization replayer. Each fails without the change by reading (totalUpdated 0 vs 2; 'ignore' vs 'upsert').
  • PR body matches the code: six sites, three rows, the census counts, the skills lines owed (verified at seeds.md:36, :52, :63, :160; evals/...json:37; SKILL.md:377; seeds.md:26 is reference data and stays right).
  • Check-runs at review time: Check Changeset, PR-body guards, Lint repo gates, Dogfood Regression Gate, Build Docs green; Test Core (8), Test UI (16), Type Check, Build & E2E UI still in progress. Not a final verdict on CI.

② Semver level

Clause-②: yes (narrowing) with Metadata-protocol: semantic is right: the accept set is the same five modes and every authored mode parses as before; what moved is the meaning of an omitted key. Minor for each of the four moved packages (spec, runtime, metadata-protocol, cloud-connection); the three examples are private: true. ADR-0087: registered seed-mode-default-create-only, the D3 entry exists in the diff, no D2 conversion (correct: a conversion would guess intent, the same class as ui-nav-group-expanded-default-open). The registry is generated and gitignored, so no registry file is owed.

③ Boundary flags

  • must-fix: the stored-row disposition stops at the compiled artifact. The install-local ledger keeps the body as installed and replays it on heal and reseed (marketplace-install-local-plugin.ts:1193-1200, :1300-1326, :2613 seedDatasetsOf(entry.manifest)), so an entry installed from a pre-change artifact carries "mode":"upsert" and keeps overwriting on every reseed and heal until the rebuilt artifact is re-installed; a rebuild alone changes nothing for it. The changeset remedy (:29 "Rebuild every package"), the D3 entry and the ledger-row reason say only "until it is rebuilt". State the ledger case and add "and re-install" to the remedy, in all three places.
  • should-fix: a stored sys_metadata seed item that declares no mode is replayed on its next publish through protocol-publish-batch.ts:82-112 and now parses to ignore where it parsed to upsert. It is the intended narrowing and "on every publish" is named in the changeset Before/Now, but the ADR-0087 stored-row answer never says that such a row changes behaviour with nothing rewritten and no signal. One sentence in the changeset and the D3 reason.
  • should-fix: no test pins that the boot inline seed site (app-plugin.ts:1920) builds its request with no defaultMode; the per-organization replayer and the install-local sites are pinned, the boot site is not. Low consequence (a restated value is dead on a parsed request), but the "one default, stated once" rule has no guard there.

④ Design judgment

The narrowing is the smallest change that meets ruling 6631016988: no new mode value, the loader untouched, the default stated once and the six restatements removed. The real-driver pin measures the behaviour the card described rather than the config shape. The census reasoning holds. The open gap is the ledger disposition: it is text, but it is the one line a customer with an installed package would act on, and as written it would leave them overwritten on the next reseed.

Implemented-by: claude/project-thread-2k4081
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: FAIL

A single-tenant boot's inline seed builds its request with multiPass and
no defaultMode, so each dataset replays in the mode it declares. The
per-organization replayer and the install-local doors were already pinned
the same way; this closes the boot site.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
… in the disposition

The stored-row disposition stopped at the compiled artifact. An
`os package install` keeps the body it was installed with in its install
ledger, and the sample-data reseed replays that body in its stored mode, so
an entry installed from a pre-change artifact keeps writing the shipped
values back on every reseed until the rebuilt artifact is re-installed; a
rebuild alone changes nothing for it. The ledger's other replays never
overwrite (the rehydrate seeds nothing, the heal seeds only empty objects,
the per-organization replay only a new organization).

A stored `seed` metadata item with no mode is not rewritten either: its
next publish parses it to `ignore`, with no signal.

Both are stated in the changeset (with "rebuild and re-install" in the
remedy, and purge-then-reseed as the way back to the shipped values), the
D3 entry's reason and acceptance criteria, the default-changes ledger
reason, the `mode` TSDoc and the seed-data guide's callout. The reseed
endpoint's docblock stops promising a clean baseline over edited rows.

Claude-Session: https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 585143c · 2026-11-04T17:45Z

Delta review of fix round 1 (the must-fix and should-fixes of review 6636509452) at CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

Contract review — delta (fix round 1)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 585143c72d6aae36af0119a8e18e4228d4562175
Local-runs: none
Scope: delta 6353695..585143c (2 PR-owned commits b5554b9, 585143c; no merge from main; 7 files, all read in full); packages/cloud-connection/src/marketplace-install-local-plugin.ts at head (:1142-1250 rehydrate, :1278-1345 heal, :1540-1600 and :4540-4600 applySideEffects, :1568-1585 buildSeedReplayer, :2288-2372 install door, :3783-3975 reseed and purge handlers, :4633-4680 runInlineSeed); packages/runtime/src/app-plugin.ts (:25, :1895-1935); packages/runtime/src/seed-datasets.test.ts (:1-120, :251-297); packages/plugins/organizations/src/organizations-plugin.ts (:385-425); packages/spec/src/data/seed.zod.ts (:170-215); packages/spec/scripts/lib/default-changes.ts; packages/spec/src/migrations/entries/semantic/18.seed-mode-default-create-only.ts; packages/spec/authorable-defaults/data.json (:98-106); content/docs/data-modeling/seed-data.mdx; content/docs/deployment/marketplace-handbook.mdx (:205-252); packages/cli/src/commands/package/install.ts (:1-60, :241); .changeset/30081-seed-mode-default-create-only.md; review record 6636509452; proposed body 30081-body-r1.md; check-runs on the head.

① Derived judgments

  • MUST (ledger disposition): closed. The changeset has the new "Installed packages" bullet (:21) naming the ledger, the reseed route and "until the rebuilt artifact is re-installed; rebuilding alone changes nothing for it", and the remedy (:26) now says "re-install the rebuilt artifact wherever the package was installed with os package install". The same case is in the shared ledger-row reason (default-changes.ts:79-84), the D3 reason (18.seed-mode-default-create-only.ts:38-42) and acceptanceCriteria (:51-52), the mode TSDoc (seed.zod.ts:189-192), the mdx callout (seed-data.mdx:122-126) and the body's ADR-0087 line and "Installed packages (round 1)" note. Consistent in every place.
  • SHOULD (stored sys_metadata seed item): closed. Changeset :22, default-changes.ts:85-88, D3 reason :45-47 and the body's "Stored seed items (round 1)" each say the item is not rewritten, its next publish parses to ignore, inserts only missing rows, with no signal. protocol-publish-batch.ts is untouched by the delta, so the round-0 reading (:82-112) stands.
  • SHOULD (boot-site pin): closed. seed-datasets.test.ts:251-297 deletes OS_MULTI_ORG_ENABLED (:263), registers objectql and metadata (:275-276, which selects the SeedLoaderService branch at app-plugin.ts:1909), starts the real AppPlugin (:289) and asserts the captured request has multiPass: true and no defaultMode (:294-295). The mock replaces ./seed-loader.js (:40-45), which is the module app-plugin.ts:25 imports, and SeedLoaderRequestSchema.parse is a passthrough (:49-52), so a restored defaultMode: 'upsert' in the config object at app-plugin.ts:1912-1924 would land on loadCalls[0].config and :295 fails. The pin is real and exercises the boot path, not the replayer.
  • Path claims verified. Rehydrate calls applySideEffects(ctx, converted, { seedNow: false }) (:1227) and seeds nothing itself; the heal it then calls (:1236) probes every seeded object and returns on the first row found (:1312-1319), so it seeds the stored body only into empty tables. The per-organization replayer (:1568-1585) reads the shared list, and its only caller runs on a sys_organization create/insert (organizations-plugin.ts:388-395), so it writes for an organization that has no rows yet. The reseed converts the ledger body and seeds it with seedNow: true (:3838-3839) through runInlineSeed, whose request carries no defaultMode (:4648-4659): the body's stored mode. The install door also replays with seedNow: true (:2351) on a reinstall or upgrade over live rows (:2354), but it replays the incoming body, not the ledger's, so that case is the compiled-artifact disposition already stated; the runInlineSeed comment (:4652-4655) names install, upgrade and reseed together, which is right. Dev's claim "only the reseed replays a ledger body against existing rows" holds.
  • "Purge then reseed" is real: purge-sample-data is mounted at :1128 and handled at :3927-3975, matching rows by the seed's externalId (an edited seed row keeps its key, so it is deleted and the reseed re-inserts the shipped values); the handbook names both doors (marketplace-handbook.mdx:216-219) and names Reinstall for a local install (:233-235). No doc states the two as a sequence outside the changeset and the reseed docblock; that is adequate.
  • The reseed docblock (:3783-3797) no longer promises a clean baseline and names the pre-change-ledger case. Nothing else changed in the delta: six text sites and one test, no code path moved.
  • Check-runs on the head: every non-skipped run is completed/success (Test Core 8/8, Test UI 16/16, Type Check, Lint and repo gates, Check Changeset, PR-body guards, Dogfood Regression Gate, Build and E2E UI, Build Docs, Spec property liveness). Skipped runs are opt-in or schedule gates.
  • Body matches the delta: the new "Installed packages" and "Stored seed items" acceptance notes, the boot-pin test entry with its ablation, the reseed-docblock bullet and the round-1 verification paragraph all correspond to what the diff contains. The local-run numbers are the dev's claim, not verified here.

② Semver level

Unchanged from round 0: Clause-②: yes (narrowing), Metadata-protocol: semantic, minor on the four moved packages. The delta changes no accept set and no runtime behaviour; the D3 entry text grew, its id and criteria shape did not. The authorable-defaults projection carries keys only (data.json:100), so no regenerated file is owed for the longer reason.

③ Boundary flags

  • should-fix (one word, text): marketplace-install-local-plugin.ts:3946 still says the table-wide match is "as the loader's upsert match is". The delta edited the neighbouring line (:3936) to drop upsert from the same docblock for this reason; the match is the keyed-mode match (ignore included), so name it that way or drop the clause.
  • nit, no fix owed: "the boot rehydrate seeds nothing, the heal seeds only when every seeded object is empty" (changeset :21, D3 :42-43, ledger reason :83-84, body) reads as if the heal were outside the rehydrate; it runs inside it (:1236). The second clause makes the overall statement true.

④ Design judgment

The round closes the one line a customer with an installed package would act on: "rebuild and re-install", stated in the changeset, the ledger rows, the TSDoc, the mdx and the body, with the reseed named as the only ledger replay that touches existing rows, which the code confirms. The boot pin is cheap and guards the last unguarded restatement site. Nothing new was introduced beyond text and that test.

Implemented-by: claude/project-thread-2k4081
Reviewed-by: session_01WGufzfgvGexKn9vLHsy89K

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

维护者速读

  • 改了什么: 种子数据的默认模式从 upsert 改成 ignore,也就是只插入缺失的行,已有行不动。
    • 客户改过的种子行,重启、升级、重新发布都不会再被改回去。
    • 作者如果确实要每次重置,写明 mode: 'upsert' 即可,或按列用 applyOn。
    • 示例 CRM、showcase、todo 去掉了显式的 upsert。
  • 为什么改: [Decision] A restart puts seeded records back: a customer's edit to a seeded row is overwritten by the next boot #30081 的裁决。第一阶段测量确认,生产环境的重启和两种升级路径都会悄悄覆盖客户的修改。
  • 风险与回滚: 这是收窄:默认值变了,属于破坏性变更。
    • 已构建的包,产物里写死了 upsert。必须重新构建,并且重新安装,才会改用新默认值;这之前,「重新灌样例数据」还会按旧行为覆盖。
    • 存量的种子元数据下次发布时会悄悄改按 ignore 处理。
    • 以上都写进了变更说明和迁移条目,没有做任何猜测性转换。回滚就是还原本 PR。
  • 先看: packages/spec/src/data/seed.zod.ts(默认值和说明);.changeset/30081-seed-mode-default-create-only.md;packages/runtime/src/seed-default-create-only-replay.integration.test.ts(真实驱动的钉)。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/data/seed.test.ts。
  • 需要你做的: 无,席位落地。

席位意见:fable 契约复审两轮。

  • 首轮 FAIL,代码没问题,缺的是已安装包的说明:安装台账里的旧包要「重新构建并重新安装」。
  • 修正后 PASS,只剩 purge 注释里一个词没改,不值得再开一轮。

席位核对 — ACCEPT · domain:spec#3 · head 585143c72d6aae36af0119a8e18e4228d4562175

  • Shape: 33+ files on origin/main f68b47ca:
    • the SeedSchema.mode default changes to ignore, with no new mode value;
    • SeedLoaderConfig.defaultMode becomes SeedSchema.shape.mode, and the six restated 'upsert' call sites are removed;
    • the save door reads the schema default;
    • three DEFAULT_CHANGES_BY_MAJOR[18] rows and a D3 semantic entry, seed-mode-default-create-only;
    • census edits in the CRM (5 datasets), showcase (18) and todo (1);
    • docs, and the ADR-0087 addendum.
  • Premise measured: phase 1 (6633189577) measured, on a production boot, that a customer's edit goes back to the seed value on every restart (os start and os serve) and on both upgrade paths. The seat's contract look is 6633227800.
  • Gates: the dev ran all 114 dispatch-gates at 585143c7 with body r1 as the event, all exit 0, and --ran reconciles 114/114. Full dogfood was green on 6353695e. The delta after that is text plus one unit test, and the check-runs on this head are all green or skipped.
  • Contract review:
    • Full review, 6636509452 on 6353695e: FAIL. The must-fix was the install-local ledger disposition; the two should-fixes were stored seed items and a boot-site pin.
    • Delta review on this head: PASS. All three are closed. The review verified the dev's path claims against the code: rehydrate, heal, the per-organization replay, reseed, and purge/reinstall.
  • Declarations: Clause-②: yes (narrowing), Metadata-protocol: semantic, and minor for all four moved packages (check-changeset-no-major exit 0). ADR-0087: D3 entry registered, no conversion, with the stored-artifact, install-ledger and stored-item dispositions stated.
  • Accepted deviations:
    • The purge docblock at marketplace-install-local-plugin.ts:3946 still says "the loader's upsert match". That is one word in a comment, and it rides the next edit of that file.
    • The full dogfood ran on the round-0 head, as stated above.
  • Carried:
    • The skills lines owed while the skills lane is paused, listed in the body: seeds.md :52, :63, :160 and objectstack-platform/SKILL.md:377.
    • Out of scope: os build bakes parsed defaults into dist. Candidate card.
  • Landing: not governed (check-governed-merges --pr 30152: 0 of 33 paths, 632 lines). The hand-off is pr_ready plus automerge_enable, through the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 17:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit fc869c2 Nov 4, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-2k4081 branch November 4, 2026 18:27
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 protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] A restart puts seeded records back: a customer's edit to a seeded row is overwritten by the next boot

1 participant