@mmnto/totem 1.74.0 → 1.75.1

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 (48) hide show
  1. package/dist/index.d.ts +6 -5
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/recurrence-stats.d.ts +15 -15
  6. package/dist/recurrence-stats.d.ts.map +1 -1
  7. package/dist/recurrence-stats.js +6 -1
  8. package/dist/recurrence-stats.js.map +1 -1
  9. package/dist/recurrence-stats.test.js +25 -1
  10. package/dist/recurrence-stats.test.js.map +1 -1
  11. package/dist/retrospect.d.ts +54 -54
  12. package/dist/retrospect.d.ts.map +1 -1
  13. package/dist/retrospect.js +8 -1
  14. package/dist/retrospect.js.map +1 -1
  15. package/dist/retrospect.test.js +33 -0
  16. package/dist/retrospect.test.js.map +1 -1
  17. package/dist/spine/classify.d.ts.map +1 -1
  18. package/dist/spine/classify.js +3 -0
  19. package/dist/spine/classify.js.map +1 -1
  20. package/dist/spine/classify.test.js +14 -2
  21. package/dist/spine/classify.test.js.map +1 -1
  22. package/dist/spine/compile.test.js +5 -1
  23. package/dist/spine/compile.test.js.map +1 -1
  24. package/dist/spine/extract.d.ts +53 -9
  25. package/dist/spine/extract.d.ts.map +1 -1
  26. package/dist/spine/extract.js +120 -45
  27. package/dist/spine/extract.js.map +1 -1
  28. package/dist/spine/extract.test.js +112 -55
  29. package/dist/spine/extract.test.js.map +1 -1
  30. package/dist/spine/ledgers.d.ts +96 -19
  31. package/dist/spine/ledgers.d.ts.map +1 -1
  32. package/dist/spine/ledgers.js +46 -13
  33. package/dist/spine/ledgers.js.map +1 -1
  34. package/dist/spine/review-normalize.d.ts +23 -0
  35. package/dist/spine/review-normalize.d.ts.map +1 -0
  36. package/dist/spine/review-normalize.js +95 -0
  37. package/dist/spine/review-normalize.js.map +1 -0
  38. package/dist/spine/review-normalize.test.d.ts +2 -0
  39. package/dist/spine/review-normalize.test.d.ts.map +1 -0
  40. package/dist/spine/review-normalize.test.js +79 -0
  41. package/dist/spine/review-normalize.test.js.map +1 -0
  42. package/dist/spine/selection-rule.d.ts +17 -0
  43. package/dist/spine/selection-rule.d.ts.map +1 -1
  44. package/dist/spine/selection-rule.js +36 -0
  45. package/dist/spine/selection-rule.js.map +1 -1
  46. package/dist/spine/selection-rule.test.js +34 -1
  47. package/dist/spine/selection-rule.test.js.map +1 -1
  48. package/package.json +1 -1
@@ -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
- * Count HUMAN review comments (fold 5): bot comments (CodeRabbit / Greptile /
65
- * Renovate / dependabot, via the shared `isBotIdentity`) and empty/whitespace
66
- * bodies do NOT count toward §6's "≥1 review comment" threshold — a bot-only or
67
- * empty thread is content-thin and must take the loud-drop path, never seed a
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 humanCommentCount(threads) {
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 (comment.body.trim().length > 0 && !isBotIdentity(comment.author))
106
+ if (isSubstantiveComment(comment))
75
107
  count++;
76
108
  }
77
109
  }
78
110
  return count;
79
111
  }
80
112
  /**
81
- * The resolution-eligibility gate (slice 5a, mmnto-ai/totem#2201). A thread is
82
- * INELIGIBLE if the author resolved it OR its diff hunk went outdated either
83
- * marks it as superseded review discussion, contamination the miner must not
84
- * draft from. The adapter SURFACES `isResolved`/`isOutdated` (it never
85
- * pre-filters); core decides here so the rejection is ledgered (§8). Returns the
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.isResolved && !t.isOutdated);
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 → resolution-eligibility gate (slice 5a: drop
146
- * `resolved-rejected` when the resolution gate empties an otherwise-human thread,
147
- * else `truncated` when thin to begin with) → completeness-check (≥1 human
148
- * comment on the survivors) → build provenance → draft zero-or-more bodies from
149
- * the SURVIVING threads only → preflight each → carry a `DraftCandidate` or
150
- * loud-drop. Every train PR ends with at least one draft or one drop (FM i,
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({ sourcePr, reasonCode, detail, ...(noDraftCause ? { noDraftCause } : {}) });
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
- // Resolution-eligibility gate (slice 5a, mmnto-ai/totem#2201) — BEFORE the
187
- // completeness check. The adapter surfaced per-thread `isResolved`/`isOutdated`
188
- // (it never pre-filters); core decides + ledgers here so every resolution
189
- // rejection is auditable (§8). Filter to eligible (non-resolved, non-outdated)
190
- // threads and recount human comments on the SURVIVORS only.
191
- const preFilterHumanCount = humanCommentCount(content.threads);
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 survivorHumanCount = humanCommentCount(survivingThreads);
194
- if (survivorHumanCount < 1) {
195
- if (preFilterHumanCount >= 1) {
196
- // The thread carried human content, but the resolution gate is what
197
- // emptied it → `resolved-rejected` (an eligibility rejection, not thin
198
- // content). Carry the concrete resolution evidence in the detail.
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, 'resolved-rejected', `${ineligible} of ${content.threads.length} threads resolved/outdated; ${survivorHumanCount} eligible human comments remain`);
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 human comments BEFORE the resolution gate) — the
204
- // existing `truncated` path, NOT a resolution rejection.
205
- drop(pr, 'truncated', 'no non-empty human review comment after bot filtering');
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. The NO-DRAFT cause (Tenet-19
231
- // diagnostic) is recorded so a parser/format/transient failure is never
232
- // conflated with a legitimate model decline.
233
- drop(pr, 'unparseable',
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":"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"}
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;AAU5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AA+HvE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC3B,YAAY,EAAE,kBAAkB,CAAC,QAAQ,EAAE;CAC5C,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,KAAK,SAAS,CAAC,EAAE;IACzE,OAAO,EACL,2FAA2F;IAC7F,IAAI,EAAE,CAAC,cAAc,CAAC;CACvB,CAAC,CAAC;AAkDL,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;AACrD,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,oBAAoB,CAAC,OAA4B;IACxD,8EAA8E;IAC9E,kFAAkF;IAClF,+EAA+E;IAC/E,6EAA6E;IAC7E,6EAA6E;IAC7E,gEAAgE;IAChE,MAAM,aAAa,GAAG,OAAO,CAAC,UAAU,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3F,IAAI,aAAa,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpD,IAAI,OAAO,CAAC,UAAU,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACxC,CAAC;AAED;;;;GAIG;AACH,SAAS,uBAAuB,CAAC,OAAgC;IAC/D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACtC,IAAI,oBAAoB,CAAC,OAAO,CAAC;gBAAE,KAAK,EAAE,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,OAAgC;IACzD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACtC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;gBAAE,SAAS;YAC7C,IAAI,OAAO,CAAC,UAAU,KAAK,KAAK;gBAAE,GAAG,EAAE,CAAC;;gBACnC,KAAK,EAAE,CAAC;QACf,CAAC;IACH,CAAC;IACD,IAAI,GAAG,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IACzC,OAAO,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;AACnC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,eAAe,CAAC,OAAgC;IACvD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,SAAiB;IACpC,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAChD,IAAI,CAAC;QACH,OAAO,oBAAoB,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC;IAClD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,8EAA8E;QAC9E,+EAA+E;QAC/E,kEAAkE;QAClE,IAAI,GAAG,YAAY,eAAe;YAAE,OAAO,KAAK,CAAC;QACjD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,eAAe,CACtB,EAAU,EACV,OAA4B;IAE5B,MAAM,MAAM,GAAG,sBAAsB,CAAC,SAAS,CAAC;QAC9C,QAAQ,EAAE,EAAE;QACZ,YAAY,EAAE,SAAS,EAAE,WAAW;QACpC,SAAS,EAAE,OAAO,CAAC,cAAc;KAClC,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;SACrE,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED,iFAAiF;AAEjF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,KAAoB,EACpB,IAAsB;IAEtB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,MAAM,GAAqB,EAAE,CAAC;IACpC,MAAM,WAAW,GAAsB,EAAE,CAAC;IAC1C,MAAM,UAAU,GAA0B,EAAE,CAAC;IAE7C,MAAM,IAAI,GAAG,CACX,QAAgB,EAChB,UAA0B,EAC1B,MAAc,EACd,YAA2B,EAC3B,UAA4B,EACtB,EAAE;QACR,WAAW,CAAC,IAAI,CAAC;YACf,QAAQ;YACR,UAAU;YACV,MAAM;YACN,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,6EAA6E;IAC7E,sDAAsD;IACtD,MAAM,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAErD,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,4EAA4E;QAC5E,wEAAwE;QACxE,oBAAoB;QACpB,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC,CAAC;QAE9E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAClC,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,MAAM,IAAI,2CAA2C,EAAE,EAAE,CAAC,CAAC;YAC1F,SAAS;QACX,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAClC,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,CAAC,MAAM,IAAI,2CAA2C,EAAE,EAAE,CAAC,CAAC;YAC1F,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAE/B,8EAA8E;QAC9E,8EAA8E;QAC9E,iFAAiF;QACjF,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YACtB,IAAI,CACF,EAAE,EACF,uBAAuB,EACvB,uBAAuB,OAAO,CAAC,EAAE,uCAAuC,EAAE,EAAE,CAC7E,CAAC;YACF,SAAS;QACX,CAAC;QAED,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,2EAA2E;QAC3E,2EAA2E;QAC3E,MAAM,yBAAyB,GAAG,uBAAuB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC3E,MAAM,gBAAgB,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1D,MAAM,wBAAwB,GAAG,uBAAuB,CAAC,gBAAgB,CAAC,CAAC;QAE3E,IAAI,wBAAwB,GAAG,CAAC,EAAE,CAAC;YACjC,IAAI,yBAAyB,IAAI,CAAC,EAAE,CAAC;gBACnC,0EAA0E;gBAC1E,uEAAuE;gBACvE,gEAAgE;gBAChE,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC;gBACpE,IAAI,CACF,EAAE,EACF,mBAAmB,EACnB,GAAG,UAAU,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,sBAAsB,wBAAwB,uCAAuC,CAChI,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,oEAAoE;gBACpE,2DAA2D;gBAC3D,IAAI,CAAC,EAAE,EAAE,WAAW,EAAE,6DAA6D,CAAC,CAAC;YACvF,CAAC;YACD,SAAS;QACX,CAAC;QAED,0EAA0E;QAC1E,MAAM,UAAU,GAAG,eAAe,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YACnB,IAAI,CAAC,EAAE,EAAE,uBAAuB,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;YACrD,SAAS;QACX,CAAC;QAED,+EAA+E;QAC/E,yEAAyE;QACzE,6EAA6E;QAC7E,+EAA+E;QAC/E,+EAA+E;QAC/E,8EAA8E;QAC9E,oEAAoE;QACpE,MAAM,eAAe,GAAwB,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;QACvF,gFAAgF;QAChF,0EAA0E;QAC1E,MAAM,UAAU,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;QACvD,iFAAiF;QACjF,yEAAyE;QACzE,gFAAgF;QAChF,MAAM,WAAW,GAAG,iBAAiB,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC;QACzF,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC;QAEvC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,4EAA4E;YAC5E,8EAA8E;YAC9E,8EAA8E;YAC9E,4EAA4E;YAC5E,6EAA6E;YAC7E,2EAA2E;YAC3E,sEAAsE;YACtE,IAAI,CACF,EAAE,EACF,UAAU;YACV,oEAAoE;YACpE,2EAA2E;YAC3E,kEAAkE;YAClE,8DAA8D,WAAW,CAAC,YAAa,GAAG,EAC1F,WAAW,CAAC,YAAY,EACxB,UAAU,CACX,CAAC;YACF,SAAS;QACX,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvB,IAAI,CAAC,EAAE,EAAE,aAAa,EAAE,2DAA2D,CAAC,CAAC;gBACrF,SAAS;YACX,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,iBAAiB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;IAE7F,OAAO;QACL,MAAM;QACN,UAAU,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE;QACpC,cAAc,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE;QAC1D,aAAa,EAAE,EAAE,mBAAmB,EAAE,IAAI,CAAC,mBAAmB,EAAE;KACjE,CAAC;AACJ,CAAC"}
@@ -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: [{ author: 'Jane Doe', body: 'a real review note' }],
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: [{ author, body }],
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 human comment (fold 5)', async () => {
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: [{ author: 'coderabbitai[bot]', body: 'nit: rename this' }],
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: [{ author: 'Jane Doe', body: ' ' }],
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('unparseable: the extractor produced no draft from a complete thread — records the NO-DRAFT cause', async () => {
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
- expect(drop.reasonCode).toBe('unparseable');
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 — resolution-eligibility gate (slice 5a)', () => {
309
- it('all-resolved-but-had-human-content drops resolved-rejected (not truncated)', async () => {
310
- const allResolved = content(1, {
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 r = await runExtractStage(solo(), deps(spySource([1], new Map([[1, { kind: 'ok', content: allResolved }]])), fixtureExtractor()));
317
- expect(dropsFor(r, 1)[0].reasonCode).toBe('resolved-rejected');
318
- expect(r.drafts).toEqual([]);
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('the resolved-rejected drop detail carries concrete resolution evidence', async () => {
321
- const allResolved = content(1, {
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', { isResolved: true }),
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: allResolved }]])), fixtureExtractor()));
328
- const detail = dropsFor(r, 1)[0].detail ?? '';
329
- expect(detail).toContain('2 of 2 threads resolved/outdated');
330
- expect(detail).toContain('0 eligible human comments remain');
331
- });
332
- it('thin-to-begin-with (0 human comments before the gate) stays truncated, not resolved-rejected', async () => {
333
- // A resolved thread that ALSO had no human comment — the resolution gate is
334
- // not what emptied it; it was already thin. Keep the existing truncated path.
335
- const botResolved = content(1, {
336
- threads: [thread('coderabbitai[bot]', 'nit: rename this', { isResolved: true })],
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: botResolved }]])), fixtureExtractor()));
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 resolution: survivors are processed; resolved threads excluded from the draft input', async () => {
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 only saw the ONE eligible thread resolved/outdated excluded.
355
- expect(extractor.seen).toHaveLength(1);
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(1);
358
- expect(seenThreads[0].comments[0].body).toBe('eligible note A');
359
- expect(seenThreads.every((t) => !t.isResolved && !t.isOutdated)).toBe(true);
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
- it('partial resolution: resolved threads do not count toward the human-comment threshold', async () => {
362
- // The single eligible thread is bot-only; the resolved thread is the only one
363
- // with a human comment. Survivors have 0 human comments → resolved-rejected.
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]', '![high](https://x/high.svg)')],
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('coderabbitai[bot]', 'nit: rename', { isResolved: false }),
367
- thread('Jane Doe', 'a real human note', { isResolved: true }),
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(dropsFor(r, 1)[0].reasonCode).toBe('resolved-rejected');
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 resolution gate (drafts + drops + ledgers)', async () => {
387
- // PR 1: a partial-resolution mix (one eligible survivor → a draft).
388
- // PR 2: all-resolved-but-had-human-content → a resolved-rejected drop.
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('resolved-rejected');
462
+ expect(dropsFor(a, 2)[0].reasonCode).toBe('outdated-rejected');
406
463
  });
407
464
  });
408
465
  //# sourceMappingURL=extract.test.js.map