Skip to content

docs(lint): 按实测改正 normalized 输入层的三条依据,并补回归 pin (#6073) - #6340

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6073-normalized-tier-preparse
Aug 7, 2026
Merged

docs(lint): 按实测改正 normalized 输入层的三条依据,并补回归 pin (#6073)#6340
hotlong merged 1 commit into
mainfrom
claude/issue-6073-normalized-tier-preparse

Conversation

@hotlong

@hotlonghotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes#6073

本单是「测量决定结论」型。分诊 2026-08-06T17:57Z 钉死三条范围:先测量后改码、必答 warnUnknownAuthoringKeys 通道覆盖问题、若修法触及 AuthoringRuleInputTier 的 pre-parse 声明或 runAuthoringRules 对外契约则升 needs-user-decision

测量做完了,结论是假设不成立 —— 但不成立的方式和单子预设的两个分支都不同,所以这里如实记录实测方向,不套模板(见「所走分支」)。未触碰升级闸:AuthoringRuleInputTier 的两个成员、runAuthoringRules 的签名与 AuthoringRuleRun 的形状一字未动。


一、测量记录(命令 + 输出)

环境:worktree @ origin/maina7e7851,packages/{spec,lint,cli} 本地构建。行号以实测为准。

1. 机制半边:defineStack 配置交给三条命令的「normalized」层是 POST-parse

loadConfig() 返回模块 default export(= defineStack(...) = result.data),lint.ts:428 / validate.ts / compile.ts 再对normalizeStackInput。实测同一个 flow 走两条门:

### (a) parse-time defaults in the tier each door produces
TRUE pre-parse flows[0].runAs = undefined status = undefined
CLI defineStack flows[0].runAs = "user" status = "draft"
⇒ the CLI's "normalized" tier is POST-parse: true

正文与 #5693 的观察成立:FlowSchema.default('user') 已生效,再 normalize 一次也回不去。

2. example app 实测:os lint / os validate,defineStack 门

examples/app-todo/objectstack.config.tsviews: [] 里加一个扁平 list view({ name, label, type: 'grid', data, columns }),即 validate-view-containers 的证据形状:

########## os lint --skip-i18n ##########
→ Loading configuration...
✗ defineStack validation failed (1 issue):
✗ views.1: Unrecognized key(s) on this view container: `type`, `data`, `columns`. …
• `type` belongs to a single VIEW, not to the container. Wrap it:
`defineView({ list: { type, data, columns, … } })`, or name it — …
LINT_EXIT=1
########## os validate ##########
→ Loading configuration...
✗ defineStack validation failed (1 issue):
✗ views.1: Unrecognized key(s) on this view container: `type`, `data`, `columns`. …
VALIDATE_EXIT=1

两条命令都在 LOAD 阶段拒绝配置,规则一条都没跑到 —— 也不需要跑。#4001 之后 ViewSchema.strict(),不是 strip:它点名了规则会点名的同一批键,而且早一层、还附修法。defineView 更早一步(view.zod.ts:1951,viewCount === 0 直接 throw)。

3. 对照:同一份 stack 走 raw 门(不经 defineStack)

同目录放一份 export default { … } 的纯对象字面量配置(= raw JSON/YAML / strict: false / 直接 API 调用这条路),同时带两条规则的证据:

########## os lint probe-raw-6073.config.ts ##########
Errors (7)
✗ object "probe_task" › listViews.my_pending: `quickFilters` is a page filters-mode control …
list-view-filters-in-views-mode at objects[0].listViews.my_pending.quickFilters
✗ object "probe_task" › listViews.my_pending: `userFilters` with `element: "tabs"` is page-only …
list-view-filters-in-views-mode at objects[0].listViews.my_pending.userFilters
✗ views[0] ("probe_flat_view"): Flat list-view object is not a view container …
view-container-shape at views[0]
…
LINT_EXIT=1
########## os validate probe-raw-6073.config.ts ##########
✗ Validation failed
objects:
✗ objects.0.listViews.my_pending.userFilters.element
invalid_value: Invalid option: expected one of "dropdown"|"toggle"
✗ objects.0.listViews.my_pending
unrecognized_keys: Unrecognized key(s) on this list view: `quickFilters`. … Did you mean `quickFilters` → `userFilters`?
views:
✗ views.0
unrecognized_keys: Unrecognized key(s) on this view container: `type`, `data`, `columns`. …
4 validation error(s) total
VALIDATE_EXIT=1

差异表:

os lint(从不 parse)os validate(parse)
defineStack TS 配置(文档化的唯一写法)配置在 LOAD 被拒,schema 点名同左
raw 对象字面量(无 defineStack)两条 normalized 规则按名报告schema 步先拒,规则不产出 finding

即:正文预测的「差异」确实存在,但方向是反的 —— defineStack 那条路是三条里最响的,不是静默的那条。

4. 逐规则测量(六条 input: 'normalized' 全覆盖)

每条规则各喂它自己的证据形状,fixture 按「补声明」原则补齐到「除刻意埋的那一个缺陷外全部 spec-valid」(两个 fixture 首轮因缺 type/edges/label 而被 schema 以无关理由拒收,已修正后重测):

规则证据TRUE pre-parsedefineStackCLI 层(post-defineStack)判定
validateViewContainers(扁平臂)views: [] 里的扁平 list view1抛错(点名 type/data/columns)被 schema 拒收
validateViewContainers(空容器臂)四个槽全缺的容器1通过1存活
validateListViewModequickFilters + userFilters: { element: 'tabs' }2抛错(键拒收 + 枚举拒收)被 schema 拒收
validateFunctionalCompletenesssummary 字段无 summaryOperations1通过1存活
validateComponentPropsproperties 里未声明的 titel1通过1存活
validateFlowTriggerReadinessschema 拒绝的 config.timeRelative3通过3存活
validateVisibilityPredicates(别名臂)visibleOn 别名0通过(发转换通知)0见下
validateVisibilityPredicates(值臂 ×2)裸标识符 / 错层根1 / 1通过1 / 1存活

没有任何一条规则处在「本该报却静默、且作者因此受损」的状态。

5. 别名臂的定位(与正文的猜测都不同)

validate-visibility-predicates.ts:8-13 自称别名「folded … during parse()」。实测:折叠由 ADR-0087 D2 的两条转换完成,而它们跑在 normalizeStackInput之内 —— 该函数的输出就是 normalized 层本身。

▸ views[].form.sections[].visibleOn
0. rule on RAW authored object (bypasses normalizeStackInput) → 1
1. rule on the `normalized` tier (normalizeStackInput output) → 0
2. defineStack() → returns; 1 warning
WARN defineStack: views[0].form.sections[0].visibleWhen: 'visibleOn' → 'visibleWhen'
(converted at load; conversion 'view-visibleOn-to-visibleWhen', retires in protocol 16). …
3. rule on CLI tier post-defineStack → 0

三个 spec-合法别名 site(views[].form.* / views[].formViews.*.* / pages[].regions[].components[])一律 0;唯一还能报的形状是 views[].sections[] —— 单测 :42 用的那个,被 strict ViewSchema 直接拒收。这与本单不是同一个机制(与 defineStack 无关,raw 门也一样),已按 Prime Directive #10 单独立观察单 #6318,本 PR 只改注释、不修法。


二、必答项:warnUnknownAuthoringKeys 通道对六条规则证据面的逐条覆盖

分诊必答的是:defineStack 在 parse 前已跑 warnUnknownAuthoringKeys(normalized)(stack.zod.ts:1237),这条通道是否已覆盖那六条规则关心的证据?

实测答案:一条都没覆盖 —— 六个 case 的 warnUnknownAuthoringKeys 输出均为 0 条。 原因分两类,都由该 walker 自己的 posture 规则决定(metadata-authoring-lint.ts:78-154:strip → 报,strict跳过,passthrough → 跳过):

规则证据性质该通道是否覆盖为什么
validateViewContainers(扁平臂)未声明的❌ 0 条ViewSchemastrict ⇒ walker 按设计闭嘴,由 parse 自己大声拒绝(实测已拒)
validateViewContainers(空容器臂)键全部已声明❌ 0 条无未知键可报;这是「声明齐全但语义空」,键级 lint 结构上看不见
validateListViewModequickFilters 是未声明键;element: 'tabs'❌ 0 条键侧同上(strict ⇒ 跳过,parse 拒);值侧本就不在键级 lint 的判定面内
validateFunctionalCompleteness键全部已声明,惰性❌ 0 条正文自己写过:「#4001 的未知键拒绝看不见它」
validateComponentPropsproperties 内未声明键❌ 0 条PageComponent.propertiesz.record(z.string(), z.unknown()),walker 走到载体就停;该规则反过来自己调用lintUnknownKeysAgainstSchema 并先按 type 派发(#5068)
validateFlowTriggerReadinessconfig.timeRelative❌ 0 条flow#4001 Tier-A strict 集 ⇒ walker 跳过;且 node config 槽按设计开放,值级判定与键级无关
validateVisibilityPredicates(别名臂)别名❌ 0 条真正覆盖它的是另一条通道:warnConversionNotice(ADR-0087 D2),实测 1 条,措辞更好且带 protocol 16 退役期

结论对修法的意义(这正是分诊说「答案决定修法」的那一步):既然该通道一条都没覆盖,「把六条规则接到 warnUnknownAuthoringKeys 那条缝上」不是可行修法 —— 它判的是键是否被声明,而这六条规则里有四条判的是值是否有意义,两者不是同一个判定面。而键侧那两条,证据早已被 strict schema 以更好的措辞拒掉了,不需要第二个声音。


三、所走分支及依据

分诊给了两支:α(规则照常报 ⇒ 假设不成立,注释按实测补句)/ β(规则不报 ⇒ #4984 族真盲点,接线修法)。

实测落在 α 的实质上,但机制是第三种,报告如实写明:六条规则里四条「照常报」(α 的字面情形),另两条不报不是因为静默失效,而是因为配置根本加载不了 —— schema 在更早一层拒绝,措辞比规则更好。对作者而言这严格更优,不是缺陷。同时 β 的前提(#4984 族「规则只在生产从不发送的形状上被证明过」)在别名臂上确实成立,但其成因与本单的 defineStack-parse 无关,故按 #10 外立 #6318,不在本 PR 修。

因此本 PR 收束为「已测量、无缺陷」形态,改动仅为注释按实测改正 + 一个回归 pin。⛔ 升级闸未触发:AuthoringRuleInputTier 的两个成员保持原样,runAuthoringRules / AuthoringRuleRun 的对外契约一字未动。

改正的四处陈述(全部是当年为真、如今为假的句子):

  1. authoring-rules.ts 的 tier 文档 —— 三个自证例子全部改写,并写下真正保留它的理由:无关 schema 错误中止 parse 时,normalized 层的 findings 仍能到达作者(raw 门实测:os validate 停在 schema 步零 finding,os lint 仍点名两条规则);外加 os lint 本就没有 parsed stack 可给。附一句给后来者的判据:normalized 读作「不需要 parsed stack」,永远不是「保证看得见 pre-parse 证据」。
  2. validate-view-containers.ts 头注 —— 「ViewSchema strips unknown keys」改为 strict 拒收;说明 looksFlat 分支故意保留,因为它仍服务 strict parse 看不到的三扇门。
  3. validate-list-view-mode.ts 头注 —— 「schema strips it at runtime (no throw, back-compat)」改为键拒收 + 枚举拒收。
  4. validate-visibility-predicates.ts 头注 + 注册表条目 —— 折叠发生在 normalizeStackInput 内而非 parse();并明确两条值级规则不受连坐(实测各报 1),避免下一个读者顺手把它们一起摘掉。

四、反向验证(先申报,后执行)

模板默认的「修复前红 / 修复后绿」在这里不成立:本 PR 非注释代码行零改动,不存在可翻转的行为。可翻转的是前提,所以反向验证做成对 pin 的变异测试 —— 先申报预测方向,再执行。

申报 A:把 premise-1 的 fixture 由扁平 view 换成规范容器 { list: { … } }预测 RED —— defineStack 不再抛错,expect(error).toBeDefined() 失败。若仍绿,说明该断言读的是某个无关 schema 错误,而非「扁平」这一事实。

实测 A:RED,2 例失败,与预测一致 ——

FAIL … defineStack REFUSES it by name instead — ViewSchema is strict since #4001
125| expect(error).toBeDefined();
FAIL … still fires on the doors the strict parse never sees (raw input, no defineStack)
AssertionError: expected [] to have a length of 1 but got +0

申报 B:把 premise-3 里喂 normalized 层的那一次改成喂原始对象(即抽掉 normalizeStackInput)。预测 RED —— 规则会返回 1 条,toEqual([]) 失败。若仍绿,说明该 pin 根本没在读转换层的折叠。

实测 B:RED,三个别名 site 全部失败,与预测一致。而且返回的 finding 里带着的正是本 PR 改正的那句陈旧措辞 ——

+ {
+ "message": "`visibility` is the deprecated spelling … It still works — it is normalized to
+ `visibleWhen` at parse — but the canonical key is `visibleWhen`.",
+ "rule": "visibility-alias-deprecated",
+ }

两个变异各自 RED、恢复后 12/12 绿,证明 pin 读的是真事实,不是空过。

另外,测量本身天然是双向的,已在上文一并取证:同一份 stack,唯一变量是走不走 defineStack / 走不走 normalizeStackInput,两个方向的输出都记录在案。


五、门禁 EXIT 表

门禁命令EXIT
packages/lint 单测pnpm --workspace-concurrency=2 --filter @objectstack/lint test0 — 62 files / 1516 passed(含新增 pin 12 例)
新 pin 单独跑vitest run src/authoring-rule-input-tier.test.ts --maxWorkers=20 — 12 passed
注册表接线闸vitest run src/authoring-rule-wiring.test.ts0 — 25 passed
typecheckpnpm --workspace-concurrency=2 --filter @objectstack/lint typecheck0
ESLint(改动的 5 个文件)npx eslint …0
控制字节闸node scripts/check-nul-bytes.mjs0 — scanned 5966,无裸控制字节
自扫(超出闸门盲区)grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'无命中
check:spec-parsed-alias / check:engine-double-contract / check:error-code-casing / check:route-envelope / check:adr-anchors / check:doc-authoring / check:role-word / check:type-check-coverage本地逐条0

CI 实测(head e6225d2)

Job结论
ESLint(含全部 35 条同族闸:engine-double-contract / error-code-casing / route-envelope / raw control-byte / spec type-alias …)success
TypeScript Type Check(含 check:type-check-coverage 与 DEBT/TEST_DEBT re-measure)success
Test Core(3 分片)success
Dogfood Regression Gate(3 分片)/ Dogfood Verify CLIsuccess
Temporal Conformance(live PG + MySQL)success
Check Changeset首跑 failure(标签竞态:该 job 在启动时重读标签,早于我写入 skip-changeset),确认标签在位后 rerun 一次 ⇒ success;由 labeled 事件触发的那次本身即 skipped
Check PR Size / Auto Label / Console Pin Freshness / 其余success

📝 一处自我更正,记录在案:本地跑 check:type-check-debt 时读到 @objectstack/spec-monorepo DEBT 80 → 84(+4)。我先按纪律排除了「是我造成的」——把本 PR 五个改动全部撤掉后重跑,读数一字不差,故与本 PR 无关(该 ledger 条目的程序面自述为「the root program is scripts/ and the top-level configs」,packages/lint 不在其中)。但当时我进一步推断「origin/main 上已经是红的」,这一步推过头了:CI 上同一条 re-measure 步骤(TypeScript Type Check 第 27 步)实测 green。所以那 +4 是我本地 worktree 环境的产物,不是 main 红。原文保留此更正而非删改,因为「把本地读数当成 main 状态」正是本单这一族(注释陈述与实测脱节)要防的错误,值得留痕。


六、不在本 PR 里

Changeset

路线 2(skip-changeset)。依据:非注释代码行改动为 0(已用 diff 过滤核实),其余是新增测试文件 —— 本 PR 不发布任何行为变化,写不出诚实的 changeset 条目。标签读回确认:["size/m", "skip-changeset"](并集写入,size/m 未被覆盖)。

#6073 要求「先测量、后行动」。测量做完了,结论是假设不成立——但不成立的方式
和单子预期的两个分支都不同,所以按实测如实记录,不套模板。
`AuthoringRuleInputTier` 的 `normalized` 层此前用三个例子自证:「规则要读
parse 会剥掉的键(views: [] 里的扁平 list view、对象 list view 上的
userFilters、visibleOn 别名)」。三条**全部实测为假**,各有各的原因:
1. defineStack 在定义期就 parse,所以 TS 配置交给三条命令的值已经是
result.data(实测:flow.runAs === 'user'、status === 'draft' 已填充);
2. 但什么都没丢——#4001 之后 ViewSchema / ObjectListViewSchema 是 strict,
不是 strip:defineStack 直接抛错并点名 type/data/columns 与 quickFilters,
还附 defineView 包裹修法。example app 上 os lint 与 os validate 均在
LOAD 阶段拒绝配置,规则根本没机会跑,而这比规则报得更早、更准;
3. visibleOn 别名在任何输入形状下都到不了这一层:ADR-0087 D2 的两条转换在
normalizeStackInput **之内**折叠它,而该函数的输出就是这一层。单独立单
#6318 记录。
保留该层的真实理由(实测确认):无关的 schema 错误会中止 parse,而
normalized 层的 findings 仍能到达作者——raw 路径上 os validate 停在 schema
步零 finding,os lint 仍点名两条规则。
改动仅为注释 + 一个新的回归 pin 测试(12 例),非注释代码行零改动;
examples/ 终态零 diff。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 7, 2026 2:11pm

Request Review

@hotlonghotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 7, 2026 — with Claude
@github-actions

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude