@mmnto/totem 1.64.2 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { firingLabelId, WindtunnelLockSchema } from './windtunnel-lock.js';
3
+ // ─── Helpers ─────────────────────────────────────────
4
+ const VALID_SHA = '0'.repeat(40);
5
+ const VALID_SHA_2 = '1'.repeat(40);
6
+ const VALID_SHA_3 = 'a'.repeat(40);
7
+ function validLock(overrides) {
8
+ return {
9
+ schema: 'windtunnel.lock.v1',
10
+ canonicalPath: '.totem/spine/gate-1/windtunnel.lock.json',
11
+ gate: 'gate-1',
12
+ phase: 'harness',
13
+ corpus: {
14
+ repo: 'mmnto-ai/liquid-city',
15
+ selectionRule: {
16
+ state: 'merged',
17
+ predicate: 'touches-code',
18
+ window: { type: 'all' },
19
+ asOfCommit: VALID_SHA,
20
+ },
21
+ resolvedPrs: [
22
+ {
23
+ pr: 1,
24
+ mergeCommit: VALID_SHA,
25
+ baseSha: VALID_SHA_2,
26
+ headSha: VALID_SHA_3,
27
+ },
28
+ {
29
+ pr: 2,
30
+ mergeCommit: VALID_SHA_2,
31
+ baseSha: VALID_SHA_3,
32
+ headSha: VALID_SHA,
33
+ },
34
+ ],
35
+ },
36
+ fpDefinition: {
37
+ rubricRef: 'controls/rubric.md',
38
+ groundTruthRef: 'controls/ground-truth-labels.json',
39
+ adjudicator: 'operator',
40
+ precisionFloor: 1.0,
41
+ },
42
+ controls: {
43
+ positiveRef: 'controls/positive/',
44
+ negativeRef: 'controls/negative/',
45
+ integrity: {
46
+ mechanism: 'git-hash-object',
47
+ fixtureSha: VALID_SHA,
48
+ },
49
+ },
50
+ cullRateThreshold: 0.25,
51
+ exposureDenominator: {
52
+ activeRulesEvaluated: { floor: 2 },
53
+ filesTouchedInWindow: { floor: 0 },
54
+ positiveControlsExercised: { floor: 0 },
55
+ },
56
+ ...overrides,
57
+ };
58
+ }
59
+ // ─── Schema acceptance ───────────────────────────────
60
+ describe('WindtunnelLockSchema acceptance', () => {
61
+ it('accepts a valid harness-phase lock', () => {
62
+ const result = WindtunnelLockSchema.safeParse(validLock());
63
+ expect(result.success).toBe(true);
64
+ });
65
+ it('accepts a certifying-phase lock', () => {
66
+ const result = WindtunnelLockSchema.safeParse(validLock({ phase: 'certifying' }));
67
+ expect(result.success).toBe(true);
68
+ });
69
+ it('accepts a bounded window lock', () => {
70
+ const lock = validLock();
71
+ lock.corpus.selectionRule.window = { type: 'bounded', n: 50 };
72
+ const result = WindtunnelLockSchema.safeParse(lock);
73
+ expect(result.success).toBe(true);
74
+ });
75
+ });
76
+ // ─── Schema rejection invariants ─────────────────────
77
+ describe('WindtunnelLockSchema rejection', () => {
78
+ it('rejects precisionFloor !== 1.0', () => {
79
+ const lock = validLock();
80
+ lock.fpDefinition.precisionFloor = 0.9;
81
+ const result = WindtunnelLockSchema.safeParse(lock);
82
+ expect(result.success).toBe(false);
83
+ });
84
+ it('rejects non-40-hex asOfCommit', () => {
85
+ const lock = validLock();
86
+ lock.corpus.selectionRule.asOfCommit = 'notahex';
87
+ const result = WindtunnelLockSchema.safeParse(lock);
88
+ expect(result.success).toBe(false);
89
+ });
90
+ it('rejects empty resolvedPrs', () => {
91
+ const lock = validLock();
92
+ lock.corpus.resolvedPrs = [];
93
+ const result = WindtunnelLockSchema.safeParse(lock);
94
+ expect(result.success).toBe(false);
95
+ });
96
+ it('rejects resolvedPrs entries missing mergeCommit (C4)', () => {
97
+ const lock = validLock();
98
+ lock.corpus.resolvedPrs = [
99
+ { pr: 1, baseSha: VALID_SHA, headSha: VALID_SHA_2 },
100
+ ];
101
+ const result = WindtunnelLockSchema.safeParse(lock);
102
+ expect(result.success).toBe(false);
103
+ });
104
+ it('rejects resolvedPrs entries missing baseSha (C4)', () => {
105
+ const lock = validLock();
106
+ lock.corpus.resolvedPrs = [
107
+ { pr: 1, mergeCommit: VALID_SHA, headSha: VALID_SHA_2 },
108
+ ];
109
+ const result = WindtunnelLockSchema.safeParse(lock);
110
+ expect(result.success).toBe(false);
111
+ });
112
+ it('rejects resolvedPrs entries missing headSha (C4)', () => {
113
+ const lock = validLock();
114
+ lock.corpus.resolvedPrs = [
115
+ { pr: 1, mergeCommit: VALID_SHA, baseSha: VALID_SHA_2 },
116
+ ];
117
+ const result = WindtunnelLockSchema.safeParse(lock);
118
+ expect(result.success).toBe(false);
119
+ });
120
+ it('rejects duplicate pr numbers in resolvedPrs (C4)', () => {
121
+ const lock = validLock();
122
+ lock.corpus.resolvedPrs = [
123
+ { pr: 1, mergeCommit: VALID_SHA, baseSha: VALID_SHA_2, headSha: VALID_SHA_3 },
124
+ { pr: 1, mergeCommit: VALID_SHA_2, baseSha: VALID_SHA_3, headSha: VALID_SHA },
125
+ ];
126
+ const result = WindtunnelLockSchema.safeParse(lock);
127
+ expect(result.success).toBe(false);
128
+ expect(JSON.stringify(result)).toContain('unique');
129
+ });
130
+ it('rejects unsorted pr numbers in resolvedPrs (C4)', () => {
131
+ const lock = validLock();
132
+ lock.corpus.resolvedPrs = [
133
+ { pr: 2, mergeCommit: VALID_SHA, baseSha: VALID_SHA_2, headSha: VALID_SHA_3 },
134
+ { pr: 1, mergeCommit: VALID_SHA_2, baseSha: VALID_SHA_3, headSha: VALID_SHA },
135
+ ];
136
+ const result = WindtunnelLockSchema.safeParse(lock);
137
+ expect(result.success).toBe(false);
138
+ expect(JSON.stringify(result)).toContain('sorted');
139
+ });
140
+ it('rejects absent cullRateThreshold (C5)', () => {
141
+ const lock = validLock();
142
+ delete lock['cullRateThreshold'];
143
+ const result = WindtunnelLockSchema.safeParse(lock);
144
+ expect(result.success).toBe(false);
145
+ });
146
+ it('rejects cullRateThreshold >= 1 (C5)', () => {
147
+ const lock = validLock({ cullRateThreshold: 1.0 });
148
+ const result = WindtunnelLockSchema.safeParse(lock);
149
+ expect(result.success).toBe(false);
150
+ });
151
+ it('rejects negative cullRateThreshold (C5)', () => {
152
+ const lock = validLock({ cullRateThreshold: -0.1 });
153
+ const result = WindtunnelLockSchema.safeParse(lock);
154
+ expect(result.success).toBe(false);
155
+ });
156
+ it('accepts cullRateThreshold = 0 (boundary)', () => {
157
+ const lock = validLock({ cullRateThreshold: 0 });
158
+ const result = WindtunnelLockSchema.safeParse(lock);
159
+ expect(result.success).toBe(true);
160
+ });
161
+ it('rejects activeRulesEvaluated.floor < 2', () => {
162
+ const lock = validLock();
163
+ lock.exposureDenominator.activeRulesEvaluated.floor = 1;
164
+ const result = WindtunnelLockSchema.safeParse(lock);
165
+ expect(result.success).toBe(false);
166
+ });
167
+ it('accepts activeRulesEvaluated.floor = 2 (boundary)', () => {
168
+ const lock = validLock();
169
+ lock.exposureDenominator.activeRulesEvaluated.floor = 2;
170
+ const result = WindtunnelLockSchema.safeParse(lock);
171
+ expect(result.success).toBe(true);
172
+ });
173
+ it('rejects unknown phase', () => {
174
+ const lock = validLock({ phase: 'unknown' });
175
+ const result = WindtunnelLockSchema.safeParse(lock);
176
+ expect(result.success).toBe(false);
177
+ });
178
+ it('rejects non-40-hex mergeCommit', () => {
179
+ const lock = validLock();
180
+ lock.corpus.resolvedPrs = [
181
+ { pr: 1, mergeCommit: 'short', baseSha: VALID_SHA, headSha: VALID_SHA_2 },
182
+ ];
183
+ const result = WindtunnelLockSchema.safeParse(lock);
184
+ expect(result.success).toBe(false);
185
+ });
186
+ });
187
+ // ─── firingLabelId ───────────────────────────────────
188
+ describe('firingLabelId (A2)', () => {
189
+ it('returns a 64-char hex SHA-256', () => {
190
+ const id = firingLabelId('rule-abc', 42, 'src/foo.ts', 'const secret = "abc"');
191
+ expect(id).toMatch(/^[0-9a-f]{64}$/);
192
+ });
193
+ it('is deterministic for the same inputs', () => {
194
+ const a = firingLabelId('rule-abc', 42, 'src/foo.ts', 'const secret = "abc"');
195
+ const b = firingLabelId('rule-abc', 42, 'src/foo.ts', 'const secret = "abc"');
196
+ expect(a).toBe(b);
197
+ });
198
+ it('differs when ruleId differs', () => {
199
+ const a = firingLabelId('rule-abc', 42, 'src/foo.ts', 'line');
200
+ const b = firingLabelId('rule-def', 42, 'src/foo.ts', 'line');
201
+ expect(a).not.toBe(b);
202
+ });
203
+ it('differs when pr differs', () => {
204
+ const a = firingLabelId('rule-abc', 1, 'src/foo.ts', 'line');
205
+ const b = firingLabelId('rule-abc', 2, 'src/foo.ts', 'line');
206
+ expect(a).not.toBe(b);
207
+ });
208
+ it('normalizes Windows backslash paths (A3)', () => {
209
+ const forward = firingLabelId('rule', 1, 'src/foo.ts', 'line');
210
+ const backward = firingLabelId('rule', 1, 'src\\foo.ts', 'line');
211
+ expect(forward).toBe(backward);
212
+ });
213
+ });
214
+ //# sourceMappingURL=windtunnel-lock.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windtunnel-lock.test.js","sourceRoot":"","sources":["../../src/spine/windtunnel-lock.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=windtunnel-parity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windtunnel-parity.test.d.ts","sourceRoot":"","sources":["../../src/spine/windtunnel-parity.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,192 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { enrichWithAstContext } from '../ast-gate.js';
6
+ import { applyAstRulesToAdditions, applyRulesToAdditions } from '../rule-engine.js';
7
+ import { cleanTmpDir, makeRuleEngineCtx } from '../test-utils.js';
8
+ /**
9
+ * Bidirectional firing-parity test for the wind-tunnel readStrategy seam
10
+ * (S1+C1, .totem/specs/2188.md Invariants).
11
+ *
12
+ * The wind-tunnel replays each PR's diff against the rule engine and must
13
+ * observe the SAME firings production `totem lint` would observe — otherwise
14
+ * it measures a different engine than Gate 2 arms. The seam that makes this
15
+ * exact is the post-image `readStrategy` injected into BOTH
16
+ * `enrichWithAstContext` (regex `astContext` classification) and
17
+ * `applyAstRulesToAdditions` (whole-file AST parsing). This file proves parity
18
+ * is bidirectional:
19
+ *
20
+ * - OVER-FIRE (C1): a regex match that lands inside a comment/string must NOT
21
+ * become a violation. Production suppresses it via `astContext`; the replay,
22
+ * fed the same post-image content through the same seam, must suppress it
23
+ * identically. Caught by comparing readStrategy-fed firings to disk-fed
24
+ * firings (the production baseline) — they must be equal.
25
+ * - UNDER-FIRE (S1): an AST/ast-grep rule needs the whole post-image file, not
26
+ * just the additions. The readStrategy supplying that post-image must fire
27
+ * identically to the same content read from disk.
28
+ */
29
+ // ─── Named constants ─────────────────────────────────
30
+ const SRC_FILE = 'src/app.ts';
31
+ const DEBUGGER_RULE = {
32
+ lessonHash: 'wt-parity-debugger',
33
+ lessonHeading: 'No debugger statements',
34
+ // Matches the literal token `debugger` — appears in both code and a comment
35
+ // in the fixture below, so astContext is what decides over-firing.
36
+ pattern: 'debugger',
37
+ message: 'debugger statement',
38
+ engine: 'regex',
39
+ compiledAt: '2026-06-17T00:00:00.000Z',
40
+ };
41
+ const CONSOLE_LOG_AST_RULE = {
42
+ lessonHash: 'wt-parity-console-log',
43
+ lessonHeading: 'No console.log',
44
+ pattern: '',
45
+ message: 'console.log call',
46
+ engine: 'ast-grep',
47
+ astGrepPattern: 'console.log($$$)',
48
+ compiledAt: '2026-06-17T00:00:00.000Z',
49
+ };
50
+ // Post-image fixture: the trigger token appears on a real code line (line 2)
51
+ // AND inside a comment (line 3). A faithful replay fires a *violation* only on
52
+ // the code line; the comment line is telemetry-only.
53
+ //
54
+ // The inline `// totem-ignore` markers below keep Totem's own corpus rules from
55
+ // flagging the intentional `debugger` / `console.log` fixture tokens in this
56
+ // test file — same pattern as ast-gate.test.ts and the pack-agent-security
57
+ // fixtures. Bare (no ticket-ref) per the established test-fixture convention.
58
+ const POST_IMAGE = [
59
+ 'function run() {', // 1
60
+ ' debugger;', // 2 — real code → violation // totem-ignore
61
+ ' // debugger; left in by mistake — but this is a comment // totem-ignore', // 3 — comment → suppressed
62
+ ' console.log("hi");', // 4
63
+ '}', // 5
64
+ ].join('\n');
65
+ const CODE_LINE = ' debugger;'; // totem-ignore
66
+ const COMMENT_LINE = ' // debugger; left in by mistake — but this is a comment // totem-ignore';
67
+ const CONSOLE_LINE = ' console.log("hi");';
68
+ // ─── Helpers ─────────────────────────────────────────
69
+ let tmpDir;
70
+ beforeEach(() => {
71
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-wt-parity-'));
72
+ fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true });
73
+ });
74
+ afterEach(() => {
75
+ cleanTmpDir(tmpDir);
76
+ });
77
+ function additionsForDebugger() {
78
+ // Both the code line and the comment line are "added" in this PR. The replay
79
+ // must classify each from the post-image and fire only on the code line.
80
+ return [
81
+ { file: SRC_FILE, line: CODE_LINE, lineNumber: 2, precedingLine: 'function run() {' },
82
+ { file: SRC_FILE, line: COMMENT_LINE, lineNumber: 3, precedingLine: CODE_LINE },
83
+ ];
84
+ }
85
+ /** Normalize a violation list into a comparable, order-independent shape. */
86
+ function fingerprint(violations) {
87
+ return violations
88
+ .map((v) => `${v.rule.lessonHash}|${v.file}|${v.lineNumber}|${v.line}`)
89
+ .sort()
90
+ .join('\n');
91
+ }
92
+ // ─── Assertion 1: over-fire suppression + disk/readStrategy parity (C1) ─
93
+ describe('bidirectional parity — over-fire suppression (C1)', () => {
94
+ it('a regex match inside a comment does NOT become a violation when classified from the post-image', async () => {
95
+ const additions = additionsForDebugger();
96
+ // Inject the post-image content through the readStrategy seam (the path the
97
+ // wind-tunnel uses): line 2 classifies as code, line 3 as comment.
98
+ await enrichWithAstContext(additions, {
99
+ cwd: tmpDir,
100
+ readStrategy: async () => POST_IMAGE,
101
+ });
102
+ expect(additions[0].astContext).toBe('code');
103
+ expect(additions[1].astContext).toBe('comment');
104
+ const ctx = makeRuleEngineCtx();
105
+ const violations = applyRulesToAdditions(ctx, [DEBUGGER_RULE], additions);
106
+ // Only the real code line fires; the comment match is telemetry-only.
107
+ expect(violations).toHaveLength(1);
108
+ expect(violations[0].lineNumber).toBe(2);
109
+ expect(violations[0].line).toBe(CODE_LINE);
110
+ });
111
+ it('readStrategy-injected content fires identically to the same content on disk (replay == production)', async () => {
112
+ // ── Production baseline: content on disk, NO readStrategy ──
113
+ fs.writeFileSync(path.join(tmpDir, SRC_FILE), POST_IMAGE, 'utf-8');
114
+ const diskAdditions = additionsForDebugger();
115
+ await enrichWithAstContext(diskAdditions, { cwd: tmpDir });
116
+ const diskCtx = makeRuleEngineCtx();
117
+ const diskViolations = applyRulesToAdditions(diskCtx, [DEBUGGER_RULE], diskAdditions);
118
+ // ── Replay: identical content fed via readStrategy (file may differ on disk) ──
119
+ const replayAdditions = additionsForDebugger();
120
+ await enrichWithAstContext(replayAdditions, {
121
+ cwd: tmpDir,
122
+ readStrategy: async () => POST_IMAGE,
123
+ });
124
+ const replayCtx = makeRuleEngineCtx();
125
+ const replayViolations = applyRulesToAdditions(replayCtx, [DEBUGGER_RULE], replayAdditions);
126
+ // Parity: the two firing sets are byte-identical.
127
+ expect(fingerprint(replayViolations)).toBe(fingerprint(diskViolations));
128
+ expect(replayViolations).toHaveLength(1);
129
+ expect(replayViolations[0].lineNumber).toBe(2);
130
+ });
131
+ it('proves the seam matters: stale on-disk content would mis-suppress without the post-image', async () => {
132
+ // On disk the file has the debugger ON A CODE LINE (no comment); but the PR
133
+ // post-image moved it into a comment. Without the readStrategy seam, the
134
+ // replay would classify against the wrong tree. Feeding the post-image makes
135
+ // line 3 a comment (suppressed) — exactly what production sees for this PR.
136
+ const staleDisk = [
137
+ 'function run() {',
138
+ ' debugger;', // totem-ignore
139
+ ' debugger;', // totem-ignore — on disk this is CODE
140
+ ' console.log("hi");',
141
+ '}',
142
+ ].join('\n');
143
+ fs.writeFileSync(path.join(tmpDir, SRC_FILE), staleDisk, 'utf-8');
144
+ const additions = additionsForDebugger();
145
+ await enrichWithAstContext(additions, {
146
+ cwd: tmpDir,
147
+ readStrategy: async () => POST_IMAGE, // the PR's actual post-image
148
+ });
149
+ // Line 3 is a comment in the post-image, so it is suppressed despite being
150
+ // code on disk — the seam decided correctly.
151
+ expect(additions[1].astContext).toBe('comment');
152
+ const ctx = makeRuleEngineCtx();
153
+ const violations = applyRulesToAdditions(ctx, [DEBUGGER_RULE], additions);
154
+ expect(violations).toHaveLength(1);
155
+ expect(violations[0].lineNumber).toBe(2);
156
+ });
157
+ });
158
+ // ─── Assertion 2: AST whole-file seam parity (S1) ────
159
+ describe('bidirectional parity — AST whole-file seam (S1)', () => {
160
+ it('applyAstRulesToAdditions fires identically via readStrategy and via disk (parity)', async () => {
161
+ const additions = [
162
+ { file: SRC_FILE, line: CONSOLE_LINE, lineNumber: 4, precedingLine: COMMENT_LINE },
163
+ ];
164
+ // ── Production baseline: content on disk, no readStrategy ──
165
+ fs.writeFileSync(path.join(tmpDir, SRC_FILE), POST_IMAGE, 'utf-8');
166
+ const diskCtx = makeRuleEngineCtx();
167
+ const diskViolations = await applyAstRulesToAdditions(diskCtx, [CONSOLE_LOG_AST_RULE], additions, tmpDir);
168
+ // ── Replay: identical post-image fed via readStrategy ──
169
+ const replayCtx = makeRuleEngineCtx();
170
+ const replayViolations = await applyAstRulesToAdditions(replayCtx, [CONSOLE_LOG_AST_RULE], additions, tmpDir, undefined, undefined, async () => POST_IMAGE);
171
+ // The AST engine parses the WHOLE post-image in both cases — firings match.
172
+ expect(fingerprint(replayViolations)).toBe(fingerprint(diskViolations));
173
+ expect(replayViolations).toHaveLength(1);
174
+ expect(replayViolations[0].lineNumber).toBe(4);
175
+ });
176
+ it('the post-image readStrategy lets the AST engine see whole-file context an additions-only feed lacks (under-fire guard)', async () => {
177
+ // The addition we evaluate is line 4 (console.log). The AST engine parses
178
+ // the full post-image (functions, braces) to resolve it as a call
179
+ // expression. A readStrategy returning ONLY the bare addition line would
180
+ // still parse, but the point of S1 is that the engine receives the full
181
+ // post-image — proven by feeding the whole file and getting the fire.
182
+ const additions = [
183
+ { file: SRC_FILE, line: CONSOLE_LINE, lineNumber: 4, precedingLine: COMMENT_LINE },
184
+ ];
185
+ const ctx = makeRuleEngineCtx();
186
+ const violations = await applyAstRulesToAdditions(ctx, [CONSOLE_LOG_AST_RULE], additions, tmpDir, undefined, undefined, async () => POST_IMAGE);
187
+ expect(violations).toHaveLength(1);
188
+ expect(violations[0].lineNumber).toBe(4);
189
+ expect(violations[0].rule.lessonHash).toBe(CONSOLE_LOG_AST_RULE.lessonHash);
190
+ });
191
+ });
192
+ //# sourceMappingURL=windtunnel-parity.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windtunnel-parity.test.js","sourceRoot":"","sources":["../../src/spine/windtunnel-parity.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ export type WindtunnelVerdictKind = 'PASS' | 'HONEST-NEGATIVE' | 'FAIL';
2
+ export type GroundTruthLabel = 'TP' | 'FP';
3
+ export interface CullLedgerEntry {
4
+ ruleId: string;
5
+ pr: number;
6
+ filePath: string;
7
+ matchedLine: string;
8
+ reason: 'negative-control-fired';
9
+ }
10
+ export interface WindtunnelVerdict {
11
+ verdict: WindtunnelVerdictKind;
12
+ /** Precision over surviving rules (after cull). */
13
+ precision: number;
14
+ mintedRuleCount: number;
15
+ culledCount: number;
16
+ survivingRuleCount: number;
17
+ /** 3-tuple: [activeRulesEvaluated, filesTouchedInWindow, positiveControlsExercised]. Never collapsed to a product. */
18
+ exposureTuple: [number, number, number];
19
+ cullLedger: CullLedgerEntry[];
20
+ /** True when all positive controls fired their target rule. */
21
+ nonVacuity: boolean;
22
+ /** Label ids of firings with no ground-truth label (operator adjudication required). */
23
+ needsAdjudication: string[];
24
+ }
25
+ export interface RuleFiring {
26
+ ruleId: string;
27
+ pr: number;
28
+ filePath: string;
29
+ matchedLine: string;
30
+ controlKind: 'corpus' | 'positive' | 'negative';
31
+ /** For positive controls: the rule that MUST fire to prove non-vacuousness. */
32
+ targetRuleId?: string;
33
+ /** Content-based label id (A2). Compute via firingLabelId from windtunnel-lock. */
34
+ labelId: string;
35
+ }
36
+ export interface ScorerInput {
37
+ firings: RuleFiring[];
38
+ /** Maps firingLabelId → TP/FP label. Unlabeled firings ⇒ needsAdjudication. */
39
+ groundTruth: Map<string, GroundTruthLabel>;
40
+ positiveControlTargets: Array<{
41
+ pr: number;
42
+ targetRuleId: string;
43
+ }>;
44
+ mintedRuleIds: string[];
45
+ cullRateThreshold: number;
46
+ exposureFloors: {
47
+ activeRulesEvaluated: number;
48
+ filesTouchedInWindow: number;
49
+ positiveControlsExercised: number;
50
+ };
51
+ actualExposure: {
52
+ activeRulesEvaluated: number;
53
+ filesTouchedInWindow: number;
54
+ positiveControlsExercised: number;
55
+ };
56
+ }
57
+ /**
58
+ * Score a wind-tunnel run. Pure function: no IO, no clock, no randomness.
59
+ * Implements ADR-110 §4/§5 done-criterion exactly per spec invariants.
60
+ *
61
+ * Verdict ordering (highest precedence first):
62
+ * 1. Exposure floor below minimum → HONEST-NEGATIVE (masquerade guard)
63
+ * 2. Cull rate exceeds threshold → HONEST-NEGATIVE (cull-laundering guard)
64
+ * 3. Positive control does not fire its target → FAIL (vacuous pass)
65
+ * 4. Any firing labeled FP → FAIL (precision < 1.0)
66
+ * 5. Any unlabeled firing → HONEST-NEGATIVE (needs adjudication, not PASS)
67
+ * 6. All labeled TP → PASS
68
+ */
69
+ export declare function scoreWindtunnel(input: ScorerInput): WindtunnelVerdict;
70
+ //# sourceMappingURL=windtunnel-scorer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windtunnel-scorer.d.ts","sourceRoot":"","sources":["../../src/spine/windtunnel-scorer.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,iBAAiB,GAAG,MAAM,CAAC;AACxE,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,wBAAwB,CAAC;CAClC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sHAAsH;IACtH,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB,wFAAwF;IACxF,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;IAChD,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,+EAA+E;IAC/E,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC3C,sBAAsB,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE;QACd,oBAAoB,EAAE,MAAM,CAAC;QAC7B,oBAAoB,EAAE,MAAM,CAAC;QAC7B,yBAAyB,EAAE,MAAM,CAAC;KACnC,CAAC;IACF,cAAc,EAAE;QACd,oBAAoB,EAAE,MAAM,CAAC;QAC7B,oBAAoB,EAAE,MAAM,CAAC;QAC7B,yBAAyB,EAAE,MAAM,CAAC;KACnC,CAAC;CACH;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,iBAAiB,CAuLrE"}