Skip to content

fix(lint): remove the primaryField phantom key from both title-face rules (#6326) - #6493

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6326-primaryfield-phantom-key
Aug 8, 2026
Merged

fix(lint): remove the primaryField phantom key from both title-face rules (#6326)#6493
hotlong merged 1 commit into
mainfrom
claude/issue-6326-primaryfield-phantom-key

Conversation

@hotlong

@hotlong hotlong commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #6326

按 2026-08-07 17:00Z 的维护者裁定:remove,不 declarenameField 已是 ADR-0079 的规范主标题指针,再立一个平行指针没有拉力,且与 Prime Directive #7「One Zod source per metadata type」相悖。本 PR 零 packages/spec 改动。

一、先复核前提:幽灵键确认仍然成立

在本 worktree 现场重跑了 issue 里那段探针(packages/spec 现场构建,17.0.0-rc.5):

--- primaryField: safeParse ---
success: false
issues: [
  {
    "code": "unrecognized_keys",
    "keys": [ "primaryField" ],
    "path": [],
    "message": "Unrecognized key(s) on this object: `primaryField`. ..."
  }
]
--- primaryField: create() ---
create() threw: ObjectSchema.create('probe_obj'): unknown key(s) — primaryField.
  • `primaryField` is not an ObjectSchema field.
--- control: nameField safeParse ---
success: true
--- control: displayNameField safeParse ---
success: true

结论:primaryField 仍被硬拒;nameFielddisplayNameField 同形状均通过——所以标题链里只摘 primaryField 一项displayNameField 是真实可声明面,保留。

二、四处文件面(行号已在 origin/main 上逐一复核,均已位移)

派发单给的行号来自分诊时的读数,#6108 的 PR 落地后已经全部前移,下面是实际行号。

文件 派发单行号 实际行号 改动
packages/lint/src/data-model-rules.ts :384 / :391 :410(谓词)/ :398-407(注释) 摘掉 `!!obj.primaryField
packages/lint/src/validate-semantic-roles.ts :190 :190 规则 (d) 标题解析链由 [nameField, primaryField, displayNameField] 收敛为 [nameField, displayNameField]
skills/objectstack-data/SKILL.md :1001 :1001 规则表该行改为只点名真实可声明面
packages/cli/test/data-model-rules.test.ts :136-138 :136-139 ✔(另有 :191-208:243-252 两处同源) 见下节

:391 那条「author-facing message」经复核在本 PR 前已经不含 primaryField——#6108 的 PR 已把文案收敛过了,本 PR 只动谓词。

文档面是当下活着的那一半。 skills/objectstack-data/SKILL.md 是 AI 编写元数据时读的技能文档,旧文案把 primaryField 明说成这条规则的合法逃逸口,等于在教 AI 写出 ObjectSchema.create() 必然硬拒的元数据。改后:

| `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`) |

三、packages/cli/test/data-model-rules.test.ts:136-139——整条替换,并附一个额外发现

派发单要求:不要只删不补,写下幸存的半个事实。复核后发现它比预想的更空:它是双重空绿。

旧断言:

it('accepts an object with a name field or primaryField', () => {
  expect(has(lintDataModel([{ name: 'a', fields: { name: { type: 'text' } } }]), )).toBe(false);
  expect(has(lintDataModel([{ name: 'b', primaryField: 'code', fields: { code: { type: 'text' } } }]), )).toBe(false);
});

它原本在钉什么:号称钉住 object/missing-name-fieldprimaryField 一支。

为什么是空绿——两条独立的理由

  1. 喂的是一个 ObjectSchema 会拒收的 fixture(unrecognized_keys),它绿着的那个形状没有任何作者写得出来;
  2. 更要命的一条,是本次现场量出来的code 本身就在 NAME_LIKE_FIELDSdata-model-rules.ts:36 的七个名字之一),所以这个对象本来就被 name-like 那一支接住。实测:
OLD :138 fixture {primaryField:'code', fields:{code}} -> flagged? false  → 摘掉支后仍然 GREEN(从未钉住该支)
same fixture WITHOUT primaryField at all            -> flagged? false  → 证明干活的一直是 name-like 支
isolated fixture {primaryField:'period_key'}        -> flagged? true   → 这个才真正依赖该支

也就是说这条断言从来没有钉住 primaryField,删掉该支它照样绿。

幸存的半个事实:第一条断言(name-like 字段即标题面)是真的,保留。

替换后钉什么:把第二条换成唯一真实存在的显式指针 nameField,且 fixture 刻意用 invoice_number——不在 NAME_LIKE_FIELDS 里,所以没有别的支能接住它,这条断言真的能失败。

同源的另外两处:

  • :191-208「leaves the primaryField and name-like limbs unchanged」——整条替换。它的判定值正好从「不报」翻成「报」,属于派发单说的第三类。改为断言精确的上报集合而不是某个字符串的缺席,两个方向都能红:把支放回去 objects[0] 掉出集合(变 []),name-like 支坏掉 objects[1] 会加进来。
  • :243-252not.toContain('primaryField')——保留,但在文件里如实标注:它是负向断言,在本 PR 之前就已经是绿的lint object/missing-name-field 谓词不读 nameField,却仍采信 ADR-0079 已退役的 titleFormat——照平台自己的迁移建议做反而多得一条告警 #6108 已清过文案,本 PR 只动谓词、不动文案),所以它不是本次移除的证据;真正的证据是上面 (c) 那条。它之所以留着,是因为它和同一个 it 里三条正向断言配对(toContain('nameField')fix 含 ADR-0079 / titleFormat),单独存在才是裸空绿。

新增一条 validate-semantic-roles 的钉子:标题链那处此前全仓没有任何测试(全仓 grep 证实没有任何 fixture 声明 primaryField 去喂它)。新增 ignores primaryField in title resolution; nameField still resolves (#6326),用的是能区分两种实现的 fixture——标题字段会被排除出 4 格 strip,所以 ref_no 算不算标题,决定了第 5 个 highlight d 是否落进 strip:读 primaryFieldd 被藏 → 报 SHADOWED;不读 → d 仍渲染 → 干净。并配一条同形状、只把键换成 nameField 的正向断言,证明前一条是「primaryField 被忽略」而不是「规则在这个 fixture 上本来就不动」。

两个新 fixture 都刻意是 off-spec 的,这正是被测对象:lintDataModel / validateSemanticRoles 跑在作者写下的元数据上,早于 schema parse,所以被拒的键物理上到得了它们;断言的是「规则忽略它」,不是「这个键合法」。文件里已写明不要把 fixture 改成 schema-valid,那会把覆盖面删掉。

四、反向验证——先声明方向,再跑

方向与常规相反:本 PR 是移除一支,所以钉住移除的断言在把支放回去时才变红。声明写在跑之前。

# 声明 实测 一致
1 CLI (c) 案 does not treat primaryField as a title faceflagged() 返回 [] 而非 ['objects[0].fields'] 红,报文逐字为 expected [] to deeply equal [ 'objects[0].fields' ]
2 semantic-roles 新钉子第一条断言 → ,多出一条 FIELD_GROUP_SHADOWED 红,expected [ { severity: 'warning', …(5) } ] to deeply equal []path: objects[0].fieldGroups、group tail、成员 d
3 同测试第二条(nameField 正向)→ 保持绿nameField 在链首,放不放该支都不影响) 未被执行:第一条断言先抛,同一个 it 就此中止。逻辑上必然绿,但本次 revert 下没有观测到,如实记录,不算已验证 ⚠️ 部分
4 替换后的 accepts an object with a name-like field, or an explicit nameField → 保持绿 绿(该文件 51 条里 50 条通过,唯一红的是 (c))
5 steers the author to nameField…(含 not.toContain)→ 保持绿 绿
6 :138 断言在摘支后仍然绿(即它从未钉住该支) 绿,见上节实测三行

方法学上有一处值得记下来:第一次跑 (c) 的反向验证时它是绿的,看起来推翻了声明。原因不是判断错,而是 packages/cli 的测试 import { lintDataModel } from '@objectstack/lint'——走的是构建产物 dist,而我在 revert 之前就已经构建过 lint。grep -c primaryField packages/lint/dist/index.js 当时是 0,即测试跑的仍是「已移除」的那份。revert 后重新 pnpm --filter @objectstack/lint buildgrep -c 变成 2)再跑,才拿到上表第 1 行的红。这正是 AGENTS.md §9 陈旧产物陷阱的镜像形态。

五、闸门

闸门 结果 说明
pnpm lint(ESLint,含仓内各族闸) ✅ pass 无输出即通过
pnpm check:type-check-debt ✅ pass 见下节,这是本单必跑的那一条
pnpm exec turbo run typecheck(packages + apps) ✅ pass 120 successful, 120 total
pnpm --filter @objectstack/lint test ✅ pass 62 files, 1542 passed
pnpm --filter @objectstack/cli test ✅ pass 91 files, 928 passed
pnpm check:nul-bytes ✅ pass 6094 个文件,无裸控制字节;另对本 PR 改动的 6 个文件单独跑了 grep -naP 控制字节自扫,干净
pnpm check:doc-authoring ✅ pass 365 个文件
pnpm check:empty-changeset ✅ pass 1 declaring changeset(s) added
pnpm check:adr-anchors ✅ pass 37 个锚定文件
pnpm check:quick-reference-counts ✅ pass
pnpm check:skill-frame-sync / -freshness / check:skill-compatibility ✅ pass 已按派发单要求确认:这些 SKILL 同步闸覆盖的是 pm-dispatch 的四份拷贝,不覆盖 objectstack-data/SKILL.md;仍全部跑过
spec check:skill-docs / check:skill-refs / check:skill-examples ✅ pass 205 个 prose example 仍能 type-check

check:type-check-debt:本单最容易咬人的一条

@objectstack/lint 是那 19 个「把自己的测试排除在 typecheck 之外」的包之一(tsconfig.jsonexclude**/*.test.ts),所以包级 typecheck 结构性看不见测试文件里的类型错误。

  • 改动前基线:39(本 worktree 现场按闸门同样的方式抬起排除项实测)
  • 首次改完:40(+1)——我新增的 .map((f) => f.rule) 引入了一条 TS7006
  • 修正后39,与基线持平

台账记的是 42,40 其实也过闸。但我没有吃这 3 格余量:那条 TS7006 的根因是该文件第 10 行的相对 import 少了 .js 扩展名(TS2835),NodeNext 下模块里每个符号退化成 any,于是每个遍历 finding 的回调都白送一条 TS7006(同文件已有 4 条同样成因的)。我把断言改成 toHaveLength(1) + 下标取值,断言强度不变、不新增台账条目,并在原地注明了原因。没有抬高台账数字。

六、Changeset

.changeset/lint-primaryfield-phantom-key-removed.md@objectstack/lint: patch

为什么是 patch:无导出增删(packages/lint/src/index.ts 未动)、零 spec 改动、不涉及可声明键的退役,因此不触发「breaking changeset 必须带迁移」。changeset 里已按要求写明:primaryField 从来不是可声明键,写了它的对象在 schema 上本来就发布不了,所以没有任何能工作的 app 会因此回归;唯一的行为变化是这类对象会新得一条 suggestion(该 severity 不失败命令),而真正的修法是改声明 nameField

七、刻意没做的事


Generated by Claude Code

…ules (#6326)

`primaryField` is declared nowhere in `packages/spec`. Measured on the
17.0.0-rc.5 dist, `ObjectSchema.safeParse` returns
`unrecognized_keys: ['primaryField']` and `ObjectSchema.create()` throws,
while the same shape with `nameField` parses clean. The key was therefore
never a declarable authoring surface, yet three consumers treated it as a
legal title face.

Per the maintainer ruling on #6326 (remove, do not declare — `nameField` is
ADR-0079's canonical title pointer and a second parallel pointer contradicts
Prime Directive #7), this drops it at every consumer, with zero change to
`packages/spec`:

- `data-model-rules.ts`: `object/missing-name-field` loses the
  `!!obj.primaryField` disjunct.
- `validate-semantic-roles.ts`: rule (d)'s title-resolution chain narrows to
  `[nameField, displayNameField]`.
- `skills/objectstack-data/SKILL.md`: the rule row now names only surfaces an
  author can actually declare. This was the live half — the skill doc is what
  an AI author reads, and it advertised a key `ObjectSchema.create()` rejects.

Test disposition, per the three-way fixture triage:

- `packages/cli/test/data-model-rules.test.ts` "accepts an object with a name
  field or primaryField": REPLACED WHOLESALE. Its `primaryField` assertion was
  vacuous twice over — the fixture is one the schema rejects, and its `code`
  field is itself in NAME_LIKE_FIELDS, so the name-like limb accepted the
  object regardless and the assertion stayed green with the limb deleted. It
  never pinned the limb. The surviving half (a name-like field is a title
  face) is kept; the replacement pins the `nameField` limb on an isolated
  fixture (`invoice_number`) that no other limb can rescue.
- The `(c)` case is re-pinned as an exact reported set rather than the absence
  of a string, so it fails in both directions.
- A new pin in `validate-semantic-roles.test.ts` covers the chain removal,
  which previously had no test at all, paired with a positive `nameField`
  assertion so neither half can pass vacuously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:00am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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/m tests tooling

Projects

None yet

2 participants