diff --git a/packages/lint/src/data-model-rules.ts b/packages/lint/src/data-model-rules.ts index 9cfe484d76..04d403af7d 100644 --- a/packages/lint/src/data-model-rules.ts +++ b/packages/lint/src/data-model-rules.ts @@ -33,6 +33,32 @@ const NUMERIC_TYPES = new Set([ 'number', 'currency', 'integer', 'decimal', 'percent', 'float', 'double', ]); const OPTION_FIELD_TYPES = new Set(['select', 'multiselect', 'radio', 'enum']); +/** + * Field names that give an object a title FACE, for R9 + * (`object/missing-name-field`). + * + * DIVERGES from spec's `NAME_ISH_EXACT` + * (`packages/spec/src/data/display-name.ts`) by exactly one entry: `code`. That + * difference is INTENTIONAL — the maintainer ruled on 2026-08-10 (#6734) to + * keep both sets as they are and write the gap down in both places rather than + * converge them. The two sets answer different questions: + * + * - R9 asks the LOOSER **"will records be anonymous?"** — is there anything + * here a human could read instead of a raw id? A `code` clears that bar, so + * it counts as a title face and is listed below. + * - ADR-0079 derivation (`resolveDisplayField`) asks **"what IS the title?"** + * — which field to PICK as the primary. A `code` is an identifier, not a + * title, so spec deliberately omits it from tier 1 (name-ish exact) and + * tier 2 (name-ish affix). A `code`-only object can still be derived at + * tier 3 ("first title-eligible field by declaration order") — by a + * different rule and a different priority, so the two are not equivalent + * even where they agree on the outcome. + * + * Nothing user-visible turns on the gap: R9 is `severity: 'suggestion'` and + * ADR-0079's `Record #` floor means no object ships without a title either + * way. Do not "fix" either set into the other without a ruling that supersedes + * the one above. + */ const NAME_LIKE_FIELDS = ['name', 'title', 'subject', 'label', 'full_name', 'display_name', 'code']; /** Child object names that read as line-items / composition (entered with the parent). */ diff --git a/packages/spec/src/data/display-name.ts b/packages/spec/src/data/display-name.ts index e1267dfe57..8e5ef5d6a4 100644 --- a/packages/spec/src/data/display-name.ts +++ b/packages/spec/src/data/display-name.ts @@ -130,7 +130,30 @@ export function isTitleEligible(fieldDef: TitleEligibleFieldDef | undefined | nu return TITLE_ELIGIBLE_TYPES.has(type); } -/** Exact name-ish field names (case-insensitive), highest derivation priority. */ +/** + * Exact name-ish field names (case-insensitive), highest derivation priority. + * + * DIVERGES from lint's `NAME_LIKE_FIELDS` + * (`packages/lint/src/data-model-rules.ts`) by exactly one entry: `code`. That + * difference is INTENTIONAL — the two sets answer different questions, and the + * maintainer ruled on 2026-08-10 (#6734) to keep both as they are and write the + * gap down in both places rather than converge them. + * + * This set answers **"what is the record's title?"** — it is tier 1 of + * {@link resolveDisplayField}'s derivation, i.e. the names whose presence is + * strong enough evidence to pick that field as the primary title outright. A + * `code` is an identifier, not a title, so it is deliberately absent here; a + * `code`-only object still gets a title, but at tier 3 ("first title-eligible + * field by declaration order") — by a different rule and a different priority. + * + * Lint R9 (`object/missing-name-field`) asks the looser **"will records be + * anonymous?"** question — does this object have any title FACE at all — and + * for that a `code` counts, which is why its set carries the extra entry. R9 is + * `severity: 'suggestion'` and the `Record #` floor below guarantees a + * title regardless, so the two sets never disagree about an outcome a user + * sees. Do not "fix" either set into the other without a ruling that supersedes + * the one above. + */ const NAME_ISH_EXACT: ReadonlySet = new Set([ 'name', 'title', 'subject', 'label', 'full_name', 'display_name', ]); diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index e26fbc4cd4..113b67787d 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -1103,6 +1103,20 @@ Data-model rules (in addition to naming/label/i18n): | `field/select-missing-options` | warning | a `select`/`multiselect`/`radio` with no `options` (or options source) | | `object/missing-name-field` | suggestion | an object with no `nameField` (ADR-0079's canonical title pointer) and no name-like field (`name`/`title`/`subject`/`label`/`full_name`/`display_name`/`code`) | +> **`code` counts for R9, but is NOT a title-derivation key.** R9's name-like +> list above is the *looser* of two "name-like" sets, and the difference is +> deliberate. R9 asks **"will records be anonymous?"** — is there any readable +> face at all — and a `code` clears that bar. ADR-0079's title derivation +> (`resolveDisplayField`) asks the narrower **"what IS the title?"**, and its +> name-ish set is `name`/`title`/`subject`/`label`/`full_name`/`display_name` +> **without `code`** — an identifier is not a title. So an object whose only +> name-ish field is `code` is R9-clean, yet its title is derived by the +> lower-priority "first title-eligible field by declaration order" tier rather +> than by name. Nothing user-visible turns on this (R9 is `suggestion`, and the +> `Record #` floor guarantees a title regardless), but do not read the R9 +> list as the derivation contract — set `nameField` explicitly when the title +> matters. + These same rules are the **rubric for AI-generated metadata** — a generation is "good" exactly when it is schema-valid and lint-clean: