Skip to content

feat(spec,metadata-protocol): a seed dataset column can be written only when its row is created (columns.FIELD.applyOn: 'create') - #25813

Queued
objectstack-fleet[bot] wants to merge 13 commits into
mainfrom
claude/project-thread-6djfn0
Queued

objectstack-fleet[bot] wants to merge 13 commits into
mainfrom
claude/project-thread-6djfn0

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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, in data/column-apply-on.zod.ts. The import door's FieldMappingEntry.applyOn (api/export.zod.ts) now reads it instead of spelling its own, and SeedColumn.applyOn reads the same one. Each keeps its own .default('always'). The import door's emitted JSON schema is byte-unchanged.
  • SeedColumnSchema is a closed per-column shape ({ applyOn }, default always). SeedSchema gains an optional columns map from a field name to its SeedColumn. A map makes two policies for one field unrepresentable.
  • SeedSchema refuses an unknown key on a column (unrecognized_keys). A dataset-level applyOn is refused with a pointer to columns (guidance).
  • defineSeed types the columns keys against the object's fields plus its injected system columns (the SeedColumns type). 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).

  • createOnlySeedColumns is the set a dataset declares applyOn: 'create', minus the fields of its externalId. 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.
  • On a row the load MATCHED (existingRecords has its natural key, the same lookup decideWriteAction makes), 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.
  • A create-only reference on a matched row is skipped in the resolution loop: not resolved, not dropped for failing to resolve, and not deferred. Pass 2's back-fill would otherwise write it after all. Create-only references resolve last, so a reference column of a composite externalId has resolved by the time the loader asks whether the row matched.
  • warnOnUndeclaredSeedColumns logs at error, once per dataset, when a loaded seed's columns names 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, because result.errors holds per-record ReferenceResolutionErrors (recordIndex is .int().min(0)) and has no shape for a dataset-level finding.
  • A row the seed creates, a dry run, and the insert / ignore / replace modes are unchanged.

Docs, ledger, generated.

  • A new section in content/docs/data-modeling/seed-data.mdx, and the defineSeed API reference.
  • A seed/columns liveness row (live, with children.applyOn) and regenerated state counts.
  • ColumnApplyOn names the author state (ADR-0122) and is pinned isomorphic in type-alias-convention.pin.test.ts (790 → 791).
  • The hand-kept schema counts move with the new module: packages/spec/llms.txt 206 → 207 (data 30 → 31), and content/docs/getting-started/quick-reference.mdx data 29 → 30.
  • Regenerated: the reference docs; the api-surface, export-origins, declaration-map, authorable-surface and json-schema manifest shards; and the two skills reference indexes.

PM assumptions, measured

  1. "Runtime: packages/runtime/src/seed-loader.ts". Refuted: that file is a re-export shim. The loader body is packages/metadata-protocol/src/seed-loader.ts, so the change lives there (file-surface deviation below).
  2. "The import door's enum lives at packages/spec/src/api/export.zod.ts (~:173)". Held, but it was spelled inline. It is extracted to data/column-apply-on.zod.ts because data/ never imports from api/, and both doors read it. A test pins one-enum parity in seed.test.ts: the options and default of FieldMappingEntrySchema's applyOn equal SeedColumnSchema's.
  3. "The seed loader has no per-column policy today". Held: at 5f5f0b42 there was no per-column key on SeedSchema, 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 calls driver.upsert: there is no upsert( call in seed-loader.ts, and a matched row is written with engine.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. isNoOpReplay therefore 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.ts has a new describe, "a column declared applyOn: create is written on first seed only (Seed datasets: extend the import door's applyOn: '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:
    • a fresh insert writes both kinds of column;
    • a re-seed after a "job" changed a create-only column keeps it while an always column re-asserts, for upsert and update, with the update's keys pinned exactly;
    • a control dataset without the policy re-asserts;
    • a no-op replay;
    • every non-key column create-only → no update;
    • a create-only reference on a matched row is neither resolved, deferred nor dropped;
    • a control: an unresolvable reference on a new row is still deferred;
    • a composite externalId with a create-only reference declared before a key reference;
    • an externalId column declared create-only stays in the update (both rows updated);
    • a dry run;
    • the error report on a misspelt column, and no report on an injected system column (owner_id).
  • packages/spec/src/data/seed.test.ts "column policies":
    • accepts both values and defaults to always;
    • refuses 'insert' with code: invalid_value at ['columns','status','applyOn'];
    • refuses an unknown column key with code: unrecognized_keys at ['columns','status'];
    • points a dataset-level applyOn at columns;
    • pins one-enum parity.
  • packages/spec/src/data/define-seed-record-keys.test.ts: four columns key 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 typecheck and pnpm --filter @objectstack/metadata-protocol typecheck are 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 HEAD empty):

mutation result
strip of create-only columns on a matched row removed 9 failed / 45
in-loop skip of a create-only reference removed 2 failed / 45
create-only references resolved in declared order 1 failed / 46
warnOnUndeclaredSeedColumns call removed 1 failed / 46
defineSeed columns-key check removed 3 failed / 13
undeclared-column report at warn instead of error 1 failed / 46

Review

An independent full review of the whole diff by an isolated fable reviewer ran in four rounds. Every round returned no must-fix. Round 1's should-fix notes were applied in 986fb436. Round 2's dataset-level result.errors note is answered in d2319759, 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/objectstack at 0a22461b derives 120 commands. All 120 ran on that head, each exit code captured before any pipe, and all 120 exited 0. The --ran reconciliation 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-only check:dev-prereqs and check:skill-frame-freshness, and check:nul-bytes, also exit 0.

