Repository navigation
feat(spec,metadata-protocol): a seed dataset column can be written only when its row is created (columns.FIELD.applyOn: 'create') - #25813
Conversation
Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv Co-authored-by: Claude <noreply@anthropic.com>
…pplyOn Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv Co-authored-by: Claude <noreply@anthropic.com>
…te key resolves Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv Co-authored-by: Claude <noreply@anthropic.com>
…pplyOn Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv Co-authored-by: Claude <noreply@anthropic.com>
…at error; llms.txt schema count A columns key that names no column of the object leaves the column it was meant for at always, so every replay overwrites a job's write while the load reports success: the AGENTS.md degradation class that logs at error. The rows still load. The externalId create-only test now pins that both rows were updated, and packages/spec/llms.txt counts the new data schema module. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
… log-only Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
…hic; quick-reference data count Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
|
Fable review — #25761 (seed dataset column An independent full review of the whole diff, by an isolated reviewer agent on model Round 1, full diff at
Round 2, delta
Round 3, delta Round 4, delta Final head reviewed: Generated by Claude Code |
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 145 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 253617e46ca6dca5829e037cfc1fe410b9ecbfd0 && git checkout 253617e46ca6dca5829e037cfc1fe410b9ecbfd0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2a728ba2acf52c13d1918c2afca7b79bf391e1f5 3eaea35f81a0acf3e43efbae67eb3e95ae6803bd && git checkout -B drift-repro 2a728ba2acf52c13d1918c2afca7b79bf391e1f5 && git merge --no-ff 3eaea35f81a0acf3e43efbae67eb3e95ae6803bd
node scripts/docs-audit/affected-docs.mjs --json 2a728ba2acf52c13d1918c2afca7b79bf391e1f5
|
Contract reviewServed-tier: Inputs: card #25761 (body, comments 6396236242 triage, 6398295510 claim, 6399376677 os-dev-report), PR #25813 (body, 36-file list, ① Derived judgmentsPublic surface the diff publishes, each judged against the ruling and the cited code:
Should-fix, not blocking (noted in the PR's Acceptance notes, carrier none): Nothing in the hits widens or narrows an existing accept set: ② Semver levelChangeset Clause-②: yes Correct: a new optional key with the prior behaviour as its default is a widening of what an author can write and nothing an author could write before is refused or re-meant, so ③ Boundary flagsos-dev-report (card comment 6399376677):
Check-runs on the head at review time: 46 runs, 26 success, 10 skipped, 0 failure, 10 still running: Implemented-by: VERDICT: PASS |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
Contract reviewServed-tier: Delta record. The previous head Inputs: card #25761 (body; comments 6396236242 triage, 6398295510 claim, 6399376677 os-dev-report, 6399689260 ACCEPT), PR #25813 (body, 35-file list, comments 6399359190, 6399360689, 6399466171, 6399673281), ① Derived judgments
Should-fix, not blocking, carried from the earlier record and unchanged: Nothing new to flag. Nothing in the hits widens or narrows an existing accept set: ② Semver levelChangeset unchanged: Clause-②: yes ③ Boundary flags
Check-runs on the head at review time: 45 runs, 16 success, 10 skipped, 0 failure, 19 still running. Green and relied on: Implemented-by: VERDICT: PASS |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
|
Regen-provenance: 6399908619 · The dev merged |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
…ounts both new modules main's #25830 moved the inventory heading to 206 with its system module, and this branch moved it to 206 with its data module: git merged the identical lines, so the total is set to 207 by hand (system 37, data 31). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv
Contract reviewServed-tier: Delta record over comment 6399908619, the delta PASS on Inputs: card #25761, PR #25813 (body, 35-file list, comments 6399466171, 6399908619, 6400166735, 6407683146, 6407694185), ① Derived judgments
Should-fix, not blocking, carried from the earlier records and unchanged: Nothing new to flag. Nothing in the hits widens or narrows an existing accept set: ② Semver levelChangeset unchanged: Clause-②: yes ③ Boundary flags
Check-runs on the head at review time: 45 runs, 25 success, 7 skipped, 0 failure, 13 still running. Green and relied on: Implemented-by: VERDICT: PASS |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
The merge of main at 493a6c4 (#25813) deferred content/docs/references/index.mdx. Regenerated from the merged tree: main's column-apply-on page and SeedColumn beside this branch's dataset duration schemas — 1544 schemas, 199 pages. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKPixnkg91qCkQCVFDJuqW
Requested by 田騏 · project thread
Fixes #25761
Clause-②: yes
Metadata-protocol: added
Ruling-ref: hotclm#41 comment 6395830052 (letter A); triage 6396236242
Ruling
Verbatim, letter A: "job-written columns are written on first seed only — via the platform".
Triage 6396236242: "Same name and semantics as the import door's
applyOn(#25577 / PR #25664): spec key first, then the seed loader's upsert path. Read beside #8740: the driver's empty-merge-set fallback must not re-admit a'create'column."What changed
Spec (
packages/spec/src/data).ColumnApplyOnSchema = z.enum(['create', 'always'])is the one enum, indata/column-apply-on.zod.ts. The import door'sFieldMappingEntry.applyOn(api/export.zod.ts) now reads it instead of spelling its own, andSeedColumn.applyOnreads the same one. Each keeps its own.default('always'). The import door's emitted JSON schema is byte-unchanged.SeedColumnSchemais a closed per-column shape ({ applyOn }, defaultalways).SeedSchemagains an optionalcolumnsmap from a field name to itsSeedColumn. A map makes two policies for one field unrepresentable.SeedSchemarefuses an unknown key on a column (unrecognized_keys). A dataset-levelapplyOnis refused with a pointer tocolumns(guidance).defineSeedtypes thecolumnskeys against the object's fields plus its injected system columns (theSeedColumnstype). It also refuses an undeclared key when it runs, in the same refusal as an undeclared record key, with a near miss.Seed loader (
packages/metadata-protocol/src/seed-loader.ts).createOnlySeedColumnsis the set a dataset declaresapplyOn: 'create', minus the fields of itsexternalId. A key field is what a matched row was found by, so it stays in that row's update, as an import's match key does.existingRecordshas its natural key, the same lookupdecideWriteActionmakes), those columns are deleted from the record before the no-op check and before the write. The batched path and the sequential self-referencing path both read that same record.externalIdhas resolved by the time the loader asks whether the row matched.warnOnUndeclaredSeedColumnslogs aterror, once per dataset, when a loaded seed'scolumnsnames a column the object does not have. The level follows AGENTS.md "Degradation log levels": the load looks normal while a job's write to the intended column is overwritten on every replay. The rows still load. The finding is log-only, becauseresult.errorsholds per-recordReferenceResolutionErrors (recordIndexis.int().min(0)) and has no shape for a dataset-level finding.insert/ignore/replacemodes are unchanged.Docs, ledger, generated.
content/docs/data-modeling/seed-data.mdx, and thedefineSeedAPI reference.seed/columnsliveness row (live, withchildren.applyOn) and regenerated state counts.ColumnApplyOnnames the author state (ADR-0122) and is pinned isomorphic intype-alias-convention.pin.test.ts(790 → 791).packages/spec/llms.txt206 → 207 (data 30 → 31), andcontent/docs/getting-started/quick-reference.mdxdata 29 → 30.PM assumptions, measured
packages/runtime/src/seed-loader.ts". Refuted: that file is a re-export shim. The loader body ispackages/metadata-protocol/src/seed-loader.ts, so the change lives there (file-surface deviation below).packages/spec/src/api/export.zod.ts(~:173)". Held, but it was spelled inline. It is extracted todata/column-apply-on.zod.tsbecausedata/never imports fromapi/, and both doors read it. A test pins one-enum parity inseed.test.ts: the options and default ofFieldMappingEntrySchema'sapplyOnequalSeedColumnSchema's.5f5f0b42there was no per-column key onSeedSchema, and nothing in the loader read one.Read beside #8740
The driver's merge-ALL fallback for an empty merge set lives on
driver.upsert. The seed path never callsdriver.upsert: there is noupsert(call inseed-loader.ts, and a matched row is written withengine.update(object, record, { where: { id } }).With every non-key column create-only, the patch is left holding only the key columns, which equal the stored row.
isNoOpReplaytherefore skips the row, and no write is issued. A test pins it ("every non-key column create-only → no update call").Tests
packages/metadata-protocol/src/seed-loader-replay.test.tshas a new describe, "a column declared applyOn: create is written on first seed only (Seed datasets: extend the import door'sapplyOn: 'create'(#25577 / PR #25664) to seed-dataset columns — a re-run of an app's seed reverts columns its own jobs wrote #25761)". It runs on the faithful engine double, over both the batched path and the sequential self-referencing path, and covers:upsertandupdate, with the update's keys pinned exactly;externalIdwith a create-only reference declared before a key reference;externalIdcolumn declared create-only stays in the update (both rows updated);errorreport on a misspelt column, and no report on an injected system column (owner_id).packages/spec/src/data/seed.test.ts"column policies":always;'insert'withcode: invalid_valueat['columns','status','applyOn'];code: unrecognized_keysat['columns','status'];applyOnatcolumns;packages/spec/src/data/define-seed-record-keys.test.ts: fourcolumnskey cases, including the compile-time refusal (@ts-expect-error).At the final head
0a22461b, under the shared verify lock (VERDICT command-exit 0):pnpm --filter @objectstack/spec typecheckandpnpm --filter @objectstack/metadata-protocol typecheckare green.pnpm --filter @objectstack/spec test: Test Files 721 passed (721), Tests 21844 passed, 1 todo.pnpm --filter @objectstack/metadata-protocol test: Test Files 263 passed, 3 skipped (266), Tests 21511 passed, 19 skipped.Ablations: each was red, and each was restored byte-identical to
HEAD(blob hash compared,git diff HEADempty):warnOnUndeclaredSeedColumnscall removeddefineSeedcolumns-key check removedwarninstead oferrorReview
An independent full review of the whole diff by an isolated
fablereviewer ran in four rounds. Every round returned no must-fix. Round 1's should-fix notes were applied in986fb436. Round 2's dataset-levelresult.errorsnote is answered ind2319759, and the reviewer accepted that answer. The verdicts and each note's disposition are posted as a comment on this PR.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat0a22461bderives 120 commands. All 120 ran on that head, each exit code captured before any pipe, and all 120 exited 0. The--ranreconciliation over those codes reads: "120 derived famil(ies) accounted for — 120 run, 0 NOT-MEASURED (a DERIVED zero — all 120 recorded an exit code and none of them is 3)". The local-onlycheck:dev-prereqsandcheck:skill-frame-freshness, andcheck:nul-bytes, also exit 0.What the first pass (before the merge and the last fixes) found:
check:llms-txtandcheck:quick-reference-counts(the hand-kept counts), andcheck:spec-parsed-alias(the ADR-0122 alias).translation/flows.screensrow:check:livenessandcheck:generated. They are green after the merge.check:plugin-teardown-shape --self-test, on the shallow clone until its pinned fixture commit was fetched;check:skill-examples,check:dual-build-cjs-loadsandcheck:lean-entry-closure, until the packages were built.The PR head
aea8b0d3adds one commit after that run, the changeset title, text only (FIELDfor an angle-bracket placeholder). The derived list is unchanged on it, and the 7 changeset families rerun there exit 0.The dogfood battery is not owed: there is no
!in the title, andClause-②: yeshas no narrowing arm. The families CI runs are reported by this PR's check runs.mainmoved three commits (#25748, #25789, #25804) after the merge.git merge-treeagainst it is clean, none of their files is in this diff, and none of them lands a sibling card.Deviations
File surface: the claim names
packages/runtime/src/seed-loader.ts, but the loader body is inpackages/metadata-protocol/src/seed-loader.ts(assumption 1). The extracted enum is a new file inpackages/spec/src/data, andpackages/spec/src/api/export.zod.tschanges one import. Hand-kept counts inpackages/spec/llms.txtandcontent/docs/getting-started/quick-reference.mdx, plus the alias pin file, moved with the new module.skills/**is touched only by generator-owned rows (gen:skill-refs). The readings:skills/objectstack-data/references/_index.md: 72 → 73 lines;skills/objectstack-platform/references/_index.md: 41 → 41 lines;SKILL.mdtogether: 4383 → 4383 lines;skills/tree: 13350 → 13351 lines.The order carried no skills net-add budget.
Line budget: 36 files, +877 / −62. Hand-written: 10 files, +352 / −25. Tests: 4 files, +411 / −4. Generated: 22 files, +114 / −33.
Acceptance notes
packages/rest/src/import-prepare.ts:620still spells'always'/'create'by hand where it validates a mapping entry'sapplyOn, instead of readingColumnApplyOnSchema. The schema itself has one spelling now, so the remaining copy is unexercised drift in another package. Carrier: none.columnskey that names no column is refused bydefineSeed, and only logged aterrorfor a seed that arrives stored (the publish door accepts it, sinceSeedSchemacannot know the object's fields). Carrying a dataset-level finding in the load result, or refusing it at the stored-seed publish door, would change the seed loader's result contract or the publish path. Carrier: none.check:livenesswas red onmain5f5f0b42(translation/flows.screens→engine.ts), unrelated to this diff.main's fix(spec): liveness ledger rows cite the seam modules that now hold the moved engine bodies #25796 fixed it, and it is green here after the merge.维护者速读(草稿)
改了什么:种子数据(seed)现在可以给单个字段声明写入策略,写法是
columns: { status: { applyOn: 'create' } }。声明为create的字段只在种子第一次创建这条记录时写入。之后每次重放(开发服务器启动、包重新发布),只要匹配到已有记录,这个字段就不再被覆盖,其余字段照旧刷新。取值和含义与导入映射的applyOn完全一致,两边共用同一个枚举。为什么改:应用自己的作业会在首次播种后改写某些字段,比如付款作业推进的状态、续约作业设置的标记。以前每次重放种子都会把这些字段改回初始值,悄悄撤销作业的结果,日志里也没有痕迹。这是维护者裁决 A:"作业写入的字段只在首次播种时写入,由平台负责"。
风险与代价(含回滚):新增的是可选键,默认值
always就是改动前的行为,不声明就没有任何变化。影响只限于upsert和update两种模式下已经匹配到的行。如果columns写错了字段名,defineSeed会直接拒绝;存储的种子则在加载时记一条 error 日志,数据照常加载。回滚只需撤销本 PR,已经写入的数据不受影响。skills/**的变化只有两份生成的索引,一共多一行。席位意见:
你要做的:审阅并批准。本 PR 触及受管路径
skills/**,需要维护者批准后才能合并。Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_018HE4VY52U3L5Bfzz51dsFv