Skip to content

docs(spec, skills): a queried composite record title is a stored computed field, not a formula; a stored field's expression is the engine's, never a client hint - #25183

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-25129-composite-title-stored-field
Oct 22, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/issue-25129-composite-title-stored-field

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25129

Clause-②: no
Metadata-protocol: none

Governance: Tier H. This diff touches skills/** (the published objectstack-data skill), a governed surface, so it lands only on an authorized APPROVED review. It stays a draft until then; the relay's pr_create requests the governed review itself.

What changes

Ruling 6308445871 (#24981, letter A), item 2: "The producer stores the title it needs to query: a composite title that must round-trip through import is a stored computed field (a non-formula field with an expression, written on write — the #24619 shape), the remedy ADR-0079 item 9 already prescribes for sort and search." PR #25127 (#25071) re-worded the record-title lint, ADR-0079 D3 and formulas.mdx to that shape. This PR finishes the same reconciliation on the copies that card did not claim, and keeps "a text formula title stays legal for display only".

Site Before After
packages/spec/src/data/object.zod.ts, the titleFormat describe "Migrate a single-field title to nameField, a composite to a formula field designated as nameField." a composite title that import, sort or search must match → a stored computed field (a non-formula field with an expression, computed and stored on every write) designated as nameField; a text formula designated as nameField stays legal for a title shown for display only
content/docs/references/data/object.mdx, references/api/metadata.mdx, references/system/migration.mdx (two rows) generated copies of that describe regenerated with pnpm --filter @objectstack/spec check:generated --fix after the build; the diff is the four titleFormat rows and nothing else
packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, the titleFormat omit-ledger note quotes the old describe quotes the new prescription; "every target is authorable"
skills/objectstack-data/SKILL.md, nameField row "or a formula field (returnType: 'text') for a composite title" "a composite title to import, sort or search by is a stored computed field (a non-formula field with an expression, computed and stored on write); a text formula is display-only"
skills/objectstack-data/SKILL.md, titleFormat row "for a composite title, designate a returnType: 'text' formula field as nameField" "a composite title follows the nameField row"
skills/objectstack-data/rules/relationships.md, the "Computed column" bullet (seat pointer 6333459023) "the server only treats type: 'formula' as computed, so on a stored field the expression is a client compute hint and the sent value is stored verbatim" "the engine computes a stored field's expression from the row the write stores and stores the result, so a client-sent value never stands"

The nameField describe carries no composite-to-formula prescription (measured; the dispatch's H2 is falsified for that site). Its TSDoc lists "formula→text" among the stored title fields, D1's wording that ADR-0079's addendum qualifies without editing; it prescribes nothing for a composite title and is left alone.

The shape is real on origin/main

  • FieldSchema.expression describe (packages/spec/src/data/field.zod.ts): "On any other (stored) field: computed by the engine from the row the write stores and stored, so a client-sent value never stands".
  • packages/objectql/src/engine.ts planStoredExpressions / replacedStoredExpressionFields; packages/spec/src/contracts/data-engine.ts: since objectql 18.x: under strictReadonlyWrites, refuse a write whose value a stored expression replaces (stored_expression), the deferred half of #24690 #25000 a strictReadonlyWrites write whose value such a field's expression replaces is refused, and a non-strict one is reported under stored_expression.
  • ADR-0079 addendum item 9: a formula value is stored nowhere; a sort by it is refused, search cannot reach it; an object that must sort or search by a composite title stores it.

Sweep (H2, H3)

  • packages/spec/src/**: "composite" with "formula", "nameField" or "title" → the describe and the reconciliation note only (control word titleFormat: 10 files, none a prescription).
  • skills/**: "composite" with "formula", "title" or "nameField" → the two SKILL.md rows only; "compute hint", "stored verbatim", "only treats", "as computed" → the relationships.md bullet only (control word titleFormat: 3 hits; the other two are the formula skill's template table and a query-filter example, neither a prescription). rules/field-types.md:124 already says a formula is virtual, consistent with the new text.
  • Repo-wide outside the card: the old sentence survives only in packages/*/CHANGELOG.md (release-owned; never edited in a code PR) and in two test docblocks describing landed in-repo migrations (packages/services/service-messaging/src/objects/notification-display-title.test.ts:18, packages/platform-objects/src/identity/identity-display-title.test.ts:17), the in-repo display_title formulas the card excludes.