What the first pass (before the merge and the last fixes) found:

  • Red, and fixed here: check:llms-txt and check:quick-reference-counts (the hand-kept counts), and check:spec-parsed-alias (the ADR-0122 alias).
  • Red only on main's translation/flows.screens row: check:liveness and check:generated. They are green after the merge.
  • Exit 3 until the environment was fixed: check:plugin-teardown-shape --self-test, on the shallow clone until its pinned fixture commit was fetched; check:skill-examples, check:dual-build-cjs-loads and check:lean-entry-closure, until the packages were built.

The PR head aea8b0d3 adds one commit after that run, the changeset title, text only (FIELD for 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, and Clause-②: yes has no narrowing arm. The families CI runs are reported by this PR's check runs.

main moved three commits (#25748, #25789, #25804) after the merge. git merge-tree against 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 in packages/metadata-protocol/src/seed-loader.ts (assumption 1). The extracted enum is a new file in packages/spec/src/data, and packages/spec/src/api/export.zod.ts changes one import. Hand-kept counts in packages/spec/llms.txt and content/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;
    • all SKILL.md together: 4383 → 4383 lines;
    • the whole 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:620 still spells 'always' / 'create' by hand where it validates a mapping entry's applyOn, instead of reading ColumnApplyOnSchema. The schema itself has one spelling now, so the remaining copy is unexercised drift in another package. Carrier: none.
  • A columns key that names no column is refused by defineSeed, and only logged at error for a seed that arrives stored (the publish door accepts it, since SeedSchema cannot 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:liveness was red on main 5f5f0b42 (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

claude added 10 commits October 25, 2026 13:41
…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
…hic; quick-reference data count

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

Copy link
Copy Markdown
Contributor Author

Fable review — #25761 (seed dataset column applyOn), 2026-10-25T15:13Z

An independent full review of the whole diff, by an isolated reviewer agent on model fable, read-only, in three rounds. Session session_018HE4VY52U3L5Bfzz51dsFv.

Round 1, full diff at ec70e1a3: PASS-WITH-NOTES, MUST-FIX none.

note disposition
SHOULD-FIX 1: a columns key naming no column was reported at warn; the load looks normal while a job's write is overwritten on every replay Fixed in 986fb436: logged at error with the consequence and the fix, once per dataset. The rows still load. Test pins error and no warn; ablation error→warn: 1 failed / 46.
SHOULD-FIX 2: packages/rest/src/import-prepare.ts:620 still spells 'always' / 'create' by hand Not changed: another package, outside the claimed file surface, a new verification surface. Listed in the PR's Acceptance notes.
SHOULD-FIX 3: the externalId create-only test could pass with zero updates Fixed in 986fb436: asserts both rows were updated before checking each update's keys.
SHOULD-FIX 4: a key grammar regex on columns Not added: record keys carry none either, and defineSeed and the loader judge every key against the object's real columns. Accepted by the reviewer in round 2.
NIT: wording "a seed that did not come through defineSeed" Fixed in 986fb436 (changeset, TSDoc, docs page).

Round 2, delta ec70e1a3..986fb436: PASS-WITH-NOTES, MUST-FIX none.

note disposition
SHOULD-FIX 1: the undeclared-column finding reaches only the log, not result.errors, and logs on a dry run Not changed, documented in d2319759: ReferenceResolutionErrorSchema is per record (recordIndex is .int().min(0)), so a dataset-level entry would change the result contract beyond this card; the misconfiguration is not simulated, so the log stays on a dry run. Accepted by the reviewer in round 3. Follow-up listed in the PR's Acceptance notes.
SHOULD-FIX 2 (downgraded): the REST door's hand spelling Acceptance notes, as above.

Round 3, delta d2319759 plus the merge of main 6679644a (ea75b759): PASS-WITH-NOTES, MUST-FIX none, SHOULD-FIX none. The merge touches only protocol.ts and protocol-registry-materialize.ts from main, nothing of this card. Its one nit, an attribution trailer naming a model, is declined: this repo's commit trailers stay model-free.

Round 4, delta 0a22461b (gate-driven, no behaviour change): PASS, MUST-FIX none, SHOULD-FIX none, NITS none. It declares ColumnApplyOn as z.input (ADR-0122), pins it isomorphic in type-alias-convention.pin.test.ts (790 → 791, header, case title and count), and moves content/docs/getting-started/quick-reference.mdx "Data Protocol" to "of 30 schemas" for the new reference page. (986fb436 had already moved packages/spec/llms.txt 206 → 207, data 30 → 31.) The reviewer withdrew its trailer nit.

Final head reviewed: 0a22461b. The PR head aea8b0d3 adds one text-only commit after it, the changeset title's placeholder spelled FIELD.


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

3 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/data-modeling/validation.mdx (via SeedLoaderService (symbol, a top-level class))
  • content/docs/protocol/objectql/state-machine.mdx (via SeedLoaderService (symbol, a top-level class))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via SeedLoaderService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 10 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/authorable-defaults/data.json, packages/spec/authorable-surface/data.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 145 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 744900d134cd3babc951f6774000bee9345344cf → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d144d0315d1da25506d79ebe5bcaba98cd608de8 — the merge of head bcb84eb1f58e03aaa711973a99ebeec0e9cb2d97 into base 744900d134cd3babc951f6774000bee9345344cf, 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 d144d0315d1da25506d79ebe5bcaba98cd608de8 && git checkout d144d0315d1da25506d79ebe5bcaba98cd608de8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 744900d134cd3babc951f6774000bee9345344cf bcb84eb1f58e03aaa711973a99ebeec0e9cb2d97 && git checkout -B drift-repro 744900d134cd3babc951f6774000bee9345344cf && git merge --no-ff bcb84eb1f58e03aaa711973a99ebeec0e9cb2d97

node scripts/docs-audit/affected-docs.mjs --json 744900d134cd3babc951f6774000bee9345344cf

⚠️ 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 744900d134cd3babc951f6774000bee9345344cf → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: aea8b0d3c1ca306ae58a37a5171d6342c1e250de
Local-runs: none
Scope: packages/spec/src/api/export.zod.ts, packages/spec/src/data/column-apply-on.zod.ts, packages/spec/src/data/index.ts, packages/spec/src/data/seed.zod.ts, skills/objectstack-data/references/_index.md, skills/objectstack-platform/references/_index.md + packages/metadata-protocol/src/seed-loader.ts, packages/metadata-protocol/src/seed-loader-replay.test.ts, packages/spec/src/data/seed.test.ts, packages/spec/src/data/define-seed-record-keys.test.ts, packages/spec/src/type-alias-convention.pin.test.ts, packages/core/src/utils/import-runner.ts (read, unchanged), packages/rest/src/import-prepare.ts (read, unchanged), packages/runtime/src/seed-loader.ts (read, unchanged: a re-export shim), .changeset/25761-seed-column-apply-on.md, packages/spec/liveness/seed.json, packages/spec/api-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/export-origins/data.json, packages/spec/json-schema.manifest/data.json, packages/spec/authorable-surface/data.json, packages/spec/authorable-defaults/data.json, packages/spec/llms.txt, content/docs/data-modeling/seed-data.mdx. The remaining 10 files (regenerated reference docs, state counts, strictness-ledger counts, quick-reference count) are the lane seat's ACCEPT at TIER_DEFAULT.

Inputs: card #25761 (body, comments 6396236242 triage, 6398295510 claim, 6399376677 os-dev-report), PR #25813 (body, 36-file list, git diff origin/main... at the head), the head's check-runs. Ruling: hotclm#41 comment 6395830052, 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; read beside #8740".

① Derived judgments

Public surface the diff publishes, each judged against the ruling and the cited code:

  1. One enum for both doors — right. ColumnApplyOnSchema = z.enum(['create', 'always']) in packages/spec/src/data/column-apply-on.zod.ts, exported from @objectstack/spec/data via data/index.ts. api/export.zod.ts changes one line (FieldMappingEntry.applyOn: ColumnApplyOnSchema.default('always')), keeping the import door's own default and .describe, so its emitted JSON schema is unchanged (the json-schema manifest diff adds data/ColumnApplyOn and data/SeedColumn only; no api/ entry moves). The seed door's SeedColumn.applyOn reads the same enum with its own .default('always'). Same name, same values, same default: the triage's "same name and semantics" is met at the schema. seed.test.ts pins the parity (every value accepted or refused alike on both doors; equal defaults).

  2. SeedColumnSchema closed, Seed.columns a record keyed by field — right. strictObject with unrecognized_keys on an unknown column key (pinned at path ['columns','status']); invalid_value on a value outside the enum (pinned at ['columns','status','applyOn']). A map makes two policies for one field unrepresentable. A dataset-level applyOn is refused with a guidance entry pointing at columns (pinned). Seed stays z.input, SeedParsed z.infer; SeedColumn / SeedColumnParsed are a proper ADR-0122 pair (the shape has a default), and ColumnApplyOn is isomorphic and pinned in type-alias-convention.pin.test.ts (790 to 791, the machine-checked count moved with the prose twice).

  3. defineSeed holds columns keys to the object's column set — right. SeedColumns types the keys as keyof TFields | InjectedSystemColumnName (compile-time refusal pinned with @ts-expect-error); assertSeedKeysDeclared judges the columns keys with the same known set as record keys, in one refusal naming both surfaces with a near miss (pinned four ways in define-seed-record-keys.test.ts). owner_id as an injected system column passes (pinned).

  4. Key-field carve-out matches the import door — right. createOnlySeedColumns(dataset, externalId) is the 'create' set minus the externalId fields. The import door does the same: import-runner.ts updatePatch keeps k when keySet.includes(k) (lines 687 to 695). Pinned: "a column of the externalId declared create-only is still matched on, and stays in the update".

  5. What "matched" means, per mode — right. existingRecords is pre-loaded only for upsert, update, ignore, and never on a dry run (loadDataset, line 1151); insert and replace match nothing, so the policy changes nothing there, as the docs and changeset say. On ignore a matched row has its create-only columns stripped and is then skipped, which is harmless (nothing is written). matchesExistingRow uses externalIdKey(record, externalId) against the same map decideWriteAction and writeRecord consult, so the policy and the write agree on the row's identity.

  6. Strip point serves both write paths and does not mutate the authored seed — right. The strip (line 1866) runs after reference resolution and before the early signal, decideWriteAction (batched path) and writeRecord (sequential self-referencing path), which read the same record. record is a shallow copy ({ ...(seedResult.value) }, line 1435), so a second load over the same in-memory seed sees the full record. isNoOpReplay iterates the record's own keys, so a matched row whose only differences are create-only columns is a skip, not an update (pinned on both paths).

  7. drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740, the driver's empty-merge-set fallback — right, by construction. The seed path never calls driver.upsert: a matched row is written by engine.update(objectName, { ...record, id }, opts) (lines 1985, 2783, 2793). With every non-key column create-only the record holds only the key fields, which equal the stored row, so isNoOpReplay skips it and no write with an empty or key-only patch is issued (pinned: "every non-key column create-only, no update call", on both paths). A 'create' column cannot be re-admitted by a merge-set fallback that is never reached.

  8. A create-only reference on a matched row: not resolved, not deferred, not dropped — right. In the resolution loop the ref is deleted and continued before isMissing, resolveReferenceItem, the deferral push and referencesDropped++, so pass 2's writeDeferredReference has nothing to back-fill (pinned: referencesDeferred 0, referencesDropped 0, errors empty, no account in any update). Create-only refs are moved to the end of recordRefs so a reference column of a composite externalId has resolved to its id before matchesExistingRow is asked (pinned with billed_to declared before account); every dataset without the policy keeps its order (createOnlyColumns.size === 0 short-circuit). A row the load creates keeps the historical deferral (control pinned).

  9. update mode never writes a create-only column — right. Every update row is a matched row or a skip; the strip applies before either (pinned for mode: 'update' on both paths, update keys exactly ['amount','id','name']).

  10. Undeclared columns key on a loaded seed: error, once per dataset, log-only — right. warnOnUndeclaredSeedColumns judges against the same set defineSeed uses (fields plus resolveInjectedSystemColumns(definition).names), has no opinion without a field map, and reports once per dataset per load. The level passes the AGENTS.md "Degradation log levels" question: the load reports success while the column the policy was meant for is re-asserted on every replay, a persisted state the author believes protected and is not. The first line carries the consequence and the fix, as the section requires. Log-only is accepted: result.errors is ReferenceResolutionError[] with recordIndex: int min 0, so a dataset-level finding has no shape there, and refusing the rows would turn a typo into missing seed data. Pinned at error with no warn twin, and owner_id not reported.

  11. Generated artefacts agree with the source — right. api-surface, declaration-map, export-origins, json-schema manifest, authorable-surface (data/Seed:columns, data/SeedColumn:applyOn) and authorable-defaults (data/SeedColumn:applyOn = "always") all name the new exports; liveness seed/columns is live with children.applyOn, evidence anchored to createOnlySeedColumns, warnOnUndeclaredSeedColumns and matchesExistingRow, state counts 13 to 14; llms.txt 206 to 207 (data 30 to 31); quick-reference data 29 to 30; strictness ledger data/ 158 to 159 (seed.zod.ts 1 to 2 strict sites). Spec property liveness and Lint · repo & tooling gates are green on the head. The two skills/** rows are generator output (SeedColumnSchema on the seed.zod.ts line; one new column-apply-on.zod.ts line) and name nothing the source does not export.

  12. Docs make no claim the code does not keep — right. seed-data.mdx and the changeset state create-vs-matched, update never writes, insert/ignore/replace unchanged, create-only reference not resolved, externalId column stays, defineSeed refusal and loader error. Each is traced to a line above.

Should-fix, not blocking (noted in the PR's Acceptance notes, carrier none): packages/rest/src/import-prepare.ts:620 still hand-spells 'always' / 'create' when it validates a mapping entry's applyOn instead of reading ColumnApplyOnSchema.options. The values are identical, so there is no semantic divergence today; it is the one remaining second spelling of the enum and belongs on a follow-up card against @objectstack/rest. Not in this PR's file surface, and not a reason to refuse it.

Nothing in the hits widens or narrows an existing accept set: columns is optional, always is the default, and a seed without it parses and loads byte-for-byte as before (control pinned on both paths).

② Semver level

Changeset .changeset/25761-seed-column-apply-on.md: @objectstack/spec: minor, @objectstack/metadata-protocol: minor, @objectstack/skills: minor. The diff moves exactly those three published packages (packages/spec, packages/metadata-protocol, skills/** which is @objectstack/skills 18.1.0); @objectstack/core, @objectstack/rest and @objectstack/runtime are not moved (the runtime file is an unchanged shim). Metadata-protocol: added (a new authorable key columns and nested applyOn) requires at least minor on each moved package: met on all three. No skip-changeset. Both declaration lines appear line-initial in the PR body and in the changeset body. Check Changeset is green on the head.

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 yes with no (narrowing) arm, and no ADR-0087 disposition is owed.

③ Boundary flags

os-dev-report (card comment 6399376677): open_questions: [], so nothing is left for this record to answer. Each flagged item, answered:

Check-runs on the head at review time: 46 runs, 26 success, 10 skipped, 0 failure, 10 still running: Test Core (1/8) through Test Core (8/8), Type Check · workspace, Type Check · consumer gates. Green and relied on: Build Core, Build Docs, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint · repo & tooling gates, Lint · docs & code guards, Lint · ESLint, Type Check · source gates, Type Check · debt ledger, Lint · CI, release & engine gates, the three scoped lint gates, Lint · PM dispatch-gates, Dogfood Regression Gate, Check Documentation Links, Check PR Size, Flag docs affected by code changes, the four PR-claim guards, Auto Label, Lint & Repo Gates. This verdict is conditional on the 10 running families finishing green; a red among them re-opens it.

Implemented-by: claude/project-thread-6djfn0
Reviewed-by: session_01K2Tat8JEt4AhLASV4V4QWE

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么
种子数据集(seed dataset)的每一列可以声明 applyOn: 'create' | 'always'(默认 always),和导入门已有的 applyOn(#25577 / PR #25664)是同一个枚举、同一个含义。标为 create 的列只在种子第一次建这行时写入;之后重跑种子、命中已有行时,这些列不再被覆盖。种子加载器(packages/metadata-protocol/src/seed-loader.ts)的批量路径和逐条路径都照此处理;匹配用的主键列始终保留。

为什么改
你在 hotclm#41 裁了 A:「job-written columns are written on first seed only — via the platform」。今天重跑 hotclm 的种子,会把定时任务已经改过的状态改回去(18 条分期从逾期退回部分付款、续约提醒被重复发送)。

风险与代价(含回滚)
新增一个可选键,默认值就是今天的行为,已有种子不受影响,不需要迁移。写错列名时,defineSeed 当场拒绝;加载时只记一条 error 日志,行照常加载。回滚 = 撤销合并。

席位意见
建议合并。开发者自己做了四轮 Fable 完整复核,最后一轮通过;席位另做了一次 Fable 契约复核,也通过。CI 在本 head 全绿。需要人批是因为改动带了 skills/** 下两行由生成器产出的索引,这条路径受治理。

你要做的
批准或直接合并本 PR(批准账户为 os-zhuang / hotlong)。

席位核对

Seat domain:spec#4 (#18917) · session_01K2Tat8JEt4AhLASV4V4QWE · head aea8b0d3c1ca · 2026-10-25T15:41Z · verdict ACCEPT (Tier H: skills/** index rows; waits for an authorized approval or the maintainer's merge).

  1. Shape. Draft, base main, Fixes #25761, Clause-②: yes, Metadata-protocol: added, ruling quoted verbatim; 36 files, +877/−62, under the 3000-line threshold; labels documentation, size/l, tests, tooling, protocol:data; reviewers hotlong, os-zhuang requested by the relay.
  2. Independent full review (the owner's rule 「spec skills adr 都需要fable 完整的复核」): four rounds by an isolated Fable reviewer in the card thread, no must-fix in any round, final PASS 6399359190.
  3. Contract review of record: PASS on this head at CONTRACT_REVIEW_TIER, 6399466171. One enum for both doors, the key-field carve-out matches the import runner, the drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740 fallback cannot re-admit a 'create' column (the seed path never calls driver.upsert), and the changeset is minor on @objectstack/spec, @objectstack/metadata-protocol and @objectstack/skills.
  4. Gates. Head-pinned check-runs on aea8b0d3c1ca: 48 completed, 38 success, 10 skipped, none failed. The ten that were still running at review time (Test Core 1–8, both Type Check jobs) all finished green, so the review's condition holds. Dev dispatch-gates --commands 120/120 exit 0, --ran 0 NOT-MEASURED.
  5. Accepted as is: packages/rest/src/import-prepare.ts still spells 'always' / 'create' by hand when it validates a mapping entry, instead of reading ColumnApplyOnSchema.options. The values are identical today; it is recorded in the PR's Acceptance notes. The seat files no card this shift (the owner closed the round to new work).
  6. Landing. Stays draft with needs-user-decision; no auto-merge, no queue. It lands by the maintainer's merge, or by an authorized APPROVED review from GOVERNED_APPROVERS, after which this seat lands it. Seed datasets cannot carry a file: a Field.file column needs a sys_file id only an upload can mint, so a declarative seed either drops the row, breaks the ADR-0104 attestation, or inserts a dangling token silently #25779 (serialised behind this card) is not dispatched this shift.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b2e758728a0430775e1e390c2f45bf365ca9476a
Local-runs: none
Scope: packages/spec/src/api/export.zod.ts, packages/spec/src/data/column-apply-on.zod.ts, packages/spec/src/data/index.ts, packages/spec/src/data/seed.zod.ts, skills/objectstack-data/references/_index.md, skills/objectstack-platform/references/_index.md + packages/spec/llms.txt, packages/spec/src/type-alias-convention.pin.test.ts (the two hand-resolved paths of merge ea568701), content/docs/references/api/package-api.mdx, content/docs/references/index.mdx (the two files of regeneration commit b2e75872), content/docs/getting-started/quick-reference.mdx, content/docs/references/data/index.mdx, content/docs/references/data/meta.json, .changeset/25761-seed-column-apply-on.md, packages/spec/api-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/export-origins/data.json, packages/spec/json-schema.manifest/data.json, packages/spec/authorable-surface/data.json, packages/spec/authorable-defaults/data.json, docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md. The remaining files of the 35-file net diff are unchanged since the PASS record on aea8b0d3 (comment 6399466171) and stay the lane seat's ACCEPT at TIER_DEFAULT.

Delta record. The previous head aea8b0d3c1ca306ae58a37a5171d6342c1e250de carries a PASS record (comment 6399466171). This head adds two commits: ea568701, a merge of origin/main at abebc4e1 (which includes #25757, the whole retirement of kernel/package-upgrade.zod.ts), and b2e75872, a regeneration of two reference docs on the merged tree. The machine test for a pure regeneration named two hand-resolved paths, packages/spec/llms.txt and packages/spec/src/type-alias-convention.pin.test.ts, so the earlier record does not carry and this one re-judges the net change against the new base.

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), git diff aea8b0d3..b2e75872, the net diff git diff origin/main...b2e75872, the previous net diff git diff 6679644a...aea8b0d3, and the check-runs on the new head.

① Derived judgments

  1. The net delta vs main is the same change the earlier record judged — right. The 35-file net diff against abebc4e1 differs from the 36-file net diff against the old base 6679644a in exactly one path and in base counts only. The one path: content/docs/references/kernel/package-upgrade.mdx is gone from the diff because main deleted it in feat(spec)!: retire kernel/package-upgrade.zod.ts whole — the upgrade plan, diff, snapshot and rollback model nothing implemented (ADR-0049) #25757 and the PR no longer regenerates it. The base-count shifts are all feat(spec)!: retire kernel/package-upgrade.zod.ts whole — the upgrade plan, diff, snapshot and rollback model nothing implemented (ADR-0049) #25757's: llms.txt 206 → 207 becomes 205 → 206 with kernel 31 becoming kernel 30; the pin test 790 → 791 becomes 786 → 787; content/docs/references/index.mdx 1549 → 1551 / 198 → 199 pages becomes 1539 → 1541 / 197 → 198 with the Kernel row at 29 | 151; one hunk header in package-api.mdx moves by one line (main removed the plan row of PackageUpgradeResponse.data). Every hunk of the six review-face hits (export.zod.ts, column-apply-on.zod.ts, data/index.ts, seed.zod.ts, the two skills/** index rows) is byte-identical between the two net diffs: git diff aea8b0d3..b2e75872 restricted to the PR's 35 files touches only quick-reference.mdx, package-api.mdx, references/index.mdx, llms.txt and the pin test, and each of those five is also a file feat(spec)!: retire kernel/package-upgrade.zod.ts whole — the upgrade plan, diff, snapshot and rollback model nothing implemented (ADR-0049) #25757 changed on main. The ColumnApplyOn / SeedColumn surface and the seed-loader change (packages/metadata-protocol/src/seed-loader.ts, seed-loader-replay.test.ts, seed.test.ts, define-seed-record-keys.test.ts) are untouched by the merge; judgments 1 to 12 of comment 6399466171 therefore stand on this head without re-derivation.

  2. packages/spec/llms.txt after the hand resolution — right. At the head the table reads 206 schemas, system 36, kernel 30, data 31, api 31, ui 18, shared 16, automation 14, ai 12, identity 5, marketplace 4, security 4, studio 3, integration 1 (qa 1 is not a table row on main either). Counted read-only from the head tree (git ls-tree -r over packages/spec/src/DOMAIN/**/*.zod.ts): 206 modules, and every per-domain figure matches the table exactly. Main at abebc4e1 has 205 (the same count minus data/column-apply-on.zod.ts), so the net diff's 205 → 206 with data 30 → 31 is the PR's one new module and nothing else; kernel 30 is main's post-retirement figure, carried unchanged.

  3. packages/spec/src/type-alias-convention.pin.test.ts after the hand resolution — right. Counted read-only with the same regex the file's runtime companion uses (^export type Iso\w+ = Assert): 787 pins at the head, 786 on main. The three prose sites agree with each other and with the count: the section header (787 isomorphic aliases), the case title (still declares all 787 isomorphic pins) and expect(pins).toHaveLength(787). The M92 import line and the four Iso_kernel_packageUpgrade__* pins are gone (main's retirement, present only in the history comment 790 to 786), and the PR's M192 import plus Iso_data_columnApplyOn__ColumnApplyOnSchema are present once. The PR's history comment now reads 786 to 787 is #25761 immediately after main's 790 to 786 entry, so the ledger of count moves is continuous. Nothing of the retired module is re-imported or re-pinned.

  4. No deleted package-upgrade surface is resurrected — right. The net diff adds no line naming package-upgrade, packageUpgrade, UpgradePlan, UpgradeSnapshot, RollbackPackage*, UpgradePackage*, MetadataDiffItem, MetadataChangeType, UpgradeImpactLevel or UpgradePhase. The generated shards (api-surface, declaration-map, export-origins, json-schema manifest, authorable-surface, authorable-defaults) add only the ColumnApplyOn / SeedColumn entries and remove nothing. The regeneration commit b2e75872 moves package-api.mdx with main (the plan row leaves PackageUpgradeResponse.data, plan?: object leaves the data summary) and references/index.mdx to main's kernel row (29 | 151, the package-upgrade.zod.ts row gone), so both now describe the merged tree rather than the pre-retirement one. The PR touches no .changeset/25608-*, no migrations/entries/retired-defs/18.*, no package-upgrade-family-retirement.test.ts and no CHANGELOG.

  5. The other hand-kept counts after the merge — right. content/docs/getting-started/quick-reference.mdx at the head reads Data Protocol (15 of 30 schemas) and Kernel Protocol (17 of 29 schemas); check-quick-reference-counts.mjs takes M from the reference page directory (readdirSync of content/docs/references/DOMAIN), and the head has 30 data pages beside index.mdx (31 .mdx files; main 30) and the kernel figure is main's. content/docs/references/data/index.mdx 29 → 30 pages and meta.json gain the column-apply-on card; docs/audits/...strictness-ledger.counts/data.md data/ 158 → 159 with seed.zod.ts 1 → 2 strict sites, unchanged from the earlier record.

  6. The merge brought nothing of its own — right. ea568701 has exactly two parents, aea8b0d3 and abebc4e1; diffed against abebc4e1, it equals the PR's previous net diff modulo the count shifts in judgment 1 (the two patches differ only on those lines). Diffed against aea8b0d3, it is main's 170 files, with the PR's own files appearing only where main also changed them (judgment 1). No PR-side hunk was dropped or altered by the resolution.

  7. The changeset and skills/** rows — right, unchanged. .changeset/25761-seed-column-apply-on.md is byte-identical to the one judged on aea8b0d3. The two skills/** index lines are the same generator rows (SeedColumnSchema on the seed.zod.ts line; one new column-apply-on.zod.ts line, 72 → 73 lines) and name nothing the source does not export.

Should-fix, not blocking, carried from the earlier record and unchanged: packages/rest/src/import-prepare.ts:620 still hand-spells 'always' / 'create' instead of reading ColumnApplyOnSchema.options. Outside this PR's file surface; a follow-up card against @objectstack/rest.

Nothing new to flag. Nothing in the hits widens or narrows an existing accept set: columns is optional, always is the default, and a seed without it parses and loads as before.

② Semver level

Changeset unchanged: @objectstack/spec: minor, @objectstack/metadata-protocol: minor, @objectstack/skills: minor, with Clause-②: yes and Metadata-protocol: added line-initial in both the changeset body and the PR body. The merge adds no moved package: the PR's net diff still touches only packages/spec, packages/metadata-protocol, skills/**, content/docs, docs/audits and .changeset. A new optional key (columns, nested applyOn) whose default is the prior behaviour is a widening, so minor with no (narrowing) arm is correct and no ADR-0087 disposition is owed. Check Changeset is green on this head.

Clause-②: yes

③ Boundary flags

  • Merge of main including feat(spec)!: retire kernel/package-upgrade.zod.ts whole — the upgrade plan, diff, snapshot and rollback model nothing implemented (ADR-0049) #25757 (a ! retirement): the PR's title carries no ! and the retirement is main's, not this PR's; this diff only stops regenerating a page main deleted. No dogfood battery is owed by this PR for main's breaking change.
  • Hand-resolved paths: both re-counted read-only against the head tree (judgments 2 and 3); each agrees with its machine source, so check:llms-txt and the pin test's self-count should hold. They belong to Lint · repo & tooling gates / Test Core, still running on this head at review time.
  • skills/** remains a governed path (Tier H). This record does not substitute for the maintainer approval the PR body asks for; the PR stays a draft with needs-user-decision.
  • Previously answered deviations (file surface, enum extraction, hand-kept counts, no skills net-add budget, draft handoff) and the two out-of-scope findings (import-prepare.ts:620, no dataset-level shape in result.errors) are unchanged by this head; the earlier record's answers stand.

Check-runs on the head at review time: 45 runs, 16 success, 10 skipped, 0 failure, 19 still running. Green and relied on: Check Changeset, Governed Surface Queue Guard, Spec property liveness, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Dogfood Regression Gate, Check Documentation Links, Check PR Size, Flag docs affected by code changes, Auto Label, filter, and the four PR-claim guards. Still running: Test Core (1/8) through Test Core (8/8), Build Core, Build Docs, Lint · docs & code guards, Lint · repo & tooling gates, Lint · ESLint, Lint · scoped · slot lookup & corpus, Lint · scoped · query-options erasure, Type Check · workspace, Type Check · consumer gates, Type Check · source gates, Type Check · debt ledger. This verdict is conditional on the 19 running families finishing green; a red among them re-opens it, and the two hand-resolved files are where a red would be expected first.

Implemented-by: claude/project-thread-6djfn0
Reviewed-by: session_01K2Tat8JEt4AhLASV4V4QWE

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么
种子数据集(seed dataset)的每一列可以声明 applyOn: 'create' | 'always'(默认 always),和导入门已有的 applyOn(#25577 / PR #25664)是同一个枚举、同一个含义。标为 create 的列只在种子第一次建这行时写入;之后重跑种子、命中已有行时,这些列不再被覆盖。种子加载器(packages/metadata-protocol/src/seed-loader.ts)的批量路径和逐条路径都照此处理;匹配用的主键列始终保留。

为什么改
你在 hotclm#41 裁了 A:「job-written columns are written on first seed only — via the platform」。今天重跑 hotclm 的种子,会把定时任务已经改过的状态改回去(18 条分期从逾期退回部分付款、续约提醒被重复发送)。

风险与代价(含回滚)
新增一个可选键,默认值就是今天的行为,已有种子不受影响,不需要迁移。写错列名时,defineSeed 当场拒绝;加载时只记一条 error 日志,行照常加载。回滚 = 撤销合并。

席位意见
建议合并。开发者自己做了四轮 Fable 完整复核,最后一轮通过;席位另做了一次 Fable 契约复核,也通过。CI 在本 head 全绿。需要人批是因为改动带了 skills/** 下两行由生成器产出的索引,这条路径受治理。

你要做的
批准或直接合并本 PR(批准账户为 os-zhuang / hotlong)。

席位核对

Seat domain:spec#4 (#18917) · session_01K2Tat8JEt4AhLASV4V4QWE · head b2e758728a04 · 2026-10-25T16:27Z · verdict ACCEPT (Tier H: skills/** index rows; waits for an authorized approval or the maintainer's merge). Supersedes seat record 6399673281 (head aea8b0d3): the dev merged main (#25757 retired kernel/package-upgrade.zod.ts) and regenerated; two hand-resolved count files meant the earlier review did not carry, so it was re-done.

  1. Shape. Draft, base main, Fixes #25761, Clause-②: yes, Metadata-protocol: added, ruling quoted verbatim; 35 files, +875/−60 (the retired package-upgrade.mdx page left the diff with main's deletion), under the 3000-line threshold; labels documentation, size/l, tests, tooling, protocol:data; reviewers hotlong, os-zhuang requested by the relay.
  2. Independent full review (the owner's rule 「spec skills adr 都需要fable 完整的复核」): four rounds by an isolated Fable reviewer in the card thread, no must-fix in any round, final PASS 6399359190.
  3. Contract review of record: PASS at aea8b0d3 (6399466171) and the delta PASS on this head (6399908619), both at CONTRACT_REVIEW_TIER; the delta confirms the six review-face hunks are byte-identical and the hand-resolved llms.txt (206) and pin test (787) counts are right. One enum for both doors, the key-field carve-out matches the import runner, the drivers(sql): upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740 fallback cannot re-admit a 'create' column (the seed path never calls driver.upsert), and the changeset is minor on @objectstack/spec, @objectstack/metadata-protocol and @objectstack/skills.
  4. Gates. Head-pinned check-runs on b2e758728a04: 48 completed, 38 success, 10 skipped, none failed; every run the delta review found in progress finished green. Dev scoped re-check after the merge (AGENTS.md §10) green; dispatch-gates --commands 120/120 exit 0 at aea8b0d3. The PR body's Gates paragraph still names aea8b0d3; this record is the current reading.
  5. Accepted as is: packages/rest/src/import-prepare.ts still spells 'always' / 'create' by hand when it validates a mapping entry, instead of reading ColumnApplyOnSchema.options. The values are identical today; it is recorded in the PR's Acceptance notes. The seat files no card this shift (the owner closed the round to new work).
  6. Landing. Stays draft with needs-user-decision; no auto-merge, no queue. It lands by the maintainer's merge, or by an authorized APPROVED review from GOVERNED_APPROVERS, after which this seat lands it. Seed datasets cannot carry a file: a Field.file column needs a sys_file id only an upload can mint, so a declarative seed either drops the row, breaks the ADR-0104 attestation, or inserts a dangling token silently #25779 (serialised behind this card) is not dispatched this shift.

@os-zhuang
os-zhuang marked this pull request as ready for review October 26, 2026 03:32
@os-zhuang
os-zhuang enabled auto-merge October 26, 2026 03:32
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 6399908619 · b2e758728a0430775e1e390c2f45bf365ca9476a → bcb84eb1f58e03aaa711973a99ebeec0e9cb2d97 · unexplainedPathsBetween (record-recognisers, base origin/main) → (empty)

The dev merged main into the branch to pick up the objectui → ui/ merge (#25825) and the follow-ups to it (#25745, #25827, #25834, #25852, #25856, #25857, #25861, #25865). No path of this PR moved, so the contract review 6399908619 carries to this head. The CI run on this head, which includes Type Check UI and Test UI, decides whether ui/ needs a consumer change.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么
没有新改动。开发者把 main(ui/ 并入 #25825、取消版本钉 #25745 及其后续)合进了分支,没有冲突,PR 自己的 35 个文件一行没动,生成物也都是最新的。

为什么改
ui 仓库并进来之后,spec 的改动要在同一个 PR 里经过 ui/ 的类型检查和测试。

风险与代价(含回滚)
ui/ 没有读这张 PR 改到的任何符号。回滚就是撤销合并。

席位意见
建议合并。席位契约复核延续到新头,没有需要解释的路径(6407683146)。os-zhuang 已经在新头批准,并打开了自动合并。

你要做的
无。

席位核对

Seat domain:spec#4 (#18917) · session_01K2Tat8JEt4AhLASV4V4QWE · head bcb84eb1f58e · 2026-10-26T03:39Z · verdict ACCEPT (Tier H; governed approval present; lands through auto-merge / the queue). This supersedes seat record 6400166735 (head b2e758728a04).

  1. Shape. Unchanged: 35 files, +875/−60. The new head is the dev's merge of main at c475b45b. None of main's new commits touches a PR file.

  2. Independent full review: final PASS 6399359190 (four rounds, Fable). It is not re-owed, because no PR-authored path moved.

  3. Contract review of record: delta PASS 6399908619 at b2e758728a04. It carries to this head by Regen-provenance 6407683146: unexplainedPathsBetween returned empty.

  4. ui/ consumers. The dev checked:

    • Nothing under ui/ reads ColumnApplyOn / SeedColumn / applyOn.
    • ui check:spec-symbols finds 0 untriaged collisions.
    • The ui vitest over @object-ui/types, core and data-objectstack passes, 701/701 files.
    • The derived dispatch-gates run 119/119 exit 0, with --ran 0 NOT-MEASURED.

    CI's Type Check UI / Test UI on this head is still running, and the queue waits for it.

  5. Governed approval. os-zhuang APPROVED at this head (review 5647036589), marked the PR ready and enabled auto-merge.

  6. Accepted as is: import-prepare.ts spells 'always' / 'create' by hand. This is carried to the clock-out brief.

  7. Landing. By auto-merge once the required checks are green. The seat confirms MERGED and the close of Seed datasets: extend the import door's applyOn: 'create' (#25577 / PR #25664) to seed-dataset columns — a re-run of an app's seed reverts columns its own jobs wrote #25761.

This branch has not been deployed

No deployments
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

3 participants