fix(lint): remove the primaryField phantom key from both title-face rules (#6326) - #6493
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
hotlong
marked this pull request as ready for review
August 8, 2026 02:05
hotlong
enabled auto-merge
August 8, 2026 02:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6326
按 2026-08-07 17:00Z 的维护者裁定:remove,不 declare。
nameField已是 ADR-0079 的规范主标题指针,再立一个平行指针没有拉力,且与 Prime Directive #7「One Zod source per metadata type」相悖。本 PR 零packages/spec改动。一、先复核前提:幽灵键确认仍然成立
在本 worktree 现场重跑了 issue 里那段探针(
packages/spec现场构建,17.0.0-rc.5):结论:
primaryField仍被硬拒;nameField与displayNameField同形状均通过——所以标题链里只摘primaryField一项,displayNameField是真实可声明面,保留。二、四处文件面(行号已在
origin/main上逐一复核,均已位移)派发单给的行号来自分诊时的读数,#6108 的 PR 落地后已经全部前移,下面是实际行号。
packages/lint/src/data-model-rules.ts:384/:391:410(谓词)/:398-407(注释)packages/lint/src/validate-semantic-roles.ts:190:190✔[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()必然硬拒的元数据。改后:三、
packages/cli/test/data-model-rules.test.ts:136-139——整条替换,并附一个额外发现派发单要求:不要只删不补,写下幸存的半个事实。复核后发现它比预想的更空:它是双重空绿。
旧断言:
它原本在钉什么:号称钉住
object/missing-name-field的primaryField一支。为什么是空绿——两条独立的理由:
ObjectSchema会拒收的 fixture(unrecognized_keys),它绿着的那个形状没有任何作者写得出来;code本身就在NAME_LIKE_FIELDS(data-model-rules.ts:36的七个名字之一),所以这个对象本来就被 name-like 那一支接住。实测:也就是说这条断言从来没有钉住
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-252的not.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 个 highlightd是否落进 strip:读primaryField→d被藏 → 报 SHADOWED;不读 →d仍渲染 → 干净。并配一条同形状、只把键换成nameField的正向断言,证明前一条是「primaryField被忽略」而不是「规则在这个 fixture 上本来就不动」。四、反向验证——先声明方向,再跑
方向与常规相反:本 PR 是移除一支,所以钉住移除的断言在把支放回去时才变红。声明写在跑之前。
does not treat primaryField as a title face→ 红,flagged()返回[]而非['objects[0].fields']expected [] to deeply equal [ 'objects[0].fields' ]FIELD_GROUP_SHADOWEDexpected [ { severity: 'warning', …(5) } ] to deeply equal [],path: objects[0].fieldGroups、grouptail、成员dnameField正向)→ 保持绿(nameField在链首,放不放该支都不影响)it就此中止。逻辑上必然绿,但本次 revert 下没有观测到,如实记录,不算已验证accepts an object with a name-like field, or an explicit nameField→ 保持绿steers the author to nameField…(含not.toContain)→ 保持绿: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 build(grep -c变成 2)再跑,才拿到上表第 1 行的红。这正是 AGENTS.md §9 陈旧产物陷阱的镜像形态。五、闸门
pnpm lint(ESLint,含仓内各族闸)pnpm check:type-check-debtpnpm exec turbo run typecheck(packages + apps)120 successful, 120 totalpnpm --filter @objectstack/lint test62 files, 1542 passedpnpm --filter @objectstack/cli test91 files, 928 passedpnpm check:nul-bytesgrep -naP控制字节自扫,干净pnpm check:doc-authoringpnpm check:empty-changeset1 declaring changeset(s) addedpnpm check:adr-anchorspnpm check:quick-reference-countspnpm check:skill-frame-sync/-freshness/check:skill-compatibilitypm-dispatch的四份拷贝,不覆盖objectstack-data/SKILL.md;仍全部跑过spec check:skill-docs/check:skill-refs/check:skill-examplescheck:type-check-debt:本单最容易咬人的一条@objectstack/lint是那 19 个「把自己的测试排除在 typecheck 之外」的包之一(tsconfig.json的exclude含**/*.test.ts),所以包级typecheck结构性看不见测试文件里的类型错误。.map((f) => f.rule)引入了一条 TS7006台账记的是 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。七、刻意没做的事
packages/spec—— 裁定是 remove 不是 declare,本单零 spec 改动。packages/spec/src/shared/suggestions.test.ts:100—— 那只是 camelCase 拼写建议的语料字符串,不是声明点,分诊已单独点出。packages/lint/src/index.ts—— PR feat(lint): view/page 可见性谓词的 CEL 语法构建期闸门 (#6253) #6472 正在改这个 barrel。复核确认:删两支谓词分支不增删任何规则 id,确实不需要动它。content/docs/releases/。.changeset/lint-missing-name-field-reads-name-field.md(lint object/missing-name-field 谓词不读 nameField,却仍采信 ADR-0079 已退役的 titleFormat——照平台自己的迁移建议做反而多得一条告警 #6108 的 changeset,其中一行说「primaryField两支行为不变」)。它是那次改动的历史记录,按时间顺序读是自洽的(那次确实没动,本次移除),改别人 PR 的 changeset 不对。validate-semantic-roles.test.ts:10的 import 扩展名。它能一次消掉该文件 5 条 TS2835/TS7006、净减台账,但属于本单文件面之外的改动;已在台账 ℹ 提示里可见,且 [P2] framework: 66 个包用 tsup 构建、无人做类型检查 —— 实测 18 个包共 380 处 code-tier 错误(#4118 的 framework 侧对应) #4311 / [finding] DEBT ledger counts in check-type-check-coverage.mjs drift silently — @objectstack/metadata-protocol records 28, actually reports 63 #5278 已在跟踪,不另立单以免重复。Generated by Claude Code