Skip to content

fix(spec): #4001 重测 —— 实测剩余站点清单,并修好测量它的那把尺 - #4852

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-4001-strictness-remeasure
Aug 3, 2026
Merged

fix(spec): #4001 重测 —— 实测剩余站点清单,并修好测量它的那把尺#4852
os-zhuang merged 4 commits into
mainfrom
claude/issue-4001-strictness-remeasure

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Refs #4001
Refs #4721

按 2026-08-03 维护者裁决「必要的全收」执行注意第 1 条 —— 先重测,再切批

本轮不改任何 schema 的 strictness 姿态。 产出是一份实测的剩余站点清单,供 PM 据实拆批。


一、先说结论:那把尺本身是坏的,而且两个方向同时坏

台账的站点计数一直是对源码做文本匹配(z.object( / strictObject()。这次用一把独立的 AST 尺去对,七个文件不一致 —— 出现在这场战役专门为了「地图不许悄悄过期」而写的那个闸门里

方向 实例
把注释算成站点 ui/action.zod.ts 声明 9,实为 8(第 9 个在一段 JSDoc 里);kernel/metadata-protection.zod.tsshared/suggestions.zod.ts 整个是注释
认不出折行的调用 prettier 把长链写成 z\n .object({,z\.object\( 匹配不到 —— ui/chart.zod.ts 漏 1、kernel/manifest.zod.ts 漏 2
不认识 z.looseObject( data/field-value.zod.ts 声明 1,实为 2

ui/ 上这两个错误恰好抵消(−1 和 +1),于是 section 总数校验因为第二个错误而通过,盖在两行错的数字上。

最要命的一个:automation/time-relative-trigger.zod.ts 计数为 0 —— 它唯一的站点是折行写的,而零站点文件会被覆盖检查刻意跳过(「没东西可分类」)。于是一个作者手写的可授权 schema 落在地图外,闸门照样打印「no undeclared schema files」。

这不是 data/driver/ 那次的重演:那次是遍历瞎了;这次遍历是好的,是喂给它的计数器返回了一个错误的零,而遍历正确地尊重了那个零。盲区往里挪了一层,走的还是一条正确的代码路径。

修法是结构性的而非再往正则里补一种拼法:计数器改读 AST,不会被格式或注释骗,也不用每出一种新写法就学一次 —— 和第 9 条发现里信封探针改走结构遍历是同一个动作。

二、同一次遍历顺带给出姿态,这才是重测真正要的东西

台账过去只能回答「这个文件里有没有 .strict()」,永远回答不了「它还有几个站点是开的」。而后者正是拆批要用的数。

裁决当时是按 strictObject( 的出现次数排的期 —— 那个写法漏掉每一个用更早的 z.object(…).strict() 关掉的 schema:

strictObject( 实测 strict 站点
ui/ 49 72
automation/ 0 8
data/ 17 40
security/ 7(已全关)
studio/ 27 27(已全关)

三、新增「剩余 strip 站点」地图,并且双向上闸

check:strictness-ledger 现在还守这张表:

  • 正向 —— 任何还有 strip 站点的文件,必须有一行,数字要对;
  • 反向 —— 一行所对应的文件一旦 strip 归零,闸门报红,该行必须删掉。

反向那一半更重要。能活过自己工作的清单,一定会活过:这个台账已经记过一次自己把「已发布的功能」列成待办,ADR-0010 欠债清单当初也是为同一个理由加的反向钉。

四、每一条绿色都先证过红

战役自己的规矩:信一个绿色读数之前,先让它在一个你确知存在的东西上变红。

  • 计数器改完后,闸门在四处已知漂移上报红(ui/action 9→8、ui/chart 6→7、data/field-value 1→2、automation/ 一个未声明文件),补完才转绿;
  • 新增的剩余站点检查,四种失败模式逐一诱发并观察到报红(行内数字错、漏行、已关闭文件残留行、表头算术),再恢复绿;
  • 姿态读数在测试里自带变异对照:把 strictObject( 还原成 z.object( 必须读成 strip,给一个 strip 站点加 .strict() 必须读成 strict。

验证

  • check:strictness-ledger PASS(两行绿:67 文件 / 5 目录;51 个未关闭文件 / 325 strip 站点)
  • check:generated 8/8
  • @objectstack/spec:294 文件 / 7369 用例通过,tsc --noEmit 干净
  • 全仓 pnpm typecheck:122/122
  • 合并 origin/main(4 个提交)后复验:merge 未触及 packages/spec/src,实测数不变,闸门仍绿

范围

只改测量脚本 / 测试 / 台账 + 一个 changeset。零 schema 改动,content/docs/releases/ 未触碰。

剩余清单、分类依据、拆批建议与 #4721 现状核实见 issue #4001 上的重测报告评论。

🤖 Generated with Claude Code

https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9


Generated by Claude Code

claude added 2 commits August 3, 2026 10:10
…r measuring it

Measurement step only — no schema changes posture in this commit. Produces the
real remaining-site list the 2026-08-03 ruling asked for before the remaining
batches are cut.

The site counter now reads the AST instead of matching source text. The textual
method was wrong in BOTH directions on seven files: it counted `z.object({ … })`
appearing inside JSDoc prose (ui/action.zod.ts declared 9 sites, has 8), and it
missed both the prettier-wrapped `z\n  .object({` form (ui/chart.zod.ts 6 -> 7)
and `z.looseObject(` (data/field-value.zod.ts 1 -> 2). On ui/ the two errors
cancelled exactly, so a correct section total sat over two wrong rows.

The consequential case: automation/time-relative-trigger.zod.ts counted ZERO,
because its only site is written wrapped — and a zero-site file is deliberately
skipped by the coverage walk. An authorable schema sat outside the map while the
gate printed "no undeclared schema files". Not the data/driver/ blind spot
repeated: that walk was blind, this walk was fine and honoured a zero the
counter got wrong. Now classified.

check:strictness-ledger gains a remaining-strip-site map: per file, how many
object sites still silently discard unknown keys. That number is what batch
plans are scheduled against and nothing measured it before — which is why the
ruling was cut against `strictObject(` occurrence counts, an idiom that misses
every schema closed with the older `z.object(…).strict()` spelling and reads
automation/ as 0 strict when it has 8. Gated in both directions: a file with
strip sites must have a row, and a row whose file reaches zero strip sites
FAILS, so a closed file drops out of the worklist rather than outliving it.

Every new check verified to go red on a known input before being trusted
(campaign rule): the four remaining-strip failure modes were each induced and
observed, and the posture reads carry mutation controls in the test file.

Refs #4001, #4721

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

vercel Bot commented Aug 3, 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 3, 2026 12:11pm

Request Review

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

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

Copy link
Copy Markdown
Contributor Author

状态说明:本 PR 从未进入合并队列,已重跑失败 job

跟进时发现 auto-merge 挂了约 100 分钟未动,而同期队列消化了 #4841 / #4870 / #4863 / #4872 / #4869 等多单 —— 排查后确认不是队列慢:

merge_group runs(近 10 次):pr-4871 / pr-4870 / pr-4841 / pr-4863 / pr-4872 /
                             pr-4869 / pr-4846 / pr-4861 / pr-4858 / pr-4857
→ 从未出现 pr-4852

原因是时序:本 PR 的 CI 红在 10:1xZ,签名是 #4796 的 5s 超时族;而那条的修复 #4856(testTimeout: 60_000)11:31 才合入 main(d40f43a79)。红检查 → auto-merge 永不触发 → 永不入队。

rerun_failed_jobs。PR CI 检出的是「本 PR 合进当前 main」的合并提交,现在的 main 已带 60s 超时,重跑应当转绿并自动入队。

若重跑后仍红且仍是 5s 超时签名,那就不是这条 flaky —— 按 #4796 的约定重新诊断,不套旧结论。


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

更正:重跑不够 —— rerun_failed_jobs 重放的是原提交,不会带上新 main

上一条我说「PR CI 检出的是合进当前 main 的合并提交,重跑应当转绿」。这是错的,实测打脸:重跑后仍是

FAIL src/cloud/tenant.test.ts > [#4739] TenantPlan(Schema) … × 5065ms
Error: Test timed out in 5000ms.
Tests  1 failed | 7368 passed (7369)

仍是 5000ms,说明 #4856testTimeout: 60_000 没生效 —— GitHub 的 re-run 复用原 run 的提交/合并 ref,不重算。要带上 main 的新提交,只能推一个新提交

#4796 的约定「再出现超时就重新诊断、不套旧结论」查完:这次的失败确实还是 5s 那条,但原因不是 flaky 复发,而是修复根本没进到这次运行里——诊断结论与处置手段的错配,记在这里免得下次再绕一圈。

已做

PM 直接完成同步(实施 agent 已收工、worktree 已拆):

  • git merge origin/main(禁 rebase / force-push),提交 ffc1884c4;
  • 本 PR 不碰任何生成物(只改测量脚本 / 测试 / 台账 + changeset),故无 os-regen 静默吞并风险,无需 checkout-重生成;
  • 但台账计数可能因 main 上的 schema 变动而漂,所以本地实跑验证过:
✓ strictness ledger: 67 file(s) across 5 triaged director(ies) — site counts match,
  no undeclared schema files, section totals balance.
✓ remaining-strip map: 51 open file(s) / 325 strip site(s) across 4 director(ies) —
  counts match, no file with strip sites is missing a row, no closed file still carries one.

两行仍绿,实测基数(51 文件 / 325 strip 站点)不变,拆批依据无需修订。

新推送会触发全新 CI,合并 ref 将包含 #4856,超时线为 60s。


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit a2ebea2 Aug 3, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4001-strictness-remeasure branch August 3, 2026 12:32
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…) (objectstack-ai#4893)

2026-08-03 的 v17 协议变更派发里,有四类已经付出过代价的经验,现行 skill 里
没有或只写了半截。同日另一车道的 objectstack-ai#4885 已沉淀八条,本单只补它覆盖边界之外的。

pm-dispatch —— Operational notes 由四条扩到八条:

- 5:rerun_failed_jobs 复用原 run 的提交/合并 ref,不重算。红的原因若是「基上
  缺一个已合并的修复」,重跑无效,只有推新提交才拿得到新的合并 ref(objectstack-ai#4852 因此
  在队列外空转 100 分钟)。与 rerun-safety-nightly.yml 无关,后者查的是测试污染。
- 6:读数纪律。cd X && cmd 短路会在错的仓里执行(跨仓一律 git -C);
  git grep -c | wc -l 数的是文件数不是命中数;裸名 grep 会被幸存家族当子串命中,
  退役核验要带引号精确名、更硬的判据是查声明式而非提及。零命中必须用确定存在的
  邻近词反查。
- 7:CI 红了先取完整日志归档。completeness check 绿只说明没有 worker 静默死掉;
  turbo 并发输出相邻不等于因果(test 的 dependsOn 只有 ^build,spec 无 pretest);
  不要只看 tail。据错误结论开的 PR 要撤回 draft 并解绑 Fixes。
- 8:共享基础设施类修复按症状复查 main。duplicate-fix-guard.yml 只覆盖「同仓 +
  同一个 Fixes #N」,objectstack-ai#4864objectstack-ai#4856 挂在不同 issue 号下,门禁看不到,而后者先合的
  60s 会被前者降回 30s。

另在 note 1 上补:「不在 main 上」是二义读数(兼容「排队中」与「没入队」);队列
分支 base sha 串成链,可读出排第几;转 draft 会同时掉 auto-merge 与队列成员资格。

step 7 之后新增「入队与落地」小节:merge=os-regen 的七条路径(含两条文档产物)、
四步同步协议、以及跟到 MERGED 而不是跟到入队为止。

spec-property-retirement:

- 新增「四张 ratchet 的可见性按路线相反」—— 枚举值收窄不可见(objectstack-ai#4391),整 def
  删除必须变化(objectstack-ai#4834:-12/-23/-5);拿错对照会双向判错。
- 修好第 2 节指向 plugin-runtime.zod.ts:243-248 的先例引用,该文件已被 objectstack-ai#4878
  整体删除。

Fixes objectstack-ai#4892


Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…t accidental (objectstack-ai#4001) (objectstack-ai#4909)

* fix(spec): make the bulk-action option item's openness deliberate, not accidental (objectstack-ai#4001)

`BulkActionParamSchema`'s `options[]` entry was bare default-strip while the
strictness ledger's prose called it and its parent "both deliberately open" —
one intent, two postures, found by the objectstack-ai#4001 2026-08-03 re-measure (objectstack-ai#4852).

Per the maintainer's verdict A the code moves to the prose: `.passthrough()` on
the option item. Verified before writing, since the prose itself had just been
proven able to drift — objectui's option TYPE is closed
(`Array<{ label; value }>`, `packages/types/src/objectql.ts:271`), but the type
is not what an authored option meets: `bulkParamToField` SPREADS every entry
(`packages/plugin-grid/src/components/bulkParamToField.ts:131`) into the field
metadata, whose vocabulary is `SelectOptionMetadata`
(`packages/types/src/field-types.ts:288`) — `color` / `icon` / `disabled` /
`visibleWhen` beyond the pair, and read (`packages/fields/src/index.tsx:1089`).
So the strip was deleting widget config the renderer would have honoured.

The new test states the openness is deliberate and names the evidence, so a
future strict-ification sweep stops there instead of "fixing" it.

Ledger: `ui/bulk-action.zod.ts` reaches zero strip sites, so its remaining-strip
row is deleted and the header arithmetic reconciled (124 → 123 of 198). Both
directions of that gate were driven red before the green was believed.

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

* docs(spec): keep the option shape readable after the passthrough (objectstack-ai#4001)

`gen:docs` renders a passthrough object inside an array as `Record<string,
any>[]`, so the reference page lost the `{ label, value }` shape the moment the
entry opened — an author-facing surface reading "shapeless" for something that
still has two required keys, which is the strip-era smell pointed the other way.

The declared pair now lives in the key's own `.describe()`, which the generator
renders verbatim, along with WHY the entry is open and which extra keys the
widget actually reads. Regenerated `content/docs/references/ui/bulk-action.mdx`
accordingly (the one artifact `check:generated` proved stale — not the set).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…objectstack-ai#4974)

* feat(spec)!: close nine authorable automation shapes against unknown keys (objectstack-ai#4001 batch 11)

zod's default `.strip` discards an undeclared key and parses on. Batch 11 closes
the nine remaining shapes in `automation/`'s main body, each rejection naming the
surface, the key, and — where the word is recognisable — the canonical spelling.

`flow.zod.ts`: the four OUTER shapes were closed earlier; their six nested
authoring blocks were not, so the gate rejected `nodee:` at node level while
`connectorConfig: { connectorID }` parsed clean and dispatched against an
undefined connector id. Now strict: `FlowNode.connectorConfig` / `.position` /
`.inputSchema` / `.waitEventConfig` / `.boundaryConfig`, `Flow.errorHandling`.
Deliberately still open, now pinned in code AND tests so a later sweep stops
rather than "finishing" the file: the node `config` slot (ADR-0018) and
`FlowVersionHistorySchema` (emitted, not authored — its `definition` is
`FlowSchema`, so the authored half is gated anyway).

Every alias is a real in-repo spelling of the same knob on a neighbouring
surface, taken from an AST census over the repo's own payloads, not guessed:
`backoffMs` from `shared/retry-policy.zod.ts` (objectstack-ai#4661), `initialDelayMs` /
`maxDelayMs` from connector `RetryConfig`, `attachedToRef` / `cancelActivity`
from BPMN 2.0. Two cases get a prescription instead of a rename because a rename
would be wrong: `inputSchema.optional` is the opposite polarity of `required`,
and `errorHandling.maxAttempts` counts the first attempt while `maxRetries` does
not — renaming it alone would quietly run one attempt fewer.

`time-relative-trigger.zod.ts`: the schema objectstack-ai#4852 made visible (its only site was
line-wrapped, the old counter read zero, and a zero-site file is skipped by the
coverage walk). It sits under the open node `config` slot, so it is the only key
gate there is — and it is `safeParse`d at BIND time, so `offsetDay` beside a
valid mode used to bind a sweep with the author's narrowing discarded and report
itself configured.

`flow-function.zod.ts`: binds at authoring only (boot reads
`normalizeFlowFunctionEntry`), stated in the code rather than implied. That is
why it matters: the reader takes two keys and ignores the rest by construction,
so a misspelled `effect` was dropped and then not looked for — the function ran
and its writes were counted as none, keeping objectstack-ai#4354's broken-sweep query silent.

`webhook.zod.ts`: the ledger's `(p)` resolved to the opposite of "spec-only" —
three parse doors, one of them `bootstrapDeclaredWebhooks` at BOOT, which warns
and SKIPS on failure. Hence the ADR-0010 envelope in the same commit:
`applyProtection` stamps `_packageId`/`_provenance` on every type, so closing the
shape without declaring them would have turned every package-shipped webhook
into a skipped subscription after a redeploy. Strictness also rides `.extend()`
onto the connector `WebhookConfigSchema` — verified against real zod and pinned
in `connector.test.ts`.

Ledger: `automation/` 67 → 58 strip; three rows reached zero and were deleted by
the objectstack-ai#4852 reverse pin. Gate re-proved red in both directions before trusting it,
and every new assertion was proved to depend on the tightening by neutering
`strictObject` (19 red, restored byte-identical).

Part of objectstack-ai#4001

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

* docs(spec): regenerate reference pages for the webhook ADR-0010 envelope

`gen:docs` only — the property tables for `automation/Webhook` and the
`integration/WebhookConfig` extension gain the `protection` block and the seven
`_lock*`/`_provenance`/`_package*` rows. Checked for the objectstack-ai#3746 trap: no page
description or frontmatter moved, so no internal comment leaked onto a public
reference page.

Part of objectstack-ai#4001

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

* fix(spec): correct three history strings that named the wrong failure

Self-review against the actual behaviour, before the claim shipped. The
`connectorConfig` / `boundaryConfig` / `position` / `inputSchema` prose named a
slip on a REQUIRED key (`connectorID`, `attachedToRef`) as the silent case. It
never was: a required key spelled wrong then reads as MISSING, which zod has
always rejected loudly. Nothing was silent about it.

What `.strip` actually swallowed is the OPTIONAL half — the mapped `input` map
(dispatched as `input ?? {}`: a successful connector call carrying nothing),
BPMN's `cancelActivity: false` (so `interrupting` fell to its `true` default and
a NON-interrupting event cancelled the host anyway), `optional: false` (so
`required` fell to false and `validateNodeInputSchemas` had nothing to require),
a canvas hint beside x/y. I.e. exactly the keys an author adds to CONSTRAIN
behaviour, replaced by a permissive default.

The corrected version is the stronger argument as well as the true one, and
getting it wrong is the failure mode this campaign has already paid for four
times (the withdrawn guidance prescriptions): rejection prose is behaviour, not
documentation. Fixed in the schemas, the module comment, the tests and the
ledger row; a test now pins the required-key case as loud so the distinction
cannot quietly re-blur.

Part of objectstack-ai#4001

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…as no-door (objectstack-ai#4001 批 14) (objectstack-ai#5042)

Eleven strip sites measured per the objectstack-ai#4852 remeasure; the counts held exactly
(1+1+2+2+3+2, confirmed against the ledger gate's own AST counter). Per-schema
door measurement then split them nine / two.

Closed with strictObject:
  ui/action.zod.ts     ActionParamSchema.options[]
  ui/sharing.zod.ts    SharingConfigSchema
  ui/report.zod.ts     ReportSortSchema, JoinedReportBlockSchema
  ui/dataset.zod.ts    DatasetDimensionSchema, DatasetMeasureSchema, .derived
  ui/dashboard.zod.ts  DashboardWidgetSchema.compareTo (object arm), .layout

Four of those are strict shells over strip children: the container was already
strict and strictness does not recurse. An action param option carrying
color/visibleWhen/icon/disabled parsed clean through getMetadataTypeSchema('action')
and came back {label, value}.

Reclassified as the fourth class (no door), NOT tightened:
  ui/notification.zod.ts  NotificationActionSchema
  ui/sharing.zod.ts       EmbedConfigSchema

Both measured three ways with positive controls in the same run: no carrier key,
unreachable in a 6860-node BFS from the 24 metadata-type roots + defineStack, zero
parse outside their own tests. Injecting a synthetic carrier flipped both, so the
verdict is a fact about the graph rather than a broken walker. sharing.zod.ts is
the ledger's first file that splits across the class.

action's option entry goes strict rather than following bulk-action's
.passthrough() (objectstack-ai#4909): measured, not inherited. That def reaches the grid
verbatim with no spec door and objectui's BulkActionParam declares an explicit
index signature; this path has a door that already strips and lands in the CLOSED
SelectOptionMetadata.

Curation anchored on named sibling contracts, not edit distance: dataset on the
Cube layer this module's header names (a Cube metric's `type` IS its aggregation),
report's order key on SortNodeSchema and the widget's flat sortBy/sortOrder,
layout on React-Grid-Layout.

Known reach limit, measured: compareTo is a union, so its prescription is produced
but not delivered (zodIssuesToFields maps only top-level issues; objectstack-ai#5014). The
rejection is unaffected. Tests pin both halves separately.

Out-of-scope findings filed unassigned: objectstack-ai#5013 (ReportSchema's `filter` alias
prescribes `filters`, which it also rejects, plus 5 dead entries), objectstack-ai#5014, objectstack-ai#5015
(ADR-0049 verdict for the two no-door shapes), objectstack-ai#5016 (should action options carry
the field-level per-option vocabulary).


Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…+ five i18n shapes as no-door (objectstack-ai#4001 批 16) (objectstack-ai#5069)

Fifteen strip sites measured per the objectstack-ai#4852 remeasure; the counts held exactly
(widget 9/9, i18n 6/6, confirmed against the ledger gate's own AST counter).
Per-schema door measurement then split them one / fourteen.

Closed with strictObject:
  ui/i18n.zod.ts   AriaPropsSchema

The highest-fan-out single site the ui/ wave has closed — carried as `aria:` on
~30 shapes under six metadata-type roots (ListView, Page, PageComponent,
DashboardWidget, ChartConfig, Action, 20 SDUI component defs). Through the view
root, `aria: { label, describedBy }` parsed clean and came back `aria: {}`: the
accessible name existed in the source file and nowhere else. Those two spellings
are objectui's own ARIA_KEY_ALIASES, i.e. what stored view metadata carries.

Reclassified as no door, NOT tightened (14 sites):
  ui/widget.zod.ts  all six shapes / nine sites
  ui/i18n.zod.ts    I18nObject, PluralRule, NumberFormat, DateFormat, LocaleConfig

Measured three ways with positive AND negative controls in the same run: no
carrier key (nothing imports widget.zod but the barrel), unreachable in a
4766-node BFS from the 24 roots + defineStack, zero parse in all three repos
(cloud checked by code search against a proven-working index). A synthetic
carrier flips all of them, so the verdict is a fact about the graph.

Curation anchored on named sibling contracts, not edit distance: the two legacy
spellings on objectui's normalizer, and `ariaRole` on this shape's own
inconsistent prefix (two of three keys carry `aria`, `role` does not). The four
the distance fallback already reaches were measured first and left to it.

`live` and `ariaLabelledBy` get a prescription rather than a rename — finding 7
discipline. `live` is real and rendered by objectui's ListView alone, which
declares it via `AriaPropsSchema.extend({ live })`, so that surface keeps
accepting it and now inherits this error map; promoting it into the shared shape
would advertise aria-live on 29 renderers that do not implement it (objectstack-ai#5058).

A `.strip()` was added to four files this batch does not otherwise touch:
animation, dnd (x2), keyboard, touch build their shapes as
`z.object({…}).merge(AriaPropsSchema.partial())`, and `.merge()` adopts the
incoming posture — closing AriaProps would have silently closed all five of those
no-door shapes with zod's generic message and no changeset. Pinned.

Ledger finding 20: the campaign's own BFS reported widget.zod.ts as REACHABLE.
Zod's `.describe()` returns a clone sharing the original `_zod.def`, so a bridge
firing on any ONE shared property under a shared name links unrelated shapes via
`name`/`label`. 2 keys of 20. The error is one-directional — it can only make a
batch tighten something dead (objectstack-ai#4583). Corrected to whole-shape overlap in
ui/door-reachability.testkit.ts, pinned as a regression test, filed as objectstack-ai#5056.

Instruments proven red before trusting green: six sabotages, one per assertion
class (walker, clone bridge, strictness, curation, finding-7 guidance,
merge-strip), each reverted.

Ledger: ui/ 118 strip of 198, authorable subtotal 82 — both recomputed from the
surviving rows, never decremented by this batch's count.

Refs objectstack-ai#4001. Filed out of scope: objectstack-ai#5055 (ADR-0049 for the 14 no-door sites),
objectstack-ai#5056, objectstack-ai#5058, objectstack-ai#5059.

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants