@theronap/agnoclast-mcp 0.9.148 → 0.9.149

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.
@@ -289,6 +289,51 @@ export function verbatimIn(needle, haystack) {
289
289
  return norm(haystack).includes(n)
290
290
  }
291
291
 
292
+ // ── SNAPPING A NEAR-QUOTE TO ITS SOURCE (ADR-0059 §4.1, added 2026-09-11) ──────────────────────────
293
+ //
294
+ // 🔴 WHY. The STRAT 421 research brief was lost on two separate runs for one word. The model quoted "…but
295
+ // just have it on your radar. That will be September 22nd as well." The lecture says "on THE radar". The
296
+ // gate above is right to refuse that quote, because a paraphrase is not evidence. But refusing the whole
297
+ // CANDIDATE threw away a real, dated deadline whose source passage is right there, differing by one word.
298
+ //
299
+ // So a quote the gate refuses gets one more chance: find the SOURCE passage it came from, and use the
300
+ // SOURCE's words. The quote must be long (SNAP_MIN_WORDS). Its first and last SNAP_ANCHOR_WORDS words must
301
+ // each appear VERBATIM in the source, in order. The span between them must be within
302
+ // SNAP_LENGTH_TOLERANCE of the quote's length. And exactly ONE such span may exist.
303
+ //
304
+ // ⚠ THIS DOES NOT LOOSEN THE GATE. What is stored is still verbatim source text — the transcript's
305
+ // words, not the model's — so "the evidence sentence is in the source" stays true. What changes is who
306
+ // wrote the evidence. A fabrication does not survive: an invented middle must still sit between two exact,
307
+ // in-order, correctly-spaced anchors, and it is then REPLACED by what the source actually says there.
308
+ // That is the text the reviewer and the person read. Not snapped: short quotes (anchors too weak), a
309
+ // quote splicing distant passages (length check), and an anchor pair that matches in two places.
310
+ const SNAP_ANCHOR_WORDS = 6
311
+ const SNAP_MIN_WORDS = 14
312
+ const SNAP_LENGTH_TOLERANCE = 0.25
313
+
314
+ /** PURE. The source passage a near-verbatim quote was taken from (whitespace-normalised), or null. */
315
+ export function snapToSource(quote, source) {
316
+ const q = String(quote ?? '').replace(/\s+/g, ' ').trim()
317
+ const words = q.split(' ')
318
+ if (!q || words.length < SNAP_MIN_WORDS) return null
319
+ const src = String(source ?? '')
320
+ const esc = (w) => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
321
+ const anchor = (ws) => new RegExp(ws.map(esc).join('\\s+'), 'g')
322
+ const lo = q.length * (1 - SNAP_LENGTH_TOLERANCE)
323
+ const hi = q.length * (1 + SNAP_LENGTH_TOLERANCE)
324
+ const found = new Set()
325
+ for (const h of src.matchAll(anchor(words.slice(0, SNAP_ANCHOR_WORDS)))) {
326
+ const tail = anchor(words.slice(-SNAP_ANCHOR_WORDS))
327
+ tail.lastIndex = h.index + h[0].length
328
+ for (let t = tail.exec(src); t; t = tail.exec(src)) {
329
+ const span = src.slice(h.index, t.index + t[0].length).replace(/\s+/g, ' ').trim()
330
+ if (span.length > hi) break
331
+ if (span.length >= lo) { found.add(span); break }
332
+ }
333
+ }
334
+ return found.size === 1 ? [...found][0] : null
335
+ }
336
+
292
337
  /**
293
338
  * Drop every obligation candidate that cannot be verified against the text the model was shown.
294
339
  *
@@ -309,18 +354,24 @@ export function keepVerifiableObligations(items, shown) {
309
354
  const evidence = typeof it.evidence === 'string' ? it.evidence.trim() : ''
310
355
  const party = typeof it.obligated_party === 'string' ? it.obligated_party.trim() : ''
311
356
  if (!subject || !evidence || !OBLIGATION_PARTIES.has(party)) continue
312
- if (!verbatimIn(evidence, shown)) continue
357
+ // Verbatim, or a near-quote snapped to the source's own words (snapToSource). Nothing else.
358
+ let ev = evidence
359
+ if (!verbatimIn(ev, shown)) {
360
+ const snapped = snapToSource(ev, shown)
361
+ if (!snapped || !verbatimIn(snapped, shown)) continue
362
+ ev = snapped
363
+ }
313
364
  const due = typeof it.due_at === 'string' && it.due_at.trim() ? it.due_at.trim() : null
314
365
  // ADR-0059 §5.2: the WHEN words, which the server resolves into a date in code. Kept only if they
315
366
  // appear inside the evidence sentence — the check that stops a paraphrase ("Sept 22" for "the
316
367
  // 22nd") or a phrase from some other sentence from deciding the date. Dropped otherwise: the server
317
368
  // then falls back to due_at, exactly as it did before phrases existed.
318
369
  const phraseRaw = typeof it.due_phrase === 'string' ? it.due_phrase.trim() : ''
319
- const due_phrase = phraseRaw && normWs(evidence).includes(normWs(phraseRaw)) ? phraseRaw.slice(0, 120) : null
370
+ const due_phrase = phraseRaw && normWs(ev).includes(normWs(phraseRaw)) ? phraseRaw.slice(0, 120) : null
320
371
  out.push({
321
372
  subject: subject.slice(0, 200), due_at: due,
322
373
  ...(due_phrase ? { due_phrase } : {}),
323
- evidence: evidence.slice(0, 500), obligated_party: party,
374
+ evidence: ev.slice(0, 500), obligated_party: party,
324
375
  })
325
376
  if (out.length >= MAX_OBLIGATIONS) break
326
377
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.148",
3
+ "version": "0.9.149",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {