Skip to content

fix(lint): a composite record title that must be queried is a stored computed field; a formula title is for display only - #25127

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-25071-composite-title-stored
Oct 22, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/issue-25071-composite-title-stored

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #25071

Clause-②: no (wording only: no verdict, severity, code, key or export moves)
Metadata-protocol: none

Governance: Tier H. This diff touches docs/adr/** (ADR-0079 D3's migration sentence), a governed surface, so it lands only on an authorized APPROVED review. It stays a draft until then.

What changes

Every sentence the card lists now gives the same remedy. A composite record title that must be queried (matched by an import, sorted, searched) is a stored computed field: a non-formula field (such as a text field) that declares an expression, which the engine computes and stores on every write. That field is designated as nameField. A formula field with returnType: 'text' stays a legal nameField for a title shown for display only.

Site Before After
packages/lint/src/validate-record-title.ts title-format-retired message and hint migrate to "a formula field designated nameField" stored field with an expression for a queried title; formula is display-only
packages/lint/src/validate-record-title.ts title-unresolvable hint "(or a formula field with returnType: 'text')" text field with an expression for a queried composite; text formula is display-only
packages/lint/src/rule-explanations.ts title-format-retired explanation composite title becomes a text formula stored computed field for a queried title; formula stays legal for display, and the explanation says why (computed on read, stored nowhere: sort refused, search never reaches it, import cannot match it)
packages/lint/src/data-model-rules.ts object/missing-name-field fix line "a stored text/autonumber field or a returnType: 'text' formula" a text field with an expression for a queried composite; text formula is display-only. Still inside the 200-character fix-line bound
docs/adr/0079-record-display-name.md D3 migration sentence only composite title becomes a text formula stored computed field when queried, citing addendum item 9 and ruling record 6308445871; text formula stays legal for display only
content/docs/data-modeling/formulas.mdx ~:99 comment migrates to a text formula text formula for display only, or a stored computed field when import, sort or search must match it

No finding fires on a different object, and no severity, rule code, key or export changes. Only the text an author reads changes.

The shape is real on origin/main

Tests

The two suites that pin the exact strings are updated: packages/lint/src/validate-record-title.test.ts and packages/lint/src/data-model-rules.fix-lines.test.ts. Both are needed because the shipped text is pinned verbatim there (the fix-line suite also enforces the 200-character bound).

Measured at head e9e0ec7bab, after merging origin/main 03966ceae0:

  • Lint dependency closure built with pnpm --filter '@objectstack/lint^...' build, under the verify lock: VERDICT command-exit 0.
  • pnpm --filter @objectstack/lint test: Test Files 153 passed (153), Tests 7085 passed (7085). pnpm --filter @objectstack/lint typecheck (including check:test-typecheck): VERDICT command-exit 0.
  • node scripts/pm/dispatch-gates.mjs --ran with exit codes recorded: 100 derived famil(ies) accounted for — 99 run, 1 NOT-MEASURED. Exit 0 on every gate run, including check:docs, check:doc-authoring, check:doc-formula-expressions, check-changeset-no-major, check-empty-changeset, check-adr-0087-registration, check:changeset-gate-self-tests, check-adr-links, check:adr-anchors and check:nul-bytes.
  • Three gates first stopped with PREREQUISITE NOT MET or a missing dist, because a sibling build was rebuilding at the same time. After the build they were re-run and exited 0: check:skill-examples (262 prose examples type-check), check:dts-closure (128/128 declarations present) and check:lean-entry-closure.
  • NOT MEASURED: check:dual-build-cjs-loads, reason: it needs every package's dist, which means a whole-repo build, and that is left to CI. The diff changes no build output shape.
  • scripts/pm/clause2-line.mjs reads the changeset declarations as {"value":"no","arm":null} and {"value":"none"}.

Changeset

@objectstack/lint patch. The text of a shipped diagnostic changes, but nothing a metadata author can write changes, so this is Metadata-protocol: none. Under AGENTS.md Post-Task Checklist item 3, none takes patch.

Acceptance notes

The same "composite title is a text formula" advice exists in other places that this card did not claim, so this PR leaves them alone. They are listed in the dev report for the seat to file or route:

  • packages/spec/src/data/object.zod.ts:2843: the titleFormat describe says "a composite to a formula field designated as nameField". The generated reference pages repeat it: content/docs/references/data/object.mdx:167, content/docs/references/api/metadata.mdx:964 and content/docs/references/system/migration.mdx:349,643. The reconciliation note packages/spec/src/system/metadata-form-zod-reconciliation.test.ts:404 quotes it too.
  • skills/objectstack-data/SKILL.md:90: "for a composite title, designate a returnType: 'text' formula field as nameField".

Open PR #25074 also edits packages/lint/src/rule-explanations.ts, but in a different region (around lines 3562 and 4880; this PR edits around 2644), so the two do not conflict.

维护者速读(草稿)

  • 改了什么:记录标题相关的 lint 提示、os explain 说明、ADR-0079 D3 迁移句和一处文档注释,措辞统一为:需要被导入匹配、排序或搜索的组合标题应使用"存储型计算字段"(非 formula 字段加 expression,写入时计算并落库);text formula 标题仍合法,但只用于展示。
  • 为什么改:旧文案把作者引向 formula 标题,而 formula 值不落库,导致排序被拒、搜索够不到、导入无法按标题匹配(ADR-0079 补充第 9 条与裁决 6308445871)。
  • 风险与代价(含回滚):仅文案,不改判定、级别、规则码或导出;回滚即还原本 PR。
  • 席位意见:
  • 你要做的:本 PR 触及 docs/adr/**,属 Tier H,需要一次授权的 APPROVED 评审后才能落地。

Generated by Claude Code

…computed field; a formula title is for display only

The title-format-retired and title-unresolvable diagnostics, the
object/missing-name-field fix line, the os explain text, ADR-0079 D3's
migration sentence and the formulas.mdx worked example no longer steer a
composite title that import, sort or search must match into a formula field.
Wording only: no verdict, severity, code, key or export moves.

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 4 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 fccbd9d0d5f6637111ec9e3f17a723fe84c6903a → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a16a9c02bcb59793cc651d78a5c542a87f67cb34
Local-runs: none
Scope: docs/adr/0079-record-display-name.md, packages/lint/src/validate-record-title.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/data-model-rules.ts, content/docs/data-modeling/formulas.mdx + their string pins and .changeset/25071-composite-title-stored.md

① Derived judgments

  • The ADR edit is one hunk inside D3's migration sentence (:140-146). D3's decision, D1, D2, D4, the alternatives and the addendum are byte-unchanged. The new sentence binds exactly the two arms of ruling 6308445871:

    • a composite title that an import, a sort or a search must match is a stored computed field designated nameField;
    • a returnType: 'text' formula stays legal for a title shown for display only.
  • The shape matches the runtime. planStoredExpressions (objectql/src/engine.ts:3041-3050) selects def.type !== 'formula' && def.expression on insert (:16210) and update (:17559). The only virtual-type guard is SEARCH_VIRTUAL_TYPES = new Set(['formula']) (spec/src/data/search-fields.ts:96), used by assertFilterIsMaterializable and the import candidate filter (core/src/utils/import-runner.ts:734-745). So such a field is an ordinary column for where, sort, search and import, and packages/rest/src/import-reference-stored-computed-title.test.ts pins the import side.

  • Strings only. No rule id, severity, condition, path or export changes. Only string literals move. The hints are 189 and 191 characters and the fix line is 198, all within the 200 bound. Tests change only their expected strings.

  • Non-blocking:

    • the title-format-retired message now carries the whole prescription, and the remedy would sit better in hint alone;
    • D3 says "written on write" where item 9 says "when its sources change". The engine recomputes on the owning record's write, so a title over a lookup can go stale. That stays item 9's to carry.
  • Round 2 (a16a9c02bc), after a Test Core red on e9e0ec7bab: packages/cli/test/data-model-rules.test.ts:291 pins that the missing-name-field fix line cites ADR-0079, and the first wording had dropped it. The delta against the reviewed head is one string in data-model-rules.ts:741-742 and its verbatim lint pin. The new line is "Set nameField (ADR-0079), never titleFormat (os explain title-format-retired); a composite title to import/sort/search by is a text field with an expression (a text formula is display-only)." It is 198 characters, the meaning is unchanged, and the CLI test is not edited. The dev grepped every other package's tests and found no other pin on the changed strings.

② Semver level

@objectstack/lint patch, Clause-②: no, Metadata-protocol: none: only diagnostic and os explain text change, and nothing a metadata author can write moves. No spec package is touched.

③ Boundary flags

Implemented-by: claude/issue-25071-composite-title-stored
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Seat re-read of the check-runs before posting: all 42 runs on a16a9c02bc are complete, and none is red. Tier H: this record does not arm auto-merge; the PR waits for an authorized APPROVED review.

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 size/s tests tooling

Projects

None yet

3 participants