@clien-ai/mcp 0.12.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/README.md +1 -1
- package/dist/tools/collections.js +183 -68
- package/dist/tools/collections.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 +41 -33
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/report-digest.js +355 -45
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/scoped-research.js +51 -53
- package/dist/tools/scoped-research.js.map +1 -1
- package/dist/types/report.js +385 -21
- 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
|
@@ -50,8 +50,10 @@ import { collapseWhitespace, safeInline, safeId, clipReceiptQuote, containsSpan,
|
|
|
50
50
|
import { canonicalContentType, contentTypeDeviationLine, contentTypeProvenanceLine, } from './content-type-display.js';
|
|
51
51
|
import { readFinalHypothesisState } from './hypothesis-final.js';
|
|
52
52
|
import { readRobustnessCounts } from '../hypothesis-semantics.js';
|
|
53
|
-
import { resolveSourceReceipt } from './receipt-children.js';
|
|
54
|
-
import {
|
|
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';
|
|
55
57
|
/**
|
|
56
58
|
* Max claims rendered per spine. Above this the digest states how many were
|
|
57
59
|
* dropped and where the full list lives — never a silent cut. Sized so a typical
|
|
@@ -307,12 +309,18 @@ function capNote(shown, total, channel, path) {
|
|
|
307
309
|
return '';
|
|
308
310
|
return `\n_(showing ${shown} of ${total} — the remaining ${total - shown} are in \`${channel}.${path}\`)_`;
|
|
309
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
|
+
}
|
|
310
318
|
/** Return the normalized URL only when it is a safe, non-credentialed web receipt. */
|
|
311
319
|
function safeReceiptUrl(raw) {
|
|
312
|
-
if (typeof raw !== 'string' ||
|
|
320
|
+
if (typeof raw !== 'string' || !/^https?:\/\//i.test(raw))
|
|
313
321
|
return null;
|
|
314
322
|
try {
|
|
315
|
-
const url = new URL(raw
|
|
323
|
+
const url = new URL(raw);
|
|
316
324
|
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password) {
|
|
317
325
|
return null;
|
|
318
326
|
}
|
|
@@ -322,6 +330,112 @@ function safeReceiptUrl(raw) {
|
|
|
322
330
|
return null;
|
|
323
331
|
}
|
|
324
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
|
+
}
|
|
325
439
|
/**
|
|
326
440
|
* Resolve the single, persona-owned presentation grant behind a GROUNDED claim.
|
|
327
441
|
*
|
|
@@ -1012,8 +1126,14 @@ function renderPersonas(reportData, channel) {
|
|
|
1012
1126
|
const seniority = safeInline(priors?.seniority, 80);
|
|
1013
1127
|
const careerPath = safeInline(priors?.careerPath, 80);
|
|
1014
1128
|
const provenance = safeInline(persona?.identityProvenance, 200);
|
|
1129
|
+
const hasFrozenPortrait = REPORT_PORTRAIT_PATHS.includes(persona?.portrait);
|
|
1015
1130
|
const bits = [];
|
|
1016
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');
|
|
1017
1137
|
if (insufficient) {
|
|
1018
1138
|
bits.push(`⚠️ insufficientEvidence${sourcesFound !== null ? ` (only ${sourcesFound} real posts found)` : ''}`);
|
|
1019
1139
|
}
|
|
@@ -1051,6 +1171,58 @@ function renderPersonas(reportData, channel) {
|
|
|
1051
1171
|
return (`${header}\n${lines.join('\n')}` +
|
|
1052
1172
|
capNote(shown.length, personas.length, channel, 'report_data.personas'));
|
|
1053
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
|
+
}
|
|
1054
1226
|
/**
|
|
1055
1227
|
* The anti-sycophancy readout — deterministically computed from the interviews,
|
|
1056
1228
|
* not model-opined, which is exactly why it belongs in the text: it is the one
|
|
@@ -1593,6 +1765,109 @@ function renderInsightCrossReferences(reportData) {
|
|
|
1593
1765
|
return ('## Insight source cross-references — model-authored findings, code-resolved destinations\n\n' +
|
|
1594
1766
|
rows.join('\n'));
|
|
1595
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
|
+
}
|
|
1596
1871
|
/**
|
|
1597
1872
|
* The approved model-authored context block.
|
|
1598
1873
|
*
|
|
@@ -1755,48 +2030,26 @@ function renderMarketSizing(reportData) {
|
|
|
1755
2030
|
const market = asRecord(asRecord(reportData.webResearch)?.marketData);
|
|
1756
2031
|
if (!market)
|
|
1757
2032
|
return '';
|
|
1758
|
-
const raw = asArray(market.marketSizing);
|
|
1759
2033
|
const reportClaims = asArray(reportData.reportClaims).map(asRecord).filter((v) => v !== null);
|
|
1760
2034
|
const reportEvidence = asArray(reportData.reportEvidence);
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
const lines =
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
if (entry.evidenceState === 'source_stated') {
|
|
1778
|
-
return `- ${labels[scope]}: ${currency} ${value} [Cited] — ${geography}; ${audience}; ${period}`;
|
|
1779
|
-
}
|
|
1780
|
-
const derivation = asRecord(entry.derivation);
|
|
1781
|
-
const inputs = asArray(derivation?.inputs).map(asRecord);
|
|
1782
|
-
const formula = safeInline(derivation?.formula, 240);
|
|
1783
|
-
if (entry.evidenceState !== 'derived' || derivation?.operation !== 'multiply' || !formula || inputs.length < 2 || inputs.some((input) => !input)) {
|
|
1784
|
-
return `- ${labels[scope]}: — (Not established)`;
|
|
1785
|
-
}
|
|
1786
|
-
const validInputs = inputs.filter((input) => input !== null);
|
|
1787
|
-
const sameBoundary = validInputs.every((input) => input.kind === 'ratio_assumption' || (input.period === entry.period && input.geography === entry.geography && input.audience === entry.audience));
|
|
1788
|
-
const inputValues = validInputs.map((input) => num(input.value));
|
|
1789
|
-
const oneCurrency = validInputs.filter((input) => input.kind === 'currency' && input.currency === currency).length === 1;
|
|
1790
|
-
const product = inputValues.every((input) => input !== null)
|
|
1791
|
-
? inputValues.reduce((total, input) => total * input, 1)
|
|
1792
|
-
: Number.NaN;
|
|
1793
|
-
if (!sameBoundary || !oneCurrency || !Number.isFinite(product) || Math.abs(product - value) > Math.max(0.01, value * 1e-9)) {
|
|
1794
|
-
return `- ${labels[scope]}: — (Not established)`;
|
|
1795
|
-
}
|
|
1796
|
-
const assumptions = asArray(derivation.assumptions).map((item) => safeInline(item, 160)).filter(Boolean);
|
|
1797
|
-
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;
|
|
1798
2051
|
});
|
|
1799
|
-
return `###
|
|
2052
|
+
return `### Market signals\n${lines.join('\n')}`;
|
|
1800
2053
|
}
|
|
1801
2054
|
/**
|
|
1802
2055
|
* The Reddit community-evidence rail's typed outcome (FUL-685 / T8, D6).
|
|
@@ -2011,6 +2264,7 @@ export function buildTrustDigest(reportData, channel) {
|
|
|
2011
2264
|
const sections = [
|
|
2012
2265
|
renderPersonaSpine(data, channel),
|
|
2013
2266
|
renderReportSpine(data, channel),
|
|
2267
|
+
renderOverviewTrust(data),
|
|
2014
2268
|
renderMarketSizing(data),
|
|
2015
2269
|
renderCompetitorSourceQuality(data),
|
|
2016
2270
|
// FUL-685: immediately above the pool the accepted rows land in, so a reader meets the
|
|
@@ -2103,11 +2357,67 @@ export function composeReportText(reportMarkdown, reportData, channel) {
|
|
|
2103
2357
|
const rationale = renderVerdictRationale(reportData);
|
|
2104
2358
|
const framing = renderModelFraming(reportData);
|
|
2105
2359
|
const data = asRecord(reportData);
|
|
2106
|
-
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
|
+
: '';
|
|
2107
2381
|
// FUL-586: the completeness banner goes FIRST — before the prose it is a caveat about.
|
|
2108
2382
|
const gaps = renderReportGaps(reportData);
|
|
2109
2383
|
return (`${gaps ? `${gaps}\n\n---\n\n` : ''}` +
|
|
2110
|
-
`${
|
|
2384
|
+
`${safeReportMarkdown}${insightReferences ? `\n\n${insightReferences}` : ''}${crossCutting ? `\n\n${crossCutting}` : ''}${communityPatterns ? `\n\n${communityPatterns}` : ''}${framing ? `\n\n${framing}` : ''}${rationale ? `\n\n${rationale}` : ''}` +
|
|
2111
2385
|
`\n\n---\n\n${buildTrustDigest(reportData, channel)}`);
|
|
2112
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
|
+
}
|
|
2113
2423
|
//# sourceMappingURL=report-digest.js.map
|