skills/** readings

Published: @objectstack/skills (packages/skills, publishConfig.access: public, in the changeset fixed group) ships skills/** as dist/skills/SKILL_NAME/… through scripts/sync-catalog.mjs, with files: ["dist", "README.md", "CHANGELOG.md"]. So the skill half owes a changeset; it rides in the same file as the spec half.

Lines (budget: at most +2 net across skills/objectstack-data/):

File / set Before After Net
skills/objectstack-data/SKILL.md 469 465 −4
skills/objectstack-data/rules/relationships.md 401 401 0
whole package, every .md under skills/objectstack-data/ 3713 3709 −4
all skills/**/SKILL.md summed 4393 4389 −4

Tokens (scripts/check-skills-token-ratchet.mjs, ceil(utf8 bytes / 4), release stamp unpriced):

File Before After Ceiling
skills/objectstack-data/SKILL.md 6123 6099 6123
skills/objectstack-data/rules/relationships.md 3676 3672 3676

The first re-wording of the nameField row put SKILL.md 48 tokens over its ceiling (6171 / 6123). Paid in the same file by deletion, never by re-wrap: the two re-worded rows tightened; the incident sentence "It used to clear both and then never match." dropped; the parenthetical "(Reminder: two consecutive os build runs with no source change must be byte-identical — see the determinism gate above.)" dropped, because that gate lives in references/seeds.md, not above; a doubled --- separator removed. No ceiling moved.

Tests

Measured at head e74783922 (base ab488d9b7; origin/main has since gained three commits that touch none of these paths, so no pre-PR merge):

  • pnpm --filter @objectstack/spec build, under the shared verify lock: VERDICT command-exit 0. packages/spec has no workspace dependencies, so the ① closure build is empty.
  • pnpm --filter @objectstack/spec check:generated: check:docs was the one stale artifact of 14; --fix regenerated it (under the lock, VERDICT command-exit 0); the re-check reads ✓ check:docs.
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 over the six spec test files that mention titleFormat (the reconciliation note among them): Test Files 6 passed (6), Tests 267 passed (267), VERDICT command-exit 0. Declared narrowing: the spec suite is 779 files, over the foreground cap; only the reconciliation note pins the changed text and no test reads the titleFormat describe; CI's Test Core runs the whole suite.
  • pnpm --filter @objectstack/spec typecheck (tsc, check:scripts-typecheck, check:test-typecheck): VERDICT command-exit 0.
  • No ablation: no tested behaviour changed (a describe string, a ledger note's prose, skill text).

Gates

Derived on the final commit with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; "gate list derived from the tree of 'objectstack-ai/objectstack' at commit e747839"): 119 commands. Each was run from the worktree root with its exit code captured before any pipe and recorded as COMMAND :: exit N; --ran then read: "119 derived famil(ies) accounted for — 118 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".

  • 118 commands exit 0, among them check:docs, check:generated, check:skill-docs, check:skill-refs, check:skill-examples (after building @objectstack/client-react / @objectstack/client, its declared prerequisite), check:skills-token-ratchet, check:skill-identifier-liveness, check:skill-frame-sync, check:skill-compatibility, check:doc-authoring, check:doc-formula-expressions (after building @objectstack/formula / @objectstack/lint, its declared prerequisite), check-empty-changeset, check-changeset-no-major, check-adr-0087-registration, check:nul-bytes, check:pm-governed-merges.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET: it reads every package's dist/, a whole-repo build); CI runs it. The diff changes no build output shape.
  • Not derived, run by hand: node scripts/check-skills-token-ratchet.mjs before and after the payment (✗ over by 48, then ✓ 54 authored bundle file(s) within their ceilings).

Changeset

.changeset/25129-composite-title-stored-field.md: @objectstack/spec patch and @objectstack/skills patch, Clause-②: no, Metadata-protocol: none. The text an author reads changes; no key, type, default or accepted value does.

