@mmnto/totem 1.72.0 → 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.
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/spine/corpus-dispositions.d.ts +274 -0
- package/dist/spine/corpus-dispositions.d.ts.map +1 -0
- package/dist/spine/corpus-dispositions.js +71 -0
- package/dist/spine/corpus-dispositions.js.map +1 -0
- package/dist/spine/corpus-dispositions.test.d.ts +2 -0
- package/dist/spine/corpus-dispositions.test.d.ts.map +1 -0
- package/dist/spine/corpus-dispositions.test.js +55 -0
- package/dist/spine/corpus-dispositions.test.js.map +1 -0
- package/dist/spine/derive-labels.d.ts +99 -0
- package/dist/spine/derive-labels.d.ts.map +1 -0
- package/dist/spine/derive-labels.js +160 -0
- package/dist/spine/derive-labels.js.map +1 -0
- package/dist/spine/derive-labels.test.d.ts +2 -0
- package/dist/spine/derive-labels.test.d.ts.map +1 -0
- package/dist/spine/derive-labels.test.js +179 -0
- package/dist/spine/derive-labels.test.js.map +1 -0
- package/dist/spine/disposition-taxonomy.d.ts +40 -0
- package/dist/spine/disposition-taxonomy.d.ts.map +1 -0
- package/dist/spine/disposition-taxonomy.js +216 -0
- package/dist/spine/disposition-taxonomy.js.map +1 -0
- package/dist/spine/disposition-taxonomy.test.d.ts +2 -0
- package/dist/spine/disposition-taxonomy.test.d.ts.map +1 -0
- package/dist/spine/disposition-taxonomy.test.js +145 -0
- package/dist/spine/disposition-taxonomy.test.js.map +1 -0
- package/dist/spine/windtunnel-firing.d.ts +12 -0
- package/dist/spine/windtunnel-firing.d.ts.map +1 -1
- package/dist/spine/windtunnel-firing.js +6 -1
- package/dist/spine/windtunnel-firing.js.map +1 -1
- package/dist/spine/windtunnel-lock.d.ts +18 -0
- package/dist/spine/windtunnel-lock.d.ts.map +1 -1
- package/dist/spine/windtunnel-lock.js +33 -0
- package/dist/spine/windtunnel-lock.js.map +1 -1
- 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 @@
|
|
|
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"}
|