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.

@hotlong
hotlong marked this pull request as ready for review August 8, 2026 02:05
@hotlong
hotlong enabled auto-merge August 8, 2026 02:06
@hotlong
hotlong added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 6ce10bd Aug 8, 2026
25 checks passed
@hotlong
hotlong deleted the claude/issue-6326-primaryfield-phantom-key branch August 8, 2026 02:31
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