Repository navigation
fix(spec): 上一个 PR 里的保护信封检查是空的 —— 它跳过了 25 个类型里的 24 个(#4001) - #4519
Merged
Merged
Conversation
…f 25 types (#4001) The invariant test added one change ago asserted two things about every registered metadata type: that it does not REJECT the ADR-0010 envelope its loader stamps (the hard-422 case), and that it does not silently lose it. The reject half worked — it found `hook` and `datasource` on its first run. The other half did not. It probed each schema with one generic body and asked whether `_packageId` survived. A type whose required fields that body did not supply failed for unrelated reasons, and the assertion returned early. TWENTY- FOUR of the twenty-five types took that early return. Only `field` was ever really checked, and the suite reported green. That is this campaign's own subject matter — a success signal covering an omission — reproduced inside the instrument built to detect it, one change after the ledger recorded the identical lesson about the strictness gate's non-recursive directory walk. A check that skips is indistinguishable from a check that passes. The declaration side is now STRUCTURAL: it walks the schema, unwrapping lazy/pipe/optional/default and expanding unions, and asks whether any resolved object shape declares the key. That needs no valid instance, so it cannot skip. Two guards keep it honest: - a type the walker cannot resolve is a hard FAILURE, not a pass. The walker going quiet is exactly when this test would otherwise stop covering something. - the debt list carries a reverse pin that fails when an entry is fixed, so the list cannot outlive the debt it tracks. What it found: 8 registered types do not declare the envelope, not 1 — `action`, `book`, `field`, `job`, `mapping`, `page`, `translation`, `validation`. Each loses protection metadata on every round-trip today and becomes a hard 422 the day its schema closes. `job` and `book` are closed here; 6 remain listed. Three occurrences now of one pattern, in three different instruments: the ledger gate's non-recursive walk, `strictObject(` not matching the site count, and this early return. Every one was a measuring tool reporting coverage it did not have. The ledger now states the rule it keeps re-deriving: before trusting a green check, make it go red on something you know is there. Verified: spec 282 files / 7141 tests, `tsc --noEmit` clean, all 8 generated artifacts current (the only regeneration is the two envelope key tables), all 15 `check:*` gates green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 1, 2026 15:31
This was referenced Aug 1, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
批 19/20) (objectstack-ai#5250) * feat(spec)!: object 内层块拒绝未知键 —— 13 个站点收紧,IndexSchema 依据实测保留 (objectstack-ai#4001 批 19/20) `data/object.zod.ts` 的顶层自 objectstack-ai#1535/objectstack-ai#4519/objectstack-ai#4522 起就会拒绝未知键,而它下面 的内层块一直是 zod 默认的 `.strip`。这个不对称正是本批的价值所在:一个已经 见过顶层拒绝拼写错误的作者,完全有理由把 `lifecycle: { maxAge: '30d' }` 的 干净解析读作"接受"。object 是本仓作者写入量最大的元数据类型。 批 20 —— 14 个站点关掉 13 个: - `access`(ADR-0066 D2)、`lifecycle` 及其四个子块(retention/ttl/storage/ archive)、`fieldGroups[]`(ADR-0085)、`external`(ADR-0015)、 `userActions`、`systemFields`、`activityMilestones[]`、`publicSharing`、 `objectExtensions[]`。 - 可达性是实测而非假定:从 24 个元数据类型根加 `ObjectStackSchema` 做 BFS (4810 节点 / 25 根),每一个都按**身份**判定 `direct`,没有一个依赖 objectstack-ai#5056 发现的 `derived-clone` 桥;同一次运行里 `ObjectSchema`/`PageSchema` 为正控制,批 14 的 `EmbedConfigSchema` 为负控制(unreachable,重叠 0.00)。 - parse 门单独探针、逐路径验证:今天顶层 REJECT、13 条嵌套路径全部静默 STRIP,所以每一次收紧转化的都是实测到的静默丢弃,而不是猜测。⚠️ `IndexSchema` 有意保留,这个"不动"才是本批的发现(objectstack-ai#5114 类,这次是在 发布**之前**抓到而不是之后)。console 为这个形状自带了一份手抄 JSON-Schema (objectui `metadata-admin/EmbeddedItemEditor.tsx`)—— 因为 `index` 是框架 不发布 schema 的内嵌子类型 —— 而这份抄件已经漂移:它用 `where` 表示部分索引 谓词,spec 声明的是 `partial`。编辑器把表单结果拼回 `object.indexes[]` 后 PUT 整个对象,`saveMetaItem` 逐字保留 body 并做校验,所以单独关掉这个形状会 让 console 自己渲染的控件 422。该能力两个方向上其实都已经死了 —— `driver-sql` 的 `syncDeclaredIndexes` 只读 `name`/`fields`/`unique` —— 这恰恰是必须先修生产者、并给 `type`/`partial` 一个 ADR-0049 结论的原因: 今天把作者指向 `partial`,是一条声称超出平台实际交付的 guidance(finding 18)。 批 19 —— `ui/app.zod.ts` 的 `BaseNavItemSchema`:`verify` 检查结论为否, 不改姿态。台账假定成员 `.extend()` 该基底(finding 16 的陷阱),实际它们 spread `...shape`;两者机制不同,并且双向实测过。九个分支各自已经 `.strict()`, 基底是模块私有且从不 parse,关掉它是保证的 no-op —— 而 objectstack-ai#4583 明确指出 no-op 收紧并非中性。`Class` 单元格未改动:它是机读的,而八个既有判定词 没有一个对"既不是门也不是死物"的形状是诚实的,加第九个属于改机读契约。 每条 alias/guidance 都是对 schema 的断言(finding 18),因此逐条锚定到具名 的兄弟契约并在测试中断言;`fieldGroups[]` 三个已弃用的 collapse 别名保持 **接受** —— 收紧一个形状不该把有文档的弃用变成拒绝。 已知并如实钉住的一处:`systemFields` 是 `false | {…}` 联合,拒绝信息是 `invalid_union` 的裸 "Invalid input"(objectstack-ai#5014 摊平);批 18 的 `discriminatedUnion` 解法在这里不可用(一个分支是字面量,没有判别键)。 三处记录齐备:schema JSDoc + `object-strictness-batch20.test.ts` / `app-strictness-batch19.test.ts` + 台账散文行;计数由 os-regen 重新生成。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * build(spec): 合并 origin/main 后重算 strictness 计数(os-regen) merge 带入 objectstack-ai#5221 的 chart 站点(482 → 483),计数文件按 merge=os-regen 的 约定从合并后的树重新生成,而不是任何一边的算术。authorable 仍为 16。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * docs(spec): 把批 19/20 的两个保留判定接到已归档的 issue 上(objectstack-ai#5247/objectstack-ai#5248/objectstack-ai#5249) IndexSchema 的保留(生产者漂移 + ADR-0049)与 BaseNavItemSchema 的 Class 词汇缺口都已单独归档,台账行与 schema JSDoc 现在指向 issue,而不是只说 '交给维护者'——一条指不出去的判定,下一个人只能重新测一遍。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * build(spec): 合并 objectstack-ai#5237 后重算 strictness 计数(os-regen)—— authorable 16 + (-1) → 15 合并队列以 MERGE_CONFLICT 踢出后重新同步 origin/main(merge,非 rebase)。 objectstack-ai#5237(objectstack-ai#5073 allowAddTab)与本批从两个不同目录改这份台账,散文行不重叠, git 干净合并;数字零手改,counts.md 按 merge=os-regen 从合并后的树整体重生成。 重算结果 15 = `ui/` 7 → 6(objectstack-ai#5237)+ `data/` 22 → 9(批 20),两边分支都没写过 这个数——正是 objectstack-ai#5107 拆分要达到的效果。这一条也补进 data/ 段落:它是第十二次 实例,也是第一次没让任何人付出代价。 objectstack-ai#5237 对 `ui/app.zod.ts` 的改动(`apis` 墓碑文案)与本批的 BaseNavItemSchema 判定互不相干,两侧都在;它对 scripts/strictness-ledger.test.ts 的 view 计数 5 也是它自己的领地。 门:spec 308 文件 / 7929 测试通过;typecheck 通过;check:generated 9/9; check:strictness-ledger 通过(484 站点,authorable 剩 15)。 Co-Authored-By: Claude Fable 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
Oct 7, 2026
…d decision in words instead of a tracker number (stage 18) (objectstack-ai#21870) Part of objectstack-ai#20749 Clause-②: no Stage 18 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the fourth name-ordered file group directly under `packages/spec/src/data/`: the 17 test files that carry an id from `filter.test.ts` to `object.test.ts`. They carried 103 messages and 114 tracker ids, citing 81 records. Every one of those ids now either states what its record decided, in words (form D), or is dropped where the title already says it. Text only: no assertion, identifier, test count or code comment changes. ## Census at the base (`c9be1f179d`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the copies stages 10 to 17 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. The base is `c9be1f179d`, one commit past the claim's `969ffba25e`. That commit (objectstack-ai#21857) touches only `plugin-security`, so the test census is the same. Both instruments read **1045 messages / 1108 ids in 225 files**, the seat's reading and stage 17's head reading. | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `ui/` | 84 | 396 / 419 | 378 / 401 | 18 / 18 | | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `data/` (this PR: the fourth group) | 35 | 185 / 200 | 184 / 199 | 1 / 1 | | `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **225** | **1045 / 1108** | **988 / 1050** | **57 / 58** | The group reads **103 messages / 114 ids in 17 files**, the seat's figures, file for file. Every one of them is a test title: | file (under `data/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `filter.test.ts` | 18 / 19 | 18 / 19 | 0 | | `form-delete-behavior-options.test.ts` | 1 / 1 | 1 / 1 | 0 | | `form-return-type-options.test.ts` | 1 / 1 | 1 / 1 | 0 | | `hook-body.test.ts` | 4 / 4 | 4 / 4 | 0 | | `hook.test.ts` | 10 / 10 | 10 / 10 | 0 | | `import-coercion.test.ts` | 2 / 2 | 2 / 2 | 0 | | `import-mapping-target.test.ts` | 2 / 2 | 2 / 2 | 0 | | `injected-system-column-provenance.test.ts` | 2 / 2 | 2 / 2 | 0 | | `injected-system-columns.test.ts` | 1 / 1 | 1 / 1 | 0 | | `inline-grid-column-currency-scale-refused.test.ts` | 5 / 5 | 5 / 5 | 0 | | `inline-related-columns.test.ts` | 3 / 3 | 3 / 3 | 0 | | `managed-api-affordance.test.ts` | 2 / 2 | 2 / 2 | 0 | | `masked-field-types.test.ts` | 1 / 1 | 1 / 1 | 0 | | `numeric-column-representation.test.ts` | 1 / 1 | 1 / 1 | 0 | | `object-image-field.test.ts` | 1 / 1 | 1 / 1 | 0 | | `object-strictness-batch20.test.ts` | 13 / 17 | 13 / 17 | 0 | | `object.test.ts` | 36 / 42 | 36 / 42 | 0 | | **17 files** | **103 / 114** | **103 / 114** | **0** | Five more test files sit in the same name range and carry no id: `hook-api`, `hook-body-stored-metadata-target`, `inline-grid-columns`, `mapping-connector-source` and `mapping`. They are not touched. - **Controls.** Lit, a title and an "other" string: `data/query.test.ts`, outside the group, reads 11 / 11 at the head as at the base, its "other" string at `:202` included. Dark: `data/import-mapping-target.test.ts` reads 0 / 0 at the head while 2 of its comment lines still carry a number. Planted in scratch copies of head files: an id put into a `hook-body.test.ts` title reads 1 / 1, and an id put into a `masked-field-types.test.ts` comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same totals as the gate pattern in 16 of the 17 files at the base. `object.test.ts` reads one more, at `:857`, which is the colour value `'#00FF00'` and not a tracker id. At the head the wider pattern reads 0 in 16 files and that same colour in `object.test.ts`. - **At the head:** 942 messages / 994 ids in 208 files. The 17 files read 0 / 0, and no other file moved. ## How the area was chosen `data/` has no subdirectory to split by, so its stages take name-ordered file groups near the ~100-id bound. Stage 17's re-cut named this group at 114, with `object.test.ts` alone carrying 42, and this census reads 114, so the rule needed no re-cut. **Named for the next stages** (re-cut from the head census, 942 / 994; `data/` 82 / 86 left, in 18 files): - `data/`, one more stage: `query-transport.test.ts` to `validation.test.ts` (11 files, 34 / 34) with `data/driver/` (7 files, 48 / 52): 82 messages / 86 ids. - `ui/` 419, about four stages. `api/` 201, two. `system/` 165, two. The files directly in `src/`, 120, one. - The three docblock needles (`ai/build-progress.test.ts:236`, `:237`, `contracts/approval-service.test.ts:274`), one stage with their docblocks. ## What each id became 34 literals (41 ids) now state a decision in words. 69 literals (73 ids) drop a number the title already explains. Every cited record was read with its comments through REST. 73 answer 200; `framework#2536` is counted there, because the repository was renamed `framework` → `objectstack` (`apps/docs/lib/layout.shared.tsx` records the rename). The old name answers 403 from this session, and the same number under the current name is the compactLayout retirement that the title names. Eight answer 404, and each decision was read from what landed, by REST GET of the landing commit and its CHANGELOG entry: objectstack-ai#6571 (`2f3e79351e`), objectstack-ai#10165 (`8012960508`), objectstack-ai#10347 (`530c1df653`), objectstack-ai#10527 (`5649efbf93`), objectstack-ai#11195 (`b372318836`), objectstack-ai#11408 (`f11fc61c51`), objectstack-ai#13644 (`34ce8e7dbe`) and objectstack-ai#18012 (`176b03582e`). | record(s) | literal (under `data/`) | now reads | |:--|:--|:--| | objectstack-ai#5685 | `filter.test.ts:77` | "string comparands — the ordering slots take a string, which is what the date macros emit". `string` joined the four ordering slots because the platform's own date-macro resolver produces only strings. | | objectstack-ai#14080 | `filter.test.ts:166` | "null comparand — refused in the four ordering slots, like a null list member". Ruled A: refused at the same entrance, in the same shape, as the null `$in` member. | | objectstack-ai#6571 (404) | `filter.test.ts:364` | "string endpoints — `$between` takes the ISO and clock strings the platform produces". The sibling half of objectstack-ai#5685, from `2f3e79351e`. | | objectstack-ai#7711 (2) | `filter.test.ts:549` | "the whole-filter face refuses these field conditions too — green while the group branch was a non-strict catch-all". The group branch became `.strict()`, so a refused member has nowhere to land. | | objectstack-ai#5222 | `filter.test.ts:576` | "leaves the four ORDERING slots taking a reference — a column-to-column comparison, and the prescribed alternative". `$field` compiles to a same-table column comparison on SQL push-down. | | objectstack-ai#5322 | `filter.test.ts:1541` | "leaves the empty-combinator identities accepted — each reduces to its boolean unit". Ruled: `{$and:[]}` is every row, `{$or:[]}` none, `{$not:{}}` none. | | objectstack-ai#14104 | `filter.test.ts:1851` | "FieldReferenceSchema.addDays — a whole-day offset on a reference, an integer or another column". Ruled A: an offset on the field reference. | | objectstack-ai#16923 | `filter.test.ts:1998` | "filter.zod.ts docblock @examples — a `$field` comparand names a column of the same row". The relation-path example was the wrong half; the same-table prose was right. | | objectstack-ai#14010 | `hook.test.ts:352` | "runAs — a hook may run as `system` or `user`, and inherits by default". Ruled: `runAs: 'system' \| 'user' \| 'inherit'`, default `'inherit'`. | | objectstack-ai#13644 (404) | `hook.test.ts:529` | "Referential-Cleanup Marker — declared, and true only on a reference-cleanup write by the engine". Adopted by maintainer ruling, from `34ce8e7dbe`. | | objectstack-ai#4269 | `hook.test.ts:924` | "defineHook — the authoring factory, so a hook is validated where it is written". The `defineDatasource` pattern: the convention-scan path got a parse at authoring time. | | objectstack-ai#3493 | `hook.test.ts:1148` | "PRESERVES `preserveAudit` through a parse (the opt-in a historical import uses to keep its audit stamps)". | | objectstack-ai#5945 | `hook.test.ts:1286` | "HookContext.api typing — the minimum scoped context the docs teach: `object()` and `transaction()`". Ruled option C. | | objectstack-ai#4173 (2) | `import-coercion.test.ts:23`, `:48` | "import boolean tokens — one table for the server coercion and the Import Wizard preview" and "import reference types — exported from spec, not copied at each consumer". | | objectstack-ai#8116 | `injected-system-column-provenance.test.ts:53` | "provenance derivation at its spec home — where the author-time linter can reach it". Ruled option 1: the derivation moved into the contract package, which the linter may import. | | objectstack-ai#5378 | `injected-system-columns.test.ts:17` | "resolveInjectedSystemColumns — the injected columns, so author-time validation resolves them too". | | objectstack-ai#20045 | `inline-grid-column-currency-scale-refused.test.ts:172` | "SHAPE PARITY with the currency FIELD refusal (its ruled text carried, not reworded)". "ruling B" is the ruling that retired `scale` on a currency field; the letter went with the id. | | objectstack-ai#7521 | `managed-api-affordance.test.ts:57` | "is the exact shape the boot only warned about, now named at authoring time (sys_environment / sys_package)". Ruled: an authoring-time check, with boot left at warn and strip. | | objectstack-ai#16318 | `numeric-column-representation.test.ts:24` | "the numeric physical-representation table — one table the driver and the migration generators both read". Ruled C: one table in `packages/spec`, both producers read it. | | objectstack-ai#4001 (2) | `object-strictness-batch20.test.ts:93`, `:229` | "批 20, unknown keys refused — …", as stage 15 wrote "batch D, unknown keys refused". `:132` already says it and keeps only "批 20". | | objectstack-ai#1535, objectstack-ai#4519, objectstack-ai#4522 | `object-strictness-batch20.test.ts:124` | "the root itself was already closed, on parse as well as create() — this batch is the level BELOW it". | | objectstack-ai#5014 | `object-strictness-batch20.test.ts:187` | "⚠️ `systemFields` is the batch's ONE union flattened to a bare `Invalid input` — …". | | objectstack-ai#5677, objectstack-ai#6365 | `object-strictness-batch20.test.ts:380` | "… — since the unit anchor is injected, the property governs `owning_business_unit_id` too". objectstack-ai#6365 is dropped: the title already states its correction. | | objectstack-ai#11195 (404) | `object-strictness-batch20.test.ts:422` | "the two `userActions` vocabularies stay disjoint, the three adopted view keys included — …". `b372318836` adopted `group` / `hideFields` / `rowColor` onto the view block. | | objectstack-ai#4001, objectui#4772 | `object-strictness-batch20.test.ts:528` | "批 20 — `IndexSchema` is closed (the held 14th site, once the console index editor converged on it)". | | objectstack-ai#10527 (404) | `object.test.ts:188` | "retention + ttl + archive triple — refused unless the ttl restates the age bound". From `5649efbf93`. | | objectstack-ai#10347 (404) | `object.test.ts:192` | "still accepts the ttl + archive pair, whose ttl cutoff picks the rows to archive (no retention)". From `530c1df653`. | | objectstack-ai#2834 | `object.test.ts:264` | "accepts retention.onlyWhen with scalar and $in predicates (mixed tables, where only terminal rows age out)". | | objectstack-ai#10165 (404) | `object.test.ts:289` | "accepts ttl.onlyWhen with the canonical null predicate — so rows whose value is absent are spared". From `8012960508`. | | objectstack-ai#3175 | `object.test.ts:1101` | "ownership record-model field — declared, so the opt-out the registry reads can be authored". | | objectstack-ai#11408 (404), objectstack-ai#10144 | `object.test.ts:1713` | "ObjectSchema editMode (declared by maintainer ruling: the renderer reads it, so the spec declares it)". From `f11fc61c51`, in objectstack-ai#10144's declare-or-rule-out family. | | objectstack-ai#14935, objectstack-ai#14637 | `object.test.ts:2135` | "isPublicSharingEnabled — the one canonical standing share-link policy predicate". The runtime mirror was retired. | **Dropped where already stated** (69 literals, 73 ids). A number goes only where the title already says its decision, for example "$field members are refused (objectstack-ai#7596)", the four `crypto.hash` titles in `hook-body.test.ts` (objectstack-ai#4391), the `objectstack-ai#20045 —` prefix on the four other describes of the currency-scale file, and twenty-nine `object.test.ts` titles such as "managedBy: retiring the overloaded `system` bucket (objectstack-ai#3355)". `(ADR-0049)`, `(ADR-0066)`, `(ADR-0100)` and `(ADR-0087 …)` stay: they cite decision records by number, not tracker ids. **Three judgements, each declared:** - `filter.test.ts:298` keeps "ruled 2026-08-31" and drops only `(objectstack-ai#13357)`. The sibling title at `:656` names "the 2026-08-31 ruling", so the date stays as its anchor. - The `批 20` label stays on the four batch-20 describes, because the file's own name and header carry it. Only `objectstack-ai#4001` and `objectui#4772` went. - `object-strictness-batch20.test.ts:563` drops "(objectstack-ai#5114 class)": the title already says what is pinned, that the tombstones keep their prescription through the strict close. ## Readers - **Test-name filters:** none. No tracked script, workflow or config passes `-t` / `--testNamePattern`. - **Snapshots:** none. No `__snapshots__` directory is tracked under `packages/spec`. - **Projects:** none of the 17 files is listed in `packages/spec/vitest.repo-tests.json`; all 17 run in the `local` project. - **By substring:** every old literal, plus a window around each id (311 needles), was searched across the tracked tree outside its own file. No gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 8 needle hits fall on 7 lines: - a code comment in `object.zod.ts:2532`; - two release-owned CHANGELOG lines; - a sibling title in this card's `system/` stage (`system/job.test.ts:471`); - sibling titles in `cli` (`extract-hook-body.test.ts:179`), `driver-sql` (`sql-driver-16318-numeric-representation.test.ts:64`) and `objectql` (`engine.test.ts:831`). None reads a spec test title. - **The files by name:** 96 references to these file names outside CHANGELOGs. The gate ledgers among them (`test-typecheck-debt.json`, `engine-double-contract.pinned.json`, `objectql-double-limit.baseline.json`) key on the file and on error signatures, not on a title, and each gate exits 0 at the head. `scripts/check-org-identifier.mjs` counts `session: { … tenantId … }` literals in `hook.test.ts`, which this PR does not touch. ADR-0129 quotes "name-as-identity", a title this PR does not change. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. This stage declares no line: every changed leaf is a title. - **Result:** 17 of 17 files SAME on all three legs, with the per-file counts predicted in writing before the run. - **Totals:** 103 changed string leaves in 103 literals, all titles. The diff's `+` and `-` lines are exactly the 103 planned lines, and every file keeps its line count. - **Controls (10 of 10 as predicted, on scratch copies, each anchor hit once in the reported run):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; an `it.each` row name given an id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF. The non-title control's first anchor matched nothing (0 hits, so nothing ran). Its anchor was corrected and all ten were run again. **Test counts:** the 17 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 670 tests, all passed, with the same count and status sequence per file in 17 of 17. 378 full test names change, and each equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## `main` since the base Re-fetched just before this PR opened, `origin/main` was two commits past the base (`9f9510f25e`: objectstack-ai#21858, objectstack-ai#21862). Neither touches any of the 17 files; the one `packages/spec` path among them is `api/error-code-ledger.zod.ts`. So `main` was not merged. `git merge-tree` onto `9f9510f25e` is clean. No open PR touches the 17 files. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 17 touched files are in it, and no `*.test.ts` at all. The controls `src/data/filter.zod.ts` and `dist/index.mjs` are in it. - In the built `dist/`, a new phrase and an old literal each read in 0 files. The control `Unrecognized key` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `21f39afe57`) - `pnpm turbo run build` over all packages: 71 / 71. - `@objectstack/spec`: - `vitest run --project local`: 615 files, 18387 passed, 1 todo. - `typecheck` exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 17 touched files, counted with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stages 13 to 17, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded. - The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - The derivation printed `STALE TREE`: `scripts/engine-double-contract.pinned.json` moved on `main` after the base. This diff adds and changes no engine double, `check:engine-double-contract` exits 0 on this tree, and the queue re-runs it on the merged generation. - **ESLint, a proven narrowing:** `--no-inline-config` over the 17 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 17 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 206 changed lines. ## Acceptance notes - **No needle in this group.** Every id was a test title; no expected value of an assertion over a source docblock was found. The three known needles are untouched. - **Same-id test titles in this card's later stages** go with those stages: 38 lines in `packages/spec/src`, 9 in `system/` (for example `system/job.test.ts:471`, the `job.timeout` twin of `hook.test.ts:1382`) and 29 in `ui/` (for example the `objectstack-ai#4001 批 15` / `批 16` / `批 18` / `批 19` describes and `ui/view-filter-rule-wire-id.test.ts`'s four `objectstack-ai#5114` describes). - **Same-id test titles in other packages** are their lanes' test-string shares. A search of `describe` / `it` / `test` lines outside `packages/spec/src` finds 140 lines citing ids this PR handled: `objectql` 23 (13 files), `service-analytics` 18 (8), `lint` 17 (9), `driver-sql` 11 (7), `cli` 10 (4), `runtime` 10 (7), `platform-objects` 5, `plugin-audit` 5, `plugin-security` 5, and fewer in 16 more places, among them two `packages/spec/scripts/*.test.ts` titles (outside `src/`) and one title each in `examples/app-crm` and `examples/app-showcase`. - **Code comments still carry ids** in these files and their sources, for example the `// [objectstack-ai#20150]` block at `import-mapping-target.test.ts:17`, the `objectstack-ai#4001 批 20` header of `object-strictness-batch20.test.ts` and `object.zod.ts:2532`. Comments are not this card's share, and none is touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
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.
紧接 #4514。那个 PR 加的不变量测试断言了两件事:每个注册元数据类型不拒收加载器盖的 ADR-0010 信封(硬 422 那一类),以及不静默丢失它。
拒收那一半是有效的——它第一次运行就抓到了
hook和datasource。另一半是空的。
它怎么空的
它用一个通用探针 body 去 parse 每个 schema,再看
_packageId有没有活下来。但一个 schema 如果有探针没提供的必填字段,就会因为无关原因解析失败,断言直接 early return:只有
field真的被检查过。整个套件报绿。这就是这场战役的主题——成功信号掩盖遗漏——出现在为了检测它而建的仪器里,而且就在账本刚刚记下闸门非递归扫描那同一课之后一个改动。
修法:结构式遍历,跳不过去
声明侧改成直接走 schema 结构——拆开
lazy/pipe/optional/default,展开 union——问「有没有哪个解析出的 object shape 声明了这个键」。这个问题不需要构造合法实例,所以不存在「因无关原因失败」的早退路径。
两个护栏让它保持诚实:
它接着找到了什么
不是 1 个,是 8 个未声明信封的注册类型:
action、book、field、job、mapping、page、translation、validation探针版本藏了其中 7 个。每一个今天都在往返中丢保护元数据,且在它的 schema 被收紧那天会变成硬 422。
本 PR 修掉
job和book,清单剩 6 个。一个开始重复的模式
这是同一个模式的第三次,出现在三个不同的仪器上:
data/driver/九个站点隐形strictObject(不算站点 → 「解决了」和「删掉了」同数三个都是测量工具报告了自己并不具备的覆盖度。账本里现在写下了这条反复被重新推导出来的规则:
验证
@objectstack/spec:282 文件 / 7141 用例通过,tsc --noEmit干净book/job两张信封键表)check:*闸门全绿参考
docs/audits/2026-07-unknown-key-strictness-ledger.mdfindings log 第 9 条🤖 Generated with Claude Code
https://claude.ai/code/session_01WnqGjQFQMqd5k81LYV8SCY
Generated by Claude Code