@mmnto/totem 1.74.0 → 1.75.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 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/spine/classify.d.ts.map +1 -1
- package/dist/spine/classify.js +3 -0
- package/dist/spine/classify.js.map +1 -1
- package/dist/spine/classify.test.js +14 -2
- package/dist/spine/classify.test.js.map +1 -1
- package/dist/spine/compile.test.js +5 -1
- package/dist/spine/compile.test.js.map +1 -1
- package/dist/spine/extract.d.ts +53 -9
- package/dist/spine/extract.d.ts.map +1 -1
- package/dist/spine/extract.js +120 -45
- package/dist/spine/extract.js.map +1 -1
- package/dist/spine/extract.test.js +112 -55
- package/dist/spine/extract.test.js.map +1 -1
- package/dist/spine/ledgers.d.ts +96 -19
- package/dist/spine/ledgers.d.ts.map +1 -1
- package/dist/spine/ledgers.js +46 -13
- package/dist/spine/ledgers.js.map +1 -1
- package/dist/spine/review-normalize.d.ts +23 -0
- package/dist/spine/review-normalize.d.ts.map +1 -0
- package/dist/spine/review-normalize.js +95 -0
- package/dist/spine/review-normalize.js.map +1 -0
- package/dist/spine/review-normalize.test.d.ts +2 -0
- package/dist/spine/review-normalize.test.d.ts.map +1 -0
- package/dist/spine/review-normalize.test.js +79 -0
- package/dist/spine/review-normalize.test.js.map +1 -0
- package/dist/spine/selection-rule.d.ts +17 -0
- package/dist/spine/selection-rule.d.ts.map +1 -1
- package/dist/spine/selection-rule.js +36 -0
- package/dist/spine/selection-rule.js.map +1 -1
- package/dist/spine/selection-rule.test.js +34 -1
- package/dist/spine/selection-rule.test.js.map +1 -1
- package/package.json +1 -1
package/dist/spine/extract.js
CHANGED
|
@@ -38,7 +38,7 @@ import { ProvenanceRecordSchema } from '../compiler-schema.js';
|
|
|
38
38
|
import { TotemParseError } from '../errors.js';
|
|
39
39
|
import { extractManualPattern } from '../lesson-pattern.js';
|
|
40
40
|
import { NoDraftCauseSchema } from './ledgers.js';
|
|
41
|
-
import { isBotIdentity } from './selection-rule.js';
|
|
41
|
+
import { isBotIdentity, reviewBotIdentity } from './selection-rule.js';
|
|
42
42
|
/**
|
|
43
43
|
* The `DraftExtractor` port's return: the zero-or-more draft bodies PLUS, when the
|
|
44
44
|
* list is empty, WHY (`noDraftCause`). The cause is the extract-stage twin of the
|
|
@@ -61,32 +61,95 @@ export const DraftResultSchema = z
|
|
|
61
61
|
});
|
|
62
62
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* hallucinated draft.
|
|
64
|
+
* Classify a comment author (slice β, strategy#709). `'bot'` iff it is a recognized
|
|
65
|
+
* review-FINDING bot (`reviewBotIdentity` allowlist — gemini/CR); `'human'`
|
|
66
|
+
* otherwise. The SINGLE classification home: the CLI mapping boundary stamps each
|
|
67
|
+
* comment's `authorKind` via this, and the count + source-tag read that field.
|
|
69
68
|
*/
|
|
70
|
-
function
|
|
69
|
+
export function classifyAuthorKind(author) {
|
|
70
|
+
return reviewBotIdentity(author) ? 'bot' : 'human';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Is this comment SUBSTANTIVE mining substrate (slice β)? Counts toward §6's
|
|
74
|
+
* "≥1 review comment" completeness threshold iff its body is non-empty AND it is
|
|
75
|
+
* either a recognized review-finding bot (`authorKind === 'bot'`) OR a human.
|
|
76
|
+
*
|
|
77
|
+
* The slice-β substrate flip (panel OQ-β2, ALLOWLIST): for this bot-reviewed cert
|
|
78
|
+
* corpus, gemini/CR review comments ARE legitimate substrate — they count. But an
|
|
79
|
+
* UNRECOGNIZED `[bot]` automation account (renovate / dependabot) is still excluded
|
|
80
|
+
* via the `isBotIdentity` denylist, so future automation noise can never launder
|
|
81
|
+
* itself in as a substantive reviewer. Empty/whitespace bodies never count.
|
|
82
|
+
*/
|
|
83
|
+
function isSubstantiveComment(comment) {
|
|
84
|
+
// Gate on what the extractor ACTUALLY consumes (greptile #2242): for a review
|
|
85
|
+
// bot that is the de-chromed `normalizedBody`, so a badge-ONLY comment (non-empty
|
|
86
|
+
// raw `body`, but `normalizedBody === ''` after the strip) is correctly thin —
|
|
87
|
+
// it would otherwise clear the gate yet hand the extractor an empty body and
|
|
88
|
+
// mislead a `no-draft` drop where `truncated` is the truth. Human bodies are
|
|
89
|
+
// never chrome-stripped, so `normalizedBody === body` for them.
|
|
90
|
+
const effectiveBody = comment.authorKind === 'bot' ? comment.normalizedBody : comment.body;
|
|
91
|
+
if (effectiveBody.trim().length === 0)
|
|
92
|
+
return false;
|
|
93
|
+
if (comment.authorKind === 'bot')
|
|
94
|
+
return true;
|
|
95
|
+
return !isBotIdentity(comment.author);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Count SUBSTANTIVE review comments across threads (slice β — replaces the old
|
|
99
|
+
* `humanCommentCount`). A thread set with zero substantive comments is content-thin
|
|
100
|
+
* and must take the loud-drop path, never seed a hallucinated draft.
|
|
101
|
+
*/
|
|
102
|
+
function substantiveCommentCount(threads) {
|
|
71
103
|
let count = 0;
|
|
72
104
|
for (const thread of threads) {
|
|
73
105
|
for (const comment of thread.comments) {
|
|
74
|
-
if (
|
|
106
|
+
if (isSubstantiveComment(comment))
|
|
75
107
|
count++;
|
|
76
108
|
}
|
|
77
109
|
}
|
|
78
110
|
return count;
|
|
79
111
|
}
|
|
80
112
|
/**
|
|
81
|
-
* The
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
113
|
+
* The substrate provenance of a thread set (slice β, panel OQ-β4): `human` /
|
|
114
|
+
* `bot` / `mixed` over its SUBSTANTIVE comments only (recognized review-bot vs
|
|
115
|
+
* human; unrecognized-bot noise is excluded, exactly as the count excludes it).
|
|
116
|
+
* Called only when ≥1 substantive comment survives the gate, so at least one axis
|
|
117
|
+
* is non-zero. A non-FM Tenet-19 diagnostic carried onto each draft + the
|
|
118
|
+
* zero-draft drop.
|
|
119
|
+
*/
|
|
120
|
+
function computeSourceKind(threads) {
|
|
121
|
+
let human = 0;
|
|
122
|
+
let bot = 0;
|
|
123
|
+
for (const thread of threads) {
|
|
124
|
+
for (const comment of thread.comments) {
|
|
125
|
+
if (!isSubstantiveComment(comment))
|
|
126
|
+
continue;
|
|
127
|
+
if (comment.authorKind === 'bot')
|
|
128
|
+
bot++;
|
|
129
|
+
else
|
|
130
|
+
human++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (bot > 0 && human > 0)
|
|
134
|
+
return 'mixed';
|
|
135
|
+
return bot > 0 ? 'bot' : 'human';
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The eligibility gate. Slice γ (strategy#709) NARROWS this from slice-5a's
|
|
139
|
+
* `!isResolved && !isOutdated` to `!isOutdated` — RESOLVED threads are now ADMITTED.
|
|
140
|
+
*
|
|
141
|
+
* The slice-5a rationale (resolved == superseded contamination) was REVERSED by the
|
|
142
|
+
* Gate-1 cert finding: a RESOLVED thread is the highest-signal LEGITIMACY marker —
|
|
143
|
+
* a defect a reviewer raised AND the author confirmed real by fixing — so excluding
|
|
144
|
+
* it discarded the very evidence the miner wants (exhibit: lc#532's fail-open-on-
|
|
145
|
+
* non-finite, dropped solely for being resolved). Only OUTDATED stays excluded: an
|
|
146
|
+
* outdated thread's diff hunk no longer matches HEAD, so its invariant may have been
|
|
147
|
+
* refactored away — that IS stale. The adapter SURFACES both flags (it never
|
|
148
|
+
* pre-filters); core decides here so every rejection is ledgered (§8). Returns the
|
|
86
149
|
* eligible (surviving) threads only.
|
|
87
150
|
*/
|
|
88
151
|
function eligibleThreads(threads) {
|
|
89
|
-
return threads.filter((t) => !t.
|
|
152
|
+
return threads.filter((t) => !t.isOutdated);
|
|
90
153
|
}
|
|
91
154
|
/**
|
|
92
155
|
* Syntactic preflight (fold 4): a draft is a usable lesson-markdown DSL body iff
|
|
@@ -142,21 +205,27 @@ function buildProvenance(pr, content) {
|
|
|
142
205
|
* CI-locked with a fixture extractor + a strict-spy fetch source.
|
|
143
206
|
*
|
|
144
207
|
* Per train PR (and ONLY train PRs): log the fetch → fetch → on unreachable /
|
|
145
|
-
* unparseable-at-source, loud-drop →
|
|
146
|
-
* `
|
|
147
|
-
* else `truncated` when thin to begin with) → completeness-check (≥1
|
|
148
|
-
* comment on the survivors) → build provenance → draft zero-or-more
|
|
149
|
-
* the SURVIVING threads only → preflight each → carry a
|
|
150
|
-
* loud-drop. Every train PR ends with at least one draft or
|
|
151
|
-
* slice-2 half).
|
|
208
|
+
* unparseable-at-source, loud-drop → eligibility gate (slice γ: drop
|
|
209
|
+
* `outdated-rejected` when the outdated filter empties an otherwise-substantive
|
|
210
|
+
* thread, else `truncated` when thin to begin with) → completeness-check (≥1
|
|
211
|
+
* substantive comment on the survivors) → build provenance → draft zero-or-more
|
|
212
|
+
* bodies from the SURVIVING threads only → preflight each → carry a
|
|
213
|
+
* `DraftCandidate` or loud-drop. Every train PR ends with at least one draft or
|
|
214
|
+
* one drop (FM i, slice-2 half).
|
|
152
215
|
*/
|
|
153
216
|
export async function runExtractStage(split, deps) {
|
|
154
217
|
const trainSet = new Set(split.trainPrs);
|
|
155
218
|
const drafts = [];
|
|
156
219
|
const dropEntries = [];
|
|
157
220
|
const apiEntries = [];
|
|
158
|
-
const drop = (sourcePr, reasonCode, detail, noDraftCause) => {
|
|
159
|
-
dropEntries.push({
|
|
221
|
+
const drop = (sourcePr, reasonCode, detail, noDraftCause, sourceKind) => {
|
|
222
|
+
dropEntries.push({
|
|
223
|
+
sourcePr,
|
|
224
|
+
reasonCode,
|
|
225
|
+
detail,
|
|
226
|
+
...(noDraftCause ? { noDraftCause } : {}),
|
|
227
|
+
...(sourceKind ? { sourceKind } : {}),
|
|
228
|
+
});
|
|
160
229
|
};
|
|
161
230
|
// Iterate the TRAIN slice ONLY — held-out / control / excluded PRs are never
|
|
162
231
|
// fetched (§6 / FM h). Deterministic ascending order.
|
|
@@ -183,26 +252,26 @@ export async function runExtractStage(split, deps) {
|
|
|
183
252
|
drop(pr, 'incomplete-provenance', `fetched content PR #${content.pr} does not match requested train PR #${pr}`);
|
|
184
253
|
continue;
|
|
185
254
|
}
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
const
|
|
255
|
+
// Eligibility gate (slice γ) — BEFORE the completeness check. The adapter
|
|
256
|
+
// surfaced per-thread `isOutdated` (it never pre-filters); core decides +
|
|
257
|
+
// ledgers here so every rejection is auditable (§8). Filter to eligible
|
|
258
|
+
// (non-outdated; RESOLVED is now admitted) threads and recount SUBSTANTIVE
|
|
259
|
+
// comments (slice β: human + recognized review-bot) on the SURVIVORS only.
|
|
260
|
+
const preFilterSubstantiveCount = substantiveCommentCount(content.threads);
|
|
192
261
|
const survivingThreads = eligibleThreads(content.threads);
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
if (
|
|
196
|
-
// The thread carried
|
|
197
|
-
// emptied it → `
|
|
198
|
-
// content). Carry the concrete
|
|
262
|
+
const survivorSubstantiveCount = substantiveCommentCount(survivingThreads);
|
|
263
|
+
if (survivorSubstantiveCount < 1) {
|
|
264
|
+
if (preFilterSubstantiveCount >= 1) {
|
|
265
|
+
// The thread carried substantive content, but the OUTDATED filter is what
|
|
266
|
+
// emptied it → `outdated-rejected` (an eligibility rejection, not thin
|
|
267
|
+
// content). Carry the concrete outdated evidence in the detail.
|
|
199
268
|
const ineligible = content.threads.length - survivingThreads.length;
|
|
200
|
-
drop(pr, '
|
|
269
|
+
drop(pr, 'outdated-rejected', `${ineligible} of ${content.threads.length} threads outdated; ${survivorSubstantiveCount} eligible substantive comments remain`);
|
|
201
270
|
}
|
|
202
271
|
else {
|
|
203
|
-
// Thin to begin with (0
|
|
204
|
-
// existing `truncated` path, NOT
|
|
205
|
-
drop(pr, 'truncated', 'no non-empty
|
|
272
|
+
// Thin to begin with (0 substantive comments BEFORE the gate) — the
|
|
273
|
+
// existing `truncated` path, NOT an eligibility rejection.
|
|
274
|
+
drop(pr, 'truncated', 'no non-empty substantive review comment after bot filtering');
|
|
206
275
|
}
|
|
207
276
|
continue;
|
|
208
277
|
}
|
|
@@ -220,6 +289,9 @@ export async function runExtractStage(split, deps) {
|
|
|
220
289
|
// catches its own LLM/network errors) — so the core needs no swallowing catch
|
|
221
290
|
// (Tenet 4). An empty list is a loud drop below, not a silent skip.
|
|
222
291
|
const eligibleContent = { ...content, threads: survivingThreads };
|
|
292
|
+
// The substrate provenance (slice β, Tenet-19 diagnostic) of THIS PR's eligible
|
|
293
|
+
// threads — carried onto every draft + the zero-draft drop (panel OQ-β4).
|
|
294
|
+
const sourceKind = computeSourceKind(survivingThreads);
|
|
223
295
|
// Parse the port result at the boundary (mirrors ClassifierResultSchema.parse in
|
|
224
296
|
// classify.ts): a contract-violating DraftResult (empty-without-cause or
|
|
225
297
|
// cause-without-empty from a buggy adapter) fails loud HERE, before the ledger.
|
|
@@ -227,14 +299,17 @@ export async function runExtractStage(split, deps) {
|
|
|
227
299
|
const draftBodies = draftResult.drafts;
|
|
228
300
|
if (draftBodies.length === 0) {
|
|
229
301
|
// A complete thread that yields no draft is a loud drop (keeps the train PR
|
|
230
|
-
// creditable under FM i), not a silent skip.
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
302
|
+
// creditable under FM i), not a silent skip. Reason code `no-draft` (slice β,
|
|
303
|
+
// strategy β-watch): the slice-α empty-draft drop reused `unparseable`, which
|
|
304
|
+
// was semantically wrong for a legitimate model decline (`none-sentinel`) —
|
|
305
|
+
// the coarse code now NAMES the no-draft case while `noDraftCause` (Tenet-19
|
|
306
|
+
// diagnostic) carries the precise sub-reason, so a parser/format/transient
|
|
307
|
+
// failure is never conflated with the model judging nothing mintable.
|
|
308
|
+
drop(pr, 'no-draft',
|
|
234
309
|
// The boundary parse above + the "cause iff empty" refine guarantee
|
|
235
310
|
// `noDraftCause` is present in this empty-drafts branch — assert it rather
|
|
236
311
|
// than defend with a `?? 'unknown'` fallback that can never fire.
|
|
237
|
-
`extractor produced no draft from a complete thread (cause: ${draftResult.noDraftCause})`, draftResult.noDraftCause);
|
|
312
|
+
`extractor produced no draft from a complete thread (cause: ${draftResult.noDraftCause})`, draftResult.noDraftCause, sourceKind);
|
|
238
313
|
continue;
|
|
239
314
|
}
|
|
240
315
|
for (const body of draftBodies) {
|
|
@@ -242,7 +317,7 @@ export async function runExtractStage(split, deps) {
|
|
|
242
317
|
drop(pr, 'unparseable', 'draft is empty or carries no usable **Pattern:**/yaml DSL');
|
|
243
318
|
continue;
|
|
244
319
|
}
|
|
245
|
-
drafts.push({ provenance: provenance.value, dslSource: body });
|
|
320
|
+
drafts.push({ provenance: provenance.value, dslSource: body, sourceKind });
|
|
246
321
|
}
|
|
247
322
|
}
|
|
248
323
|
// Recompute the held-out-fetch count from the frozen split rather than trust a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/spine/extract.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,6EAA6E;AAC7E,8EAA8E;AAC9E,iFAAiF;AACjF,oEAAoE;AACpE,kFAAkF;AAClF,mEAAmE;AACnE,EAAE;AACF,iFAAiF;AACjF,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,YAAY;AACZ,EAAE;AACF,2CAA2C;AAC3C,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,iFAAiF;AACjF,8EAA8E;AAC9E,4EAA4E;AAC5E,iFAAiF;AACjF,mEAAmE;AACnE,mFAAmF;AACnF,6EAA6E;AAC7E,mFAAmF;AACnF,iFAAiF;AACjF,sDAAsD;AACtD,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAChF,0DAA0D;AAE1D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAyB,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/spine/extract.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { runExtractStage, } from './extract.js';
|
|
2
|
+
import { classifyAuthorKind, runExtractStage, } from './extract.js';
|
|
3
|
+
import { normalizeReviewChrome } from './review-normalize.js';
|
|
3
4
|
import { SplitArtifactSchema } from './split.js';
|
|
4
5
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
5
6
|
const sha = (n) => String(n).padStart(40, '0');
|
|
7
|
+
/**
|
|
8
|
+
* Build a slice-β-enriched comment exactly as the CLI mapping boundary does:
|
|
9
|
+
* `authorKind` via core `classifyAuthorKind`, `normalizedBody` = de-chromed for a
|
|
10
|
+
* recognized review bot, else the raw body. Keeps test comments honest w.r.t. the
|
|
11
|
+
* shipped classification rather than hand-stamping fields.
|
|
12
|
+
*/
|
|
13
|
+
function comment(author, body) {
|
|
14
|
+
const authorKind = classifyAuthorKind(author);
|
|
15
|
+
return {
|
|
16
|
+
author,
|
|
17
|
+
body,
|
|
18
|
+
authorKind,
|
|
19
|
+
normalizedBody: authorKind === 'bot' ? normalizeReviewChrome(body) : body,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
6
22
|
function split(overrides) {
|
|
7
23
|
return SplitArtifactSchema.parse({
|
|
8
24
|
asOfCommit: sha(100),
|
|
@@ -39,7 +55,7 @@ function content(pr, overrides) {
|
|
|
39
55
|
threads: [
|
|
40
56
|
{
|
|
41
57
|
path: 'packages/core/src/x.ts',
|
|
42
|
-
comments: [
|
|
58
|
+
comments: [comment('Jane Doe', 'a real review note')],
|
|
43
59
|
isResolved: false,
|
|
44
60
|
isOutdated: false,
|
|
45
61
|
},
|
|
@@ -51,7 +67,7 @@ function content(pr, overrides) {
|
|
|
51
67
|
function thread(author, body, flags) {
|
|
52
68
|
return {
|
|
53
69
|
path: 'packages/core/src/x.ts',
|
|
54
|
-
comments: [
|
|
70
|
+
comments: [comment(author, body)],
|
|
55
71
|
isResolved: flags?.isResolved ?? false,
|
|
56
72
|
isOutdated: flags?.isOutdated ?? false,
|
|
57
73
|
};
|
|
@@ -140,12 +156,14 @@ describe('runExtractStage — drop reason codes', () => {
|
|
|
140
156
|
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: content(1, { threads: [] }) }]])), fixtureExtractor()));
|
|
141
157
|
expect(dropsFor(r, 1)[0].reasonCode).toBe('truncated');
|
|
142
158
|
});
|
|
143
|
-
it('truncated: a bot-only thread does not satisfy ≥1
|
|
159
|
+
it('truncated: a NOISE-bot-only thread does not satisfy ≥1 substantive comment (slice β denylist)', async () => {
|
|
160
|
+
// dependabot/renovate are NOT in the review-finding allowlist → still excluded
|
|
161
|
+
// (unlike gemini/CR, which now count — see the slice-β substrate tests).
|
|
144
162
|
const botThread = content(1, {
|
|
145
163
|
threads: [
|
|
146
164
|
{
|
|
147
165
|
path: 'x.ts',
|
|
148
|
-
comments: [
|
|
166
|
+
comments: [comment('dependabot[bot]', 'bump lodash to 4.17.21')],
|
|
149
167
|
isResolved: false,
|
|
150
168
|
isOutdated: false,
|
|
151
169
|
},
|
|
@@ -159,7 +177,7 @@ describe('runExtractStage — drop reason codes', () => {
|
|
|
159
177
|
threads: [
|
|
160
178
|
{
|
|
161
179
|
path: 'x.ts',
|
|
162
|
-
comments: [
|
|
180
|
+
comments: [comment('Jane Doe', ' ')],
|
|
163
181
|
isResolved: false,
|
|
164
182
|
isOutdated: false,
|
|
165
183
|
},
|
|
@@ -191,14 +209,18 @@ describe('runExtractStage — drop reason codes', () => {
|
|
|
191
209
|
expect(dropsFor(r, 1)[0].reasonCode).toBe('unparseable');
|
|
192
210
|
expect(r.drafts).toEqual([]);
|
|
193
211
|
});
|
|
194
|
-
it('
|
|
212
|
+
it('no-draft: the extractor produced no draft from a complete thread — records cause + sourceKind (slice β)', async () => {
|
|
195
213
|
const r = await runExtractStage(solo(), deps(spySource([1]), fixtureExtractor(new Map([[1, []]]))));
|
|
196
214
|
const drop = dropsFor(r, 1)[0];
|
|
197
|
-
|
|
215
|
+
// Slice β: the zero-draft drop is now its own `no-draft` reason code (was the
|
|
216
|
+
// misnamed `unparseable`), with the precise sub-cause + the substrate tag.
|
|
217
|
+
expect(drop.reasonCode).toBe('no-draft');
|
|
198
218
|
// The Tenet-19 diagnostic is carried onto the drop (fixtureExtractor tags an
|
|
199
219
|
// empty list 'all-filtered') — never silently dropped as a bare [].
|
|
200
220
|
expect(drop.noDraftCause).toBe('all-filtered');
|
|
201
221
|
expect(drop.detail).toContain('all-filtered');
|
|
222
|
+
// The default `content(1)` thread is a single human comment → human substrate.
|
|
223
|
+
expect(drop.sourceKind).toBe('human');
|
|
202
224
|
});
|
|
203
225
|
it('boundary-parse fails loud on a contract-violating DraftResult (empty-without-cause)', async () => {
|
|
204
226
|
// A buggy adapter returning { drafts: [] } with no cause violates the
|
|
@@ -305,40 +327,43 @@ function recordingExtractor(byPr) {
|
|
|
305
327
|
},
|
|
306
328
|
};
|
|
307
329
|
}
|
|
308
|
-
describe('runExtractStage —
|
|
309
|
-
it('
|
|
310
|
-
const
|
|
311
|
-
threads: [
|
|
312
|
-
thread('Jane Doe', 'a real review note', { isResolved: true }),
|
|
313
|
-
thread('John Roe', 'another note', { isOutdated: true }),
|
|
314
|
-
],
|
|
330
|
+
describe('runExtractStage — eligibility gate (slice γ: RESOLVED admitted, only OUTDATED excluded)', () => {
|
|
331
|
+
it('a RESOLVED thread is now ADMITTED (drafts; reaches the extractor input)', async () => {
|
|
332
|
+
const resolved = content(1, {
|
|
333
|
+
threads: [thread('Jane Doe', 'a real review note', { isResolved: true })],
|
|
315
334
|
});
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
expect(r.drafts).
|
|
335
|
+
const extractor = recordingExtractor();
|
|
336
|
+
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: resolved }]])), extractor));
|
|
337
|
+
expect(r.drafts).toHaveLength(1);
|
|
338
|
+
expect(dropsFor(r, 1)).toEqual([]);
|
|
339
|
+
// γ: the resolved thread is no longer pre-filtered — the extractor saw it.
|
|
340
|
+
expect(extractor.seen[0].threads).toHaveLength(1);
|
|
341
|
+
expect(extractor.seen[0].threads[0].isResolved).toBe(true);
|
|
319
342
|
});
|
|
320
|
-
it('
|
|
321
|
-
const
|
|
343
|
+
it('all-OUTDATED-but-had-substantive-content drops outdated-rejected (not truncated)', async () => {
|
|
344
|
+
const allOutdated = content(1, {
|
|
322
345
|
threads: [
|
|
323
|
-
thread('Jane Doe', 'a real review note', {
|
|
346
|
+
thread('Jane Doe', 'a real review note', { isOutdated: true }),
|
|
324
347
|
thread('John Roe', 'another note', { isOutdated: true }),
|
|
325
348
|
],
|
|
326
349
|
});
|
|
327
|
-
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content:
|
|
328
|
-
const
|
|
329
|
-
expect(
|
|
330
|
-
expect(detail).toContain('
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
350
|
+
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: allOutdated }]])), fixtureExtractor()));
|
|
351
|
+
const drop = dropsFor(r, 1)[0];
|
|
352
|
+
expect(drop.reasonCode).toBe('outdated-rejected');
|
|
353
|
+
expect(drop.detail).toContain('2 of 2 threads outdated');
|
|
354
|
+
expect(drop.detail).toContain('0 eligible substantive comments remain');
|
|
355
|
+
expect(r.drafts).toEqual([]);
|
|
356
|
+
});
|
|
357
|
+
it('thin-to-begin-with (0 substantive comments before the gate) stays truncated, not outdated-rejected', async () => {
|
|
358
|
+
// A noise-bot-only outdated thread — the gate is not what emptied it; it was
|
|
359
|
+
// already thin (dependabot is not a recognized review bot). Keep `truncated`.
|
|
360
|
+
const botOutdated = content(1, {
|
|
361
|
+
threads: [thread('dependabot[bot]', 'bump dep', { isOutdated: true })],
|
|
337
362
|
});
|
|
338
|
-
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content:
|
|
363
|
+
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: botOutdated }]])), fixtureExtractor()));
|
|
339
364
|
expect(dropsFor(r, 1)[0].reasonCode).toBe('truncated');
|
|
340
365
|
});
|
|
341
|
-
it('partial
|
|
366
|
+
it('partial: OUTDATED threads excluded from the draft input; resolved + fresh survive', async () => {
|
|
342
367
|
const mixed = content(1, {
|
|
343
368
|
threads: [
|
|
344
369
|
thread('Jane Doe', 'eligible note A'),
|
|
@@ -348,33 +373,65 @@ describe('runExtractStage — resolution-eligibility gate (slice 5a)', () => {
|
|
|
348
373
|
});
|
|
349
374
|
const extractor = recordingExtractor();
|
|
350
375
|
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: mixed }]])), extractor));
|
|
351
|
-
// The PR is processed (a draft, no drop).
|
|
352
376
|
expect(r.drafts).toHaveLength(1);
|
|
353
377
|
expect(dropsFor(r, 1)).toEqual([]);
|
|
354
|
-
// The extractor
|
|
355
|
-
|
|
378
|
+
// The extractor saw the two NON-outdated threads (incl. the resolved one); only
|
|
379
|
+
// the outdated thread was excluded (γ inverts the slice-5a resolved exclusion).
|
|
356
380
|
const seenThreads = extractor.seen[0].threads;
|
|
357
|
-
expect(seenThreads).toHaveLength(
|
|
358
|
-
expect(seenThreads
|
|
359
|
-
expect(seenThreads.
|
|
381
|
+
expect(seenThreads).toHaveLength(2);
|
|
382
|
+
expect(seenThreads.every((t) => !t.isOutdated)).toBe(true);
|
|
383
|
+
expect(seenThreads.map((t) => t.comments[0].body).sort()).toEqual([
|
|
384
|
+
'eligible note A',
|
|
385
|
+
'resolved note B',
|
|
386
|
+
]);
|
|
387
|
+
});
|
|
388
|
+
it('a fully-eligible thread is unaffected by the gate (no regression)', async () => {
|
|
389
|
+
const r = await runExtractStage(solo(), deps(spySource([1]), fixtureExtractor()));
|
|
390
|
+
expect(r.drafts).toHaveLength(1);
|
|
391
|
+
expect(dropsFor(r, 1)).toEqual([]);
|
|
360
392
|
});
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
393
|
+
});
|
|
394
|
+
// ─── Slice β: bot-review substrate + sourceKind diagnostic ────────────────────
|
|
395
|
+
describe('runExtractStage — slice β (bot-review substrate + sourceKind)', () => {
|
|
396
|
+
it('a RECOGNIZED review-bot (coderabbitai) comment now COUNTS as substrate → drafts', async () => {
|
|
397
|
+
const crOnly = content(1, {
|
|
398
|
+
threads: [thread('coderabbitai[bot]', 'Potential issue: guard against NaN here')],
|
|
399
|
+
});
|
|
400
|
+
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: crOnly }]])), fixtureExtractor()));
|
|
401
|
+
expect(r.drafts).toHaveLength(1);
|
|
402
|
+
expect(dropsFor(r, 1)).toEqual([]);
|
|
403
|
+
expect(r.drafts[0].sourceKind).toBe('bot');
|
|
404
|
+
});
|
|
405
|
+
it('gemini-code-assist (no [bot] suffix) counts as substrate', async () => {
|
|
406
|
+
const gca = content(1, {
|
|
407
|
+
threads: [thread('gemini-code-assist', 'require is_finite() before the divide')],
|
|
408
|
+
});
|
|
409
|
+
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: gca }]])), fixtureExtractor()));
|
|
410
|
+
expect(r.drafts).toHaveLength(1);
|
|
411
|
+
expect(r.drafts[0].sourceKind).toBe('bot');
|
|
412
|
+
});
|
|
413
|
+
it('sourceKind is human for a human-only thread', async () => {
|
|
414
|
+
const r = await runExtractStage(solo(), deps(spySource([1]), fixtureExtractor()));
|
|
415
|
+
expect(r.drafts[0].sourceKind).toBe('human');
|
|
416
|
+
});
|
|
417
|
+
it('a badge-ONLY review-bot comment is NOT substantive — de-chromed empty → truncated, not no-draft (greptile #2242)', async () => {
|
|
418
|
+
const badgeOnly = content(1, {
|
|
419
|
+
threads: [thread('coderabbitai[bot]', '')],
|
|
420
|
+
});
|
|
421
|
+
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: badgeOnly }]])), fixtureExtractor()));
|
|
422
|
+
// normalizedBody strips to '' → not substantive → thin from the start.
|
|
423
|
+
expect(dropsFor(r, 1)[0].reasonCode).toBe('truncated');
|
|
424
|
+
expect(r.drafts).toEqual([]);
|
|
425
|
+
});
|
|
426
|
+
it('sourceKind is mixed when human + review-bot comments both survive', async () => {
|
|
364
427
|
const mixed = content(1, {
|
|
365
428
|
threads: [
|
|
366
|
-
thread('
|
|
367
|
-
thread('
|
|
429
|
+
thread('Jane Doe', 'the human rationale'),
|
|
430
|
+
thread('coderabbitai[bot]', 'the bot finding'),
|
|
368
431
|
],
|
|
369
432
|
});
|
|
370
433
|
const r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: mixed }]])), fixtureExtractor()));
|
|
371
|
-
expect(
|
|
372
|
-
expect(r.drafts).toEqual([]);
|
|
373
|
-
});
|
|
374
|
-
it('a fully-eligible thread is unaffected by the gate (no regression)', async () => {
|
|
375
|
-
const r = await runExtractStage(solo(), deps(spySource([1]), fixtureExtractor()));
|
|
376
|
-
expect(r.drafts).toHaveLength(1);
|
|
377
|
-
expect(dropsFor(r, 1)).toEqual([]);
|
|
434
|
+
expect(r.drafts[0].sourceKind).toBe('mixed');
|
|
378
435
|
});
|
|
379
436
|
});
|
|
380
437
|
// ─── Determinism ──────────────────────────────────────────────────────────────
|
|
@@ -383,9 +440,9 @@ describe('runExtractStage — determinism', () => {
|
|
|
383
440
|
const run = () => runExtractStage(split(), deps(spySource([1, 2], new Map([[2, { kind: 'unreachable' }]])), fixtureExtractor(new Map([[1, [USABLE_DSL, NO_PATTERN_DSL]]]))));
|
|
384
441
|
expect(await run()).toEqual(await run());
|
|
385
442
|
});
|
|
386
|
-
it('identical inputs are deterministic across the
|
|
387
|
-
// PR 1: a partial
|
|
388
|
-
// PR 2: all-
|
|
443
|
+
it('identical inputs are deterministic across the eligibility gate (drafts + drops + ledgers)', async () => {
|
|
444
|
+
// PR 1: a partial mix (resolved + fresh both survive → a draft).
|
|
445
|
+
// PR 2: all-outdated-but-had-substantive-content → an outdated-rejected drop.
|
|
389
446
|
const pr1 = content(1, {
|
|
390
447
|
threads: [
|
|
391
448
|
thread('Jane Doe', 'eligible note', { isResolved: false }),
|
|
@@ -402,7 +459,7 @@ describe('runExtractStage — determinism', () => {
|
|
|
402
459
|
const a = await run();
|
|
403
460
|
const b = await run();
|
|
404
461
|
expect(a).toEqual(b);
|
|
405
|
-
expect(dropsFor(a, 2)[0].reasonCode).toBe('
|
|
462
|
+
expect(dropsFor(a, 2)[0].reasonCode).toBe('outdated-rejected');
|
|
406
463
|
});
|
|
407
464
|
});
|
|
408
465
|
//# sourceMappingURL=extract.test.js.map
|