@clien-ai/mcp 0.10.3 → 0.10.5
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/tools/content-type-display.js +13 -0
- package/dist/tools/content-type-display.js.map +1 -1
- package/dist/tools/personas.js +6 -3
- package/dist/tools/personas.js.map +1 -1
- package/dist/tools/registry.js +39 -34
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/report-digest.js +266 -166
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/research.js +17 -9
- package/dist/tools/research.js.map +1 -1
- package/package.json +1 -1
|
@@ -217,23 +217,6 @@ function renderId(raw, absent = '(no id)') {
|
|
|
217
217
|
return absent;
|
|
218
218
|
return safeId(raw) ?? '(unrenderable id)';
|
|
219
219
|
}
|
|
220
|
-
/**
|
|
221
|
-
* What a GROUNDED claim says when its receipt pointer did not render.
|
|
222
|
-
*
|
|
223
|
-
* The same absent-vs-unrenderable split as `renderId`, and here it changes what
|
|
224
|
-
* the reader should DO. "no receipt id — unresolvable" tells an agent the claim
|
|
225
|
-
* is grounded in something the report failed to record, which is a defect in the
|
|
226
|
-
* run. A pointer that exists but is unrenderable is a defect in the POINTER: the
|
|
227
|
-
* receipt is in `structuredContent` / `_meta` and can still be resolved there.
|
|
228
|
-
* Printing the first for the second sends the agent to re-run research it does
|
|
229
|
-
* not need.
|
|
230
|
-
*/
|
|
231
|
-
function groundedWithoutReceipt(rawSourceId) {
|
|
232
|
-
const absent = rawSourceId === undefined || rawSourceId === null || String(rawSourceId).trim() === '';
|
|
233
|
-
return absent
|
|
234
|
-
? ' · GROUNDED but no receipt id — unresolvable'
|
|
235
|
-
: ' · GROUNDED, receipt id present but unrenderable — resolve it from the raw report data';
|
|
236
|
-
}
|
|
237
220
|
function num(value) {
|
|
238
221
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
239
222
|
}
|
|
@@ -273,12 +256,61 @@ function capNote(shown, total, channel, path) {
|
|
|
273
256
|
return '';
|
|
274
257
|
return `\n_(showing ${shown} of ${total} — the remaining ${total - shown} are in \`${channel}.${path}\`)_`;
|
|
275
258
|
}
|
|
276
|
-
|
|
259
|
+
/** Return the normalized URL only when it is a safe, non-credentialed web receipt. */
|
|
260
|
+
function safeReceiptUrl(raw) {
|
|
261
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
262
|
+
return null;
|
|
263
|
+
try {
|
|
264
|
+
const url = new URL(raw.trim());
|
|
265
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return url.href;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Resolve the single, persona-owned presentation grant behind a GROUNDED claim. */
|
|
275
|
+
function resolvePersonaReceipt(rawClaim, personas) {
|
|
276
|
+
const claim = asRecord(rawClaim);
|
|
277
|
+
if (str(claim?.state) !== 'GROUNDED')
|
|
278
|
+
return null;
|
|
279
|
+
const personaIndex = personas.findIndex((_, index) => str(claim?.personaId) === `persona-${index}`);
|
|
280
|
+
if (personaIndex < 0)
|
|
281
|
+
return null;
|
|
282
|
+
const sources = asArray(asRecord(personas[personaIndex])?.sources);
|
|
283
|
+
const sourceIndex = sources.findIndex((_, index) => str(claim?.sourceId) === `RCP-p${personaIndex}-s${index}`);
|
|
284
|
+
if (sourceIndex < 0)
|
|
285
|
+
return null;
|
|
286
|
+
const source = asRecord(sources[sourceIndex]);
|
|
287
|
+
const href = safeReceiptUrl(source?.url);
|
|
288
|
+
if (!source || !href)
|
|
289
|
+
return null;
|
|
290
|
+
return {
|
|
291
|
+
sourceId: `RCP-p${personaIndex}-s${sourceIndex}`,
|
|
292
|
+
href,
|
|
293
|
+
quote: collapseWhitespace(str(source.quote) ?? ''),
|
|
294
|
+
source,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function resolvePersonaRows(reportData) {
|
|
298
|
+
const personas = asArray(reportData.personas);
|
|
299
|
+
return asArray(reportData.claims).map((claim) => ({
|
|
300
|
+
claim,
|
|
301
|
+
receipt: resolvePersonaReceipt(claim, personas),
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
function tallyPersonaStates(rows) {
|
|
277
305
|
const tally = { grounded: 0, speculation: 0, noReceipt: 0, other: 0 };
|
|
278
|
-
for (const raw of
|
|
306
|
+
for (const { claim: raw, receipt } of rows) {
|
|
279
307
|
const state = str(asRecord(raw)?.state);
|
|
280
|
-
if (state === 'GROUNDED')
|
|
281
|
-
|
|
308
|
+
if (state === 'GROUNDED') {
|
|
309
|
+
if (receipt)
|
|
310
|
+
tally.grounded += 1;
|
|
311
|
+
else
|
|
312
|
+
tally.noReceipt += 1;
|
|
313
|
+
}
|
|
282
314
|
else if (state === 'SPECULATION')
|
|
283
315
|
tally.speculation += 1;
|
|
284
316
|
else if (state === 'NO_RECEIPT')
|
|
@@ -308,7 +340,7 @@ function tallyLine(tally, total) {
|
|
|
308
340
|
* itself is what an agent pattern-matches on — a digest that says "this claim has
|
|
309
341
|
* a receipt" without printing `RCP-p0-s2` leaves the agent unable to follow it.
|
|
310
342
|
*/
|
|
311
|
-
function renderPersonaClaim(raw) {
|
|
343
|
+
function renderPersonaClaim(raw, resolvedReceipt) {
|
|
312
344
|
const claim = asRecord(raw);
|
|
313
345
|
if (!claim)
|
|
314
346
|
return '- (unreadable claim entry)';
|
|
@@ -323,92 +355,103 @@ function renderPersonaClaim(raw) {
|
|
|
323
355
|
//
|
|
324
356
|
// The GROUNDED gates keep reading the RAW `str()` value: flattening trims, so
|
|
325
357
|
// gating on the flattened one would newly admit `" GROUNDED "` as grounded and
|
|
326
|
-
// put the row's badge at odds with `
|
|
358
|
+
// put the row's badge at odds with `tallyPersonaStates`, which reads raw. Flattening
|
|
327
359
|
// is for the RENDER; it must not widen what counts as a receipt.
|
|
328
360
|
const rawState = str(claim.state);
|
|
329
|
-
const
|
|
361
|
+
const storedState = safeInline(claim.state, 40) ?? 'UNKNOWN_STATE';
|
|
362
|
+
const state = rawState === 'GROUNDED' && !resolvedReceipt ? 'NO_RECEIPT' : storedState;
|
|
330
363
|
const id = renderId(claim.id);
|
|
331
364
|
const personaId = safeId(claim.personaId);
|
|
332
365
|
const sourceId = safeId(claim.sourceId);
|
|
333
366
|
const text = safeInline(claim.text, CLAIM_TEXT_MAX);
|
|
334
367
|
// Gated on `state`, not on `sourceId` presence — see `renderReportClaim` for
|
|
335
368
|
// why a pointer on a non-GROUNDED claim must never render as a receipt.
|
|
336
|
-
let
|
|
337
|
-
if (
|
|
338
|
-
|
|
369
|
+
let provenance = '';
|
|
370
|
+
if (resolvedReceipt) {
|
|
371
|
+
provenance = ` ← ${resolvedReceipt.sourceId}`;
|
|
372
|
+
}
|
|
373
|
+
else if (rawState === 'GROUNDED') {
|
|
374
|
+
provenance = ' · Stored grade: GROUNDED — receipt unavailable; do not cite';
|
|
339
375
|
}
|
|
340
376
|
else if (sourceId) {
|
|
341
|
-
|
|
377
|
+
provenance = ` (carries ${sourceId}, which does NOT grant grounding — state is ${storedState})`;
|
|
342
378
|
}
|
|
343
379
|
const who = personaId ? ` (${personaId})` : '';
|
|
344
380
|
const body = text ? ` — "${text}"` : '';
|
|
345
|
-
return `- [${state}] ${id}${who}${
|
|
381
|
+
return `- [${state}] ${id}${who}${provenance}${body}`;
|
|
346
382
|
}
|
|
347
383
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* This digest is the THIRD mirror of that label inside the MCP package — the tool
|
|
356
|
-
* advert in `registry.ts` is the other two — and it is the one that renders into
|
|
357
|
-
* the payload an agent actually READS. When the re-map first landed it was missed:
|
|
358
|
-
* the advert promised a claim "marked \"Inferred\"" while this line still emitted
|
|
359
|
-
* `model-attested`, so an agent keying on the advert's word would have found
|
|
360
|
-
* nothing, and the same server described one tier two different ways.
|
|
361
|
-
*
|
|
362
|
-
* Neither reading is grounding. The attestation's `sourceId` is printed under an
|
|
363
|
-
* explicit `cited →` label rather than the bare `←` used for real receipts, so
|
|
364
|
-
* the two can never be skim-read as the same thing. That distinction matters MORE now
|
|
365
|
-
* that the word itself is shared with a span-verified receipt.
|
|
366
|
-
*
|
|
367
|
-
* FUL-615: an attested claim now also carries the WINDOW the judge read, clipped to
|
|
368
|
-
* `RECEIPT_QUOTE_MAX` on a labelled continuation line. `cited` was the one provenance
|
|
369
|
-
* tier on this surface whose supporting text reached no agent-visible channel at all —
|
|
370
|
-
* a GROUNDED claim's span is readable as the `RCP-` excerpt it steers, but an attested
|
|
371
|
-
* claim's pointer resolves into `reportEvidence`, whose rows deliberately carry no quote
|
|
372
|
-
* (FUL-560). So the tier a reader is asked to weigh ("stronger than recall, still NOT a
|
|
373
|
-
* receipt") was the one tier they could not see for themselves.
|
|
374
|
-
*
|
|
375
|
-
* ⚠️ THE ABSENCE OF A QUOTE USED TO BE HALF THE SEPARATION, and this change spends it.
|
|
376
|
-
* What replaces it is `ATTESTED_WINDOW_LABEL` — see its doc for why a bare indented
|
|
377
|
-
* quote would have made an attested row read as a grounded one.
|
|
378
|
-
*
|
|
379
|
-
* DIGEST COST, in the idiom `RECEIPT_QUOTE_MAX`'s own doc sets, because a digest an
|
|
380
|
-
* agent truncates is a digest it does not read. Bounded by construction: this spine
|
|
381
|
-
* renders at most `CLAIM_RENDER_CAP` (40) rows and only attested ones get a window, so
|
|
382
|
-
* the worst case is 40 × (200 + ~40 for the label, indent, quotes and newline) ≈ 9.6 KB.
|
|
383
|
-
* Today it is 0 on every prod report — the A2 grantor writes no attestations yet.
|
|
384
|
+
* Resolve the one presentation grant behind either report-level Cited treatment.
|
|
385
|
+
*
|
|
386
|
+
* A stored GROUNDED grade and a model-attestation marker are both untrusted report JSON, not proof
|
|
387
|
+
* by themselves. Either earns its presentation only when its canonical pointer indexes the exact
|
|
388
|
+
* `reportEvidence` pool and the target has a non-credentialed HTTP(S) destination. An attestation
|
|
389
|
+
* additionally needs its nonblank captured window. Keeping this as one resolver lets the row,
|
|
390
|
+
* tally, pointer and window consume the same answer instead of minting orphan citations.
|
|
384
391
|
*/
|
|
385
|
-
function
|
|
392
|
+
function resolveReportReceipt(rawClaim, reportEvidence) {
|
|
393
|
+
const claim = asRecord(rawClaim);
|
|
394
|
+
const rawState = str(claim?.state);
|
|
395
|
+
let kind;
|
|
396
|
+
let sourceId = '';
|
|
397
|
+
let quote = '';
|
|
398
|
+
if (rawState === 'GROUNDED') {
|
|
399
|
+
kind = 'grounded';
|
|
400
|
+
sourceId = typeof claim?.sourceId === 'string' ? claim.sourceId.trim() : '';
|
|
401
|
+
}
|
|
402
|
+
else if (rawState === 'NO_RECEIPT') {
|
|
403
|
+
kind = 'attested';
|
|
404
|
+
const attestation = asRecord(claim?.attestation);
|
|
405
|
+
sourceId = typeof attestation?.sourceId === 'string' ? attestation.sourceId.trim() : '';
|
|
406
|
+
quote = typeof attestation?.quote === 'string' ? collapseWhitespace(attestation.quote) : '';
|
|
407
|
+
if (!quote)
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
if (!sourceId)
|
|
414
|
+
return null;
|
|
415
|
+
const parsed = /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId);
|
|
416
|
+
if (!parsed)
|
|
417
|
+
return null;
|
|
418
|
+
const sourceIndex = Number(parsed[1]);
|
|
419
|
+
if (!Number.isSafeInteger(sourceIndex))
|
|
420
|
+
return null;
|
|
421
|
+
const source = asRecord(reportEvidence[sourceIndex]);
|
|
422
|
+
if (!source || !safeReceiptUrl(source.url))
|
|
423
|
+
return null;
|
|
424
|
+
return { kind, sourceId, quote };
|
|
425
|
+
}
|
|
426
|
+
function renderReportClaim(raw, receipt) {
|
|
386
427
|
const claim = asRecord(raw);
|
|
387
428
|
if (!claim)
|
|
388
429
|
return '- (unreadable claim entry)';
|
|
389
430
|
// FUL-253: same guard and same raw-gate split as `renderPersonaClaim` — this
|
|
390
431
|
// spine's rows are the ones a forged `[GROUNDED] … ← RRCP-s0` would hide in.
|
|
391
432
|
const rawState = str(claim.state);
|
|
392
|
-
const
|
|
433
|
+
const storedState = safeInline(claim.state, 40) ?? 'UNKNOWN_STATE';
|
|
434
|
+
const state = rawState === 'GROUNDED' && receipt?.kind !== 'grounded'
|
|
435
|
+
? 'NO_RECEIPT'
|
|
436
|
+
: storedState;
|
|
393
437
|
const id = renderId(claim.id);
|
|
394
438
|
const section = safeInline(claim.section, 40);
|
|
395
439
|
const sourceId = safeId(claim.sourceId);
|
|
396
440
|
const text = safeInline(claim.text, CLAIM_TEXT_MAX);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// types `state` and `sourceId` independently and `.passthrough()`s unknown
|
|
403
|
-
// shapes, and `getReport` hands the RAW payload through when Zod rejects it, so
|
|
404
|
-
// `{ state: 'NO_RECEIPT', sourceId: 'RRCP-s0' }` genuinely reaches this code.
|
|
405
|
-
// An agent following that arrow would treat a NO_RECEIPT figure as source-checked.
|
|
441
|
+
// The bare `←` arrow means RECEIPT, so raw state and pointer presence are both
|
|
442
|
+
// insufficient. The shared resolver must also find the exact evidence row and a
|
|
443
|
+
// safe destination; otherwise even a stored GROUNDED grade degrades to neutral
|
|
444
|
+
// audit copy. `getReport` hands RAW payload through after schema drift, so every
|
|
445
|
+
// one of those checks is a render-boundary responsibility.
|
|
406
446
|
let provenance = '';
|
|
407
|
-
if (
|
|
408
|
-
provenance =
|
|
447
|
+
if (receipt?.kind === 'grounded') {
|
|
448
|
+
provenance = ` ← ${receipt.sourceId}`;
|
|
409
449
|
}
|
|
410
|
-
else if (rawState === '
|
|
411
|
-
provenance =
|
|
450
|
+
else if (rawState === 'GROUNDED') {
|
|
451
|
+
provenance = ' · Stored grade: GROUNDED — receipt unavailable; do not cite';
|
|
452
|
+
}
|
|
453
|
+
else if (receipt?.kind === 'attested') {
|
|
454
|
+
provenance = ` · ${ATTESTED_LABEL} → ${receipt.sourceId} (NOT a receipt)`;
|
|
412
455
|
}
|
|
413
456
|
else if (rawState === 'NO_RECEIPT') {
|
|
414
457
|
provenance = ` · ${UNSOURCED_LABEL} — from model knowledge, unverified`;
|
|
@@ -416,11 +459,11 @@ function renderReportClaim(raw) {
|
|
|
416
459
|
// A pointer on a non-grounded claim is shown but explicitly disarmed, so it is
|
|
417
460
|
// neither hidden from the reader nor readable as grounding.
|
|
418
461
|
if (rawState !== 'GROUNDED' && sourceId) {
|
|
419
|
-
provenance += ` (carries ${sourceId}, which does NOT grant grounding — state is ${
|
|
462
|
+
provenance += ` (carries ${sourceId}, which does NOT grant grounding — state is ${storedState})`;
|
|
420
463
|
}
|
|
421
464
|
const where = section ? ` (${section})` : '';
|
|
422
465
|
const body = text ? ` — "${text}"` : '';
|
|
423
|
-
return `- [${state}] ${id}${where}${provenance}${body}${attestedWindow(
|
|
466
|
+
return `- [${state}] ${id}${where}${provenance}${body}${attestedWindow(receipt)}`;
|
|
424
467
|
}
|
|
425
468
|
/**
|
|
426
469
|
* The attested claim's window: the captured snapshot text an independent judge read
|
|
@@ -445,31 +488,48 @@ function renderReportClaim(raw) {
|
|
|
445
488
|
* budget of its own, for the reason that constant's doc gives: an agent reading the
|
|
446
489
|
* same source text clipped two different ways has to wonder which is the real wording.
|
|
447
490
|
*/
|
|
448
|
-
function attestedWindow(
|
|
449
|
-
if (
|
|
450
|
-
return '';
|
|
451
|
-
const quote = collapseWhitespace(str(rawQuote) ?? '');
|
|
452
|
-
if (!quote)
|
|
491
|
+
function attestedWindow(receipt) {
|
|
492
|
+
if (receipt?.kind !== 'attested')
|
|
453
493
|
return '';
|
|
454
|
-
return `\n ${ATTESTED_WINDOW_LABEL}: "${clipReceiptQuote(quote, [], RECEIPT_QUOTE_MAX)}"`;
|
|
494
|
+
return `\n ${ATTESTED_WINDOW_LABEL}: "${clipReceiptQuote(receipt.quote, [], RECEIPT_QUOTE_MAX)}"`;
|
|
495
|
+
}
|
|
496
|
+
function tallyReportStates(rows) {
|
|
497
|
+
const tally = { grounded: 0, speculation: 0, noReceipt: 0, other: 0 };
|
|
498
|
+
for (const { claim: raw, receipt } of rows) {
|
|
499
|
+
const state = str(asRecord(raw)?.state);
|
|
500
|
+
if (state === 'GROUNDED') {
|
|
501
|
+
if (receipt?.kind === 'grounded')
|
|
502
|
+
tally.grounded += 1;
|
|
503
|
+
else
|
|
504
|
+
tally.noReceipt += 1;
|
|
505
|
+
}
|
|
506
|
+
else if (state === 'SPECULATION')
|
|
507
|
+
tally.speculation += 1;
|
|
508
|
+
else if (state === 'NO_RECEIPT')
|
|
509
|
+
tally.noReceipt += 1;
|
|
510
|
+
else
|
|
511
|
+
tally.other += 1;
|
|
512
|
+
}
|
|
513
|
+
return tally;
|
|
455
514
|
}
|
|
456
515
|
function renderPersonaSpine(reportData, channel) {
|
|
457
|
-
const
|
|
458
|
-
if (
|
|
516
|
+
const rows = resolvePersonaRows(reportData);
|
|
517
|
+
if (rows.length === 0) {
|
|
459
518
|
return ('### Persona claim spine — NOT PRESENT on this run\n' +
|
|
460
519
|
'No persona claim registry was produced, so NOTHING in the persona/interview prose above ' +
|
|
461
520
|
'has been machine-checked. Absent is not clean: treat those statements as unverified.');
|
|
462
521
|
}
|
|
463
|
-
const tally =
|
|
464
|
-
const shown =
|
|
465
|
-
const lines = shown.map(renderPersonaClaim);
|
|
466
|
-
return (`### Persona claim spine — ${tallyLine(tally,
|
|
522
|
+
const tally = tallyPersonaStates(rows);
|
|
523
|
+
const shown = rows.slice(0, CLAIM_RENDER_CAP);
|
|
524
|
+
const lines = shown.map(({ claim, receipt }) => renderPersonaClaim(claim, receipt));
|
|
525
|
+
return (`### Persona claim spine — ${tallyLine(tally, rows.length)}\n` +
|
|
467
526
|
'A GROUNDED claim\'s receipt id (`RCP-p{i}-s{j}`) indexes `personas[i].sources[j]` — the cached ' +
|
|
468
|
-
'forum post its quote span was code-verified against.
|
|
527
|
+
'forum post its quote span was code-verified against. A stored GROUNDED grade whose canonical, ' +
|
|
528
|
+
'persona-owned safe receipt is unavailable is rendered and counted NO_RECEIPT. NO_RECEIPT can mean a bad claim OR merely ' +
|
|
469
529
|
'sparse evidence: check that persona\'s `insufficientEvidence` / `sourcesFound` below before ' +
|
|
470
530
|
'discounting it. Anything not GROUNDED is unverified.\n' +
|
|
471
531
|
`${lines.join('\n')}` +
|
|
472
|
-
capNote(shown.length,
|
|
532
|
+
capNote(shown.length, rows.length, channel, 'report_data.claims'));
|
|
473
533
|
}
|
|
474
534
|
function renderReportSpine(reportData, channel) {
|
|
475
535
|
const claims = asArray(reportData.reportClaims);
|
|
@@ -479,14 +539,16 @@ function renderReportSpine(reportData, channel) {
|
|
|
479
539
|
'executive-summary assertion in the prose above has been machine-checked. Treat every such ' +
|
|
480
540
|
'figure as unverified.');
|
|
481
541
|
}
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
})
|
|
542
|
+
const reportEvidence = asArray(reportData.reportEvidence);
|
|
543
|
+
const rows = claims.map((claim) => ({
|
|
544
|
+
claim,
|
|
545
|
+
receipt: resolveReportReceipt(claim, reportEvidence),
|
|
546
|
+
}));
|
|
547
|
+
const tally = tallyReportStates(rows);
|
|
548
|
+
const attested = rows.filter(({ receipt }) => receipt?.kind === 'attested').length;
|
|
487
549
|
const attestedNote = attested > 0 ? ` (of which ${attested} ${ATTESTED_LABEL})` : '';
|
|
488
|
-
const shown =
|
|
489
|
-
const lines = shown.map(renderReportClaim);
|
|
550
|
+
const shown = rows.slice(0, CLAIM_RENDER_CAP);
|
|
551
|
+
const lines = shown.map(({ claim, receipt }) => renderReportClaim(claim, receipt));
|
|
490
552
|
return (`### Report claim spine — ${tallyLine(tally, claims.length)}${attestedNote}\n` +
|
|
491
553
|
'A SEPARATE pool from the persona spine — the two never cross. Sections are `market` (sizing/' +
|
|
492
554
|
'trend figures), `competitor` (profile facts) and `summary` (an assertion quoted VERBATIM from ' +
|
|
@@ -497,8 +559,9 @@ function renderReportSpine(reportData, channel) {
|
|
|
497
559
|
'supporting page an independent check vouched for, but no exact span was pinned — stronger than ' +
|
|
498
560
|
`recall, still NOT a receipt. The \`${ATTESTED_WINDOW_LABEL}\` line beneath such a row is that page's ` +
|
|
499
561
|
'text as we captured it: read it as the context the judge weighed, never as a span checked against ' +
|
|
500
|
-
'the claim. Unmarked NO_RECEIPT is recalled model knowledge, unverified. ' +
|
|
501
|
-
'
|
|
562
|
+
'the claim. Unmarked NO_RECEIPT is recalled model knowledge, unverified. A stored GROUNDED grade ' +
|
|
563
|
+
'whose receipt is unavailable is shown as neutral audit context and counted NO_RECEIPT. ' +
|
|
564
|
+
'Key on the rendered state and receipt arrow, never on stored state or source-id presence alone.\n' +
|
|
502
565
|
`${lines.join('\n')}` +
|
|
503
566
|
capNote(shown.length, claims.length, channel, 'report_data.reportClaims'));
|
|
504
567
|
}
|
|
@@ -506,9 +569,9 @@ function renderReportSpine(reportData, channel) {
|
|
|
506
569
|
* The verified spans each persona receipt has to be able to SHOW, keyed by the
|
|
507
570
|
* receipt id its claims point at (FUL-252).
|
|
508
571
|
*
|
|
509
|
-
* Gated on
|
|
510
|
-
*
|
|
511
|
-
*
|
|
572
|
+
* Gated on the same resolved receipt as `renderPersonaClaim`: only a GROUNDED claim
|
|
573
|
+
* with a canonical, persona-owned safe receipt earned a badge, so only that claim's
|
|
574
|
+
* span is what the excerpt owes the reader. Keying on the mere presence of
|
|
512
575
|
* `quoteSpan` would let a SPECULATION claim steer the window — pushing the excerpt
|
|
513
576
|
* away from the sentence a real receipt was verified against, in favour of one no
|
|
514
577
|
* check ever ran on. `getReport` hands the RAW payload through when Zod rejects it,
|
|
@@ -518,21 +581,20 @@ function renderReportSpine(reportData, channel) {
|
|
|
518
581
|
* has no visible badge, so windowing a quote to support it would spend the excerpt
|
|
519
582
|
* on a claim the agent cannot see, at the cost of one it can.
|
|
520
583
|
*/
|
|
521
|
-
function collectVerifiedSpans(
|
|
584
|
+
function collectVerifiedSpans(rows) {
|
|
522
585
|
const spans = new Map();
|
|
523
|
-
for (const raw of
|
|
586
|
+
for (const { claim: raw, receipt } of rows.slice(0, CLAIM_RENDER_CAP)) {
|
|
524
587
|
const claim = asRecord(raw);
|
|
525
|
-
if (!claim ||
|
|
588
|
+
if (!claim || !receipt)
|
|
526
589
|
continue;
|
|
527
|
-
const sourceId = str(claim.sourceId);
|
|
528
590
|
const span = str(claim.quoteSpan);
|
|
529
|
-
if (!
|
|
591
|
+
if (!span)
|
|
530
592
|
continue;
|
|
531
|
-
const existing = spans.get(sourceId);
|
|
593
|
+
const existing = spans.get(receipt.sourceId);
|
|
532
594
|
if (existing)
|
|
533
595
|
existing.push(span);
|
|
534
596
|
else
|
|
535
|
-
spans.set(sourceId, [span]);
|
|
597
|
+
spans.set(receipt.sourceId, [span]);
|
|
536
598
|
}
|
|
537
599
|
return spans;
|
|
538
600
|
}
|
|
@@ -561,54 +623,55 @@ function collectVerifiedSpans(reportData) {
|
|
|
561
623
|
* from them — `publishedDate`, the figure's actual recency (FUL-148).
|
|
562
624
|
*/
|
|
563
625
|
function renderReceiptPools(reportData, channel) {
|
|
564
|
-
const personas = asArray(reportData.personas);
|
|
565
626
|
const reportEvidence = asArray(reportData.reportEvidence);
|
|
566
|
-
const
|
|
627
|
+
const personaRows = resolvePersonaRows(reportData).slice(0, CLAIM_RENDER_CAP);
|
|
628
|
+
const spansByReceipt = collectVerifiedSpans(personaRows);
|
|
629
|
+
const personaReceiptById = new Map();
|
|
630
|
+
for (const { receipt } of personaRows) {
|
|
631
|
+
if (receipt)
|
|
632
|
+
personaReceiptById.set(receipt.sourceId, receipt);
|
|
633
|
+
}
|
|
567
634
|
const personaReceipts = [];
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const deviationLine = personaType === 'user_voice' ? '' : contentTypeDeviationLine(source?.contentType);
|
|
609
|
-
personaReceipts.push(`- ${receiptId} — ${platform} — ${url}${quoteLine}${deviationLine}`);
|
|
610
|
-
});
|
|
611
|
-
});
|
|
635
|
+
for (const [receiptId, receipt] of personaReceiptById) {
|
|
636
|
+
const source = receipt.source;
|
|
637
|
+
// FUL-253: the quote below was already flattened — these two were not, and
|
|
638
|
+
// they sit on the SAME `- RCP-…` line, so a newline in either forges the
|
|
639
|
+
// receipt row the comment below says a forged quote would.
|
|
640
|
+
const platform = safeInline(source?.platform, 40) ?? 'unknown platform';
|
|
641
|
+
const url = safeInline(receipt.href, 200) ?? '(no url)';
|
|
642
|
+
// Indented under its own receipt line so the pool still scans as a list of
|
|
643
|
+
// pointers; a receipt with no cached quote simply has no second line, which
|
|
644
|
+
// is itself worth seeing — it is a pointer that grounds nothing until fetched.
|
|
645
|
+
//
|
|
646
|
+
// `collapseWhitespace` is a FORGERY GUARD, not formatting: this pool is a
|
|
647
|
+
// list of `- RCP-i-j — …` lines, and a quote containing a newline plus a
|
|
648
|
+
// convincing `- RCP-` prefix would add an entry pointing at a source nobody
|
|
649
|
+
// retrieved. A whitespace-only quote also collapses to '' here and correctly
|
|
650
|
+
// renders as a bare pointer rather than as `""` — "the source said nothing".
|
|
651
|
+
//
|
|
652
|
+
// FUL-252: WHICH `RECEIPT_QUOTE_MAX` characters is chosen by the spans of the
|
|
653
|
+
// GROUNDED claims pointing at THIS receipt, not by the quote's head. Passing
|
|
654
|
+
// the receipt's own id is the whole wiring — `RCP-p{i}-s{j}` is the pointer
|
|
655
|
+
// `renderPersonaClaim` prints, so the two sides of the arrow are built from
|
|
656
|
+
// the same expression and cannot drift into windowing the wrong receipt.
|
|
657
|
+
const quote = receipt.quote;
|
|
658
|
+
const clipped = clipReceiptQuote(quote, spansByReceipt.get(receiptId) ?? [], RECEIPT_QUOTE_MAX);
|
|
659
|
+
const quoteLine = quote ? `\n "${clipped}"` : '';
|
|
660
|
+
// FUL-560: the source type, stated ONCE for the pool (in the header below) and inline only
|
|
661
|
+
// on a row that breaks the stated rule. This pool is the one place in the package where the
|
|
662
|
+
// type is a near-constant: `stampEvidenceMetadata` (agent-side) prunes every non-`user_voice`
|
|
663
|
+
// source out of `persona.sources`, so a per-row line here would print the same word down
|
|
664
|
+
// forty rows and bury the one that mattered. The header carries the claim; this carries the
|
|
665
|
+
// exception, in the shared wording.
|
|
666
|
+
//
|
|
667
|
+
// ⚠️ ABSENT AND NON-CANONICAL ARE BOTH DEVIATIONS. `canonicalContentType` fails closed, so a
|
|
668
|
+
// legacy untyped receipt (M0a predates plenty of stored runs) and a token from a newer
|
|
669
|
+
// producer both land here rather than being silently absorbed by the header's claim — which
|
|
670
|
+
// would be exactly the "silence reads as reassurance" fail-open this change exists to close.
|
|
671
|
+
const personaType = canonicalContentType(source?.contentType);
|
|
672
|
+
const deviationLine = personaType === 'user_voice' ? '' : contentTypeDeviationLine(source?.contentType);
|
|
673
|
+
personaReceipts.push(`- ${receiptId} — ${platform} — ${url}${quoteLine}${deviationLine}`);
|
|
674
|
+
}
|
|
612
675
|
const evidenceReceipts = reportEvidence.map((raw, n) => {
|
|
613
676
|
const source = asRecord(raw);
|
|
614
677
|
// FUL-253: every field on this row is scraped-page metadata, and the row is
|
|
@@ -640,13 +703,13 @@ function renderReceiptPools(reportData, channel) {
|
|
|
640
703
|
});
|
|
641
704
|
if (personaReceipts.length === 0 && evidenceReceipts.length === 0) {
|
|
642
705
|
return ('### Receipt pools — EMPTY\n' +
|
|
643
|
-
'No
|
|
644
|
-
'Any receipt id referenced in the prose is unresolvable.');
|
|
706
|
+
'No visible claim resolves to a canonical, persona-owned safe receipt and no report evidence ' +
|
|
707
|
+
'was attached to this run. Any receipt id referenced in the prose is unresolvable.');
|
|
645
708
|
}
|
|
646
709
|
const sections = ['### Receipt pools'];
|
|
647
710
|
if (personaReceipts.length > 0) {
|
|
648
711
|
const shown = personaReceipts.slice(0, CLAIM_RENDER_CAP);
|
|
649
|
-
sections.push(`**Persona receipts** (${plural(personaReceipts.length, 'cached forum post')}) —
|
|
712
|
+
sections.push(`**Persona receipts** (${plural(personaReceipts.length, 'cached forum post')}) — safe targets of the visible \`RCP-\` pointers above. ` +
|
|
650
713
|
'Every receipt here is a first-hand `user_voice` post unless its own row says otherwise:\n' +
|
|
651
714
|
shown.join('\n') +
|
|
652
715
|
capNote(shown.length, personaReceipts.length, channel, 'report_data.personas[].sources'));
|
|
@@ -838,12 +901,48 @@ function renderSycophancy(reportData, channel) {
|
|
|
838
901
|
*/
|
|
839
902
|
function hypothesisDetail(result) {
|
|
840
903
|
const statement = safeInline(result?.statement, HYPOTHESIS_STATEMENT_MAX);
|
|
841
|
-
const scopeCaveat = safeInline(result?.scopeCaveat, HYPOTHESIS_NOTE_MAX);
|
|
842
904
|
const evidenceReading = safeInline(result?.evidenceReading, HYPOTHESIS_NOTE_MAX);
|
|
843
905
|
return ((statement ? `\n ${HYPOTHESIS_LABEL}: "${statement}"` : '') +
|
|
844
|
-
(
|
|
906
|
+
scopeCaveatDetail(result) +
|
|
845
907
|
(evidenceReading ? `\n ${EVIDENCE_READING_LABEL}: ${evidenceReading}` : ''));
|
|
846
908
|
}
|
|
909
|
+
/** The exact quoted/labelled FUL-614 shape, shared by robust and caveat-only rows. */
|
|
910
|
+
function scopeCaveatDetail(result) {
|
|
911
|
+
const scopeCaveat = safeInline(result?.scopeCaveat, HYPOTHESIS_NOTE_MAX);
|
|
912
|
+
return scopeCaveat ? `\n ${SCOPE_CAVEAT_LABEL}: "${scopeCaveat}"` : '';
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Scope caveats for hypotheses that do not have a robustness row (FUL-626).
|
|
916
|
+
*
|
|
917
|
+
* A caveat already rides on {@link hypothesisDetail} when the verdict was re-tested. This separate
|
|
918
|
+
* block covers only the other half so existing robustness rows stay byte-identical and a
|
|
919
|
+
* caveat-only fact never sits under a heading that claims robustness. Values are MODEL-WRITTEN,
|
|
920
|
+
* therefore quoted, labelled and passed through `safeInline` before entering a `- ` row.
|
|
921
|
+
*
|
|
922
|
+
* Deliberately no `LIST_RENDER_CAP`: the acceptance contract is that every nonblank caveat reaches
|
|
923
|
+
* `content`, and silently dropping the 26th trust qualification would be worse than a larger
|
|
924
|
+
* digest. The measured production rate was 26/371 rows (7%) on 2026-08-24, each value is bounded by
|
|
925
|
+
* `HYPOTHESIS_NOTE_MAX`, and ordinary runs carry only 5-8 hypotheses. The full result array remains
|
|
926
|
+
* the natural runaway bound, while version-skewed/malformed rows fail closed through `asArray`,
|
|
927
|
+
* `asRecord`, `renderId` and `safeInline`.
|
|
928
|
+
*/
|
|
929
|
+
function renderUnretestedScopeCaveats(reportData) {
|
|
930
|
+
const lines = asArray(reportData.hypothesisResults).flatMap((raw) => {
|
|
931
|
+
const result = asRecord(raw);
|
|
932
|
+
if (!result || asRecord(result.robustness) !== null)
|
|
933
|
+
return [];
|
|
934
|
+
const detail = scopeCaveatDetail(result);
|
|
935
|
+
if (!detail)
|
|
936
|
+
return [];
|
|
937
|
+
return [`- ${renderId(result.hypothesisId)}${detail}`];
|
|
938
|
+
});
|
|
939
|
+
if (lines.length === 0)
|
|
940
|
+
return '';
|
|
941
|
+
return ('### Scope caveats on hypotheses not re-tested\n' +
|
|
942
|
+
'These model-written qualifications apply to single-shot verdicts above; they are not ' +
|
|
943
|
+
'robustness results.\n' +
|
|
944
|
+
lines.join('\n'));
|
|
945
|
+
}
|
|
847
946
|
/**
|
|
848
947
|
* What a persona name that arrived unreadable renders as (FUL-616).
|
|
849
948
|
*
|
|
@@ -1298,6 +1397,7 @@ export function buildTrustDigest(reportData, channel) {
|
|
|
1298
1397
|
renderReceiptPools(data, channel),
|
|
1299
1398
|
renderPersonas(data, channel),
|
|
1300
1399
|
renderSycophancy(data, channel),
|
|
1400
|
+
renderUnretestedScopeCaveats(data),
|
|
1301
1401
|
renderRobustness(data, channel),
|
|
1302
1402
|
].filter((section) => section.length > 0);
|
|
1303
1403
|
return `${DIGEST_HEADING}\n\n${sections.join('\n\n')}`;
|