@clien-ai/mcp 0.13.0 → 0.13.2

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.
@@ -47,13 +47,14 @@
47
47
  * rule, which a reader applies positionally.
48
48
  */
49
49
  import { collapseWhitespace, safeInline, safeId, clipReceiptQuote, containsSpan, hasVisibleContent, RECEIPT_QUOTE_MAX, } from './render-safety.js';
50
- import { canonicalContentType, contentTypeDeviationLine, contentTypeProvenanceLine, } from './content-type-display.js';
50
+ import { canonicalContentType, contentTypeDeviationLine, contentTypeProvenanceLine, hypothesisContentTypeProvenanceLine, } from './content-type-display.js';
51
51
  import { readFinalHypothesisState } from './hypothesis-final.js';
52
- import { readRobustnessCounts } from '../hypothesis-semantics.js';
52
+ import { finalClauseCoverageMatchesIntegrity, readRobustnessCounts, reportClaimNeedsIndependentMarketAuthority, sourceAuthenticatesMarketEconomics, } from '../hypothesis-semantics.js';
53
53
  import { parseReceiptId, resolveSourceReceipt } from './receipt-children.js';
54
54
  import { resolveMarketSignals } from './market-signals.js';
55
55
  import { MCP_OVERVIEW_REF_CAP, MCP_OVERVIEW_SPAN_CAP, REPORT_PORTRAIT_PATHS, sanitizeMcpMarkdownTitle, sanitizeMcpReportTitle, } from '../types/report.js';
56
56
  import { readSemanticTheme } from '../types/semantic-theme.js';
