Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/strictness-ledger-numbers-prose-split.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
---
'@objectstack/spec': patch
---

tooling: strictness 台账「数字/散文分家」—— 计数转生成物走 os-regen,判定与依据保持手写 (#5107, #5072)

`docs/audits/2026-07-unknown-key-strictness-ledger.md` 是战役期间全仓最热的合并冲突点,而冲突全部落在它的**数字**上:两个批次各按自己那份正确的增量去减表头,git 把互不重叠的**行**干净合并,而与谁都不冲突的**小计行**「干净合并、两边都错」—— 单日 7 例,`ui/` 小计被三个批次分别写成 119 / 110 / 100,而合并后的正确值 91 三边都没写过。**散文当天只冲突过一次,而且那次是有意义的。**

所以数字走了。新增生成物 `docs/audits/2026-07-unknown-key-strictness-ledger.counts.md`(`gen:strictness-ledger`),承载每文件站点数/strip 数、各段表头、按类小计、posture 分布与未细分目录的总数;`.gitattributes` 把它加进 `merge=os-regen`(#4675),合并时不做文本合并、整体重生成,`pre-commit` 在重生成之前拒绝提交。台账本体只留承重的部分:`Class` 判定、依据、findings log、豁免记录 —— 这些是判断,不是算术,重生成会**删掉别人的证据**,所以 `docs/audits/**` 仍在 `NOT_DRIVER_MANAGED` 里。

`check:strictness-ledger` 的职责随之反转,双向闸语义完整保留:

- **生成物新鲜**:整份重新渲染后逐字节比对,失败信息把「某个数字动了 = 有 schema 在没人重新审视的 `Class` 判定下被增删/改姿态」这句话说出来 —— 这正是旧的手写计数唯一值得留下的那一半;
- **散文自洽**:每条手写行必须指向一个仍然存在、仍然有站点的文件(旧的计数检查顺带买到的性质),有站点却没有行仍然红,strip 行归零仍然红(反向钉)。

两条红路径都先证了红再信绿:改生成物里一个数字 → EXIT=1;删一条手写行 → EXIT=1(并且被删的行不会静默变成 0,它会以 `⚠️ unclassified` 出现在生成物里)。

小计是「对判断做算术」,所以 remaining-strip 那张表的 `Class` 单元格现在有语法:`<verdict> [(p)] [· <n> <verdict>, …]`。已解决的 `mixed`/`split` **必须**声明自己的拆分,闸门拒绝猜 —— 一个宽容的解析器会把这些数字原封不动地送回它们刚被搬走的地方,而且是在一个绿色的文件里。迁移后所有已发布的数字逐一复现(235 strip / 36 open files / authorable 29 / unresolved 33 / no door 38 / no gate 31)。

**同一把尺的搭车修复(#5072)**:`postureOf()` 对战役自己的 helper 短路 —— `strictObject(` 直接返回 `strict`,不看链上挂了什么,于是 `strictObject(…).passthrough()`(运行期**开放**的形状)在台账里被记成 **strict**。全仓恰好 2 处,都在 `ui/view.zod.ts`(`GanttConfigSchema` / `TreeConfigSchema`),两处的 `.passthrough()` 都是刻意的。现在 idiom 只决定**起始**姿态,链一律走完,最后一个显式调用赢。`ui/` 的 strict 读数 119 → 117、passthrough 3 → 5;**strip 计数不变**,所以 remaining-strip 那张双向表的数字一个都没动。零 `*.zod.ts` 语义改动。
9 changes: 7 additions & 2 deletions .claude/skills/pm-dispatch/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -915,8 +915,8 @@ Verdict per issue:

**A. 碰生成物的 PR,入队前必须先同步 + 整体重生成。** 第 3 步只保证**同一批内**
file-disjoint;它管不到**先后两单都碰 `packages/spec` 生成物**的情形 —— 而协议变更
几乎必然如此。`.gitattributes` 把这七条路径路由到 `merge=os-regen`(⛔ 别只记住前
五条 —— 后两条是文档产物,同样会被静默吞):
几乎必然如此。`.gitattributes` 把这八条路径路由到 `merge=os-regen`(⛔ 别只记住前
五条 —— 后三条是文档产物,同样会被静默吞):

```
packages/spec/spec-changes.json
Expand All@@ -925,9 +925,14 @@ packages/spec/json-schema.manifest.json
packages/spec/api-surface.json
packages/spec/api-surface-signatures.json
docs/protocol-upgrade-guide.md
docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
content/docs/references/**
```

最后那条是 #5107 加的:strictness 台账的**数字**转成了生成物(`gen:strictness-ledger`),
散文仍手写在同名的 `.md` 里。派 #4001 后续批次时要分清 —— **台账正文照常文本合并
(它一直合得很干净),只有 `.counts.md` 走驱动**。

权威清单是 `.gitattributes` 本身(`grep os-regen .gitattributes`),不是这份拷贝 ——
它增删过,以文件为准。

Expand Down
22 changes: 15 additions & 7 deletions .gitattributes
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,11 +27,19 @@
# dual-source-exports.baseline.json (shrink-only ratchets — recomputing can
# WIDEN them), variant-docs.json and the migrations/conversions registries
# (hand-written). Those conflicts are for a human. See NOT_DRIVER_MANAGED.
#
# The strictness ledger's COUNTS file joined at #5107 — the ledger's numbers were
# the repo's hottest conflict surface and merged in the one way that hides: two
# batches each decrement a header by their own correct delta, the rows merge
# cleanly because they do not overlap, and the subtotal merges clean and WRONG
# (seven cases in one day). Note it is the counts file, not the ledger — the
# ledger's prose is hand-written and must never be resolved by regenerating.

packages/spec/spec-changes.json merge=os-regen
packages/spec/authorable-surface.json merge=os-regen
packages/spec/json-schema.manifest.json merge=os-regen
packages/spec/api-surface.json merge=os-regen
packages/spec/api-surface-signatures.json merge=os-regen
docs/protocol-upgrade-guide.md merge=os-regen
content/docs/references/** merge=os-regen
packages/spec/spec-changes.json merge=os-regen
packages/spec/authorable-surface.json merge=os-regen
packages/spec/json-schema.manifest.json merge=os-regen
packages/spec/api-surface.json merge=os-regen
packages/spec/api-surface-signatures.json merge=os-regen
docs/protocol-upgrade-guide.md merge=os-regen
docs/audits/2026-07-unknown-key-strictness-ledger.counts.md merge=os-regen
content/docs/references/** merge=os-regen
283 changes: 283 additions & 0 deletions docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
<!-- GENERATED — DO NOT EDIT BY HAND. -->
<!-- Regenerate: pnpm --filter @objectstack/spec gen:strictness-ledger -->

# Unknown-key strictness ledger — the counts (generated)

Every number the #4001 strictness ledger publishes, computed from the AST
(`packages/spec/scripts/lib/strictness-ledger.ts`). The verdicts, the evidence and
the exemption rationales live in [the ledger itself](./2026-07-unknown-key-strictness-ledger.md) and
are hand-written; **this file has no prose to preserve** and is regenerated whole.

Split out at #5107. These numbers were the ledger's entire merge-conflict surface:
two batches each decrement a header by their own delta, git merges the rows cleanly,
and the subtotal — which conflicts with nothing — merges clean and wrong. Seven cases
in one day. The correct resolution was always "recompute from the merged tree", so the
path carries `merge=os-regen` (#4675) and the recomputation is now mandatory rather
than remembered. **Never hand-patch a number here** — fix the code or the verdict and
regenerate.

## Global

| Measure | Value |
|---|---|
| Triaged directories | 5 |
| Object sites in them | 482 |
| Still-open (strip) sites | 235 |
| Files carrying at least one | 36 |

Remaining strip sites by class:

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 29 |
| unresolved — needs a per-schema verdict | 33 |
| wire / open — out of forced scope | 104 |
| no door — no carrier, ADR-0049 territory | 38 |
| no gate — carrier live, no parse | 31 |

## Posture, per triaged directory

The `strict` column is the one the campaign schedules against; it counts both the
`strictObject(` helper and the older `z.object(…).strict()` spelling, and — since
#5072 — no longer counts a `strictObject(…).passthrough()` chain as closed.

| Dir | Sites | strict | passthrough | catchall | strip |
|---|---|---|---|---|---|
| `ui/` | 198 | 117 | 5 | 0 | 76 |
| `data/` | 162 | 41 | 1 | 0 | 120 |
| `automation/` | 75 | 49 | 0 | 0 | 26 |
| `security/` | 20 | 7 | 0 | 0 | 13 |
| `studio/` | 27 | 27 | 0 | 0 | 0 |
| **total** | **482** | **241** | **6** | **0** | **235** |

## File-level triage — site counts

Object sites per file: every `z.object(` / `strictObject(` / `z.strictObject(` /
`z.looseObject(` CALL, read from the AST. A file with zero sites has nothing to
classify and is not listed (it becomes reportable the day it grows its first site).

### `ui/` — sites

| File | Sites |
|---|---|
| `action.zod.ts` | 8 |
| `animation.zod.ts` | 4 |
| `app.zod.ts` | 18 |
| `bulk-action.zod.ts` | 3 |
| `chart.zod.ts` | 7 |
| `component.zod.ts` | 29 |
| `dashboard.zod.ts` | 11 |
| `dataset.zod.ts` | 4 |
| `dnd.zod.ts` | 4 |
| `i18n.zod.ts` | 6 |
| `keyboard.zod.ts` | 4 |
| `notification.zod.ts` | 1 |
| `offline.zod.ts` | 3 |
| `page.zod.ts` | 7 |
| `report.zod.ts` | 3 |
| `responsive.zod.ts` | 4 |
| `sharing.zod.ts` | 2 |
| `theme.zod.ts` | 14 |
| `touch.zod.ts` | 7 |
| `view.zod.ts` | 50 |
| `widget.zod.ts` | 9 |
| **total** | **198** |

### `data/` — sites

| File | Sites |
|---|---|
| `analytics.zod.ts` | 8 |
| `data-engine.zod.ts` | 13 |
| `datasource.zod.ts` | 6 |
| `document.zod.ts` | 8 |
| `driver-nosql.zod.ts` | 10 |
| `driver-sql.zod.ts` | 2 |
| `driver.zod.ts` | 9 |
| `driver/memory.zod.ts` | 6 |
| `driver/mongo.zod.ts` | 1 |
| `driver/mysql.zod.ts` | 1 |
| `driver/postgres.zod.ts` | 1 |
| `driver/sqlite.zod.ts` | 2 |
| `external-catalog.zod.ts` | 4 |
| `external-lookup.zod.ts` | 12 |
| `field-value.zod.ts` | 2 |
| `field.zod.ts` | 11 |
| `filter.zod.ts` | 11 |
| `hook-body.zod.ts` | 2 |
| `hook.zod.ts` | 6 |
| `mapping.zod.ts` | 3 |
| `object.zod.ts` | 20 |
| `query.zod.ts` | 5 |
| `seed-loader.zod.ts` | 12 |
| `seed.zod.ts` | 1 |
| `validation.zod.ts` | 6 |
| **total** | **162** |

### `automation/` — sites

| File | Sites |
|---|---|
| `approval.zod.ts` | 4 |
| `bpmn-interop.zod.ts` | 5 |
| `builtin-node-config.zod.ts` | 8 |
| `control-flow.zod.ts` | 5 |
| `etl.zod.ts` | 10 |
| `execution.zod.ts` | 13 |
| `flow-function.zod.ts` | 1 |
| `flow.zod.ts` | 11 |
| `io-node-config.zod.ts` | 2 |
| `node-executor.zod.ts` | 4 |
| `schemaless-node-config.zod.ts` | 4 |
| `state-machine.zod.ts` | 6 |
| `time-relative-trigger.zod.ts` | 1 |
| `webhook.zod.ts` | 1 |
| **total** | **75** |

### `security/` — sites

| File | Sites |
|---|---|
| `explain.zod.ts` | 11 |
| `permission.zod.ts` | 4 |
| `rls.zod.ts` | 3 |
| `sharing.zod.ts` | 2 |
| **total** | **20** |

### `studio/` — sites

| File | Sites |
|---|---|
| `flow-builder.zod.ts` | 7 |
| `object-designer.zod.ts` | 12 |
| `plugin.zod.ts` | 8 |
| **total** | **27** |

## Remaining strip sites — the batch-planning map

Per file, how many of its sites still silently discard unknown keys. The `Class`
column that decides the bucket split is hand-written in the ledger; the arithmetic
over it is here.

### `ui/` — open

**76 strip of 198**, in 13 file(s).

| File | Strip | Sites |
|---|---|---|
| `animation.zod.ts` | 4 | 4 |
| `app.zod.ts` | 1 | 18 |
| `chart.zod.ts` | 2 | 7 |
| `component.zod.ts` | 29 | 29 |
| `dnd.zod.ts` | 4 | 4 |
| `i18n.zod.ts` | 5 | 6 |
| `keyboard.zod.ts` | 4 | 4 |
| `notification.zod.ts` | 1 | 1 |
| `offline.zod.ts` | 3 | 3 |
| `sharing.zod.ts` | 1 | 2 |
| `touch.zod.ts` | 7 | 7 |
| `view.zod.ts` | 6 | 50 |
| `widget.zod.ts` | 9 | 9 |
| **total** | **76** | **198** |

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 7 |
| unresolved — needs a per-schema verdict | 0 |
| wire / open — out of forced scope | 0 |
| no door — no carrier, ADR-0049 territory | 38 |
| no gate — carrier live, no parse | 31 |

### `data/` — open

**120 strip of 162**, in 16 file(s).

| File | Strip | Sites |
|---|---|---|
| `analytics.zod.ts` | 8 | 8 |
| `data-engine.zod.ts` | 13 | 13 |
| `document.zod.ts` | 8 | 8 |
| `driver-nosql.zod.ts` | 10 | 10 |
| `driver-sql.zod.ts` | 2 | 2 |
| `driver.zod.ts` | 9 | 9 |
| `driver/memory.zod.ts` | 5 | 6 |
| `external-catalog.zod.ts` | 4 | 4 |
| `external-lookup.zod.ts` | 12 | 12 |
| `field-value.zod.ts` | 1 | 2 |
| `field.zod.ts` | 3 | 11 |
| `filter.zod.ts` | 11 | 11 |
| `hook.zod.ts` | 4 | 6 |
| `object.zod.ts` | 14 | 20 |
| `query.zod.ts` | 4 | 5 |
| `seed-loader.zod.ts` | 12 | 12 |
| **total** | **120** | **162** |

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 22 |
| unresolved — needs a per-schema verdict | 33 |
| wire / open — out of forced scope | 65 |
| no door — no carrier, ADR-0049 territory | 0 |
| no gate — carrier live, no parse | 0 |

### `automation/` — open

**26 strip of 75**, in 5 file(s).

| File | Strip | Sites |
|---|---|---|
| `bpmn-interop.zod.ts` | 5 | 5 |
| `etl.zod.ts` | 3 | 10 |
| `execution.zod.ts` | 13 | 13 |
| `flow.zod.ts` | 1 | 11 |
| `node-executor.zod.ts` | 4 | 4 |
| **total** | **26** | **75** |

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 0 |
| unresolved — needs a per-schema verdict | 0 |
| wire / open — out of forced scope | 26 |
| no door — no carrier, ADR-0049 territory | 0 |
| no gate — carrier live, no parse | 0 |

### `security/` — open

**13 strip of 20**, in 2 file(s).

| File | Strip | Sites |
|---|---|---|
| `explain.zod.ts` | 11 | 11 |
| `rls.zod.ts` | 2 | 3 |
| **total** | **13** | **20** |

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 0 |
| unresolved — needs a per-schema verdict | 0 |
| wire / open — out of forced scope | 13 |
| no door — no carrier, ADR-0049 territory | 0 |
| no gate — carrier live, no parse | 0 |

### `studio/` — open

**0 strip of 27**, in 0 file(s).

This directory is closed.

## Other directories (untriaged)

Site totals only — these directories are classified coarsely in the ledger, per
directory rather than per file.

| Dir | Sites |
|---|---|
| `ai/` | 77 |
| `api/` | 393 |
| `cloud/` | 82 |
| `identity/` | 33 |
| `integration/` | 10 |
| `kernel/` | 319 |
| `qa/` | 6 |
| `shared/` | 25 |
| `system/` | 368 |
Loading
Loading