Acceptance notes

  • The nameField TSDoc's "(text / autonumber / formula→text)" lists a formula among the "real stored" title fields, D1's wording that the ADR-0079 addendum qualifies without editing; it prescribes nothing and is not in scope. Noted, not filed.
  • The two in-repo test docblocks and the CHANGELOG entries named under Sweep keep the old sentence as a description of landed migrations; the card excludes those packages. Noted, not filed.

维护者速读(草稿)

  • 改了什么:titleFormat 的 describe(及其三页生成的参考文档)、引用它的表单对账注记,以及已发布的 objectstack-data 技能包里 nameField / titleFormat 两行和「Computed column」一条。措辞统一为:需要被导入匹配、排序或搜索的组合标题应使用存储型计算字段(非 formula 字段加 expression,写入时由引擎计算并落库);text formula 标题仍合法,但只用于展示。「Computed column」改为:存储字段的 expression 由引擎按写入行计算并落库,客户端送来的值不作数。
  • 为什么改:旧文案把作者引向 formula 标题,而 formula 值不落库,导致排序被拒、搜索够不到、导入无法按标题匹配(ADR-0079 补充第 9 条与裁决 6308445871);PR fix(lint): a composite record title that must be queried is a stored computed field; a formula title is for display only #25127 已改了 lint 与 ADR,这是同一次对账剩下的几处。技能包里「expression 只是客户端提示、值按送达原样存储」与 main 上的引擎行为相反,是 AI 作者在同一个包里读到的自相矛盾。
  • 风险与代价(含回滚):仅文案;无键、类型、默认值或接受值变化;技能包行数净 −4,token 在天花板内(6099/6123),天花板未动;为付 token 账删掉的三处都是失效指引或事故叙述。回滚即还原本 PR。
  • 席位意见:
  • 你要做的:本 PR 触及 skills/**,属 Tier H,需要一次授权的 APPROVED 评审后才能落地。

Generated by Claude Code

os-bill and others added 3 commits October 22, 2026 05:47
…uted field, not a formula

Re-words the `titleFormat` describe, the reconciliation note that quotes it,
the objectstack-data skill's `nameField` / `titleFormat` rows and its
"Computed column" bullet to the ruling's shape: a composite title that import,
sort or search must match is a stored computed field (a non-formula field with
an `expression`, computed and stored on every write); a text formula stays
legal for display only; a stored field's `expression` is computed by the
engine, never a client hint.

Claude-Session: https://claude.ai/code/session_01T1Y1g92DfSEz93iuCisju4
Co-authored-by: Claude <noreply@anthropic.com>
`pnpm --filter @objectstack/spec check:generated --fix` after the build; only
`check:docs` was stale, and the diff is the four `titleFormat` rows on the
object, metadata and migration reference pages.

Claude-Session: https://claude.ai/code/session_01T1Y1g92DfSEz93iuCisju4
Co-authored-by: Claude <noreply@anthropic.com>
…ting stale text

The re-worded `nameField` row put the file 48 tokens over its ceiling. Paid
in the same file: the two re-worded rows are tightened, the incident sentence
"It used to clear both and then never match" is dropped, the parenthetical
pointing at "the determinism gate above" (which lives in references/seeds.md,
not above) is dropped, and a doubled `---` separator is removed. The file now
reads 6099 of 6123 tokens.

Claude-Session: https://claude.ai/code/session_01T1Y1g92DfSEz93iuCisju4
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

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

  • content/docs/data-modeling/formulas.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))
  • content/docs/data-modeling/objects.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))
  • content/docs/data-modeling/schema-design.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))

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

  • content/docs/releases/v17/17-0.mdx (via titleFormat (symbol, a field of const object ObjectSchemaBase, an authorable key of data/Object))

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
  • 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 — 144 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 b6e870707b95d7b8ee9cedd68ad358b51de0da91 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 b6e870707b95d7b8ee9cedd68ad358b51de0da91 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-zhuang
os-zhuang marked this pull request as ready for review October 22, 2026 07:30
@os-zhuang
os-zhuang enabled auto-merge October 22, 2026 07:31
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 22, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e747839220be43ed47f475df379e33bc482f340f
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, skills/objectstack-data/SKILL.md, skills/objectstack-data/rules/relationships.md + packages/spec/src/data/field.zod.ts, packages/objectql/src/engine.ts, packages/spec/src/contracts/data-engine.ts, packages/spec/src/data/display-name.ts, packages/lint/src/validate-record-title.ts, docs/adr/0079-record-display-name.md, skills/objectstack-data/references/seeds.md, skills/objectstack-data/references/data-hooks.md, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, content/docs/data-modeling/formulas.mdx, scripts/check-skills-token-ratchet.mjs, packages/skills/package.json, packages/skills/scripts/sync-catalog.mjs, .changeset/config.json

The rest of the diff — the changeset file and the three regenerated reference pages under content/docs/references/** — is the lane seat's ACCEPT at TIER_DEFAULT, exactly as a standalone PR of those files. Read against origin/main b6e870707 (the PR's current base) and the PR's net diff against main at the head; the card (#25129, every comment) and ruling record 6308445871 (#24981, letter A) read in full. Inputs were the brief's list and nothing else; the dispatching seat's conclusions were not taken as evidence.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. titleFormat describe, packages/spec/src/data/object.zod.ts:2844 — RIGHT. A .describe() string only; the key stays TemplateExpressionInputSchema.optional(), deprecated, render-only, nameField precedence unchanged. The new prescription is the ruling's item 2 shape and matches, word for word in substance, the two places PR fix(lint): a composite record title that must be queried is a stored computed field; a formula title is for display only #25127 already landed it: ADR-0079 D3 (docs/adr/0079-record-display-name.md:140-148, "a composite title that must be queried (matched by an import, sorted, searched) becomes a stored computed field (a non-formula field with an expression, written on write), designated as the nameField … a formula field with returnType: 'text' … stays legal for a title shown for display only") and the enforcing lint packages/lint/src/validate-record-title.ts:80-88 and :103-106 (title-format-retired message and hint: "a composite title that import, sort or search must match is a stored field with an expression designated nameField (a formula title is for display only)"). Rule and gate now say one thing. The four regenerated rows (object, metadata, migration x2) carry the identical sentence; check:docs runs in Type Check · source gates, green on the head.

    • The claim "computed and stored on every write" is real on main: planStoredExpressions (engine.ts:3030-3058) plans every non-formula field declaring an expression; the insert path computes over the FINAL rows after hooks and strips (engine.ts:16925-16960); update by id computes over the stored row merged with the patch and applyStoredExpressionsToPatch writes it (:18650-18670, :3138-3146); a predicate update computes before/after images and settleBulkStoredExpressions (:3102-3131) deletes the caller's key, stores one uniform value when an input moved, refuses on divergence. Consistent with the FieldSchema.expression describe (field.zod.ts:1856). One elision, not a defect: a predicate update that moves no input recomputes nothing — the stored value is then still the expression over the stored row, so the prescription holds.
    • A stored computed field is a legal nameField: an explicit pointer is honoured whatever the field's eligibility (ADR-0079 D4; display-name.ts:resolveDisplayField), and a text field carrying an expression is title-eligible by type anyway (TITLE_ELIGIBLE_TYPES, display-name.ts:102-130). No lint refuses the shape the describe now prescribes.
  2. nameField describe and TSDoc left alone (object.zod.ts:2826-2836) — RIGHT. The describe prescribes nothing for a composite title; the TSDoc's "(text / autonumber / formula→text)" is D1's wording, which the ADR-0079 addendum qualifies in place without editing (0079:404-407: "D1 lists a text formula among the 'real STORED' title fields. It is a declared field of the object, but no column holds its value"). Inside the dispatch's conditional ("if it carries the same prescription"); no surface change.

  3. SKILL.md nameField row — RIGHT. Same shape as the lint hint and the ruling. "a text formula is display-only" is ADR-0079 addendum item 9 (sort refused 400 INVALID_SORT, search cannot reach it) plus ruling item 1 (a formula-titled import target keeps answering reference_not_found). The package stays self-consistent: references/data-hooks.md:369 ("ctx.title() resolves this object's nameField, a formula included") is a display read, which item 9 leaves legal.

  4. SKILL.md titleFormat row — RIGHT. "a composite title follows the nameField row" delegates to the row two lines above it; one prescription in the file, nothing left to drift.

  5. rules/relationships.md "Computed column" bullet — RIGHT, and the deleted text was a live contradiction with main. The old bullet ("the server only treats type: 'formula' as computed, so on a stored field the expression is a client compute hint and the sent value is stored verbatim") is the opposite of the engine: engine.ts:3030-3040 says verbatim "A client-sent value never stands: it is overwritten by the computed one"; under strictReadonlyWrites such a write is REFUSED and nothing is written (stored_expression, packages/spec/src/contracts/data-engine.ts:126-135), and a non-strict write is completed with the computed value and reported through onFieldsDropped. The bullet's surviving half ("Keep it stored (NOT a formula field) so the parent summary can roll it up") agrees with field.zod.ts:1856. Seat pointer 6333459023 discharged.

  6. The three deletions that paid the SKILL.md token ceiling — each judged for load-bearing content and reachability, all RIGHT:

    • "It used to clear both and then never match." (SKILL.md:158): incident narrative; the rule it trailed survives whole in the same paragraph (refused, not silent; searchable-field-unsearchable at os validate; 400 INVALID_FIELD at request time). Not load-bearing for an author; the history remains for doc readers at content/docs/data-modeling/schema-design.mdx:152.
    • "(Reminder: two consecutive os build runs with no source change must be byte-identical — see the determinism gate above.)" (SKILL.md:458-459): the pointer was dangling — nothing above it in SKILL.md states a determinism gate. The gate lives in the same package at references/seeds.md:129-130 ("Determinism gate: two consecutive os build runs with no source changes must produce byte-identical dist/objectstack.json"), reachable from SKILL.md:434's link to references/seeds.md, and is restated in evals/hooks-security-seeds-datasources.json:37. Information still reachable; a wrong cross-reference removed.
    • The doubled --- separator (SKILL.md:392-394): cosmetic.
    • Token readings reproduced from the head files' bytes (a read of the blobs, not a run): SKILL.md 24413 bytes minus the 20-byte version: stamp line gives ceil(24393 / 4) = 6099 against ceiling 6123 (check-skills-token-ratchet.mjs:404); rules/relationships.md 14687 bytes gives 3672 against 3676 (:534). The diff touches no ceiling row. Net lines: SKILL.md 469 to 465, relationships.md 401 to 401. The ratchet and its self-test run in Type Check · source gates (lint.yml:5636-5641), green on the head; so do check:skill-refs, check:skill-frame-sync, check:skill-compatibility, check:skill-top-level-keys, check:skill-docs, check:authorable-surface, check:api-surface.
  7. Reconciliation ledger note, metadata-form-zod-reconciliation.test.ts:404 — RIGHT. why is free prose asserted only for a length longer than 20 characters (:996, :1092); the new text quotes the new describe faithfully, and "every target is authorable" stays true (nameField is authorable; a non-formula field with an expression is authorable).

  8. Published-surface inventory. @objectstack/spec: one JSON-schema description moves, no key, type, default, accepted value or export (check:api-surface, check:authorable-surface green). @objectstack/skills: ships skills/** byte for byte (packages/skills/scripts/sync-catalog.mjs; files: ["dist", …], publishConfig.access: public), so the skill text change publishes; gen:skill-docs reads frontmatter only (packages/spec/scripts/build-skill-docs.ts header), untouched here. No runtime path changes anywhere in the diff.

  9. Sweep, my own on origin/main b6e870707. skills/**: "composite" with formula / title / nameField hits only the two SKILL.md rows; returnType: 'text' only those two; "stored verbatim" / "compute hint" only the relationships.md bullet. packages/spec/src/**: the describe and the ledger note only. Hand-written docs the Docs Drift bot named: formulas.mdx:99 already carries the new shape (from fix(lint): a composite record title that must be queried is a stored computed field; a formula title is for display only #25127); objects.mdx:70 and schema-design.mdx:59 describe titleFormat as deprecated render-only with no migration prescription — nothing to re-word. The dev's H2 and H3 hold.

② Semver level

.changeset/25129-composite-title-stored-field.md: @objectstack/spec patch and @objectstack/skills patch — RIGHT. Both are released packages the diff moves, both in the changeset fixed group (.changeset/config.json). Nothing an author can write changes, so Metadata-protocol: none and the level none takes is patch; the accepted set neither widens nor narrows, so Clause-②: no with no arm. Both declaration lines are line-initial in the PR body and in the changeset body, and the two agree. skip-changeset would have been wrong: the skill text ships in the @objectstack/skills tarball. Not breaking, so no ADR-0087 disposition marker is owed. Check Changeset green on the head.

Clause-②: no

③ Boundary flags

Dev report 6334913572 — seven deviations, each answered:

  1. Model-named co-author trailer refused by the pre-push hook and reworded before publish — verified on the PR's three commits (cadb0751e, a7e789dac, e74783922): each ends with the model-free trailer pair only; no published history rewritten. Closed.
  2. Spec test run narrowed to the six titleFormat files (6 of 779) — the gate is CI's Test Core; its conclusion is reported below. Closed by the check-run, not by the local reading.
  3. pnpm check:dual-build-cjs-loads NOT MEASURED locally — it runs in ci.yml Build Core (ci.yml:2710), completed success on the head. Closed.
  4. Verify-lock exit 99 and the backgrounded client build — process notes; no effect on the committed tree. Closed.
  5. The 119 derived gates run in chunks — process note. Closed.
  6. No pre-PR merge of main — mergeable: true against the current base b6e870707; the PR's CI and the drift bot read the merge ref (6bfb7adf8), which is what lands. Closed.
  7. The changeset names both packages where the dispatch suggested only spec — judged right in ②. Closed.

open_questions: none declared, none found.

out_of_scope_findings, both "noted, not filed", both accepted under Prime Directive #10 (neither is a defect, a contract violation or an authoring trap): (a) the nameField TSDoc lists formula→text among stored title fields — a source comment the ADR-0079 addendum already qualifies in place; (b) packages/*/CHANGELOG.md entries (release-owned) and two test docblocks describe landed migrations as history, not as prescriptions.

