Skip to content

Commit 2e5746d

Browse files
test(spec): state the registered-type closure rule instead of pinning closed/total counts (#26509)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB5sm8QBuDxvf14zMDnQes6X)_ Fixes #26437 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`metadata-type-schemas.test.ts` 末尾两个手写计数(closed 27 / types 28)换成规则断言:从注册表算出「未闭合的类型集合」,必须恰好等于文件里具名的例外表 `STILL_STRIP`(今天只有 `view`)。计数的递增记录挪到文件头作为历史。 - 为什么改:每加一种元数据类型就得手改这两个数,改数不证明任何事;规则断言只在真正违约时变红,并直接点名违约的类型。 - 风险与回滚:只改一个测试文件,不发布任何东西;回滚就是 revert 这一个提交。 - 先看:`packages/spec/src/kernel/metadata-type-schemas.test.ts` 的 `the open set, computed from the registry, IS STILL_STRIP` 用例(文件末尾),再看文件头新加的 History 节。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/kernel/metadata-type-schemas.test.ts`,预期 134/134 通过。 - 需要你做的:无,席位落地。 ## What changes `packages/spec/src/kernel/metadata-type-schemas.test.ts` only. - The closure `describe` ended in `expect(closed.length).toBe(27)` and `expect(types.length).toBe(28)`, bumped by hand on every new metadata kind. That test is replaced by the rule the counts stood in for: the registered types whose top-level schema posture is not `strict`, computed from `listMetadataTypeSchemaTypes()` with the file's own `topLevelPosture` walker, must equal `STILL_STRIP` (today `['view']`). `types.length > 0` is kept. - The bump log (25 → 24 `validation`, 24 → 25 `api`, 25 → 26 `capability`, `api` closing, 26 → 27 `picklist`, 27 → 28 `sharing_rule`) moves to a `## History` section in the file header. - Two pieces of prose that still referred to the count are updated: the STILL_STRIP reverse-pin message no longer says "and update the count", and the `view` note no longer says "24 of 25 are closed". ## Why Part of #26431 (retire the transcription pins). A hand-typed total is a transcription of the registry, not a check on it: adding a closed kind turned the suite red for no defect, and the fix was always to type the new number. The rule form fails only on a real breach (a new open kind, or an exception that closed) and names the offending types in the diff. ## Tests Run at head `120bfb17`. - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/kernel/metadata-type-schemas.test.ts` under the verify lock: `Test Files 1 passed (1)`, `Tests 134 passed (134)`, `VERDICT command-exit 0`. - `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec typecheck`: `VERDICT command-exit 0`; `check:test-typecheck: OK` (test layer compiles under `tsconfig.test.json`, debt ledger held). <details><summary>Ablation: one closed type made non-strict reds the new assertion</summary> Via `scripts/ablation-replace.mjs` on `packages/spec/src/data/picklist.zod.ts`, anchor `export const PicklistSchema = lazySchema(() => strictObject({` replaced with `z.object({` (anchor x1 → x0, replacement x0 → x1, blob `56447b6a03a5` → `9ee381dbfaaf`). The test imports the registry by relative `src` path, so no build sits between the mutation and the run. ``` FAIL registered-type closure is derived, not tallied > the open set, computed from the registry, IS `STILL_STRIP` — no count to bump AssertionError: the registered types whose schema is not closed must be exactly STILL_STRIP: expected [ 'picklist', 'view' ] to deeply equal [ 'view' ] Tests 4 failed | 130 passed (134) ``` The other three reds are the pre-existing per-type pins on `picklist` (REJECTS unknown keys, and the two envelope pins, since `strictObject` is what spreads the envelope fields). Restore: blob after restore `56447b6a03a5` == HEAD, `git diff HEAD` empty. </details> ## Acceptance notes - Adding a closed metadata kind changes no test: the new assertion derives both sides from the registry and the named list; nothing counts. - `git grep -n 'closed.length).toBe(' packages/spec` returns 0 hits (exit 1). - No changeset: the diff is a test file, outside every `files[]` entry `@objectstack/spec` publishes (`src/**/*.zod.ts`, `dist`, …). Built `dist` has zero hits for the new test's text; positive control `listMetadataTypeSchemaTypes` hits `dist/browser/kernel/index.js`. Labelled `skip-changeset`. ## Verification <details><summary>Derived local gates: 73 derived, 69 run green, 4 not measured, 0 red</summary> `node scripts/pm/dispatch-gates.mjs --commands` at `120bfb17` derived 73 families for the one changed path. All 73 were run with exit codes recorded; `dispatch-gates --ran` reconciles: `73 derived famil(ies) accounted for — 69 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3)`. NOT MEASURED (exit 3, prerequisite refusal, owned by CI): - `node scripts/check-plugin-teardown-shape.mjs --self-test`, reason: shallow clone cannot reach its pinned positive-control commit. - `pnpm --filter @objectstack/lint run check:doc-formula-expressions`, reason: `@objectstack/formula` and `@objectstack/lint` not built in this worktree. - `pnpm check:dual-build-cjs-loads`, reason: reads built output of packages this diff does not touch (no `dist/`). - `pnpm check:lean-entry-closure`, reason: `@objectstack/objectql` not built. `node scripts/check-nul-bytes.mjs`: exit 0. Repo-wide scans (`pnpm lint`) and the full test farm are CI's. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (CI's run is the reading.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Publishes nothing; `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Test file only; no artifact reads it.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. Dispatched by spec seat 1 (`session_01WGufzfhobWEgUUrjz4rrbh`); implemented in card thread `session_01WGufzgtamNuTZbD2fSuQ3Y`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzgtamNuTZbD2fSuQ3Y --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzgtamNuTZbD2fSuQ3Y)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent d2ca707 commit 2e5746d

1 file changed

Lines changed: 53 additions & 45 deletions

File tree

‎packages/spec/src/kernel/metadata-type-schemas.test.ts‎

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,45 @@
5353
* `objectShapes` walker and the same `rejectedEnvelopeKeys` probe defined here,
5454
* so the two iterations cannot drift into judging the property differently;
5555
* only the SET each walks differs, which is the whole point of the split.
56+
*
57+
* ## History: the closure tally this file used to pin
58+
*
59+
* The closure `describe` below once ended in two hand-typed numbers — the
60+
* closed count and the registered total — bumped by hand every time a metadata
61+
* kind joined or left the registry. They now state the rule they protected
62+
* instead: the set of registered types whose schema is not closed, computed from
63+
* the registry, equals `STILL_STRIP`. Adding a closed kind changes no test. The
64+
* bump log is kept here as history, because each entry records why a kind landed
65+
* on the side it did:
66+
*
67+
* - 25 → 24 on 2026-08-02: `validation` left the registry entirely (#4509,
68+
* ADR-0088 retirement), taking an already-closed schema with it. The closure
69+
* ratio was unchanged — one fewer registered type, not one fewer closed one.
70+
* - 24 → 25 on 2026-08-04: `api` JOINED the registry (#5271, part of #5206),
71+
* the first new registered kind since the campaign ended. It declared the
72+
* protection envelope (so `UNDECLARED_ENVELOPE` stayed empty) but went onto
73+
* `STILL_STRIP` — measured, see that list's note: the same schema parsed
74+
* stored rows carrying `packageId` / `state`, so closing it broke the
75+
* load-time backstop and the publish gate. Closed count unchanged at 23.
76+
* - 25 → 26 on 2026-08-08: `capability` JOINED the registry (#5961), under the
77+
* same template `api` used. Unlike `api` it landed CLOSED —
78+
* `CapabilityDeclarationSchema` is only an AUTHORING surface, so `strict()`
79+
* cost nothing: nothing re-parses a stored `sys_capability` row through it
80+
* (`bootstrapDeclaredCapabilities` reads named fields off the body via
81+
* `capabilityRowFields`), which is exactly the property `api` lacked. Closed
82+
* 23 → 24; `STILL_STRIP` did not grow.
83+
* - 24 → 25 CLOSED on 2026-08-08: `api` closed (#5384). The registered total
84+
* did not move — only the posture did. #5309 (PR #6576) gave the stored
85+
* envelope a layer of its own (`peelStoredEnvelope`), so nothing re-parses a
86+
* stored `api` row through this schema either, and the conversion became an
87+
* ordinary #4001 one. `STILL_STRIP` shrank to `view` alone, which IS the end
88+
* state — its open members are wire shapes with nowhere else to live.
89+
* - 26 → 27 on 2026-09-30: `picklist` JOINED the registry, closed (a
90+
* `strictObject` authoring surface).
91+
* - 27 → 28 on 2026-10-19: `sharing_rule` JOINED the registry (ADR-0131 D6,
92+
* regime C), closed — `SharingRuleSchema` is `.strict()` and an authoring
93+
* surface only. This was the last bump: closed 27 of 28 when the counts were
94+
* replaced by the rule.
5695
*/
5796

5897
import { describe, expect, it } from 'vitest';
@@ -483,8 +522,8 @@ describe('each bound entry IS its stack collection\'s schema', () => {
483522
*
484523
* ## `view` is the end state, not the last item of debt
485524
*
486-
* 24 of 25 are closed and the 25th will not be, for a reason worth stating so
487-
* nobody "finishes the job" by force. The registered `view` schema is a UNION
525+
* Every other registered type is closed and `view` will not be, for a reason
526+
* worth stating so nobody "finishes the job" by force. The registered `view` schema is a UNION
488527
* over the runtime shapes a `view` body really takes, and THREE of its four
489528
* members are deliberately `.strip()`:
490529
*
@@ -609,51 +648,20 @@ describe('registered-type closure is derived, not tallied', () => {
609648
expect(types).toContain(type);
610649
expect(
611650
topLevelPosture(getMetadataTypeSchema(type)),
612-
`'${type}' is now strict — remove it from STILL_STRIP and update the count.`,
651+
`'${type}' is now strict — remove it from STILL_STRIP.`,
613652
).toBe('strip');
614653
});
615654

616-
it('reports the campaign number so a reader never has to count', () => {
617-
const closed = types.filter((t) => !STILL_STRIP.has(t));
618-
expect(closed.length + STILL_STRIP.size).toBe(types.length);
619-
// 25 → 24 on 2026-08-02: `validation` left the registry entirely (#4509,
620-
// ADR-0088 retirement), taking an already-closed schema with it. The
621-
// campaign's closure ratio is unchanged — one fewer registered type, not
622-
// one fewer closed one.
623-
//
624-
// 24 → 25 on 2026-08-04: `api` JOINED the registry (#5271, part of #5206),
625-
// the first new registered kind since the campaign ended. It declares the
626-
// protection envelope (so `UNDECLARED_ENVELOPE` stays empty) but goes onto
627-
// STILL_STRIP — measured, see that list's note: the same schema parses
628-
// stored rows carrying `packageId` / `state`, so closing it breaks the
629-
// load-time backstop and the publish gate. Closed count is therefore
630-
// unchanged at 23 while the registered total moves to 25.
631-
//
632-
// 25 → 26 on 2026-08-08: `capability` JOINED the registry (#5961), under
633-
// the same template `api` used. Unlike `api` it lands CLOSED —
634-
// `CapabilityDeclarationSchema` is only an AUTHORING surface, so
635-
// `strict()` cost nothing: nothing re-parses a stored `sys_capability` row
636-
// through it (`bootstrapDeclaredCapabilities` reads named fields off the
637-
// body via `capabilityRowFields`), which is exactly the property `api`
638-
// lacks. So the closed count moves with the total, 23 → 24, and
639-
// `STILL_STRIP` does not grow.
640-
//
641-
// 24 → 25 CLOSED on 2026-08-08: `api` closed (#5384). The registered total
642-
// does not move — only the posture does. The property `capability` had and
643-
// `api` lacked above turned out not to be a permanent difference: #5309
644-
// (PR #6576) gave the stored envelope a layer of its own
645-
// (`peelStoredEnvelope`), so nothing re-parses a stored `api` row through
646-
// this schema either, and the conversion became an ordinary #4001 one.
647-
// `STILL_STRIP` shrinks to `view` alone, which IS the end state — its open
648-
// members are wire shapes with nowhere else to live (see that list's note).
649-
//
650-
// 26 → 27 on 2026-09-30: `picklist` JOINED the registry, closed (a
651-
// `strictObject` authoring surface), so the closed count moves with it.
652-
//
653-
// 27 → 28 on 2026-10-19: `sharing_rule` JOINED the registry (ADR-0131 D6,
654-
// regime C), closed — `SharingRuleSchema` is `.strict()` and an authoring
655-
// surface only — so the closed count moves with it.
656-
expect(closed.length).toBe(27);
657-
expect(types.length).toBe(28);
655+
it('the open set, computed from the registry, IS `STILL_STRIP` — no count to bump', () => {
656+
// The rule the old `closed 27 / types 28` pin stood in for, stated directly:
657+
// every registered type is closed except the named exceptions. A new closed
658+
// kind changes nothing here; a new open one, or an exception that closed,
659+
// reds with the offending names. History of the old counts: file header.
660+
expect(types.length).toBeGreaterThan(0);
661+
const open = types.filter((t) => topLevelPosture(getMetadataTypeSchema(t)) !== 'strict');
662+
expect(
663+
[...open].sort(),
664+
'the registered types whose schema is not closed must be exactly STILL_STRIP',
665+
).toEqual([...STILL_STRIP].sort());
658666
});
659667
});

0 commit comments

Comments
 (0)