Skip to content

Commit 459afea

Browse files
fix(lint): a composite record title that must be queried is a stored computed field; a formula title is for display only (#25127)
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 - `packages/objectql/src/engine.ts:3028-3050`, `planStoredExpressions`: every field that is **not** a `formula` and declares an `expression` (`def.type !== 'formula' && def.expression`, :3046) is computed on every write and stored in its own column (#24619). Write paths call it at :16210 (insert) and :17559 (update). - `packages/rest/src/import-reference-stored-computed-title.test.ts` (on main) pins the import side end to end: a lookup cell matches a target titled by a stored computed field, and the same title declared as a formula answers `reference_not_found` (ruling 6308445871). - ADR-0079 addendum item 9 (`docs/adr/0079-record-display-name.md:400-406`): a formula value is stored nowhere, a sort by it is refused (`400 INVALID_SORT`), and search cannot reach it. ## 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](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4c7dcb4 commit 459afea

8 files changed

Lines changed: 40 additions & 19 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
fix(lint): the record-title diagnostics no longer steer a composite title that must be queried into a formula field
6+
7+
Clause-②: no (wording only: no verdict, severity, code, key or export moves)
8+
Metadata-protocol: none
9+
10+
The `title-format-retired` and `title-unresolvable` messages and fix lines, the `object/missing-name-field` fix line, and the `os explain title-format-retired` text now say the same thing: a composite record title that an import, a sort or a search must match 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, designated as `nameField`. A `formula` field with `returnType: 'text'` stays a legal `nameField` for a title shown for display only: its value is computed on read and stored nowhere, so a sort by it is refused, search never reaches it, and an import cannot match a lookup cell against it.
11+
12+
No finding fires on a different object than before, and no severity changes. Only the text an author reads changes.

‎content/docs/data-modeling/formulas.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
9696
export const Invoice = ObjectSchema.create({
9797
name: 'invoice',
9898
sharingModel: 'private',
99-
nameField: 'display_title', // ADR-0079 — the record title is a designated field; composite titles migrate off the deprecated `titleFormat` to a text formula
99+
nameField: 'display_title', // ADR-0079 — the record title is a designated field; a composite title migrates off the deprecated `titleFormat` to a text formula for display only, or to a stored computed field (a non-formula field with an `expression`, written on write) when import, sort or search must match it
100100
fields: {
101101
// Composite record title as a text formula, surfaced via `nameField` above.
102102
display_title: Field.formula({

‎docs/adr/0079-record-display-name.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,12 @@ the diagnostic is advisory, not an error:
138138
> consumed by the release)
139139
140140
**Migration is stated, not left to the author**: a single-field title becomes
141-
`nameField: '<field>'`; a **composite** title becomes a `formula` field with
142-
`returnType: 'text'`, designated as the `nameField`
141+
`nameField: '<field>'`; a **composite** title that must be queried (matched by
142+
an import, sorted, searched) becomes a stored computed field (a non-formula
143+
field with an `expression`, written on write), designated as the `nameField`,
144+
the remedy addendum item 9 states for sort and search and ruling record
145+
6308445871 extends to import; a `formula` field with `returnType: 'text'`
146+
designated as the `nameField` stays legal for a title shown for display only
143147
(`packages/lint/src/validate-record-title.ts`; the worked example is
144148
`content/docs/data-modeling/formulas.mdx`).
145149

‎packages/lint/src/data-model-rules.fix-lines.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ describe('[#22161] data-model-rules.ts — one remedy line per fix', () => {
150150
it('object/missing-name-field: the pointer, and where the titleFormat reasoning lives', () => {
151151
const fix = fixOf(record(lintDataModel([{ name: LONG_OBJECT, fields: { amount: { type: 'number' } } }])), MISSING_NAME_FIELD);
152152
expect(fix).toBe(
153-
"Set `nameField: '<field>'` (ADR-0079's title pointer) to a stored text/autonumber field or a " +
154-
"`returnType: 'text'` formula; `titleFormat` does not count (`os explain title-format-retired`).",
153+
"Set `nameField` (ADR-0079), never `titleFormat` (`os explain title-format-retired`); a composite title " +
154+
"to import/sort/search by is a text field with an `expression` (a text formula is display-only).",
155155
);
156156
// The command the line names answers, and says why `titleFormat` does not count.
157157
expect(explainRule('title-format-retired')!.paragraphs.join('\n')).toContain('render-only');

‎packages/lint/src/data-model-rules.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,8 +738,8 @@ export function lintDataModel(objects: any[]): LintIssue[] {
738738
// [#22161] One remedy line; why `titleFormat` does not count is
739739
// `title-format-retired`'s `os explain` text.
740740
fix:
741-
"Set `nameField: '<field>'` (ADR-0079's title pointer) to a stored text/autonumber field or a " +
742-
"`returnType: 'text'` formula; `titleFormat` does not count (`os explain title-format-retired`).",
741+
"Set `nameField` (ADR-0079), never `titleFormat` (`os explain title-format-retired`); a composite title " +
742+
"to import/sort/search by is a text field with an `expression` (a text formula is display-only).",
743743
});
744744
}
745745

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,9 +2644,13 @@ const TITLE_FORMAT_RETIRED_EXPLANATION: RuleExplanation = {
26442644
'alias), or one derived from the object\'s fields. `titleFormat` is a render-only template: the ' +
26452645
'server can neither return the title it draws nor query by it, the shared title predicate ' +
26462646
'(`objectTitleCompleteness`) never reads it, and an explicit `nameField` takes precedence over it.',
2647-
'For a single-field title, set `nameField` to that field. For a composite title, add a formula ' +
2648-
'field with `returnType: \'text\'` and point `nameField` at it: a formula whose result is text is ' +
2649-
'title-eligible. Then drop `titleFormat`, which this warning reports for as long as it is declared.',
2647+
'For a single-field title, set `nameField` to that field. A composite title that must be queried ' +
2648+
'(matched by an import, sorted, searched) is a stored computed field: a non-formula field, such as ' +
2649+
'a `text` field, that declares an `expression`, which the engine computes and stores on every ' +
2650+
'write; point `nameField` at it. A formula with `returnType: \'text\'` is title-eligible too and ' +
2651+
'stays legal for a title shown for display only: its value is computed on read and stored nowhere, ' +
2652+
'so a sort by it is refused, search never reaches it, and an import cannot match a lookup cell ' +
2653+
'against it. Then drop `titleFormat`, which this warning reports for as long as it is declared.',
26502654
'A warning, not an error: the schema still parses the key, so existing metadata keeps loading.',
26512655
],
26522656
};

‎packages/lint/src/validate-record-title.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,12 @@ describe('[#22161] one-line fixes — title-format-retired and title-unresolvabl
131131
const findings = validateRecordTitle(objStack({ titleFormat: '{{record.amount}}', fields: { amount: { type: 'currency' } } }));
132132
const byRule = new Map(findings.map((f) => [f.rule, f.hint]));
133133
expect(byRule.get(TITLE_FORMAT_RETIRED)).toBe(
134-
"Set nameField: '<field>' for a single-field title, or add a formula field (returnType: 'text') and " +
135-
'designate it via nameField; then drop titleFormat',
134+
"Set nameField: '<field>' for one field; a composite title to import, sort or search by is a text " +
135+
'field with an expression, stored on write (a text formula is display-only); drop titleFormat',
136136
);
137137
expect(byRule.get(TITLE_UNRESOLVABLE)).toBe(
138-
"Set nameField to a text/email field (or a formula field with returnType: 'text'), or add a text " +
139-
'field named "name"/"title"',
138+
'Set nameField to a text/email field (a composite title to import, sort or search by: a text field ' +
139+
'with an expression; a text formula is display-only), or add a text field named "name"/"title"',
140140
);
141141
});
142142

‎packages/lint/src/validate-record-title.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,13 @@ export function validateRecordTitle(stack: AnyRec): RecordTitleFinding[] {
7979
path,
8080
message:
8181
`${objName}: titleFormat is retired (ADR-0079) — migrate to nameField ` +
82-
`(single field) or a formula field designated nameField`,
82+
`(single field); a composite title that import, sort or search must match is a stored ` +
83+
`field with an expression designated nameField (a formula title is for display only)`,
8384
// [#22161] One remedy line; why the template is retired (render-only,
8485
// neither returned nor queryable) is `os explain title-format-retired`.
8586
hint:
86-
`Set nameField: '<field>' for a single-field title, or add a formula field (returnType: 'text') ` +
87-
`and designate it via nameField; then drop titleFormat`,
87+
`Set nameField: '<field>' for one field; a composite title to import, sort or search by is a ` +
88+
`text field with an expression, stored on write (a text formula is display-only); drop titleFormat`,
8889
});
8990
}
9091

@@ -104,8 +105,8 @@ export function validateRecordTitle(stack: AnyRec): RecordTitleFinding[] {
104105
// [#22161] One remedy line; the runtime's fallback title, and why this
105106
// is only a warning, are `os explain title-unresolvable`.
106107
hint:
107-
`Set nameField to a text/email field (or a formula field with returnType: 'text'), or add a ` +
108-
`text field named "name"/"title"`,
108+
`Set nameField to a text/email field (a composite title to import, sort or search by: a text ` +
109+
`field with an expression; a text formula is display-only), or add a text field named "name"/"title"`,
109110
});
110111
}
111112
}

0 commit comments

Comments
 (0)