@mmnto/totem 1.72.1 → 1.73.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.
Files changed (37) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/spine/corpus-dispositions.d.ts +274 -0
  6. package/dist/spine/corpus-dispositions.d.ts.map +1 -0
  7. package/dist/spine/corpus-dispositions.js +71 -0
  8. package/dist/spine/corpus-dispositions.js.map +1 -0
  9. package/dist/spine/corpus-dispositions.test.d.ts +2 -0
  10. package/dist/spine/corpus-dispositions.test.d.ts.map +1 -0
  11. package/dist/spine/corpus-dispositions.test.js +55 -0
  12. package/dist/spine/corpus-dispositions.test.js.map +1 -0
  13. package/dist/spine/derive-labels.d.ts +99 -0
  14. package/dist/spine/derive-labels.d.ts.map +1 -0
  15. package/dist/spine/derive-labels.js +160 -0
  16. package/dist/spine/derive-labels.js.map +1 -0
  17. package/dist/spine/derive-labels.test.d.ts +2 -0
  18. package/dist/spine/derive-labels.test.d.ts.map +1 -0
  19. package/dist/spine/derive-labels.test.js +179 -0
  20. package/dist/spine/derive-labels.test.js.map +1 -0
  21. package/dist/spine/disposition-taxonomy.d.ts +40 -0
  22. package/dist/spine/disposition-taxonomy.d.ts.map +1 -0
  23. package/dist/spine/disposition-taxonomy.js +216 -0
  24. package/dist/spine/disposition-taxonomy.js.map +1 -0
  25. package/dist/spine/disposition-taxonomy.test.d.ts +2 -0
  26. package/dist/spine/disposition-taxonomy.test.d.ts.map +1 -0
  27. package/dist/spine/disposition-taxonomy.test.js +145 -0
  28. package/dist/spine/disposition-taxonomy.test.js.map +1 -0
  29. package/dist/spine/windtunnel-firing.d.ts +12 -0
  30. package/dist/spine/windtunnel-firing.d.ts.map +1 -1
  31. package/dist/spine/windtunnel-firing.js +6 -1
  32. package/dist/spine/windtunnel-firing.js.map +1 -1
  33. package/dist/spine/windtunnel-lock.d.ts +18 -0
  34. package/dist/spine/windtunnel-lock.d.ts.map +1 -1
  35. package/dist/spine/windtunnel-lock.js +33 -0
  36. package/dist/spine/windtunnel-lock.js.map +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ import type { CorpusDisposition } from './corpus-dispositions.js';
