Skip to content

Commit 9a15446

Browse files
baozhoutaoclaude
andauthored
test(spec): 门测量 BFS 只留一份实现,并给仪器本身补上对照 (#5056) (#5833)
`chart.test.ts` 仍带着 `reachableFromMetadataRoots()` 的内联拷贝,而且是 **缺陷版**:共享测试工具 `door-reachability.testkit.ts` 早已换成整形状重合度 (≥ 0.5)的 derived-clone bridge,这份拷贝还停在「任意单属性命中即判可达」。 删掉拷贝,改为 import `measureDoors()`,全仓只剩一份实现。 同时补齐两处此前没人拥有的东西: 1. `chart.test.ts` 的门测量原本只有正对照,现在补上负对照与合成载体翻转对照。 2. 新增 `door-reachability.testkit.test.ts` —— 仪器自己的对照。三个消费者各自 为自己的形状带对照,却没人为走图器带,于是它的两条腿都可能在一片绿里烂掉: - `.describe()` 共享 def 这条**前提**被钉住(zod 升级改了 clone 语义要响); - derived-clone bridge 的**正对照**被钉住 —— 实测所有消费者断言可达的形状 都是 `direct`,这条腿在全仓没有任何测试走到,把它收紧到「永不触发」全绿。 Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4615a18 commit 9a15446

3 files changed

Lines changed: 272 additions & 131 deletions

File tree

packages/spec/src/ui/chart.test.ts

Lines changed: 41 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -14,111 +14,9 @@ import {
1414
} from './chart.zod';
1515
import { ReportChartSchema, ReportSchema } from './report.zod';
1616
import { REACT_BLOCKS } from './react-blocks';
17-
import { getMetadataTypeSchema, listMetadataTypeSchemaTypes } from '../kernel/metadata-type-schemas';
18-
import { ObjectStackSchema } from '../stack.zod';
19-
20-
/**
21-
* Reachability of a schema from every metadata-type root plus `defineStack`'s
22-
* `ObjectStackSchema`, by BFS over this build's in-memory Zod graph.
23-
*
24-
* Mirrors `computeSurfaceReachability` in `scripts/build-schemas.ts` (the
25-
* #4650 closure). `derived-clone` counts as reachable: `.extend()` / `.strip()`
26-
* produce a clone that shares no identity with the original but DOES share its
27-
* per-property schema instances, which is exactly how `ChartConfigSchema` is
28-
* reached through `ReportChartSchema`.
29-
*
30-
* ⚠️ Identity-keyed, so it must see the REAL schema instances. `lazySchema`
31-
* returns a Proxy unless `OS_EAGER_SCHEMAS=1`, and comparing a Proxy against
32-
* the instance stored in the graph reports every root as unreachable — which
33-
* is precisely how the first run of this measurement produced three failing
34-
* positive controls. Hence the resolve step.
35-
*/
36-
function reachableFromMetadataRoots(): (schema: unknown) => boolean {
37-
// Identity is the schema's `_zod.def` OBJECT, never the schema binding.
38-
// `lazySchema` hands out a Proxy unless `OS_EAGER_SCHEMAS=1`, and the graph
39-
// holds the real instances — so comparing bindings reports every root as
40-
// unreachable. That is not hypothetical: it is what the first run of this
41-
// assertion did, and the positive controls above are the only reason it was
42-
// caught instead of shipping as a green that proved nothing. `def` survives
43-
// the Proxy (the `_zod` facade delegates to the real internals), so it is
44-
// the one stable key for both identities.
45-
const defOf = (s: unknown): unknown => (s as { _zod?: { def?: unknown } })?._zod?.def;
46-
47-
const childrenOf = (node: unknown): unknown[] => {
48-
const out: unknown[] = [];
49-
const seen = new Set<unknown>();
50-
const walk = (v: unknown): void => {
51-
// `typeof v !== 'object'` alone is WRONG here and silently halves the
52-
// graph: `lazySchema`'s Proxy target is `function lazyZod() {}`, so every
53-
// lazy schema is `typeof 'function'`. Skipping those made the BFS stop at
54-
// the first lazy node and report the whole chart family unreachable —
55-
// caught only because the positive controls above went red.
56-
// (`build-schemas.ts`'s equivalent walk never hit this: it runs under
57-
// `OS_EAGER_SCHEMAS=1`, where there are no proxies at all.)
58-
if (v === null || (typeof v !== 'object' && typeof v !== 'function') || seen.has(v)) return;
59-
seen.add(v);
60-
if (defOf(v)) { out.push(v); return; }
61-
if (Array.isArray(v)) { for (const x of v) walk(x); return; }
62-
if (v instanceof Map) { for (const x of v.values()) walk(x); return; }
63-
for (const x of Object.values(v as Record<string, unknown>)) walk(x);
64-
};
65-
walk(defOf(node));
66-
return out;
67-
};
68-
const shapeOf = (node: unknown): Record<string, unknown> | null => {
69-
const def = defOf(node) as { type?: string; shape?: Record<string, unknown> } | undefined;
70-
return def?.type === 'object' && def.shape ? def.shape : null;
71-
};
72-
73-
const roots: unknown[] = [];
74-
for (const type of listMetadataTypeSchemaTypes()) {
75-
const s = getMetadataTypeSchema(type);
76-
if (s) roots.push(s);
77-
}
78-
roots.push(ObjectStackSchema);
79-
80-
const visitedDefs = new Set<unknown>();
81-
const visitedNodes: unknown[] = [];
82-
const queue = [...roots];
83-
while (queue.length > 0) {
84-
const node = queue.pop();
85-
const def = defOf(node);
86-
if (!def || visitedDefs.has(def)) continue;
87-
visitedDefs.add(def);
88-
visitedNodes.push(node);
89-
for (const child of childrenOf(node)) queue.push(child);
90-
}
91-
92-
// (propName → prop def) pairs of every visited object node — the bridge that
93-
// recognises a derived clone (`.extend()` / `.strip()` share no identity with
94-
// the original but DO share its per-property schema instances, which is how
95-
// `ChartConfigSchema` is reached through `ReportChartSchema`).
96-
const bridged = new Map<unknown, Set<string>>();
97-
for (const node of visitedNodes) {
98-
const shape = shapeOf(node);
99-
if (!shape) continue;
100-
for (const [name, prop] of Object.entries(shape)) {
101-
const d = defOf(prop);
102-
if (!d) continue;
103-
let names = bridged.get(d);
104-
if (!names) { names = new Set<string>(); bridged.set(d, names); }
105-
names.add(name);
106-
}
107-
}
108-
109-
return (schema: unknown): boolean => {
110-
const def = defOf(schema);
111-
if (!def) return false;
112-
if (visitedDefs.has(def)) return true;
113-
const shape = shapeOf(schema);
114-
if (!shape) return false;
115-
for (const [name, prop] of Object.entries(shape)) {
116-
const d = defOf(prop);
117-
if (d && bridged.get(d)?.has(name)) return true;
118-
}
119-
return false;
120-
};
121-
}
17+
import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas';
18+
import { measureDoors } from './door-reachability.testkit';
19+
import { z } from 'zod';
12220

12321
describe('ChartTypeSchema', () => {
12422
it('should accept all comparison chart types', () => {
@@ -563,21 +461,46 @@ describe('#4001 批 15 — the two chart sites deliberately LEFT OPEN (measured,
563461
// closure `build-schemas.ts` uses for the #4650 deletion check — NOT a
564462
// string search over a serialized schema, which cannot see a shape at all
565463
// and would pass no matter what (the vacuous-green this campaign keeps
566-
// paying for). The positive controls below are what prove that.
567-
const reachable = reachableFromMetadataRoots();
568-
569-
// Positive controls, in the SAME run: the five closed sites of this file
570-
// resolve. An instrument that says "unreachable" to everything is broken,
571-
// not informative.
572-
expect(reachable(ChartConfigSchema), 'positive control').toBe(true);
573-
expect(reachable(ChartAxisSchema), 'positive control').toBe(true);
574-
expect(reachable(ChartSeriesSchema), 'positive control').toBe(true);
575-
expect(reachable(ChartAnnotationSchema), 'positive control').toBe(true);
576-
expect(reachable(ChartInteractionSchema), 'positive control').toBe(true);
464+
// paying for). The controls below are what prove that.
465+
//
466+
// ⚠️ #5056: this file used to carry its OWN copy of that walk, and the copy
467+
// kept the defective `any one shared property ⇒ derived clone` bridge after
468+
// the shared walker had been fixed to a whole-shape overlap ratio. One
469+
// implementation now, in `door-reachability.testkit.ts`, whose own controls
470+
// live in `door-reachability.testkit.test.ts`.
471+
const { verdict, nodeCount, rootCount } = measureDoors();
472+
473+
// Controls FIRST, in the SAME run. An instrument that reached nothing at
474+
// all produces the same output as a correct "no door" verdict.
475+
expect(rootCount, 'roots must include every metadata type plus ObjectStackSchema').toBeGreaterThan(20);
476+
expect(nodeCount, 'the graph must actually have been walked').toBeGreaterThan(1000);
477+
478+
// Positive controls: the five closed sites of this file resolve.
479+
expect(verdict(ChartConfigSchema), 'positive control').toBe('direct');
480+
expect(verdict(ChartAxisSchema), 'positive control').toBe('direct');
481+
expect(verdict(ChartSeriesSchema), 'positive control').toBe('direct');
482+
expect(verdict(ChartAnnotationSchema), 'positive control').toBe('direct');
483+
expect(verdict(ChartInteractionSchema), 'positive control').toBe('direct');
484+
485+
// Negative control: a shape this graph has never seen must stay out.
486+
expect(verdict(z.object({ osChartProbe: z.string() })), 'negative control').toBe('unreachable');
577487

578488
// The measurement itself.
579-
expect(reachable(ChartAggregateSchema), 'a carrier key would make this reachable — re-read chart.zod.ts').toBe(false);
580-
expect(reachable(ChartGroupBySchema), 'a carrier key would make this reachable — re-read chart.zod.ts').toBe(false);
489+
expect(verdict(ChartAggregateSchema), 'a carrier key would make this reachable — re-read chart.zod.ts').toBe('unreachable');
490+
expect(verdict(ChartGroupBySchema), 'a carrier key would make this reachable — re-read chart.zod.ts').toBe('unreachable');
491+
});
492+
493+
it('a synthetic carrier flips both — the verdict is the graph, not the walker', () => {
494+
// The third control #5056 requires and this file never had. Without it the
495+
// assertion above is satisfiable by a walker that finds no doors anywhere,
496+
// which is the vacuous green 批 15 shipped once already.
497+
const carrier = z.object({
498+
aggregate: ChartAggregateSchema,
499+
groupBy: ChartGroupBySchema,
500+
});
501+
const { verdict } = measureDoors([carrier]);
502+
expect(verdict(ChartAggregateSchema), 'must become reachable once something carries it').toBe('direct');
503+
expect(verdict(ChartGroupBySchema), 'must become reachable once something carries it').toBe('direct');
581504
});
582505
});
583506

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #5056 — the controls the door measurement itself owes.
5+
*
6+
* `door-reachability.testkit.ts` decides whether a #4001 batch tightens a shape
7+
* or reclassifies it as `no door`. Its consumers (`chart.test.ts`,
8+
* `widget.test.ts`, `i18n.test.ts`) each carry the three controls for THEIR
9+
* shapes; nothing carried the controls for the instrument. This file does, so
10+
* that the walker's own limbs — the `.describe()` premise the bridge rests on,
11+
* the derived-clone bridge, and the overlap threshold that bounds it — cannot
12+
* rot behind green consumer tests.
13+
*
14+
* Every number asserted here was measured on this build, not reasoned about.
15+
*/
16+
17+
import { describe, it, expect } from 'vitest';
18+
import { z } from 'zod';
19+
import { measureDoors } from './door-reachability.testkit';
20+
import { PageSchema } from './page.zod';
21+
import { ObjectListViewSchema } from './view.zod';
22+
import { WidgetManifestSchema } from './widget.zod';
23+
import { I18nLabelSchema } from './i18n.zod';
24+
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
25+
26+
/** The walker's identity key, mirrored here so the premise tests can assert on it. */
27+
const defOf = (s: unknown): unknown => (s as { _zod?: { def?: unknown } })?._zod?.def;
28+
29+
// ============================================================================
30+
// 1. The PREMISE the derived-clone bridge rests on.
31+
//
32+
// The bridge exists because zod builders return a clone that shares the base's
33+
// per-property schema INSTANCES. Which builders share the `_zod.def` OBJECT
34+
// itself, and which do not, is what decides whether the bridge sees a real
35+
// derivation or a coincidence. A zod upgrade that changes `clone()` semantics
36+
// changes the bridge's meaning, and that must be LOUD rather than silent.
37+
// ============================================================================
38+
describe('#5056 premise — which zod builders share the `_zod.def` object', () => {
39+
it('`.describe()` shares the def of the instance it was called on', () => {
40+
// THE mechanism behind #5056. `clone(inst)` without an explicit def reuses
41+
// `inst._zod.def`, so a described clone is def-IDENTICAL to its receiver.
42+
const base = z.string();
43+
expect(defOf(base.describe('x')), '.describe() clone vs its receiver').toBe(defOf(base));
44+
expect(defOf(base.describe('x')), 'two different descriptions of ONE instance').toBe(defOf(base.describe('y')));
45+
});
46+
47+
it('but two independently constructed schemas do NOT share a def', () => {
48+
// #5056's issue body spells the fact as
49+
// `defOf(z.string().describe('x')) === defOf(z.string())` → true
50+
// and that spelling is FALSE on this build: two separate `z.string()` calls
51+
// build two separate defs, so there is nothing to share. Measured, not
52+
// assumed — the corrected statement is the one above: sharing follows the
53+
// RECEIVER INSTANCE, not the shape. That distinction is the whole reason
54+
// the bug bites this repo: the spec funnels dozens of shapes through a
55+
// handful of SHARED leaf instances, and every `.describe()` of one of them
56+
// is def-identical to every other.
57+
expect(defOf(z.string().describe('x'))).not.toBe(defOf(z.string()));
58+
expect(defOf(z.string())).not.toBe(defOf(z.string()));
59+
});
60+
61+
it('the repo\'s shared leaves are therefore def-identical across every site that describes them', () => {
62+
// `SnakeCaseIdentifierSchema` and `I18nLabelSchema` are the two the campaign
63+
// actually tripped over: `name:` and `label:` are on nearly every authorable
64+
// shape in this repo, described in place at each site.
65+
expect(defOf(SnakeCaseIdentifierSchema.describe('a'))).toBe(defOf(SnakeCaseIdentifierSchema.describe('b')));
66+
expect(defOf(I18nLabelSchema.describe('a'))).toBe(defOf(I18nLabelSchema.describe('b')));
67+
});
68+
69+
it('`.optional()` / `.extend()` / `.strip()` each build a NEW def', () => {
70+
// The other half of the premise: these are the builders whose clones the
71+
// bridge must still recognise, and they can only be recognised through
72+
// shared PROPERTY instances, because the def itself is new.
73+
const base = z.string();
74+
expect(defOf(base.optional())).not.toBe(defOf(base));
75+
const obj = z.object({ a: z.string() });
76+
expect(defOf(obj.extend({ b: z.number() }))).not.toBe(defOf(obj));
77+
expect(defOf(obj.strip())).not.toBe(defOf(obj));
78+
});
79+
});
80+
81+
// ============================================================================
82+
// 2. The three controls, on the instrument itself.
83+
// ============================================================================
84+
describe('#5056 controls — the walker finds doors, and only real ones', () => {
85+
it('positive: live authoring roots resolve, and the graph is really walked', () => {
86+
const { verdict, nodeCount, rootCount } = measureDoors();
87+
expect(rootCount, 'every metadata type plus ObjectStackSchema').toBeGreaterThan(20);
88+
expect(nodeCount, 'the graph must actually have been walked').toBeGreaterThan(1000);
89+
expect(verdict(PageSchema), 'positive control').toBe('direct');
90+
expect(verdict(ObjectListViewSchema), 'positive control').toBe('direct');
91+
});
92+
93+
it('positive: a GENUINE derived clone is recognised — this is the bridge limb', () => {
94+
// The limb #5056 narrowed. Nothing else in the repo exercises it: every
95+
// shape the consumers assert as reachable measures `direct`, so a bridge
96+
// that had been narrowed all the way to "never fires" would leave all
97+
// three consumer files green. `.extend()` and `.strip()` are exactly the
98+
// builders the bridge was written for.
99+
const { verdict, cloneOverlap } = measureDoors();
100+
101+
const extended = PageSchema.extend({ osDoorProbeExtra: z.string() });
102+
expect(verdict(extended), 'PageSchema.extend(...) is a real derivation').toBe('derived-clone');
103+
expect(cloneOverlap(extended), 'one added key out of 25 kept').toBeGreaterThan(0.9);
104+
105+
const stripped = ObjectListViewSchema.strip();
106+
expect(verdict(stripped), 'ObjectListViewSchema.strip() is a real derivation').toBe('derived-clone');
107+
expect(cloneOverlap(stripped), 'strip() keeps the whole shape').toBe(1);
108+
});
109+
110+
it('negative: a look-alike that shares no property instance stays out', () => {
111+
// #5056's own negative control, verbatim: deliberately spelled to look like
112+
// every authorable shape in the repo, but built from fresh `z.string()`
113+
// instances, so it shares nothing with the graph.
114+
const { verdict, cloneOverlap } = measureDoors();
115+
const lookAlike = z.object({ name: z.string(), label: z.string() });
116+
expect(verdict(lookAlike), 'negative control').toBe('unreachable');
117+
expect(cloneOverlap(lookAlike)).toBe(0);
118+
});
119+
120+
it('flip: injecting a synthetic carrier turns an unreachable shape reachable', () => {
121+
// Without this, "unreachable" and "the walker is broken" are the same
122+
// output. `extraRoots` exists for exactly this control.
123+
const orphan = z.object({ osDoorProbeOrphanKey: z.string() });
124+
expect(measureDoors().verdict(orphan), 'before').toBe('unreachable');
125+
const carrier = z.object({ orphan });
126+
expect(measureDoors([carrier]).verdict(orphan), 'after — a carrier makes the door').toBe('direct');
127+
});
128+
});
129+
130+
// ============================================================================
131+
// 3. The #5056 regression boundary itself.
132+
// ============================================================================
133+
describe('#5056 regression — the any-one-shared-property bridge stays dead', () => {
134+
it('WidgetManifestSchema shares leaves with the live graph but is NOT derived from it', () => {
135+
// The reverse verification, standing rather than one-shot.
136+
//
137+
// The OLD bridge fired when ANY one property of the candidate was def-equal
138+
// to a same-named property of ANY visited object node. `cloneOverlap` is
139+
// the max shared-property count over visited shapes, normalised — so
140+
// `cloneOverlap(x) > 0` is EXACTLY the condition under which the old bridge
141+
// fired. Asserting both halves here pins the fix without keeping a second,
142+
// defective copy of the walker alive to demonstrate it:
143+
//
144+
// > 0 ⇒ the old bridge WOULD have called this file reachable, and did
145+
// < 0.5 ⇒ the fixed bridge correctly does not.
146+
//
147+
// 2 shared keys (`name`, `label`, both described shared LEAVES) out of 19.
148+
// A coincidence, not a derivation — and a false door here would have spent
149+
// a v17 breaking to tighten a file nothing imports (#4583's "precisely
150+
// validated dead slot").
151+
const { verdict, cloneOverlap } = measureDoors();
152+
const overlap = cloneOverlap(WidgetManifestSchema);
153+
expect(overlap, 'it DOES share leaves — the old bridge fired on exactly this').toBeGreaterThan(0);
154+
expect(overlap, 'but nothing structural: measured 2/19').toBeLessThan(0.2);
155+
expect(verdict(WidgetManifestSchema)).toBe('unreachable');
156+
});
157+
158+
it('the threshold discriminates by SHARE OF SHAPE, not by count of shared keys', () => {
159+
// Why 0.5 and not "at least 2 shared keys": the same two shared leaves
160+
// decide opposite verdicts depending on how much of the shape they are.
161+
const { cloneOverlap } = measureDoors();
162+
const shared = {
163+
name: SnakeCaseIdentifierSchema.describe('Machine name'),
164+
label: I18nLabelSchema.describe('Display label'),
165+
};
166+
const withFiller = z.object({ ...shared, a: z.string(), b: z.string(), c: z.string() });
167+
expect(cloneOverlap(withFiller), '2 shared of 5 keys').toBeCloseTo(0.4, 5);
168+
});
169+
170+
it('KNOWN BOUND — a shape made ENTIRELY of shared leaves still bridges, whatever the threshold', () => {
171+
// Measured limitation, pinned deliberately rather than left latent for a
172+
// later batch to rediscover as a second false door.
173+
//
174+
// The ratio is taken over the CANDIDATE's own keys, so a 2-key shape whose
175+
// both keys are shared leaves scores 1.0 and is judged `derived-clone` —
176+
// no threshold in (0, 1] excludes it, because a genuine `.strip()` of a
177+
// live 2-key schema scores 1.0 too and must stay reachable. The instrument
178+
// cannot separate those two by overlap alone.
179+
//
180+
// It costs nothing today: this is a synthetic shape, and the smallest real
181+
// `no door` shapes the campaign has measured sit far below the threshold
182+
// (WidgetManifestSchema at 2/19). It becomes a real hazard the moment a
183+
// batch measures a SMALL shape (roughly 4 keys or fewer) whose keys are all
184+
// shared leaves — measure `cloneOverlap` and read this pin before trusting
185+
// a `derived-clone` verdict on one. Filed for the campaign as #5828.
186+
const { verdict, cloneOverlap } = measureDoors();
187+
const allSharedLeaves = z.object({
188+
name: SnakeCaseIdentifierSchema.describe('Machine name'),
189+
label: I18nLabelSchema.describe('Display label'),
190+
});
191+
expect(cloneOverlap(allSharedLeaves), '2 shared of 2 keys').toBe(1);
192+
expect(verdict(allSharedLeaves), 'the residual false-reachable case — see #5828').toBe('derived-clone');
193+
});
194+
});

0 commit comments

Comments
 (0)