Repository navigation
feat(spec)!: a seed is create-only by default — an omitted mode is ignore - #30152
Conversation
…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>
📓 Docs Drift CheckThis PR changes 4 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 178 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
|
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 |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe 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: 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>
|
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 |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract review — delta (fix round 1)Served-tier: ① Derived judgments
② Semver levelUnchanged from round 0: ③ Boundary flags
④ Design judgmentThe 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: VERDICT: PASS |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审两轮。
席位核对 — ACCEPT · domain:spec#3 · head
|
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. Anos package installkeeps 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 storedseedmetadata item with nomodeis not rewritten: its next publish parses it toignore, 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 asmode: 'upsert'),:52and:63(upsertnamed the default),:160("Default toupsert");skills/objectstack-data/evals/hooks-security-seeds-datasources.json:37(anapplyOn: 'create'column on a dataset with no mode, which now changes nothing);skills/objectstack-platform/SKILL.md:377(upsertdefault).seeds.md:26(reference data declaringupsert) stays correct.Prior rulings read: seed,seedmode,applyon → 18 ADR hits (
check-prior-rulings --card 30081); none rules on a seed'smodedefault. ADR-0057 D6 and ADR-0066 D5 cover the boot reconcile of stack-declared roles, units and policies, a separate path fromSeedSchemadatasets; 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 anupsertdataset. 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。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/**几行待技能通道恢复后补改;其余无需操作。What changes
@objectstack/spec.SeedSchema.modedefaults toignore. The loader already treatsignoreas 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. Themodedescribe and TSDoc say so, namemode: 'upsert'(withcolumns: { FIELD: { applyOn: 'create' } }) as the way to keep reference data current, and state the compiled-artifact disposition.SeedLoaderConfig.defaultModeis nowSeedSchema.shape.modeitself, so the default is stated once. ThreeDEFAULT_CHANGES_BY_MAJOR[18]rows (Seed:mode,SeedLoaderConfig:defaultMode,SeedLoaderRequest:config) and the D3 semantic entryseed-mode-default-create-onlydeclare the move. The authorable-defaults projection and the generated reference pages follow from the build.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, andpackages/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.mode: 'upsert'. Each file says why it declares no mode, and thatos dev --freshis how a seed edit is seen again.upsertto reference data, and carries a callout on packages built before the change. The getting-started example and the stack'sdatadocblock follow.@objectstack/spec,@objectstack/runtime,@objectstack/metadata-protocoland@objectstack/cloud-connection. It carries BREAKING; before and now; compiled artifacts, installed packages and storedseeditems; and the remedy (rebuild, re-install whatos package installinstalled, purge then reseed to get the shipped values back).Phase 1 measurement (comment 6633189577,
f68b47ca, before any change)A seed dataset with no
mode; a customerPATCHto a seeded row; then a replay. Every boot ran the built CLI withNODE_ENVunset, which the CLI defaults to production.os start --artifact, restart on the same homeupdated: 1)os serve, restartos package install1.0.0, edit, then 1.1.0inline seed … updated=1)No signal reached the customer: the replay logs at
infounder awarndefault, the banner readsSeeds: … 2 rows, and the install prints nothing about rewritten rows.Census: in-tree seeds that relied on the overwrite
examples/app-crm/src/data/index.tsmode: 'upsert'examples/app-showcase/src/data/seed/index.tssys_business_unit), teams, fieldZoo, invoices, invoiceLines, expenseReports, expenseLines, preferences, announcements, clientBriefs (18)mode: 'upsert'orgUnitsis environment-owned admin data by its own comment; its fixed ids, which metadata references, are still inserted when missing.membershipsalready declaredignoreand is unchanged.examples/app-todo/src/data/index.tsmode: 'upsert'upsertcreate-objectstacktemplatesdefineSeed/SeedSchema.parsedataset ships outsidepackages/specitself and the three examples.packages/cli/test/fixtures/option-b-collection-zoo.ts:246,packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.ts:72mode: 'upsert'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
packages/runtime/src/seed-default-create-only-replay.integration.test.ts, new).SqlDriver(better-sqlite3) underObjectQL. 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 itsupdated_at(Datefaked per step, so the stamps differ). Control,mode: 'upsert'declared: the shipped value is written back over the edit and onto the untouched row, whoseupdated_atmoves.upsert, the default arm fails (expected 2 to be 0updates) and the parse case fails (expected 'upsert' to be 'ignore'); the control still passes.packages/cloud-connection/src/marketplace-install-local-reseed.test.ts). It captures the request the reseed hands the loader and parses it with the realSeedLoaderRequestSchema. A mode-less dataset arrives asignore, a declaredupsertstaysupsert, and the config carries nodefaultMode. The heal test stops expecting the restateddefaultMode: 'upsert'.defaultMode: 'upsert'restored inrunInlineSeed, both the reseed pin and the heal test fail.packages/runtime/src/seed-datasets.test.ts, round 1). A single-tenant boot's inline seed hands the loader a multi-pass request with nodefaultMode. The per-organization replayer and the install-local doors are pinned the same way.defaultMode: 'upsert'restored atapp-plugin.ts's inline config, it fails (expected { multiPass: true, …(2) } to not have property "defaultMode").ignore; every authored mode parses as written;SeedLoaderConfig.defaultModeequals the seed default; a mode-less dataset in a parsedSeedLoaderRequestreaches the loader asignore.Acceptance notes
os buildwrites the parsed seed intodist/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 stopos buildfrom writing a seed's defaultedmodeinto the artifact, so a future default move reaches a package without a rebuild; that is outside this card's scope.marketplace-install-local-plugin.ts, rehydrate andseedDatasetsOf(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.seeditems (round 1). Asys_metadataseed item with nomodeis replayed on its next publish (protocol-publish-batch.ts). It now parses toignore, so that publish inserts only missing rows. Nothing is rewritten and nothing signals it; the changeset and the D3 reason say so.daysFromNow/daysAgo) now resolves them once, at first insert. Each example file says so and points atos dev --fresh.envdefault.Verification
Round 1 (
585143c7, contract review 6636509452):pnpm --filter @objectstack/spec build, thencheck:generated: green. Spec seed, seed-loader, migrations andscripts/libtests: 276 passed. The cloud-connection reseed and heal tests: 15 passed.seed-datasets.test.ts: 8 passed.dispatch-gates --commandsat585143c7: 114 derived, run one by one with this body as the event, all 114 exit 0;--ranreconciles 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, thencheck:generated(aftergen: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 measuredminoron every package the diff moves.node scripts/check-adr-0087-registration.mjs: 1 declared-breaking changeset, registeredseed-mode-default-create-only.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 restateddefaultMode: 'upsert'. It now asserts that the request carries no mode of its own (7 of 7 green), in6353695e.node scripts/pm/dispatch-gates.mjs --commands: 114 derived commands, run one by one at6353695ewith this body as the event. All 114 exit 0.check-plugin-teardown-shape --self-testfirst exited 3 on the shallow clone, and exited 0 once its fixture commit was fetched.--ranreconciles: 114 run, 0 NOT-MEASURED, 0 UNRUN.6353695e: full build (119 of 119 tasks), then@objectstack/spec test:repo(90 files, 1,539 tests), then the full@objectstack/dogfoodbattery (417 files passed, 3 skipped; 3,462 tests passed, 28 skipped). All green.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufziyEchnN2d8EyH3Jc6