2
+ import type { GroundTruthLabel, RuleFiring } from './windtunnel-scorer.js';
3
+ /**
4
+ * strategy#709 5d-iii — the ground-truth label deriver (pure core).
5
+ *
6
+ * Produces the cert-run answer key (`firingLabelId → TP|FP`) by joining the
7
+ * enumerated `RuleFiring`s against the frozen held-out `CorpusDisposition`s, then
8
+ * classifying each bound thread through the closed 5d-i taxonomy. The CLI
9
+ * `derive-labels` command supplies firings enumerated byte-identically to the
10
+ * certifying run (shared firing-setup) and the integrity-gated dispositions;
11
+ * this function is the deterministic, zero-LLM transform between them.
12
+ *
13
+ * Span-join invariant (codex hard fold): a corpus firing binds to a disposition
14
+ * thread on the SAME pr ONLY when (a) the thread's path matches the firing's
15
+ * file and (b) the firing's normalized `matchedLine` equals an ADDED (`+`)
16
+ * post-image row of the thread's `diffHunk`. Context rows, removed rows, hunk
17
+ * headers, and file headers are INELIGIBLE — a disposition labels a firing only
18
+ * by content the PR actually added, never by a line it merely sits near. 0 or
19
+ * >1 bound threads ⟹ omit (the scorer routes the un-keyed firing to
20
+ * `needsAdjudication`).
21
+ */
22
+ /** Why a non-negative firing received no label (diagnostic only; never in the answer key). */
23
+ export type UnlabeledReason =
24
+ /** corpus firing: no disposition thread bound by path + added-line content. */
25
+ 'no-matching-disposition'
26
+ /** corpus firing: >1 disposition thread bound — ambiguous, never labels. */
27
+ | 'ambiguous-multiple-dispositions'
28
+ /** corpus firing: a thread bound, but its taxonomy class is non-label-bearing (UNLABELED). */
29
+ | 'unlabeled-class'
30
+ /** positive-control firing that is NOT the declared (pr, targetRuleId) target. */
31
+ | 'incidental-positive';
32
+ /**
33
+ * Deriver-side data-quality diagnostics (gemini: deriver reports DATA QUALITY,
34
+ * the scorer reports MODEL PERFORMANCE). Deterministic + zero-LLM. Surfaced so a
35
+ * sparse first verdict reads as "here's the coverage + why", not a silent fail.
36
+ */
37
+ export interface DeriveLabelDiagnostics {
38
+ /** Total firings enumerated (all control kinds). */
39
+ totalFirings: number;
40
+ /** Negative-control firings (no label — the scorer culls the rule). */
41
+ negativeFirings: number;
42
+ /** Corpus firings (the real precision surface). */
43
+ corpusFirings: number;
44
+ /** Positive-control firings. */
45
+ positiveFirings: number;
46
+ /** Corpus firings that received a TP/FP label. */
47
+ boundCorpusFirings: number;
48
+ /** boundCorpusFirings / corpusFirings — 0 when there are no corpus firings. */
49
+ dispositionDensity: number;
50
+ /** Non-negative firings with no label (the scorer's future `needsAdjudication` set). */
51
+ unlabeledFirings: number;
52
+ /** unlabeledFirings / (corpusFirings + positiveFirings) — 0 when that denominator is 0. */
53
+ unlabeledRate: number;
54
+ /** Label counts in the emitted answer key. */
55
+ labelCounts: {
56
+ TP: number;
57
+ FP: number;
58
+ };
59
+ /** Per-rule labeled-firing counts (ruleId → {TP, FP}); only rules that labeled appear. */
60
+ perRuleLabeled: Record<string, {
61
+ TP: number;
62
+ FP: number;
63
+ }>;
64
+ /** Breakdown of why firings went unlabeled. */
65
+ unlabeledByReason: Record<UnlabeledReason, number>;
66
+ }
67
+ /**
68
+ * Provenance for one emitted label — links the answer-key entry back to its
69
+ * disposition source for audit. NOT part of the hashed answer key (whose values
70
+ * stay a bare `TP|FP`); surfaced in the deriver's report only.
71
+ */
72
+ export interface LabelEvidence {
73
+ labelId: string;
74
+ label: GroundTruthLabel;
75
+ pr: number;
76
+ ruleId: string;
77
+ filePath: string;
78
+ /** Source disposition thread id (corpus labels only; positive-target labels omit it). */
79
+ threadId?: string;
80
+ /** Root review-comment databaseId of the bound thread (corpus labels only). */
81
+ commentId?: number;
82
+ source: 'corpus-disposition' | 'positive-control-target';
83
+ }
84
+ export interface DeriveLabelsResult {
85
+ /**
86
+ * The answer key: `firingLabelId → TP|FP`. The ONLY thing written to
87
+ * `ground-truth-labels.json` (and the bytes `groundTruthSha` covers).
88
+ */
89
+ labels: Record<string, GroundTruthLabel>;
90
+ diagnostics: DeriveLabelDiagnostics;
91
+ /** Per-label provenance (audit; surfaced in the report, never in the hashed key). */
92
+ evidence: LabelEvidence[];
93
+ }
94
+ /**
95
+ * Derive the cert-run ground-truth answer key from enumerated firings + frozen
96
+ * held-out dispositions. Pure + deterministic — no I/O, no clock, no LLM.
97
+ */
98
+ export declare function deriveLabelsFromDispositions(firings: readonly RuleFiring[], dispositions: readonly CorpusDisposition[]): DeriveLabelsResult;
99
+ //# sourceMappingURL=derive-labels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-labels.d.ts","sourceRoot":"","sources":["../../src/spine/derive-labels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAA2B,MAAM,0BAA0B,CAAC;AAG3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE3E;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8FAA8F;AAC9F,MAAM,MAAM,eAAe;AACzB,+EAA+E;AAC7E,yBAAyB;AAC3B,4EAA4E;GAC1E,iCAAiC;AACnC,8FAA8F;GAC5F,iBAAiB;AACnB,kFAAkF;GAChF,qBAAqB,CAAC;AAE1B;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,eAAe,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,gBAAgB,EAAE,MAAM,CAAC;IACzB,2FAA2F;IAC3F,aAAa,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,WAAW,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,0FAA0F;IAC1F,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,+CAA+C;IAC/C,iBAAiB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,oBAAoB,GAAG,yBAAyB,CAAC;CAC1D;AAED,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACzC,WAAW,EAAE,sBAAsB,CAAC;IACpC,qFAAqF;IACrF,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAwBD;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,YAAY,EAAE,SAAS,iBAAiB,EAAE,GACzC,kBAAkB,CA8IpB"}
@@ -0,0 +1,160 @@
1
+ import { classifyDisposition, dispositionToLabel } from './disposition-taxonomy.js';
2
+ import { normalizeMatchedLine } from './windtunnel-firing.js';
3
+ /**
4
+ * Extract the normalized ADDED (`+`) post-image rows of a unified-diff hunk.
5
+ * Only `+`-prefixed content rows are eligible: context rows (leading space),
6
+ * removed rows (`-`), the hunk header (`@@`), file headers (`+++`/`---`), and
7
+ * the no-newline marker (`\`) are all excluded. Each eligible row is stripped of
8
+ * its leading `+` and normalized with `normalizeMatchedLine` (the SAME rule the
9
+ * firing's `matchedLine` is built with) so the content bind keys on identical
10
+ * bytes.
11
+ */
12
+ function addedHunkLines(diffHunk) {
13
+ const added = new Set();
14
+ for (const row of diffHunk.split('\n')) {
15
+ if (row.charCodeAt(0) !== 0x2b /* '+' */)
16
+ continue; // added rows only
17
+ // The unified-diff file header is "+++ <path>" — three '+' THEN A SPACE. A real
18
+ // added line such as `++i` appears as the diff row "+++i" (no space) and MUST
19
+ // bind, so match the header by its trailing space, not a bare `+++` prefix (CR).
20
+ if (/^\+\+\+ /.test(row))
21
+ continue; // file header, not added content
22
+ added.add(normalizeMatchedLine(row.slice(1)));
23
+ }
24
+ return added;
25
+ }
26
+ /**
27
+ * Derive the cert-run ground-truth answer key from enumerated firings + frozen
28
+ * held-out dispositions. Pure + deterministic — no I/O, no clock, no LLM.
29
+ */
30
+ export function deriveLabelsFromDispositions(firings, dispositions) {
31
+ // Index dispositions by PR. Fail loud on a duplicate-PR entry (greptile): a producer
32
+ // bug in `fetch-dispositions`, or a partially-valid manual edit that still passes the
33
+ // re-stamped `corpusDispositionsSha` gate, could ship two entries for one PR — and a
34
+ // silent last-wins Map would drop the first entry's threads, so firings that should
35
+ // bind to them get a wrong `no-matching-disposition` with no diagnostic. A structural
36
+ // input-contract violation throws (Tenet 4); only DATA ambiguity fails soft to UNLABELED.
37
+ const dispByPr = new Map();
38
+ for (const d of dispositions) {
39
+ if (dispByPr.has(d.pr)) {
40
+ throw new Error(`deriveLabelsFromDispositions: duplicate disposition entry for PR ${d.pr} — the ` +
41
+ `dispositions array must carry exactly one entry per PR (a producer or edit bug).`);
42
+ }
43
+ dispByPr.set(d.pr, d);
44
+ }
45
+ // Memoize the parsed added-line set per disposition THREAD: the span-join scans every
46
+ // thread for each corpus firing, so without this `addedHunkLines` would re-split +
47
+ // re-normalize the same `diffHunk` once per firing (GCA). The WeakMap keys on the
48
+ // thread object, so it's GC-friendly and stays internal to this call (still pure).
49
+ const addedLinesCache = new WeakMap();
50
+ const getAddedLines = (thread) => {
51
+ let cached = addedLinesCache.get(thread);
52
+ if (!cached) {
53
+ cached = addedHunkLines(thread.diffHunk);
54
+ addedLinesCache.set(thread, cached);
55
+ }
56
+ return cached;
57
+ };
58
+ const labels = {};
59
+ const evidence = [];
60
+ const perRuleLabeled = {};
61
+ const labelCounts = { TP: 0, FP: 0 };
62
+ const unlabeledByReason = {
63
+ 'no-matching-disposition': 0,
64
+ 'ambiguous-multiple-dispositions': 0,
65
+ 'unlabeled-class': 0,
66
+ 'incidental-positive': 0,
67
+ };
68
+ let negativeFirings = 0;
69
+ let corpusFirings = 0;
70
+ let positiveFirings = 0;
71
+ let boundCorpusFirings = 0;
72
+ const emit = (firing, label, ev) => {
73
+ labels[firing.labelId] = label;
74
+ labelCounts[label] += 1;
75
+ (perRuleLabeled[firing.ruleId] ??= { TP: 0, FP: 0 })[label] += 1;
76
+ evidence.push(ev);
77
+ };
78
+ for (const firing of firings) {
79
+ switch (firing.controlKind) {
80
+ case 'negative':
81
+ // Negative controls never label — the scorer culls the firing rule (S2/C5).
82
+ negativeFirings += 1;
83
+ break;
84
+ case 'positive': {
85
+ positiveFirings += 1;
86
+ // TP structurally ONLY for the declared (pr, targetRuleId) target firing
87
+ // (codex BLOCKING-1). An incidental non-target firing on a positive
88
+ // fixture is NOT laundered TP — omit it and report; the scorer routes the
89
+ // un-keyed firing to needsAdjudication.
90
+ if (firing.targetRuleId !== undefined && firing.ruleId === firing.targetRuleId) {
91
+ emit(firing, 'TP', {
92
+ labelId: firing.labelId,
93
+ label: 'TP',
94
+ pr: firing.pr,
95
+ ruleId: firing.ruleId,
96
+ filePath: firing.filePath,
97
+ source: 'positive-control-target',
98
+ });
99
+ }
100
+ else {
101
+ unlabeledByReason['incidental-positive'] += 1;
102
+ }
103
+ break;
104
+ }
105
+ case 'corpus': {
106
+ corpusFirings += 1;
107
+ const disp = dispByPr.get(firing.pr);
108
+ const bound = disp
109
+ ? disp.threads.filter((t) => t.path.replace(/\\/g, '/') === firing.filePath &&
110
+ getAddedLines(t).has(firing.matchedLine))
111
+ : [];
112
+ if (bound.length !== 1) {
113
+ unlabeledByReason[bound.length === 0 ? 'no-matching-disposition' : 'ambiguous-multiple-dispositions'] += 1;
114
+ break;
115
+ }
116
+ const thread = bound[0];
117
+ const label = dispositionToLabel(classifyDisposition(thread.comments));
118
+ if (label === null) {
119
+ unlabeledByReason['unlabeled-class'] += 1;
120
+ break;
121
+ }
122
+ boundCorpusFirings += 1;
123
+ emit(firing, label, {
124
+ labelId: firing.labelId,
125
+ label,
126
+ pr: firing.pr,
127
+ ruleId: firing.ruleId,
128
+ filePath: firing.filePath,
129
+ threadId: thread.threadId,
130
+ commentId: thread.comments[0]?.commentId,
131
+ source: 'corpus-disposition',
132
+ });
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ const unlabeledFirings = unlabeledByReason['no-matching-disposition'] +
138
+ unlabeledByReason['ambiguous-multiple-dispositions'] +
139
+ unlabeledByReason['unlabeled-class'] +
140
+ unlabeledByReason['incidental-positive'];
141
+ const scoredDenominator = corpusFirings + positiveFirings;
142
+ return {
143
+ labels,
144
+ evidence,
145
+ diagnostics: {
146
+ totalFirings: firings.length,
147
+ negativeFirings,
148
+ corpusFirings,
149
+ positiveFirings,
150
+ boundCorpusFirings,
151
+ dispositionDensity: corpusFirings === 0 ? 0 : boundCorpusFirings / corpusFirings,
152
+ unlabeledFirings,
153
+ unlabeledRate: scoredDenominator === 0 ? 0 : unlabeledFirings / scoredDenominator,
154
+ labelCounts,
155
+ perRuleLabeled,
156
+ unlabeledByReason,
157
+ },
158
+ };
159
+ }
160
+ //# sourceMappingURL=derive-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-labels.js","sourceRoot":"","sources":["../../src/spine/derive-labels.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=derive-labels.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-labels.test.d.ts","sourceRoot":"","sources":["../../src/spine/derive-labels.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,179 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { deriveLabelsFromDispositions } from './derive-labels.js';
3
+ // ─── builders ────────────────────────────────────────
4
+ function firing(over) {
5
+ return {
6
+ ruleId: 'rule-a',
7
+ pr: 1,
8
+ filePath: 'src/a.ts',
9
+ matchedLine: 'forbiddenCall()',
10
+ controlKind: 'corpus',
11
+ ...over,
12
+ };
13
+ }
14
+ function thread(over) {
15
+ return {
16
+ path: 'src/a.ts',
17
+ diffHunk: '@@ -1,2 +1,3 @@\n context();\n+forbiddenCall()',
18
+ isResolved: false,
19
+ isOutdated: false,
20
+ comments: [{ author: 'Jane', body: 'fixed' }],
21
+ ...over,
22
+ };
23
+ }
24
+ function disposition(pr, threads) {
25
+ return { pr, mergeCommitSha: String(pr).padStart(40, '0'), threads };
26
+ }
27
+ // ─── span-join: added-line-only binding (codex hard fold) ────────────────────
28
+ describe('deriveLabelsFromDispositions — span-join (added-line-only)', () => {
29
+ it('binds a corpus firing to a disposition via an ADDED (+) hunk row', () => {
30
+ const { labels, diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'L1' })], [disposition(1, [thread({})])]);
31
+ expect(labels['L1']).toBe('TP'); // 'fixed' → accepted-fix → TP
32
+ expect(diagnostics.boundCorpusFirings).toBe(1);
33
+ });
34
+ it('does NOT bind to a CONTEXT row (leading space) — context-only ⟹ no label', () => {
35
+ const { labels, diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'L1' })],
36
+ // same text present ONLY as a context row — never as an added row.
37
+ [disposition(1, [thread({ diffHunk: '@@ -1,2 +1,2 @@\n context();\n forbiddenCall()' })])]);
38
+ expect(labels['L1']).toBeUndefined();
39
+ expect(diagnostics.unlabeledByReason['no-matching-disposition']).toBe(1);
40
+ });
41
+ it('binds via the ADDED row even when the same text also appears as a context row', () => {
42
+ const { labels } = deriveLabelsFromDispositions([firing({ labelId: 'L1' })], [
43
+ disposition(1, [
44
+ thread({ diffHunk: '@@ -1,3 +1,4 @@\n forbiddenCall()\n-old()\n+forbiddenCall()' }),
45
+ ]),
46
+ ]);
47
+ expect(labels['L1']).toBe('TP');
48
+ });
49
+ it('ignores a +++ file header that coincidentally matches the line', () => {
50
+ const { labels, diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'L1', matchedLine: '+ b/forbiddenCall()' })], [disposition(1, [thread({ diffHunk: '+++ b/forbiddenCall()\n context();' })])]);
51
+ expect(labels['L1']).toBeUndefined();
52
+ expect(diagnostics.unlabeledByReason['no-matching-disposition']).toBe(1);
53
+ });
54
+ it('binds a real added line that itself starts with `++` (diff row "+++i") — not a file header', () => {
55
+ const { labels } = deriveLabelsFromDispositions([firing({ labelId: 'L1', matchedLine: '++i;' })],
56
+ // source line `++i;` appears in a unified diff as the row "+++i;" (add-marker + content);
57
+ // it must bind, unlike the `+++ <path>` file header (CR #10 false-negative guard).
58
+ [disposition(1, [thread({ diffHunk: '@@ -1,1 +1,2 @@\n+++i;' })])]);
59
+ expect(labels['L1']).toBe('TP');
60
+ });
61
+ it('does NOT bind across files (path mismatch) even on identical added content', () => {
62
+ const { labels } = deriveLabelsFromDispositions([firing({ labelId: 'L1', filePath: 'src/a.ts' })], [disposition(1, [thread({ path: 'src/other.ts' })])]);
63
+ expect(labels['L1']).toBeUndefined();
64
+ });
65
+ it('matches trailing-whitespace-drifted content (mirrors normalizeMatchedLine)', () => {
66
+ const { labels } = deriveLabelsFromDispositions([firing({ labelId: 'L1', matchedLine: 'forbiddenCall()' })],
67
+ // the added row carries trailing whitespace; the bind must still hold.
68
+ [disposition(1, [thread({ diffHunk: '@@ -1,1 +1,2 @@\n+forbiddenCall() ' })])]);
69
+ expect(labels['L1']).toBe('TP');
70
+ });
71
+ });
72
+ // ─── ambiguity never labels (codex: 0 or >1 ⟹ omit) ──────────────────────────
73
+ describe('deriveLabelsFromDispositions — ambiguity', () => {
74
+ it('omits when ZERO dispositions bind', () => {
75
+ const { labels, diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'L1', pr: 99 })], [disposition(1, [thread({})])]);
76
+ expect(labels['L1']).toBeUndefined();
77
+ expect(diagnostics.unlabeledByReason['no-matching-disposition']).toBe(1);
78
+ });
79
+ it('omits when MORE THAN ONE disposition binds (ambiguous)', () => {
80
+ const { labels, diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'L1' })], [
81
+ disposition(1, [
82
+ thread({}),
83
+ thread({ comments: [{ author: 'Jo', body: 'false positive' }] }),
84
+ ]),
85
+ ]);
86
+ expect(labels['L1']).toBeUndefined();
87
+ expect(diagnostics.unlabeledByReason['ambiguous-multiple-dispositions']).toBe(1);
88
+ });
89
+ it('throws loud on a duplicate-PR dispositions entry (no silent last-wins)', () => {
90
+ expect(() => deriveLabelsFromDispositions([firing({ labelId: 'L1' })], [disposition(1, [thread({})]), disposition(1, [thread({})])])).toThrow(/duplicate disposition entry for PR 1/);
91
+ });
92
+ });
93
+ // ─── taxonomy projection (5d-i) ──────────────────────────────────────────────
94
+ describe('deriveLabelsFromDispositions — taxonomy projection', () => {
95
+ it('accepted-fix ⟹ TP, declined-as-false-positive ⟹ FP, soft-decline ⟹ UNLABELED', () => {
96
+ const { labels, diagnostics } = deriveLabelsFromDispositions([
97
+ firing({ labelId: 'TPf', pr: 1, filePath: 'src/a.ts' }),
98
+ firing({ labelId: 'FPf', pr: 2, filePath: 'src/b.ts' }),
99
+ firing({ labelId: 'UNf', pr: 3, filePath: 'src/c.ts' }),
100
+ ], [
101
+ disposition(1, [thread({ comments: [{ author: 'Jane', body: 'fixed' }] })]),
102
+ disposition(2, [
103
+ thread({
104
+ path: 'src/b.ts',
105
+ comments: [{ author: 'Jane', body: 'this is a false positive' }],
106
+ }),
107
+ ]),
108
+ disposition(3, [
109
+ thread({
110
+ path: 'src/c.ts',
111
+ comments: [{ author: 'Jane', body: 'out of scope for this PR' }],
112
+ }),
113
+ ]),
114
+ ]);
115
+ expect(labels['TPf']).toBe('TP');
116
+ expect(labels['FPf']).toBe('FP');
117
+ expect(labels['UNf']).toBeUndefined(); // soft decline routes to UNLABELED
118
+ expect(diagnostics.unlabeledByReason['unlabeled-class']).toBe(1);
119
+ });
120
+ });
121
+ // ─── control kinds ───────────────────────────────────────────────────────────
122
+ describe('deriveLabelsFromDispositions — control kinds', () => {
123
+ it('negative-control firings never label (the scorer culls)', () => {
124
+ const { labels, diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'N1', controlKind: 'negative' })], []);
125
+ expect(labels['N1']).toBeUndefined();
126
+ expect(diagnostics.negativeFirings).toBe(1);
127
+ });
128
+ it('positive-control: TP ONLY for the declared (pr,targetRuleId) target; incidental omitted', () => {
129
+ const { labels, diagnostics } = deriveLabelsFromDispositions([
130
+ // declared target: ruleId === targetRuleId ⟹ structural TP
131
+ firing({ labelId: 'PT', controlKind: 'positive', pr: 5, ruleId: 'R1', targetRuleId: 'R1' }),
132
+ // incidental: a different rule fired on the positive fixture ⟹ omit + report
133
+ firing({ labelId: 'PI', controlKind: 'positive', pr: 5, ruleId: 'R2', targetRuleId: 'R1' }),
134
+ ], []);
135
+ expect(labels['PT']).toBe('TP');
136
+ expect(labels['PI']).toBeUndefined();
137
+ expect(diagnostics.unlabeledByReason['incidental-positive']).toBe(1);
138
+ expect(diagnostics.positiveFirings).toBe(2);
139
+ });
140
+ });
141
+ // ─── diagnostics + evidence ──────────────────────────────────────────────────
142
+ describe('deriveLabelsFromDispositions — diagnostics + evidence', () => {
143
+ it('reports density, per-rule counts, evidence-refs, and is deterministic', () => {
144
+ const firings = [
145
+ firing({ labelId: 'a', ruleId: 'rule-x', pr: 1, filePath: 'src/a.ts' }),
146
+ firing({ labelId: 'b', ruleId: 'rule-x', pr: 2, filePath: 'src/b.ts' }), // unbound
147
+ ];
148
+ const dispositions = [
149
+ disposition(1, [
150
+ thread({ threadId: 'T_abc', comments: [{ commentId: 42, author: 'Jane', body: 'fixed' }] }),
151
+ ]),
152
+ ];
153
+ const first = deriveLabelsFromDispositions(firings, dispositions);
154
+ const second = deriveLabelsFromDispositions(firings, dispositions);
155
+ expect(first).toEqual(second); // deterministic
156
+ expect(first.diagnostics.corpusFirings).toBe(2);
157
+ expect(first.diagnostics.boundCorpusFirings).toBe(1);
158
+ expect(first.diagnostics.dispositionDensity).toBe(0.5);
159
+ expect(first.diagnostics.labelCounts).toEqual({ TP: 1, FP: 0 });
160
+ expect(first.diagnostics.perRuleLabeled['rule-x']).toEqual({ TP: 1, FP: 0 });
161
+ // evidence-ref links the emitted label back to its disposition source (audit).
162
+ expect(first.evidence).toHaveLength(1);
163
+ expect(first.evidence[0]).toMatchObject({
164
+ labelId: 'a',
165
+ label: 'TP',
166
+ threadId: 'T_abc',
167
+ commentId: 42,
168
+ source: 'corpus-disposition',
169
+ });
170
+ // the answer key carries ONLY TP|FP values (no evidence leaks into the hashed key).
171
+ expect(Object.values(first.labels)).toEqual(['TP']);
172
+ });
173
+ it('density is 0 (not NaN) when there are no corpus firings', () => {
174
+ const { diagnostics } = deriveLabelsFromDispositions([firing({ labelId: 'N', controlKind: 'negative' })], []);
175
+ expect(diagnostics.dispositionDensity).toBe(0);
176
+ expect(diagnostics.unlabeledRate).toBe(0);
177
+ });
178
+ });
179
+ //# sourceMappingURL=derive-labels.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-labels.test.js","sourceRoot":"","sources":["../../src/spine/derive-labels.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { GroundTruthLabel } from './windtunnel-scorer.js';
2
+ /**
3
+ * The closed disposition taxonomy (strategy#709 RULED). `accepted-fix` and
4
+ * `declined-as-false-positive` are the only LABEL-BEARING classes; the rest are
5
+ * deliberately UNLABELED outcomes — a valid-but-not-actioned decline is NOT
6
+ * evidence the code is clean, so it never labels a firing.
7
+ */
8
+ export type DispositionClass = 'accepted-fix' | 'declined-as-false-positive' | 'scope' | 'defer' | 'superseded' | 'style' | 'ambiguous';
9
+ /** A single review-thread comment (the provider-neutral subset the taxonomy reads). */
10
+ export interface DispositionComment {
11
+ author: string;
12
+ body: string;
13
+ }
14
+ /**
15
+ * Classify a held-out review thread's disposition under the closed taxonomy.
16
+ *
17
+ * Reads the HUMAN (non-bot) comments only — the human is the one who disposes; a
18
+ * bot's own follow-up is not a disposition (and `isResolved` alone is never TP,
19
+ * codex WARNING-5). Conservative precedence:
20
+ * 1. No human disposition comment → ambiguous (UNLABELED)
21
+ * 2. accepted-fix AND false-positive present → ambiguous (conflicting signals)
22
+ * 3. accepted-fix, no decline of any kind → accepted-fix (TP)
23
+ * 4. false-positive, no fix and no soft decline → declined-as-false-positive (FP)
24
+ * 5. otherwise a soft decline present → scope | defer | superseded | style (UNLABELED)
25
+ * 6. no recognizable signal → ambiguous (UNLABELED)
26
+ *
27
+ * Steps 3/4 require a CLEAN signal: a fix that is also scoped/deferred, or an FP
28
+ * rebuttal that is also a scope/defer decline, collapses to ambiguous — the
29
+ * answer key must not credit a contradictory disposition. This defeats codex's
30
+ * falsifying case ("declined, too broad / tracked for later" never becomes FP).
31
+ */
32
+ export declare function classifyDisposition(comments: ReadonlyArray<DispositionComment>): DispositionClass;
33
+ /**
34
+ * Project a taxonomy class onto a cert-run ground-truth label. The two
35
+ * label-bearing classes map to TP/FP; every UNLABELED class returns `null` — the
36
+ * deriver OMITS the firing's labelId from `ground-truth-labels.json`, and the
37
+ * scorer routes the un-keyed firing to `needsAdjudication` → HONEST-NEGATIVE.
38
+ */
39
+ export declare function dispositionToLabel(cls: DispositionClass): GroundTruthLabel | null;
40
+ //# sourceMappingURL=disposition-taxonomy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disposition-taxonomy.d.ts","sourceRoot":"","sources":["../../src/spine/disposition-taxonomy.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI/D;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,4BAA4B,GAC5B,OAAO,GACP,OAAO,GACP,YAAY,GACZ,OAAO,GACP,WAAW,CAAC;AAEhB,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAgHD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,aAAa,CAAC,kBAAkB,CAAC,GAAG,gBAAgB,CA6CjG;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,IAAI,CAajF"}