@clien-ai/mcp 0.11.0 → 0.12.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.
- package/CHANGELOG.md +50 -0
- package/README.md +26 -8
- package/dist/hypothesis-semantics.js +230 -0
- package/dist/hypothesis-semantics.js.map +1 -0
- package/dist/tools/collections.js +183 -68
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/hypothesis-final.js +55 -0
- package/dist/tools/hypothesis-final.js.map +1 -0
- package/dist/tools/interview-scripts.js +50 -21
- package/dist/tools/interview-scripts.js.map +1 -1
- package/dist/tools/market-signals.js +121 -0
- package/dist/tools/market-signals.js.map +1 -0
- package/dist/tools/market-sizing-proof.js +9 -3
- package/dist/tools/market-sizing-proof.js.map +1 -1
- package/dist/tools/output-schemas.js +20 -11
- package/dist/tools/output-schemas.js.map +1 -1
- package/dist/tools/registry.js +54 -40
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/report-digest.js +471 -73
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/research.js +28 -3
- package/dist/tools/research.js.map +1 -1
- package/dist/tools/scoped-research.js +62 -56
- package/dist/tools/scoped-research.js.map +1 -1
- package/dist/tools/status.js +46 -2
- package/dist/tools/status.js.map +1 -1
- package/dist/types/report.js +518 -22
- package/dist/types/report.js.map +1 -1
- package/dist/types/semantic-theme.js +43 -0
- package/dist/types/semantic-theme.js.map +1 -0
- package/package.json +1 -1
|
@@ -48,8 +48,12 @@
|
|
|
48
48
|
*/
|
|
49
49
|
import { collapseWhitespace, safeInline, safeId, clipReceiptQuote, containsSpan, hasVisibleContent, RECEIPT_QUOTE_MAX, } from './render-safety.js';
|
|
50
50
|
import { canonicalContentType, contentTypeDeviationLine, contentTypeProvenanceLine, } from './content-type-display.js';
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
51
|
+
import { readFinalHypothesisState } from './hypothesis-final.js';
|
|
52
|
+
import { readRobustnessCounts } from '../hypothesis-semantics.js';
|
|
53
|
+
import { parseReceiptId, resolveSourceReceipt } from './receipt-children.js';
|
|
54
|
+
import { resolveMarketSignals } from './market-signals.js';
|
|
55
|
+
import { MCP_OVERVIEW_REF_CAP, MCP_OVERVIEW_SPAN_CAP, REPORT_PORTRAIT_PATHS, sanitizeMcpMarkdownTitle, sanitizeMcpReportTitle, } from '../types/report.js';
|
|
56
|
+
import { readSemanticTheme } from '../types/semantic-theme.js';
|
|
53
57
|
/**
|
|
54
58
|
* Max claims rendered per spine. Above this the digest states how many were
|
|
55
59
|
* dropped and where the full list lives — never a silent cut. Sized so a typical
|
|
@@ -185,6 +189,8 @@ const UNSOURCED_LABEL = 'unsourced';
|
|
|
185
189
|
const HYPOTHESIS_LABEL = 'hypothesis';
|
|
186
190
|
const SCOPE_CAVEAT_LABEL = '⚠️ scope caveat — persona support from OUTSIDE its credibility domain; directional, NOT grounded';
|
|
187
191
|
const EVIDENCE_READING_LABEL = 'rests on';
|
|
192
|
+
/** FUL-358 — the reliability continuation line's label. */
|
|
193
|
+
const RELIABILITY_LABEL = 'confidence in this verdict';
|
|
188
194
|
/**
|
|
189
195
|
* The marker on a receipt-CHILD line, and the pool sentence that explains it (FUL-685 / T8).
|
|
190
196
|
*
|
|
@@ -303,12 +309,18 @@ function capNote(shown, total, channel, path) {
|
|
|
303
309
|
return '';
|
|
304
310
|
return `\n_(showing ${shown} of ${total} — the remaining ${total - shown} are in \`${channel}.${path}\`)_`;
|
|
305
311
|
}
|
|
312
|
+
/** Honest overflow note for overview entries omitted before their public allowlist is evaluated. */
|
|
313
|
+
function overviewCapNote(truncated, cap, noun) {
|
|
314
|
+
if (!truncated)
|
|
315
|
+
return '';
|
|
316
|
+
return `\n_(showing at most ${cap} stored ${noun} — additional entries were omitted before public allowlisting)_`;
|
|
317
|
+
}
|
|
306
318
|
/** Return the normalized URL only when it is a safe, non-credentialed web receipt. */
|
|
307
319
|
function safeReceiptUrl(raw) {
|
|
308
|
-
if (typeof raw !== 'string' ||
|
|
320
|
+
if (typeof raw !== 'string' || !/^https?:\/\//i.test(raw))
|
|
309
321
|
return null;
|
|
310
322
|
try {
|
|
311
|
-
const url = new URL(raw
|
|
323
|
+
const url = new URL(raw);
|
|
312
324
|
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password) {
|
|
313
325
|
return null;
|
|
314
326
|
}
|
|
@@ -318,6 +330,112 @@ function safeReceiptUrl(raw) {
|
|
|
318
330
|
return null;
|
|
319
331
|
}
|
|
320
332
|
}
|
|
333
|
+
const OVERVIEW_ASSERTION_MAX = 320;
|
|
334
|
+
function overviewReceiptSupportsClaim(claim, receipt) {
|
|
335
|
+
if (claim.length > OVERVIEW_ASSERTION_MAX || receipt.length > OVERVIEW_ASSERTION_MAX)
|
|
336
|
+
return false;
|
|
337
|
+
const normalize = (value) => value
|
|
338
|
+
.normalize('NFC')
|
|
339
|
+
.replace(/\s+/g, ' ')
|
|
340
|
+
.trim();
|
|
341
|
+
const normalizedClaim = normalize(claim);
|
|
342
|
+
return normalizedClaim !== '' && normalizedClaim === normalize(receipt);
|
|
343
|
+
}
|
|
344
|
+
function resolveOverviewRef(ref, claimSpan, reportEvidence, forumThreads) {
|
|
345
|
+
const record = asRecord(ref);
|
|
346
|
+
const kind = str(record?.kind);
|
|
347
|
+
if (kind === 'report_evidence') {
|
|
348
|
+
const sourceId = str(record?.sourceId);
|
|
349
|
+
const match = sourceId ? /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId) : null;
|
|
350
|
+
if (!match)
|
|
351
|
+
return null;
|
|
352
|
+
const source = asRecord(reportEvidence[Number(match[1])]);
|
|
353
|
+
const href = safeReceiptUrl(source?.url);
|
|
354
|
+
const quote = str(source?.quote);
|
|
355
|
+
if (!source || !href || !quote || !overviewReceiptSupportsClaim(claimSpan, quote))
|
|
356
|
+
return null;
|
|
357
|
+
return `${sourceId} -> ${href}`;
|
|
358
|
+
}
|
|
359
|
+
if (kind === 'forum_receipt') {
|
|
360
|
+
const receiptId = safeId(record?.receiptId);
|
|
361
|
+
if (!receiptId || !parseReceiptId(receiptId))
|
|
362
|
+
return null;
|
|
363
|
+
const matches = forumThreads.flatMap((rawThread) => {
|
|
364
|
+
const thread = asRecord(rawThread);
|
|
365
|
+
return asArray(thread?.receipts).flatMap((rawReceipt) => {
|
|
366
|
+
const receipt = asRecord(rawReceipt);
|
|
367
|
+
const excerpt = str(receipt?.excerpt);
|
|
368
|
+
if (str(receipt?.receiptId) !== receiptId ||
|
|
369
|
+
!excerpt ||
|
|
370
|
+
!hasVisibleContent(excerpt) ||
|
|
371
|
+
!overviewReceiptSupportsClaim(claimSpan, excerpt))
|
|
372
|
+
return [];
|
|
373
|
+
const href = safeReceiptUrl(thread?.url);
|
|
374
|
+
return [`${receiptId}${href
|
|
375
|
+
? ` -> ${href}`
|
|
376
|
+
: ` -> cached excerpt: "${safeInline(excerpt, OVERVIEW_ASSERTION_MAX) ?? '[unavailable]'}"`}`];
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
380
|
+
}
|
|
381
|
+
// A hypothesis is reasoning context, not an external receipt. Interview refs are private.
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
function renderOverviewTrust(data) {
|
|
385
|
+
const reportEvidence = asArray(data.reportEvidence);
|
|
386
|
+
const forum = asRecord(data.forumResearch);
|
|
387
|
+
const forumThreads = asArray(forum?.threads);
|
|
388
|
+
const sections = ['marketOverview', 'communityOverview'].flatMap((key) => {
|
|
389
|
+
const overview = asRecord(data[key]);
|
|
390
|
+
const overviewText = str(overview?.text);
|
|
391
|
+
if (!overview || !overviewText || !hasVisibleContent(overviewText))
|
|
392
|
+
return [];
|
|
393
|
+
const rawClaims = asArray(overview.claimSpans);
|
|
394
|
+
const claimsTruncated = overview.claimSpansTruncated === true || rawClaims.length > MCP_OVERVIEW_SPAN_CAP;
|
|
395
|
+
const ranges = rawClaims.slice(0, MCP_OVERVIEW_SPAN_CAP).flatMap((rawClaim, claimIndex) => {
|
|
396
|
+
const claim = asRecord(rawClaim);
|
|
397
|
+
const span = str(claim?.span);
|
|
398
|
+
if (!span)
|
|
399
|
+
return [];
|
|
400
|
+
const start = overviewText.indexOf(span);
|
|
401
|
+
if (start < 0 || overviewText.indexOf(span, start + 1) >= 0)
|
|
402
|
+
return [];
|
|
403
|
+
return [{ start, end: start + span.length, span, claim, claimIndex }];
|
|
404
|
+
}).sort((a, b) => a.start - b.start);
|
|
405
|
+
const lines = [];
|
|
406
|
+
let cursor = 0;
|
|
407
|
+
for (const range of ranges) {
|
|
408
|
+
if (range.start < cursor)
|
|
409
|
+
continue;
|
|
410
|
+
const uncovered = overviewText.slice(cursor, range.start);
|
|
411
|
+
if (/[\p{L}\p{N}]/u.test(uncovered))
|
|
412
|
+
lines.push(`- Unsourced: "${safeInline(uncovered)}"`);
|
|
413
|
+
if (str(range.claim?.claimType) === 'inference') {
|
|
414
|
+
lines.push(`- Inferred: "${safeInline(range.span)}"`);
|
|
415
|
+
cursor = range.end;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const rawRefs = asArray(range.claim?.evidenceRefs);
|
|
419
|
+
const refsTruncated = range.claim?.evidenceRefsTruncated === true || rawRefs.length > MCP_OVERVIEW_REF_CAP;
|
|
420
|
+
const refs = [...new Set(rawRefs.slice(0, MCP_OVERVIEW_REF_CAP)
|
|
421
|
+
.map((ref) => resolveOverviewRef(ref, range.span, reportEvidence, forumThreads))
|
|
422
|
+
.filter((ref) => Boolean(ref)))];
|
|
423
|
+
const line = refs.length > 0
|
|
424
|
+
? `- Cited (${refs.length}): "${safeInline(range.span, OVERVIEW_ASSERTION_MAX)}" — ${refs.join('; ')}`
|
|
425
|
+
: `- Unsourced: "${safeInline(range.span)}"`;
|
|
426
|
+
lines.push(line + overviewCapNote(refsTruncated, MCP_OVERVIEW_REF_CAP, 'evidence-reference positions'));
|
|
427
|
+
cursor = range.end;
|
|
428
|
+
}
|
|
429
|
+
const trailing = overviewText.slice(cursor);
|
|
430
|
+
if (/[\p{L}\p{N}]/u.test(trailing))
|
|
431
|
+
lines.push(`- Unsourced: "${safeInline(trailing)}"`);
|
|
432
|
+
if (lines.length === 0)
|
|
433
|
+
return [];
|
|
434
|
+
const heading = key === 'marketOverview' ? 'Market overview trust' : 'Community overview trust';
|
|
435
|
+
return [`### ${heading}\n${lines.join('\n')}${overviewCapNote(claimsTruncated, MCP_OVERVIEW_SPAN_CAP, 'claim-span positions')}`];
|
|
436
|
+
});
|
|
437
|
+
return sections.join('\n\n');
|
|
438
|
+
}
|
|
321
439
|
/**
|
|
322
440
|
* Resolve the single, persona-owned presentation grant behind a GROUNDED claim.
|
|
323
441
|
*
|
|
@@ -694,7 +812,26 @@ function renderReportSpine(reportData, channel) {
|
|
|
694
812
|
const attestedNote = attested > 0 ? ` (of which ${attested} ${ATTESTED_LABEL})` : '';
|
|
695
813
|
const shown = rows.slice(0, CLAIM_RENDER_CAP);
|
|
696
814
|
const lines = shown.map(({ claim, receipt }) => renderReportClaim(claim, receipt));
|
|
697
|
-
|
|
815
|
+
// FUL-358 — SAY WHICH POPULATION THE HEADLINE COUNTED.
|
|
816
|
+
//
|
|
817
|
+
// The prose below has always explained that a `summary` claim usually restates a `market` or
|
|
818
|
+
// `competitor` one and "must not be counted as a second independent finding" — while the
|
|
819
|
+
// headline directly above it counted exactly that. The report markdown in the SAME tool
|
|
820
|
+
// result scopes its own tally to the market+competitor spine and names it
|
|
821
|
+
// (`of N market & competitor claims`), so the two numbers arrived side by side looking like
|
|
822
|
+
// one of them was an arithmetic error. Both are defensible; only one said what it counted.
|
|
823
|
+
//
|
|
824
|
+
// The tally still covers every claim — this section is the audit surface, and a graded
|
|
825
|
+
// summary claim must be visible in it. What is added is the split, so a reader can reconcile
|
|
826
|
+
// the two headlines instead of choosing between them.
|
|
827
|
+
const summaryClaims = rows.filter(({ claim }) => asRecord(claim)?.section === 'summary').length;
|
|
828
|
+
const spineClaims = claims.length - summaryClaims;
|
|
829
|
+
const populationNote = `\nDENOMINATOR: ${claims.length} = ${spineClaims} market & competitor claim(s) + ` +
|
|
830
|
+
`${summaryClaims} executive-summary restatement(s). The report markdown above tallies the ` +
|
|
831
|
+
`${spineClaims} market & competitor claims ONLY, and names that scope; this section tallies ` +
|
|
832
|
+
'all three sections because it is the audit surface. The two headlines are the same run ' +
|
|
833
|
+
'counted over two populations, not a discrepancy.';
|
|
834
|
+
return (`### Report claim spine — ${tallyLine(tally, claims.length)}${attestedNote}${populationNote}\n` +
|
|
698
835
|
'A SEPARATE pool from the persona spine — the two never cross. Sections are `market` (sizing/' +
|
|
699
836
|
'trend figures), `competitor` (profile facts) and `summary` (an assertion quoted VERBATIM from ' +
|
|
700
837
|
'the executive summary, graded against the same evidence — so a grounded `summary` claim ' +
|
|
@@ -989,8 +1126,14 @@ function renderPersonas(reportData, channel) {
|
|
|
989
1126
|
const seniority = safeInline(priors?.seniority, 80);
|
|
990
1127
|
const careerPath = safeInline(priors?.careerPath, 80);
|
|
991
1128
|
const provenance = safeInline(persona?.identityProvenance, 200);
|
|
1129
|
+
const hasFrozenPortrait = REPORT_PORTRAIT_PATHS.includes(persona?.portrait);
|
|
992
1130
|
const bits = [];
|
|
993
1131
|
bits.push(`${plural(sources.length, 'receipt')}`);
|
|
1132
|
+
// State availability, not the path: a raw drift payload must never interpolate an arbitrary
|
|
1133
|
+
// remote URL or an identifier-bearing decoration into agent-visible report text.
|
|
1134
|
+
bits.push(hasFrozenPortrait
|
|
1135
|
+
? 'frozen portrait decoration available'
|
|
1136
|
+
: 'initials fallback');
|
|
994
1137
|
if (insufficient) {
|
|
995
1138
|
bits.push(`⚠️ insufficientEvidence${sourcesFound !== null ? ` (only ${sourcesFound} real posts found)` : ''}`);
|
|
996
1139
|
}
|
|
@@ -1028,6 +1171,58 @@ function renderPersonas(reportData, channel) {
|
|
|
1028
1171
|
return (`${header}\n${lines.join('\n')}` +
|
|
1029
1172
|
capNote(shown.length, personas.length, channel, 'report_data.personas'));
|
|
1030
1173
|
}
|
|
1174
|
+
/**
|
|
1175
|
+
* A literal full-report Community summary. Internal pain-point counts/examples stay available in
|
|
1176
|
+
* structured report_data; they are deliberately not promoted into a user-facing `Threads` column.
|
|
1177
|
+
*/
|
|
1178
|
+
function renderCommunityPatterns(reportData, channel) {
|
|
1179
|
+
const threads = asArray(asRecord(reportData.forumResearch)?.threads).filter((rawThread) => safeInline(asRecord(rawThread)?.title, MODEL_FRAMING_TEXT_MAX) !== null);
|
|
1180
|
+
if (threads.length === 0)
|
|
1181
|
+
return '';
|
|
1182
|
+
const shown = threads.slice(0, LIST_RENDER_CAP);
|
|
1183
|
+
const lines = shown.map((rawThread) => {
|
|
1184
|
+
const thread = asRecord(rawThread);
|
|
1185
|
+
const sentiment = safeInline(thread?.sentimentSummary, MODEL_FRAMING_TEXT_MAX);
|
|
1186
|
+
const title = safeInline(thread?.title, MODEL_FRAMING_TEXT_MAX) ?? 'Untitled discussion';
|
|
1187
|
+
const theme = readSemanticTheme(thread?.theme, title);
|
|
1188
|
+
const href = safeReceiptUrl(thread?.url);
|
|
1189
|
+
return [
|
|
1190
|
+
`- Theme: ${theme ?? 'not recorded'}`,
|
|
1191
|
+
` Sentiment: ${sentiment ?? 'not recorded'}`,
|
|
1192
|
+
` Discussion: ${title}${href ? ` — ${href}` : ''}`,
|
|
1193
|
+
].join('\n');
|
|
1194
|
+
});
|
|
1195
|
+
return (`### Community patterns — ${plural(threads.length, 'discussion')}\n` +
|
|
1196
|
+
'Themes are producer-authored semantic labels; Discussion preserves the original source title.\n' +
|
|
1197
|
+
lines.join('\n') +
|
|
1198
|
+
capNote(shown.length, threads.length, channel, 'report_data.forumResearch.threads'));
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Current agent Markdown already owns the literal Community rows. Detect only its code-owned,
|
|
1202
|
+
* three-line row grammar so full-report tools do not append the same evidence a second time.
|
|
1203
|
+
* Legacy Markdown lacks this grammar and still receives the compatibility projection above.
|
|
1204
|
+
*/
|
|
1205
|
+
function hasCurrentCommunityPatternRows(reportMarkdown, reportData) {
|
|
1206
|
+
const expectedRows = asArray(asRecord(reportData.forumResearch)?.threads).filter((rawThread) => safeInline(asRecord(rawThread)?.title, MODEL_FRAMING_TEXT_MAX) !== null).length;
|
|
1207
|
+
if (expectedRows === 0)
|
|
1208
|
+
return false;
|
|
1209
|
+
const lines = reportMarkdown.split('\n');
|
|
1210
|
+
const heading = lines.indexOf('### Relevant Discussions');
|
|
1211
|
+
if (heading < 0)
|
|
1212
|
+
return false;
|
|
1213
|
+
const nextSection = lines.findIndex((line, index) => index > heading && /^#{2,3}\s/u.test(line));
|
|
1214
|
+
const end = nextSection < 0 ? lines.length : nextSection;
|
|
1215
|
+
let foundRows = 0;
|
|
1216
|
+
for (let index = heading + 1; index + 2 < end; index += 1) {
|
|
1217
|
+
if (lines[index]?.startsWith('- **Theme:** ') &&
|
|
1218
|
+
lines[index + 1]?.startsWith(' - **Sentiment:** ') &&
|
|
1219
|
+
lines[index + 2]?.startsWith(' - **Discussion:** ')) {
|
|
1220
|
+
foundRows += 1;
|
|
1221
|
+
index += 2;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return foundRows === expectedRows;
|
|
1225
|
+
}
|
|
1031
1226
|
/**
|
|
1032
1227
|
* The anti-sycophancy readout — deterministically computed from the interviews,
|
|
1033
1228
|
* not model-opined, which is exactly why it belongs in the text: it is the one
|
|
@@ -1127,18 +1322,61 @@ function renderSycophancy(reportData, channel) {
|
|
|
1127
1322
|
* FUL-263 — before anything rendered them, which is that file's whole premise — so its
|
|
1128
1323
|
* assertions bit on this change with no fixture edit.
|
|
1129
1324
|
*/
|
|
1130
|
-
function hypothesisDetail(result) {
|
|
1325
|
+
function hypothesisDetail(result, includeScopeCaveat = true) {
|
|
1131
1326
|
const statement = safeInline(result?.statement, HYPOTHESIS_STATEMENT_MAX);
|
|
1132
1327
|
const evidenceReading = safeInline(result?.evidenceReading, HYPOTHESIS_NOTE_MAX);
|
|
1133
1328
|
return ((statement ? `\n ${HYPOTHESIS_LABEL}: "${statement}"` : '') +
|
|
1134
|
-
scopeCaveatDetail(result) +
|
|
1329
|
+
(includeScopeCaveat ? scopeCaveatDetail(result) : '') +
|
|
1330
|
+
reliabilityDetail(result) +
|
|
1135
1331
|
(evidenceReading ? `\n ${EVIDENCE_READING_LABEL}: ${evidenceReading}` : ''));
|
|
1136
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* The FUL-358 reliability line — how much to trust this verdict, and how much evidence is
|
|
1335
|
+
* behind it.
|
|
1336
|
+
*
|
|
1337
|
+
* ## Why this section renders a reliability at all, having settled not to render `confidence`
|
|
1338
|
+
*
|
|
1339
|
+
* FUL-614 deliberately withheld the numeric `confidence`: it was model-assigned, and printing
|
|
1340
|
+
* it beside the code-derived survival count invited a reader to average two things that are not
|
|
1341
|
+
* comparable. That reasoning holds and this does not contradict it — `final.confidence` is
|
|
1342
|
+
* itself CODE-DERIVED, from the evidence the row carries and from what these very re-asks did.
|
|
1343
|
+
* It is the same KIND of value as the survival count beside it, which is exactly what the
|
|
1344
|
+
* withheld number was not. The retired numeric field stays withheld.
|
|
1345
|
+
*
|
|
1346
|
+
* Values come from a closed enum, so they cannot forge a line and need no `safeInline`; an
|
|
1347
|
+
* unrecognised value emits nothing rather than being quoted through. A legacy row has no block
|
|
1348
|
+
* and emits nothing — its numeric `confidence` is a different measure and is not restated here.
|
|
1349
|
+
*/
|
|
1350
|
+
function reliabilityDetail(result) {
|
|
1351
|
+
const final = readFinalHypothesisState(result);
|
|
1352
|
+
if (!final.fromContract)
|
|
1353
|
+
return '';
|
|
1354
|
+
const { confidence, sufficiency, confidenceBasis: basis } = final;
|
|
1355
|
+
const reliability = confidence
|
|
1356
|
+
? `\`${confidence}\``
|
|
1357
|
+
: basis === 'withheld_no_evidence'
|
|
1358
|
+
? 'WITHHELD — the run attached no evidence to this hypothesis'
|
|
1359
|
+
: null;
|
|
1360
|
+
if (!reliability && !sufficiency)
|
|
1361
|
+
return '';
|
|
1362
|
+
const parts = [
|
|
1363
|
+
reliability ? `${RELIABILITY_LABEL}: ${reliability}` : null,
|
|
1364
|
+
sufficiency ? `evidence ${sufficiency}` : null,
|
|
1365
|
+
].filter(Boolean);
|
|
1366
|
+
return `\n ${parts.join('; ')} (reliability of the verdict, NOT the probability the hypothesis is true)`;
|
|
1367
|
+
}
|
|
1137
1368
|
/** The exact quoted/labelled FUL-614 shape, shared by robust and caveat-only rows. */
|
|
1138
1369
|
function scopeCaveatDetail(result) {
|
|
1139
1370
|
const scopeCaveat = safeInline(result?.scopeCaveat, HYPOTHESIS_NOTE_MAX);
|
|
1140
1371
|
return scopeCaveat ? `\n ${SCOPE_CAVEAT_LABEL}: "${scopeCaveat}"` : '';
|
|
1141
1372
|
}
|
|
1373
|
+
/** A present, non-null value records an attempted re-test even when its shape is unreadable. */
|
|
1374
|
+
function hasRawRobustnessAttempt(result) {
|
|
1375
|
+
return (result !== null &&
|
|
1376
|
+
Object.prototype.hasOwnProperty.call(result, 'robustness') &&
|
|
1377
|
+
result.robustness !== null &&
|
|
1378
|
+
result.robustness !== undefined);
|
|
1379
|
+
}
|
|
1142
1380
|
/**
|
|
1143
1381
|
* Scope caveats for hypotheses that do not have a robustness row (FUL-626).
|
|
1144
1382
|
*
|
|
@@ -1157,7 +1395,7 @@ function scopeCaveatDetail(result) {
|
|
|
1157
1395
|
function renderUnretestedScopeCaveats(reportData) {
|
|
1158
1396
|
const lines = asArray(reportData.hypothesisResults).flatMap((raw) => {
|
|
1159
1397
|
const result = asRecord(raw);
|
|
1160
|
-
if (!result ||
|
|
1398
|
+
if (!result || hasRawRobustnessAttempt(result))
|
|
1161
1399
|
return [];
|
|
1162
1400
|
const detail = scopeCaveatDetail(result);
|
|
1163
1401
|
if (!detail)
|
|
@@ -1340,13 +1578,16 @@ function renderRobustness(reportData, channel) {
|
|
|
1340
1578
|
const results = asArray(reportData.hypothesisResults);
|
|
1341
1579
|
if (results.length === 0)
|
|
1342
1580
|
return '';
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1581
|
+
const renderable = results.filter((raw) => {
|
|
1582
|
+
const result = asRecord(raw);
|
|
1583
|
+
return hasRawRobustnessAttempt(result) || readFinalHypothesisState(result).fromContract;
|
|
1584
|
+
});
|
|
1585
|
+
if (renderable.length === 0) {
|
|
1345
1586
|
return ('### Hypothesis robustness — NOT RE-TESTED on this run\n' +
|
|
1346
1587
|
'No verdict was re-asked under a reworded framing, so no verdict above carries a survival ' +
|
|
1347
1588
|
'count. Treat each as a single-shot answer.');
|
|
1348
1589
|
}
|
|
1349
|
-
const shown =
|
|
1590
|
+
const shown = renderable.slice(0, LIST_RENDER_CAP);
|
|
1350
1591
|
const lines = shown.map((raw) => {
|
|
1351
1592
|
const result = asRecord(raw);
|
|
1352
1593
|
// FUL-253: a `- ` row whose whole point is the ⚠️ FLIPPED warning. A newline
|
|
@@ -1356,43 +1597,62 @@ function renderRobustness(reportData, channel) {
|
|
|
1356
1597
|
const id = renderId(result?.hypothesisId);
|
|
1357
1598
|
const status = safeInline(result?.status, 40) ?? 'unknown';
|
|
1358
1599
|
const robustness = asRecord(result?.robustness);
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
1363
|
-
const count =
|
|
1600
|
+
const counts = readRobustnessCounts(robustness);
|
|
1601
|
+
const reading = readFinalHypothesisState(result);
|
|
1602
|
+
const outcome = reading.robustness;
|
|
1603
|
+
const effectiveVerdict = reading.verdict ?? status;
|
|
1604
|
+
const count = counts ? `held ${counts.survived}/${counts.total} framings` : 'survival count unavailable';
|
|
1364
1605
|
// Appended to EVERY branch, not just the flipped one. A held verdict is the row a
|
|
1365
1606
|
// reader is most likely to act on unexamined, so it is the last one that should be
|
|
1366
1607
|
// identified by an id alone.
|
|
1367
|
-
|
|
1368
|
-
|
|
1608
|
+
// A single-shot row's caveat stays in the dedicated un-retested-caveats block below rather
|
|
1609
|
+
// than being duplicated here. Its statement, reliability and evidence reading still belong
|
|
1610
|
+
// on this final-state row.
|
|
1611
|
+
const detail = hypothesisDetail(result, outcome !== 'not_retested');
|
|
1612
|
+
if (outcome === 'not_retested') {
|
|
1613
|
+
return `- ${id}: final verdict \`${effectiveVerdict}\` — NOT RE-TESTED under rephrasing; treat this verdict as a single-shot answer.${detail}`;
|
|
1614
|
+
}
|
|
1615
|
+
if (outcome === 'flipped') {
|
|
1616
|
+
const verdictInstruction = effectiveVerdict === status
|
|
1617
|
+
? `The final verdict remains \`${effectiveVerdict}\`; the flip lowers its reliability.`
|
|
1618
|
+
: `The verdict to act on is \`${effectiveVerdict}\`, NOT \`${status}\`.`;
|
|
1369
1619
|
return (`- ${id}: reported \`${status}\` — ⚠️ FLIPPED under rephrasing (${count}). ` +
|
|
1370
|
-
|
|
1620
|
+
verdictInstruction +
|
|
1371
1621
|
detail);
|
|
1372
1622
|
}
|
|
1373
1623
|
// An unreadable `flipped` is NOT "held" — say so rather than printing the
|
|
1374
1624
|
// verdict as if it had survived re-asking.
|
|
1375
|
-
if (
|
|
1376
|
-
|
|
1625
|
+
if (outcome === null) {
|
|
1626
|
+
const weakerVerdict = effectiveVerdict !== status
|
|
1627
|
+
? ` The verdict to act on is \`${effectiveVerdict}\`, NOT \`${status}\`.`
|
|
1628
|
+
: '';
|
|
1629
|
+
return `- ${id}: reported \`${status}\` — ⚠️ flip status UNREADABLE (${count}); the robustness outcome could not be read safely, so do not rely on it.${weakerVerdict}${detail}`;
|
|
1377
1630
|
}
|
|
1378
|
-
return `- ${id}: \`${
|
|
1631
|
+
return `- ${id}: \`${effectiveVerdict}\` — ${count}${detail}`;
|
|
1379
1632
|
});
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1633
|
+
const outcomes = results.map((raw) => {
|
|
1634
|
+
const result = asRecord(raw);
|
|
1635
|
+
return readFinalHypothesisState(result).robustness;
|
|
1636
|
+
});
|
|
1637
|
+
const flippedCount = outcomes.filter((outcome) => outcome === 'flipped').length;
|
|
1638
|
+
const unreadableFlips = outcomes.filter((outcome) => outcome === null).length;
|
|
1382
1639
|
// The un-retested hypotheses are counted against the FULL result set, not just
|
|
1383
1640
|
// the re-tested subset: a bare "no verdict flipped" over a partially-tested run
|
|
1384
1641
|
// reads as "every verdict survived", when most may never have been re-asked.
|
|
1385
|
-
const untested =
|
|
1642
|
+
const untested = outcomes.filter((outcome) => outcome === 'not_retested').length;
|
|
1643
|
+
const retested = results.length - untested;
|
|
1386
1644
|
const untestedNote = untested > 0
|
|
1387
|
-
? ` (${
|
|
1645
|
+
? ` (${retested}/${results.length} verdicts re-tested; the other ${untested} were NOT re-asked — treat those as single-shot)`
|
|
1388
1646
|
: '';
|
|
1389
|
-
const header =
|
|
1390
|
-
?
|
|
1391
|
-
:
|
|
1392
|
-
? `### Hypothesis robustness — ⚠️
|
|
1393
|
-
:
|
|
1647
|
+
const header = untested === results.length
|
|
1648
|
+
? '### Hypothesis robustness — NOT RE-TESTED on this run'
|
|
1649
|
+
: flippedCount > 0
|
|
1650
|
+
? `### Hypothesis robustness — ⚠️ ${flippedCount} verdict(s) flipped under rephrasing${untestedNote}`
|
|
1651
|
+
: unreadableFlips > 0
|
|
1652
|
+
? `### Hypothesis robustness — ⚠️ flip status UNREADABLE for ${unreadableFlips} verdict(s) (not a clean result)${untestedNote}`
|
|
1653
|
+
: `### Hypothesis robustness — no re-tested verdict flipped${untestedNote}`;
|
|
1394
1654
|
return (`${header}\n${lines.join('\n')}` +
|
|
1395
|
-
capNote(shown.length,
|
|
1655
|
+
capNote(shown.length, renderable.length, channel, 'report_data.hypothesisResults'));
|
|
1396
1656
|
}
|
|
1397
1657
|
// ---------------------------------------------------------------------------
|
|
1398
1658
|
// Model-authored framing — prose side of the proof separator (FUL-570)
|
|
@@ -1505,6 +1765,109 @@ function renderInsightCrossReferences(reportData) {
|
|
|
1505
1765
|
return ('## Insight source cross-references — model-authored findings, code-resolved destinations\n\n' +
|
|
1506
1766
|
rows.join('\n'));
|
|
1507
1767
|
}
|
|
1768
|
+
function resolveCrossCuttingEvidence(reportData, ref) {
|
|
1769
|
+
if (ref.kind === 'report_evidence') {
|
|
1770
|
+
const sourceId = safeId(ref.sourceId);
|
|
1771
|
+
const match = sourceId ? /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId) : null;
|
|
1772
|
+
const source = match ? asRecord(asArray(reportData.reportEvidence)[Number(match[1])]) : null;
|
|
1773
|
+
if (!source)
|
|
1774
|
+
return null;
|
|
1775
|
+
const label = safeInline(source.platform, MODEL_FRAMING_TEXT_MAX) || 'Web source';
|
|
1776
|
+
const href = safeReceiptUrl(source.url);
|
|
1777
|
+
const quote = safeInline(source.quote, RECEIPT_QUOTE_MAX);
|
|
1778
|
+
if (!href)
|
|
1779
|
+
return null;
|
|
1780
|
+
return `${label}${href ? ` — ${href}` : ''}${quote ? `\n > ${quote}` : ''}`;
|
|
1781
|
+
}
|
|
1782
|
+
if (ref.kind === 'forum_receipt') {
|
|
1783
|
+
const receiptId = safeId(ref.receiptId);
|
|
1784
|
+
if (!receiptId)
|
|
1785
|
+
return null;
|
|
1786
|
+
const matches = asArray(asRecord(reportData.forumResearch)?.threads).flatMap((threadRaw) => {
|
|
1787
|
+
const thread = asRecord(threadRaw);
|
|
1788
|
+
return asArray(thread?.receipts).flatMap((receiptRaw) => {
|
|
1789
|
+
const receipt = asRecord(receiptRaw);
|
|
1790
|
+
return safeId(receipt?.receiptId) === receiptId ? [{ thread, receipt }] : [];
|
|
1791
|
+
});
|
|
1792
|
+
});
|
|
1793
|
+
if (matches.length !== 1)
|
|
1794
|
+
return null;
|
|
1795
|
+
const match = matches[0];
|
|
1796
|
+
const label = safeInline(match.thread?.platform, MODEL_FRAMING_TEXT_MAX) || 'Community source';
|
|
1797
|
+
const href = safeReceiptUrl(match.thread?.url);
|
|
1798
|
+
const quote = safeInline(match.receipt?.excerpt, RECEIPT_QUOTE_MAX);
|
|
1799
|
+
if (!quote)
|
|
1800
|
+
return null;
|
|
1801
|
+
return `${label}${href ? ` — ${href}` : ''}${quote ? `\n > ${quote}` : ''}`;
|
|
1802
|
+
}
|
|
1803
|
+
if (ref.kind === 'interview_quote_v2') {
|
|
1804
|
+
const interviewId = safeId(ref.interviewId);
|
|
1805
|
+
const quoteId = safeId(ref.quoteId);
|
|
1806
|
+
if (!interviewId || !quoteId)
|
|
1807
|
+
return null;
|
|
1808
|
+
const matches = asArray(reportData.interviewHighlights).flatMap((highlightRaw) => {
|
|
1809
|
+
const highlight = asRecord(highlightRaw);
|
|
1810
|
+
const evidence = asRecord(highlight?.evidenceReference);
|
|
1811
|
+
if (safeId(evidence?.interviewId) !== interviewId)
|
|
1812
|
+
return [];
|
|
1813
|
+
return asArray(evidence?.quotes).flatMap((quoteRaw) => {
|
|
1814
|
+
const quote = asRecord(quoteRaw);
|
|
1815
|
+
return safeId(quote?.quoteId) === quoteId ? [{ highlight, quote }] : [];
|
|
1816
|
+
});
|
|
1817
|
+
});
|
|
1818
|
+
if (matches.length !== 1)
|
|
1819
|
+
return null;
|
|
1820
|
+
const match = matches[0];
|
|
1821
|
+
const persona = safeInline(match.highlight?.personaName, MODEL_FRAMING_TEXT_MAX) || 'Persona';
|
|
1822
|
+
const quote = safeInline(match.quote?.text, RECEIPT_QUOTE_MAX);
|
|
1823
|
+
return quote ? `Synthetic interview · ${persona}\n > ${quote}` : null;
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
function renderCrossCuttingFindings(reportData) {
|
|
1828
|
+
if (!Object.prototype.hasOwnProperty.call(reportData, 'crossCuttingFindings'))
|
|
1829
|
+
return '';
|
|
1830
|
+
const rows = asArray(reportData.crossCuttingFindings).slice(0, 5).flatMap((rawFinding) => {
|
|
1831
|
+
const finding = asRecord(rawFinding);
|
|
1832
|
+
const claim = safeInline(finding?.claim, MODEL_FRAMING_TEXT_MAX);
|
|
1833
|
+
const why = safeInline(finding?.whyItMatters, MODEL_FRAMING_TEXT_MAX);
|
|
1834
|
+
const confidence = safeInline(finding?.confidence, 12);
|
|
1835
|
+
if (!claim || !why || !confidence || !['low', 'medium', 'high'].includes(confidence))
|
|
1836
|
+
return [];
|
|
1837
|
+
const rawRefs = asArray(finding?.evidenceRefs).slice(0, 8);
|
|
1838
|
+
const resolvedRefs = rawRefs.flatMap((rawRef) => {
|
|
1839
|
+
const ref = asRecord(rawRef);
|
|
1840
|
+
const rendered = ref ? resolveCrossCuttingEvidence(reportData, ref) : null;
|
|
1841
|
+
return ref && rendered ? [{ ref, rendered }] : [];
|
|
1842
|
+
});
|
|
1843
|
+
const methods = new Set(resolvedRefs.flatMap(({ ref }) => ref.kind === 'report_evidence' ? ['web']
|
|
1844
|
+
: ref.kind === 'forum_receipt' ? ['forum']
|
|
1845
|
+
: ref.kind === 'interview_quote_v2' ? ['interview'] : []));
|
|
1846
|
+
const uniqueRefs = new Set(resolvedRefs.map(({ ref }) => JSON.stringify(ref)));
|
|
1847
|
+
if (rawRefs.length < 2 || resolvedRefs.length !== rawRefs.length || uniqueRefs.size < 2 || methods.size < 2)
|
|
1848
|
+
return [];
|
|
1849
|
+
const evidence = resolvedRefs.map(({ rendered }) => ` - Evidence: ${rendered}`);
|
|
1850
|
+
return [[
|
|
1851
|
+
`- ${claim}`,
|
|
1852
|
+
` - Why it matters: ${why}`,
|
|
1853
|
+
` - Confidence: ${confidence}`,
|
|
1854
|
+
...evidence,
|
|
1855
|
+
].join('\n')];
|
|
1856
|
+
});
|
|
1857
|
+
return rows.length > 0
|
|
1858
|
+
? `## Cross-cutting findings\n\n${rows.join('\n')}`
|
|
1859
|
+
: '## Cross-cutting findings\n\nNo cross-cutting finding met the evidence bar.';
|
|
1860
|
+
}
|
|
1861
|
+
function removeLegacyInsightOutput(markdown) {
|
|
1862
|
+
return markdown
|
|
1863
|
+
.replace(/^## (?:Key )?Insights(?: \(legacy\))?\s*\n[\s\S]*?(?=^## |$(?![\s\S]))/gm, '')
|
|
1864
|
+
.replace(/^## Cross-cutting findings\s*\n[\s\S]*?(?=^## |$(?![\s\S]))/gm, '')
|
|
1865
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1866
|
+
.trimEnd();
|
|
1867
|
+
}
|
|
1868
|
+
function labelLegacyInsightOutput(markdown) {
|
|
1869
|
+
return markdown.replace(/^## (?:Key )?Insights$/gm, '## Insights (legacy)');
|
|
1870
|
+
}
|
|
1508
1871
|
/**
|
|
1509
1872
|
* The approved model-authored context block.
|
|
1510
1873
|
*
|
|
@@ -1667,48 +2030,26 @@ function renderMarketSizing(reportData) {
|
|
|
1667
2030
|
const market = asRecord(asRecord(reportData.webResearch)?.marketData);
|
|
1668
2031
|
if (!market)
|
|
1669
2032
|
return '';
|
|
1670
|
-
const raw = asArray(market.marketSizing);
|
|
1671
2033
|
const reportClaims = asArray(reportData.reportClaims).map(asRecord).filter((v) => v !== null);
|
|
1672
2034
|
const reportEvidence = asArray(reportData.reportEvidence);
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const lines =
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
if (entry.evidenceState === 'source_stated') {
|
|
1690
|
-
return `- ${labels[scope]}: ${currency} ${value} [Cited] — ${geography}; ${audience}; ${period}`;
|
|
1691
|
-
}
|
|
1692
|
-
const derivation = asRecord(entry.derivation);
|
|
1693
|
-
const inputs = asArray(derivation?.inputs).map(asRecord);
|
|
1694
|
-
const formula = safeInline(derivation?.formula, 240);
|
|
1695
|
-
if (entry.evidenceState !== 'derived' || derivation?.operation !== 'multiply' || !formula || inputs.length < 2 || inputs.some((input) => !input)) {
|
|
1696
|
-
return `- ${labels[scope]}: — (Not established)`;
|
|
1697
|
-
}
|
|
1698
|
-
const validInputs = inputs.filter((input) => input !== null);
|
|
1699
|
-
const sameBoundary = validInputs.every((input) => input.kind === 'ratio_assumption' || (input.period === entry.period && input.geography === entry.geography && input.audience === entry.audience));
|
|
1700
|
-
const inputValues = validInputs.map((input) => num(input.value));
|
|
1701
|
-
const oneCurrency = validInputs.filter((input) => input.kind === 'currency' && input.currency === currency).length === 1;
|
|
1702
|
-
const product = inputValues.every((input) => input !== null)
|
|
1703
|
-
? inputValues.reduce((total, input) => total * input, 1)
|
|
1704
|
-
: Number.NaN;
|
|
1705
|
-
if (!sameBoundary || !oneCurrency || !Number.isFinite(product) || Math.abs(product - value) > Math.max(0.01, value * 1e-9)) {
|
|
1706
|
-
return `- ${labels[scope]}: — (Not established)`;
|
|
1707
|
-
}
|
|
1708
|
-
const assumptions = asArray(derivation.assumptions).map((item) => safeInline(item, 160)).filter(Boolean);
|
|
1709
|
-
return `- ${labels[scope]}: ${currency} ${value} [Estimate / Inferred] — ${geography}; ${audience}; ${period}; method: ${formula}${assumptions.length ? `; assumptions: ${assumptions.join('; ')}` : ''}`;
|
|
2035
|
+
const signals = resolveMarketSignals(market.marketSignals, asArray(market.marketSizing), reportClaims, reportEvidence);
|
|
2036
|
+
if (signals.length === 0)
|
|
2037
|
+
return '### Market signals\nNo cited market signals were established.';
|
|
2038
|
+
const lines = signals.map((signal) => {
|
|
2039
|
+
const context = safeInline(signal.context, 360) ?? 'Context unavailable';
|
|
2040
|
+
const sources = (signal.sources ?? (signal.source ? [signal.source] : []))
|
|
2041
|
+
.map((source) => safeInline(source.url, 240))
|
|
2042
|
+
.filter((url) => Boolean(url));
|
|
2043
|
+
const citations = sources.length > 0 ? ` ← ${sources.join(', ')}` : '';
|
|
2044
|
+
if (signal.kind !== 'size')
|
|
2045
|
+
return `- ${signal.label}: ${context} [Cited]${citations}`;
|
|
2046
|
+
const formula = safeInline(signal.formula, 240);
|
|
2047
|
+
const assumptions = (signal.assumptions ?? []).map((item) => safeInline(item, 160)).filter(Boolean);
|
|
2048
|
+
return `- ${signal.label}: ${signal.currency} ${signal.value} — ${context}` +
|
|
2049
|
+
(formula ? ` [Estimate / Inferred]; method: ${formula}${assumptions.length ? `; assumptions: ${assumptions.join('; ')}` : ''}` : ' [Cited]') +
|
|
2050
|
+
citations;
|
|
1710
2051
|
});
|
|
1711
|
-
return `###
|
|
2052
|
+
return `### Market signals\n${lines.join('\n')}`;
|
|
1712
2053
|
}
|
|
1713
2054
|
/**
|
|
1714
2055
|
* The Reddit community-evidence rail's typed outcome (FUL-685 / T8, D6).
|
|
@@ -1923,6 +2264,7 @@ export function buildTrustDigest(reportData, channel) {
|
|
|
1923
2264
|
const sections = [
|
|
1924
2265
|
renderPersonaSpine(data, channel),
|
|
1925
2266
|
renderReportSpine(data, channel),
|
|
2267
|
+
renderOverviewTrust(data),
|
|
1926
2268
|
renderMarketSizing(data),
|
|
1927
2269
|
renderCompetitorSourceQuality(data),
|
|
1928
2270
|
// FUL-685: immediately above the pool the accepted rows land in, so a reader meets the
|
|
@@ -2015,11 +2357,67 @@ export function composeReportText(reportMarkdown, reportData, channel) {
|
|
|
2015
2357
|
const rationale = renderVerdictRationale(reportData);
|
|
2016
2358
|
const framing = renderModelFraming(reportData);
|
|
2017
2359
|
const data = asRecord(reportData);
|
|
2018
|
-
const
|
|
2360
|
+
const markdownTitle = reportMarkdown.startsWith('# ')
|
|
2361
|
+
? reportMarkdown.slice(2, reportMarkdown.indexOf('\n') >= 0 ? reportMarkdown.indexOf('\n') : undefined)
|
|
2362
|
+
: undefined;
|
|
2363
|
+
const safeTitle = data?.reportTitle === undefined
|
|
2364
|
+
? normalizeLegacyMcpHeading(markdownTitle)
|
|
2365
|
+
: escapeMcpMarkdownHeading(sanitizeMcpReportTitle(data.reportTitle));
|
|
2366
|
+
const titledReportMarkdown = reportMarkdown.startsWith('# ')
|
|
2367
|
+
? `# ${safeTitle}${reportMarkdown.includes('\n') ? reportMarkdown.slice(reportMarkdown.indexOf('\n')) : ''}`
|
|
2368
|
+
: reportMarkdown;
|
|
2369
|
+
const hasCrossCutting = Boolean(data && Object.prototype.hasOwnProperty.call(data, 'crossCuttingFindings'));
|
|
2370
|
+
const safeReportMarkdown = hasCrossCutting
|
|
2371
|
+
? removeLegacyInsightOutput(titledReportMarkdown)
|
|
2372
|
+
: labelLegacyInsightOutput(titledReportMarkdown);
|
|
2373
|
+
const insightReferences = data && !hasCrossCutting ? renderInsightCrossReferences(data) : '';
|
|
2374
|
+
const crossCutting = data && hasCrossCutting ? renderCrossCuttingFindings(data) : '';
|
|
2375
|
+
// Community themes, sentiment, and source titles are producer/source-authored framing, not
|
|
2376
|
+
// machine-checked claims. Keep them above the authority separator while preserving the literal
|
|
2377
|
+
// Theme → Sentiment → Discussion contract for every full-report caller.
|
|
2378
|
+
const communityPatterns = data && !hasCurrentCommunityPatternRows(safeReportMarkdown, data)
|
|
2379
|
+
? renderCommunityPatterns(data, channel)
|
|
2380
|
+
: '';
|
|
2019
2381
|
// FUL-586: the completeness banner goes FIRST — before the prose it is a caveat about.
|
|
2020
2382
|
const gaps = renderReportGaps(reportData);
|
|
2021
2383
|
return (`${gaps ? `${gaps}\n\n---\n\n` : ''}` +
|
|
2022
|
-
`${
|
|
2384
|
+
`${safeReportMarkdown}${insightReferences ? `\n\n${insightReferences}` : ''}${crossCutting ? `\n\n${crossCutting}` : ''}${communityPatterns ? `\n\n${communityPatterns}` : ''}${framing ? `\n\n${framing}` : ''}${rationale ? `\n\n${rationale}` : ''}` +
|
|
2023
2385
|
`\n\n---\n\n${buildTrustDigest(reportData, channel)}`);
|
|
2024
2386
|
}
|
|
2387
|
+
/** Keep a model-authored structured title inert when replacing the report Markdown H1. */
|
|
2388
|
+
function escapeMcpMarkdownHeading(value) {
|
|
2389
|
+
if (/(?:\b[a-z][a-z0-9+.-]{1,31}:\/\/|\bwww\.|[^\s@]+@[^\s@]+)/i.test(value)) {
|
|
2390
|
+
const longestRun = (value.match(/`+/g) ?? []).reduce((n, run) => Math.max(n, run.length), 0);
|
|
2391
|
+
const fence = '`'.repeat(longestRun + 1);
|
|
2392
|
+
const pad = value.startsWith('`') || value.endsWith('`') ? ' ' : '';
|
|
2393
|
+
return `${fence}${pad}${value}${pad}${fence}`;
|
|
2394
|
+
}
|
|
2395
|
+
return value
|
|
2396
|
+
.replace(/[\\`*_[\]~&]/g, '\\$&')
|
|
2397
|
+
.replace(/<(?=[a-zA-Z/?!])/g, '\\<')
|
|
2398
|
+
.replace(/#+$/, (run) => run.replace(/#/g, '\\#'));
|
|
2399
|
+
}
|
|
2400
|
+
/** Normalize producer-escaped legacy H1 text before applying the current inert-heading rules. */
|
|
2401
|
+
function normalizeLegacyMcpHeading(value) {
|
|
2402
|
+
if (typeof value !== 'string')
|
|
2403
|
+
return 'Report';
|
|
2404
|
+
const serialized = value
|
|
2405
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, ' ')
|
|
2406
|
+
.replace(/\s+/g, ' ')
|
|
2407
|
+
.trim();
|
|
2408
|
+
if (!serialized)
|
|
2409
|
+
return 'Report';
|
|
2410
|
+
const openingFence = /^`+/.exec(serialized)?.[0];
|
|
2411
|
+
const closingFence = /`+$/.exec(serialized)?.[0];
|
|
2412
|
+
if (openingFence && closingFence?.length === openingFence.length && serialized.length >= openingFence.length * 2) {
|
|
2413
|
+
const inner = serialized.slice(openingFence.length, -openingFence.length);
|
|
2414
|
+
const innerRuns = inner.match(/`+/g) ?? [];
|
|
2415
|
+
if (innerRuns.every((run) => run.length < openingFence.length)) {
|
|
2416
|
+
const visible = /^ .* $/.test(inner) && /\S/.test(inner) ? inner.slice(1, -1) : inner;
|
|
2417
|
+
return sanitizeMcpMarkdownTitle(visible) === visible ? serialized : 'Report';
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
const plain = serialized.replace(/\\([\\`*_[\]~&<#])/g, '$1');
|
|
2421
|
+
return escapeMcpMarkdownHeading(sanitizeMcpMarkdownTitle(plain));
|
|
2422
|
+
}
|
|
2025
2423
|
//# sourceMappingURL=report-digest.js.map
|