57
+ import { isDisplaySuitableExcerpt } from '../lib/excerpt-suitability.js';
57
58
  /**
58
59
  * Max claims rendered per spine. Above this the digest states how many were
59
60
  * dropped and where the full list lives — never a silent cut. Sized so a typical
@@ -352,7 +353,10 @@ function resolveOverviewRef(ref, claimSpan, reportEvidence, forumThreads) {
352
353
  const source = asRecord(reportEvidence[Number(match[1])]);
353
354
  const href = safeReceiptUrl(source?.url);
354
355
  const quote = str(source?.quote);
355
- if (!source || !href || !quote || !overviewReceiptSupportsClaim(claimSpan, quote))
356
+ if (!source || !href || !quote || !isDisplaySuitableExcerpt(quote) || !overviewReceiptSupportsClaim(claimSpan, quote))
357
+ return null;
358
+ if (reportClaimNeedsIndependentMarketAuthority('summary', claimSpan) &&
359
+ !sourceAuthenticatesMarketEconomics(source))
356
360
  return null;
357
361
  return `${sourceId} -> ${href}`;
358
362
  }
@@ -368,6 +372,7 @@ function resolveOverviewRef(ref, claimSpan, reportEvidence, forumThreads) {
368
372
  if (str(receipt?.receiptId) !== receiptId ||
369
373
  !excerpt ||
370
374
  !hasVisibleContent(excerpt) ||
375
+ !isDisplaySuitableExcerpt(excerpt) ||
371
376
  !overviewReceiptSupportsClaim(claimSpan, excerpt))
372
377
  return [];
373
378
  const href = safeReceiptUrl(thread?.url);
@@ -531,6 +536,13 @@ function resolvePersonaReceipt(rawClaim, personas) {
531
536
  // additive field would start changing what a legacy reader sees.
532
537
  return null;
533
538
  }
539
+ else if (Object.prototype.hasOwnProperty.call(source, 'quote') &&
540
+ !isDisplaySuitableExcerpt(source.quote)) {
541
+ // A legacy parent has no child identity to fall back to. Its exact cached quote is the only
542
+ // evidence the positional receipt authenticates, so an unsuitable quote withdraws the
543
+ // grounding rather than leaving a bare URL carrying a GROUNDED label.
544
+ return null;
545
+ }
534
546
  return {
535
547
  sourceId: `RCP-p${personaIndex}-s${sourceIndex}`,
536
548
  href,
@@ -662,7 +674,7 @@ function resolveReportReceipt(rawClaim, reportEvidence) {
662
674
  // non-empty string, clears the guard, and the claim earns `cited → RRCP-s0` plus an
663
675
  // `Attested window: ""` line. A citation marker over a window nobody can read is the
664
676
  // presentation this resolver exists to withhold.
665
- if (!hasVisibleContent(quote))
677
+ if (!hasVisibleContent(quote) || !isDisplaySuitableExcerpt(quote))
666
678
  return null;
667
679
  }
668
680
  else {
@@ -677,7 +689,12 @@ function resolveReportReceipt(rawClaim, reportEvidence) {
677
689
  if (!Number.isSafeInteger(sourceIndex))
678
690
  return null;
679
691
  const source = asRecord(reportEvidence[sourceIndex]);
680
- if (!source || !safeReceiptUrl(source.url))
692
+ if (!source ||
693
+ !safeReceiptUrl(source.url) ||
694
+ (kind === 'grounded' && !isDisplaySuitableExcerpt(source.quote)))
695
+ return null;
696
+ if (reportClaimNeedsIndependentMarketAuthority(claim?.section, claim?.text) &&
697
+ !sourceAuthenticatesMarketEconomics(source))
681
698
  return null;
682
699
  return { kind, sourceId, quote };
683
700
  }
@@ -1062,7 +1079,15 @@ function renderReceiptPools(reportData, channel) {
1062
1079
  // No `safeInline` and no FUL-253 concern: `canonicalContentType` admits three literal tokens
1063
1080
  // and nothing else, which is strictly stronger than flattening — a newline cannot survive a
1064
1081
  // set-membership test, so this field cannot forge a row.
1065
- return `- RRCP-s${n} (${section}) — ${platform} — ${published} — ${url}${contentTypeProvenanceLine(source?.contentType)}`;
1082
+ const sourceTypeLine = section === 'hypothesis'
1083
+ ? hypothesisContentTypeProvenanceLine(source?.contentType, source?.publisherSubjectRelationship)
1084
+ : contentTypeProvenanceLine(source?.contentType);
1085
+ const marketAuthority = source?.marketEconomicsAuthority === 'independent' ||
1086
+ source?.marketEconomicsAuthority === 'commercial_report_seller'
1087
+ ? source.marketEconomicsAuthority
1088
+ : 'unknown';
1089
+ const marketAuthorityLine = `\n market economics authority: ${marketAuthority}`;
1090
+ return `- RRCP-s${n} (${section}) — ${platform} — ${published} — ${url}${sourceTypeLine}${marketAuthorityLine}`;
1066
1091
  });
1067
1092
  if (personaReceipts.length === 0 && evidenceReceipts.length === 0) {
1068
1093
  return ('### Receipt pools — EMPTY\n' +
@@ -1198,30 +1223,21 @@ function renderCommunityPatterns(reportData, channel) {
1198
1223
  capNote(shown.length, threads.length, channel, 'report_data.forumResearch.threads'));
1199
1224
  }
1200
1225
  /**
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.
1226
+ * Persisted Markdown predates the display projection and may contain source excerpts verbatim.
1227
+ * Remove its entire discussions subsection before rebuilding the safe structured projection.
1204
1228
  */
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');
1229
+ function removeRelevantDiscussionsOutput(markdown) {
1230
+ const lines = markdown.split('\n');
1231
+ const heading = lines.findIndex((line) => /^###\s+Relevant Discussions\s*$/iu.test(line.trim()));
1211
1232
  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;
1233
+ return markdown;
1234
+ let end = heading + 1;
1235
+ while (end < lines.length && !/^#{1,3}\s+/u.test(lines[end] ?? ''))
1236
+ end += 1;
1237
+ return [...lines.slice(0, heading), ...lines.slice(end)]
1238
+ .join('\n')
1239
+ .replace(/\n{3,}/g, '\n\n')
1240
+ .trimEnd();
1225
1241
  }
1226
1242
  /**
1227
1243
  * The anti-sycophancy readout — deterministically computed from the interviews,
@@ -1752,7 +1768,9 @@ function renderInsightCrossReferences(reportData) {
1752
1768
  const match = sourceId ? /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId) : null;
1753
1769
  const index = match ? Number(match[1]) : Number.NaN;
1754
1770
  const source = Number.isSafeInteger(index) ? asRecord(evidence[index]) : null;
1755
- const href = source ? safeReceiptUrl(source.url) : null;
1771
+ const authorityEligible = !reportClaimNeedsIndependentMarketAuthority('summary', statement) ||
1772
+ sourceAuthenticatesMarketEconomics(source);
1773
+ const href = source && authorityEligible ? safeReceiptUrl(source.url) : null;
1756
1774
  return sourceId && href
1757
1775
  ? `${label} → ${sourceId} — ${href}`
1758
1776
  : `${label} (no resolvable receipt)`;
@@ -1765,16 +1783,24 @@ function renderInsightCrossReferences(reportData) {
1765
1783
  return ('## Insight source cross-references — model-authored findings, code-resolved destinations\n\n' +
1766
1784
  rows.join('\n'));
1767
1785
  }
1768
- function resolveCrossCuttingEvidence(reportData, ref) {
1786
+ function resolveCrossCuttingEvidence(reportData, ref, claim) {
1769
1787
  if (ref.kind === 'report_evidence') {
1770
1788
  const sourceId = safeId(ref.sourceId);
1771
1789
  const match = sourceId ? /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId) : null;
1772
1790
  const source = match ? asRecord(asArray(reportData.reportEvidence)[Number(match[1])]) : null;
1773
1791
  if (!source)
1774
1792
  return null;
1793
+ if (reportClaimNeedsIndependentMarketAuthority('summary', claim) &&
1794
+ !sourceAuthenticatesMarketEconomics(source))
1795
+ return null;
1775
1796
  const label = safeInline(source.platform, MODEL_FRAMING_TEXT_MAX) || 'Web source';
1776
1797
  const href = safeReceiptUrl(source.url);
1777
- const quote = safeInline(source.quote, RECEIPT_QUOTE_MAX);
1798
+ const hasQuote = Object.prototype.hasOwnProperty.call(source, 'quote');
1799
+ if (hasQuote && !isDisplaySuitableExcerpt(source.quote))
1800
+ return null;
1801
+ const quote = hasQuote
1802
+ ? safeInline(source.quote, RECEIPT_QUOTE_MAX)
1803
+ : null;
1778
1804
  if (!href)
1779
1805
  return null;
1780
1806
  return `${label}${href ? ` — ${href}` : ''}${quote ? `\n > ${quote}` : ''}`;
@@ -1795,7 +1821,9 @@ function resolveCrossCuttingEvidence(reportData, ref) {
1795
1821
  const match = matches[0];
1796
1822
  const label = safeInline(match.thread?.platform, MODEL_FRAMING_TEXT_MAX) || 'Community source';
1797
1823
  const href = safeReceiptUrl(match.thread?.url);
1798
- const quote = safeInline(match.receipt?.excerpt, RECEIPT_QUOTE_MAX);
1824
+ const quote = isDisplaySuitableExcerpt(match.receipt?.excerpt)
1825
+ ? safeInline(match.receipt?.excerpt, RECEIPT_QUOTE_MAX)
1826
+ : null;
1799
1827
  if (!quote)
1800
1828
  return null;
1801
1829
  return `${label}${href ? ` — ${href}` : ''}${quote ? `\n > ${quote}` : ''}`;
@@ -1837,7 +1865,7 @@ function renderCrossCuttingFindings(reportData) {
1837
1865
  const rawRefs = asArray(finding?.evidenceRefs).slice(0, 8);
1838
1866
  const resolvedRefs = rawRefs.flatMap((rawRef) => {
1839
1867
  const ref = asRecord(rawRef);
1840
- const rendered = ref ? resolveCrossCuttingEvidence(reportData, ref) : null;
1868
+ const rendered = ref ? resolveCrossCuttingEvidence(reportData, ref, claim) : null;
1841
1869
  return ref && rendered ? [{ ref, rendered }] : [];
1842
1870
  });
1843
1871
  const methods = new Set(resolvedRefs.flatMap(({ ref }) => ref.kind === 'report_evidence' ? ['web']
@@ -1861,10 +1889,47 @@ function renderCrossCuttingFindings(reportData) {
1861
1889
  function removeLegacyInsightOutput(markdown) {
1862
1890
  return markdown
1863
1891
  .replace(/^## (?:Key )?Insights(?: \(legacy\))?\s*\n[\s\S]*?(?=^## |$(?![\s\S]))/gm, '')
1892
+ .replace(/\n{3,}/g, '\n\n')
1893
+ .trimEnd();
1894
+ }
1895
+ /** Persisted cross-cutting Markdown may contain raw receipt excerpts; rebuild it from projection. */
1896
+ function removeCrossCuttingFindingsOutput(markdown) {
1897
+ return markdown
1864
1898
  .replace(/^## Cross-cutting findings\s*\n[\s\S]*?(?=^## |$(?![\s\S]))/gm, '')
1865
1899
  .replace(/\n{3,}/g, '\n\n')
1866
1900
  .trimEnd();
1867
1901
  }
1902
+ /** Historical agent Markdown copied cached quotes into these sections; the digest replaces them. */
1903
+ function removeHistoricalSourceBearingOutput(markdown) {
1904
+ return markdown
1905
+ .replace(/^## (?:Claim Provenance|Report Sources)\s*\n[\s\S]*?(?=^## |$(?![\s\S]))/gm, '')
1906
+ .replace(/\n{3,}/g, '\n\n')
1907
+ .trimEnd();
1908
+ }
1909
+ /** Rebuild historical source sections from the projected claim registries, never persisted text. */
1910
+ function renderProjectedSourceBearingOutput(reportData, channel) {
1911
+ const sections = [];
1912
+ if (asArray(reportData.claims).length > 0) {
1913
+ sections.push(`## Claim Provenance\n\n${renderPersonaSpine(reportData, channel)}`);
1914
+ }
1915
+ if (asArray(reportData.reportClaims).length > 0) {
1916
+ sections.push(`## Report Sources\n\n${renderReportSpine(reportData, channel)}`);
1917
+ }
1918
+ else {
1919
+ const evidenceRows = asArray(reportData.reportEvidence).slice(0, LIST_RENDER_CAP).flatMap((raw, index) => {
1920
+ const source = asRecord(raw);
1921
+ const href = safeReceiptUrl(source?.url);
1922
+ if (!source || !href)
1923
+ return [];
1924
+ const section = safeInline(source.section, 40);
1925
+ const platform = safeInline(source.platform, 40) ?? 'Source';
1926
+ return [`- RRCP-s${index}${section ? ` (${section})` : ''} — ${platform} — ${href}`];
1927
+ });
1928
+ if (evidenceRows.length > 0)
1929
+ sections.push(`## Report Sources\n\n${evidenceRows.join('\n')}`);
1930
+ }
1931
+ return sections.join('\n\n');
1932
+ }
1868
1933
  function labelLegacyInsightOutput(markdown) {
1869
1934
  return markdown.replace(/^## (?:Key )?Insights$/gm, '## Insights (legacy)');
1870
1935
  }
@@ -2258,7 +2323,19 @@ function renderRedditRetrieval(reportData) {
2258
2323
  * digest and a clean one must not look the same.
2259
2324
  */
2260
2325
  export function buildTrustDigest(reportData, channel) {
2261
- const data = asRecord(reportData);
2326
+ const rawData = asRecord(reportData);
2327
+ const outerVersion = rawData?.hypothesisSemanticsVersion;
2328
+ const outerVersionPresent = rawData !== null && Object.prototype.hasOwnProperty.call(rawData, 'hypothesisSemanticsVersion');
2329
+ const data = outerVersionPresent && outerVersion !== 1
2330
+ ? {
2331
+ ...rawData,
2332
+ hypothesisResults: asArray(rawData.hypothesisResults).map((raw) => {
2333
+ const row = asRecord(raw);
2334
+ const final = asRecord(row?.final);
2335
+ return row && (outerVersion !== 2 || final?.version !== 2) ? { ...row, final: { version: 2 } } : raw;
2336
+ }),
2337
+ }
2338
+ : rawData;
2262
2339
  if (!data)
2263
2340
  return NO_TRUST_DATA;
2264
2341
  const sections = [
@@ -2354,34 +2431,55 @@ function renderReportGaps(reportData) {
2354
2431
  * that may resemble it.
2355
2432
  */
2356
2433
  export function composeReportText(reportMarkdown, reportData, channel) {
2357
- const rationale = renderVerdictRationale(reportData);
2358
- const framing = renderModelFraming(reportData);
2359
2434
  const data = asRecord(reportData);
2360
- const markdownTitle = reportMarkdown.startsWith('# ')
2361
- ? reportMarkdown.slice(2, reportMarkdown.indexOf('\n') >= 0 ? reportMarkdown.indexOf('\n') : undefined)
2435
+ const rows = asArray(data?.hypothesisResults);
2436
+ const semanticsVersionPresent = data !== null &&
2437
+ Object.prototype.hasOwnProperty.call(data, 'hypothesisSemanticsVersion');
2438
+ const rowClaimsV2 = rows.some((raw) => {
2439
+ const row = asRecord(raw);
2440
+ return asRecord(row?.final)?.version === 2 ||
2441
+ (row !== null && Object.prototype.hasOwnProperty.call(row, 'clauseIntegrity'));
2442
+ });
2443
+ const requiresClauseIntegrity = rowClaimsV2 ||
2444
+ (semanticsVersionPresent && data?.hypothesisSemanticsVersion !== 1);
2445
+ const clauseFailure = requiresClauseIntegrity && (rows.length === 0 ||
2446
+ rows.some((raw) => {
2447
+ const final = asRecord(asRecord(raw)?.final);
2448
+ return final?.version !== 2 ||
2449
+ !finalClauseCoverageMatchesIntegrity(raw, data) ||
2450
+ final.clauseCoverage === 'invalid' ||
2451
+ final.clauseCoverage !== 'complete' && final.originalVerdict !== final.verdict;
2452
+ }));
2453
+ const rationale = clauseFailure ? '' : renderVerdictRationale(reportData);
2454
+ const framing = clauseFailure ? '' : renderModelFraming(reportData);
2455
+ const sourceMarkdown = clauseFailure
2456
+ ? '# Report\n\nThis run did not establish every material part of at least one hypothesis. Treat the affected hypotheses as unresolved.'
2457
+ : reportMarkdown;
2458
+ const markdownTitle = sourceMarkdown.startsWith('# ')
2459
+ ? sourceMarkdown.slice(2, sourceMarkdown.indexOf('\n') >= 0 ? sourceMarkdown.indexOf('\n') : undefined)
2362
2460
  : undefined;
2363
2461
  const safeTitle = data?.reportTitle === undefined
2364
2462
  ? normalizeLegacyMcpHeading(markdownTitle)
2365
2463
  : escapeMcpMarkdownHeading(sanitizeMcpReportTitle(data.reportTitle));
2366
- const titledReportMarkdown = reportMarkdown.startsWith('# ')
2367
- ? `# ${safeTitle}${reportMarkdown.includes('\n') ? reportMarkdown.slice(reportMarkdown.indexOf('\n')) : ''}`
2368
- : reportMarkdown;
2464
+ const titledReportMarkdown = sourceMarkdown.startsWith('# ')
2465
+ ? `# ${safeTitle}${sourceMarkdown.includes('\n') ? sourceMarkdown.slice(sourceMarkdown.indexOf('\n')) : ''}`
2466
+ : sourceMarkdown;
2467
+ const excerptFreeMarkdown = removeHistoricalSourceBearingOutput(removeCrossCuttingFindingsOutput(removeRelevantDiscussionsOutput(titledReportMarkdown)));
2369
2468
  const hasCrossCutting = Boolean(data && Object.prototype.hasOwnProperty.call(data, 'crossCuttingFindings'));
2370
2469
  const safeReportMarkdown = hasCrossCutting
2371
- ? removeLegacyInsightOutput(titledReportMarkdown)
2372
- : labelLegacyInsightOutput(titledReportMarkdown);
2470
+ ? removeLegacyInsightOutput(excerptFreeMarkdown)
2471
+ : labelLegacyInsightOutput(excerptFreeMarkdown);
2373
2472
  const insightReferences = data && !hasCrossCutting ? renderInsightCrossReferences(data) : '';
2374
2473
  const crossCutting = data && hasCrossCutting ? renderCrossCuttingFindings(data) : '';
2474
+ const sourceBearing = data ? renderProjectedSourceBearingOutput(data, channel) : '';
2375
2475
  // Community themes, sentiment, and source titles are producer/source-authored framing, not
2376
2476
  // machine-checked claims. Keep them above the authority separator while preserving the literal
2377
2477
  // Theme → Sentiment → Discussion contract for every full-report caller.
2378
- const communityPatterns = data && !hasCurrentCommunityPatternRows(safeReportMarkdown, data)
2379
- ? renderCommunityPatterns(data, channel)
2380
- : '';
2478
+ const communityPatterns = data ? renderCommunityPatterns(data, channel) : '';
2381
2479
  // FUL-586: the completeness banner goes FIRST — before the prose it is a caveat about.
2382
2480
  const gaps = renderReportGaps(reportData);
2383
2481
  return (`${gaps ? `${gaps}\n\n---\n\n` : ''}` +
2384
- `${safeReportMarkdown}${insightReferences ? `\n\n${insightReferences}` : ''}${crossCutting ? `\n\n${crossCutting}` : ''}${communityPatterns ? `\n\n${communityPatterns}` : ''}${framing ? `\n\n${framing}` : ''}${rationale ? `\n\n${rationale}` : ''}` +
2482
+ `${safeReportMarkdown}${insightReferences ? `\n\n${insightReferences}` : ''}${crossCutting ? `\n\n${crossCutting}` : ''}${sourceBearing ? `\n\n${sourceBearing}` : ''}${communityPatterns ? `\n\n${communityPatterns}` : ''}${framing ? `\n\n${framing}` : ''}${rationale ? `\n\n${rationale}` : ''}` +
2385
2483
  `\n\n---\n\n${buildTrustDigest(reportData, channel)}`);
2386
2484
  }
2387
2485
  /** Keep a model-authored structured title inert when replacing the report Markdown H1. */