Governance: skills/** is Tier H in the register (scripts/pm/check-governed-merges.mjs: "Published skills/** stays Tier H"). This record is the contract-tier review of record the non-test packages/spec/src/** change owes before the queue; it is not, and cannot stand in for, the authorized APPROVED review the Tier H paths need — the PR is a draft with the governed review requested (hotlong, os-zhuang), which is the correct resting state. No review is submitted on the PR by this record.

Check-runs on e747839220be43ed47f475df379e33bc482f340f, read at 2026-10-22T07:40Z immediately before this post, after the last Test Core shard closed: 43 runs, every one completed — 37 success, 6 skipped by design (the opt-in 18.x upgrade harness and Packed-tarball smoke, and the full-battery Console Pin Gate, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI and the dogfood shard matrix, which run on main, not on a PR), none failure, none in_progress. The five required contexts are all success: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Build Core, Governed Surface Queue Guard (two runs, both green). The gates this review leans on sit in green lanes: check:docs, check:authorable-surface, check:api-surface, check:skill-docs, check:skill-refs, check:skill-frame-sync, check:skill-compatibility, check:skill-top-level-keys and the published-skills token ratchet in Type Check · source gates; check:skill-identifier-liveness in Lint · docs & code guards; check:dual-build-cjs-loads in Build Core; the reconciliation ledger test in Test Core; Check Changeset standalone. These conclusions are the gate verdicts of this record; nothing was built, run or re-run locally.

Implemented-by: claude/issue-25129-composite-title-stored-field
Reviewed-by: session_01T1Y1g92DfSEz93iuCisju4

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

  • 改了什么:titleFormat 的说明(及其在 object / metadata / migration 三张参考页上的生成副本)、引用该说明的表单对账注记,以及对外发布的 objectstack-data 技能包里 nameField、titleFormat 两行和 rules/relationships.md 的「Computed column」一条。统一改为:需要被导入匹配、排序或搜索的组合标题,用存储型计算字段(非 formula 字段加 expression,每次写入由引擎计算并落库);text formula 标题仍合法,只用于展示。「Computed column」改为:存储字段的 expression 由引擎按写入行计算并落库,客户端送来的值不作数。
  • 为什么改:旧文案把作者(包括 AI)引向 formula 标题,而 formula 值不落库,导致按标题排序被拒、搜索够不到、导入无法按标题匹配(裁决 6308445871 A 与 ADR-0079 补充第 9 条)。技能包原话「expression 只是客户端提示、发什么存什么」与 main 上引擎的实际行为相反。
  • 风险与代价(含回滚):只改文字,没有任何键、类型、默认值或接受集变化(Clause-②: no,Metadata-protocol: none,两个包各一个 patch)。技能包净 −4 行,token 在既有天花板内(SKILL.md 6099/6123),天花板未动;为付 token 账删掉的三处是事故叙述、一处指向已不在本文件的交叉引用、一道重复分隔线,契约复核判定无承重信息丢失。回滚 = 还原本 PR。
  • 席位意见:同意落地。契约复审档复核 6335473304 PASS;本席逐句对照 diff 与 origin/main 核过,改后文字与 lint、ADR-0079 D3 及引擎行为一致。
  • 你要做的:无。你已于 07:30Z 以 os-zhuang 批准,并已翻 ready、入合并队列;本席跟到合并为止。

席位核对

ACCEPT · domain:spec seat 1 (seat post #6017) · os-bill · session session_01T1Y1g92DfSEz93iuCisju4 · 2026-10-22T07:42Z · reviewed head:

e747839220be43ed47f475df379e33bc482f340f
  • Form: base main, first line Fixes #25129, Clause-②: no and Metadata-protocol: none line-initial; the only closing keyword in the body names spec + skills: the titleFormat describe and the objectstack-data skill still steer a composite record title into a formula field (follow-up to #25071, ruling 6308445871) #25129. Assignee os-bill. 8 files, 43 changed lines.
  • Governed: check-governed-merges.mjs --pr 25183: 2 of 8 paths hit the register (skills/objectstack-data/SKILL.md, skills/objectstack-data/rules/relationships.md), so the PR is Tier H whole. The relay requested review from both GOVERNED_APPROVERS (os-zhuang, hotlong) at pr_create. os-zhuang APPROVED on this head at 07:30:55Z, then readied the PR, armed auto-merge, and it entered the merge queue at 07:39:43Z — all the approver's own acts, recorded here, not the seat's.
  • Premises measured: the four generated titleFormat rows and the reconciliation note quote the new describe byte for byte. On origin/main the old relationships.md sentence contradicted the FieldSchema.expression describe ("a client-sent value never stands") and engine.ts's stored-expression computation. H2 (the nameField describe) was falsified with evidence and correctly left alone.
  • Contract review of record: 6335473304 on this head, Served-tier: CONTRACT_REVIEW_TIER, Local-runs: none, PASS. It covers the object.zod.ts describe and every skills/** hunk, the three token-ceiling deletions included. It was posted at 07:41Z, 84 s after the approver queued the PR; the approval itself is this Tier H PR's landing record.
  • Gates (pinned to the head above): all 43 check-runs completed, 0 non-green; Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core and Governed Surface Queue Guard succeeded; the 6 skips are the opt-in and full-battery lanes.
  • Accepted deviations: one changeset names @objectstack/spec and @objectstack/skills patch (H4 measured the skill package as published); the local spec test run was narrowed to the six titleFormat files and declared (CI Test Core ran the suite green); check:dual-build-cjs-loads was NOT MEASURED locally (CI Build Core green). Token budget: whole package −4 lines against the seat's +2 ceiling.
  • Out of scope: two notes ("noted, not filed": the nameField TSDoc's "formula→text" wording; the old sentence in release-owned CHANGELOGs and two landed-migration test docblocks), each below the filing gate. Nothing filed.
  • Landing: in the merge queue by the approver's act; this seat confirms MERGED and closes out the card.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants