@clien-ai/mcp 0.14.0 → 0.15.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 +46 -0
- package/dist/hypothesis-semantics.js +9 -1
- package/dist/hypothesis-semantics.js.map +1 -1
- package/dist/tools/market-signals.js +3 -12
- package/dist/tools/market-signals.js.map +1 -1
- package/dist/tools/market-sizing-proof.js +3 -10
- package/dist/tools/market-sizing-proof.js.map +1 -1
- package/dist/tools/output-schemas.js +7 -2
- package/dist/tools/output-schemas.js.map +1 -1
- package/dist/tools/projects.js +22 -4
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/registry.js +10 -5
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/report-digest.js +139 -40
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/run-manifest.js +13 -2
- package/dist/tools/run-manifest.js.map +1 -1
- package/dist/tools/scoped-research.js +4 -2
- package/dist/tools/scoped-research.js.map +1 -1
- package/dist/types/receipt-url.js +105 -0
- package/dist/types/receipt-url.js.map +1 -0
- package/dist/types/report.js +211 -16
- package/dist/types/report.js.map +1 -1
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ import { readFinalHypothesisState } from './hypothesis-final.js';
|
|
|
52
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
|
-
import { MCP_OVERVIEW_REF_CAP, MCP_OVERVIEW_SPAN_CAP, REPORT_PORTRAIT_PATHS, sanitizeMcpMarkdownTitle, sanitizeMcpReportTitle, } from '../types/report.js';
|
|
55
|
+
import { MCP_OVERVIEW_REF_CAP, MCP_OVERVIEW_SPAN_CAP, REPORT_PORTRAIT_PATHS, isModelAttestedReportClaim, parseUnverifiedSourceId, receiptRowHasSafeDestination, safeSourceUrl, sanitizeMcpMarkdownTitle, sanitizeMcpReportTitle, } from '../types/report.js';
|
|
56
56
|
import { readSemanticTheme } from '../types/semantic-theme.js';
|
|
57
57
|
import { isDisplaySuitableExcerpt } from '../lib/excerpt-suitability.js';
|
|
58
58
|
/**
|
|
@@ -165,6 +165,28 @@ const ATTESTED_WINDOW_LABEL = 'cited window (NOT a verified span)';
|
|
|
165
165
|
* carries the meaning the shared word no longer can.
|
|
166
166
|
*/
|
|
167
167
|
const UNSOURCED_LABEL = 'unsourced';
|
|
168
|
+
/**
|
|
169
|
+
* FUL-1219: the words on an unverified-source line (Alex D1), keyed by the stored label. An unknown
|
|
170
|
+
* label has no entry and renders nothing: a guessed word would be a claim about the page.
|
|
171
|
+
*
|
|
172
|
+
* ⚠️ THE LINE SHAPE IS THE GUARD, as with `ATTESTED_WINDOW_LABEL`. A receipt is `← RRCP-s{n}` on
|
|
173
|
+
* the row or an indented `"…"` excerpt in a pool; this line has neither, names itself
|
|
174
|
+
* `unverified source` and ends `(NOT a receipt)`, so no reader keying on the receipt shapes can
|
|
175
|
+
* take it for one. Pinned by `unverified-sources.test.ts`.
|
|
176
|
+
*/
|
|
177
|
+
const UNVERIFIED_SOURCE_WORDS = {
|
|
178
|
+
paid_market_report: 'paid market report',
|
|
179
|
+
company_own_page: 'company\'s own page',
|
|
180
|
+
related_source: 'related source',
|
|
181
|
+
};
|
|
182
|
+
const UNVERIFIED_SOURCE_LABEL = 'unverified source';
|
|
183
|
+
/** Plan D8: at most three lines per claim; the rest become one "+N more" line. */
|
|
184
|
+
const UNVERIFIED_SOURCE_CAP = 3;
|
|
185
|
+
/**
|
|
186
|
+
* Replaces `from model knowledge, unverified` on an unsourced row that shows an unverified source:
|
|
187
|
+
* the page listed beneath contradicts "model knowledge", but it is still no receipt.
|
|
188
|
+
*/
|
|
189
|
+
const NO_VERIFIED_RECEIPT = 'no verified receipt';
|
|
168
190
|
/**
|
|
169
191
|
* The three labels on a robustness row's continuation lines (FUL-614).
|
|
170
192
|
*
|
|
@@ -316,20 +338,18 @@ function overviewCapNote(truncated, cap, noun) {
|
|
|
316
338
|
return '';
|
|
317
339
|
return `\n_(showing at most ${cap} stored ${noun} — additional entries were omitted before public allowlisting)_`;
|
|
318
340
|
}
|
|
319
|
-
/**
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
341
|
+
/** FUL-1225: printed where a receipt's link would be when its stored URL is unsafe to show. */
|
|
342
|
+
const URL_WITHHELD = '(url withheld)';
|
|
343
|
+
/**
|
|
344
|
+
* FUL-1225: a receipt row's link, split into the two questions it answers. `reachable` is FUL-534's
|
|
345
|
+
* grant condition (a safe destination, shown or withheld by the projection) and decides state and
|
|
346
|
+
* tally. `href` is what may be printed: only a URL `safeSourceUrl` accepts, else null. Keeping them
|
|
347
|
+
* apart is what lets an unprintable URL go unlinked without the claim going unsourced.
|
|
348
|
+
*/
|
|
349
|
+
function receiptLink(source) {
|
|
350
|
+
if (!source)
|
|
351
|
+
return { reachable: false, href: null };
|
|
352
|
+
return { reachable: receiptRowHasSafeDestination(source), href: safeSourceUrl(source.url) };
|
|
333
353
|
}
|
|
334
354
|
const OVERVIEW_ASSERTION_MAX = 320;
|
|
335
355
|
function overviewReceiptSupportsClaim(claim, receipt) {
|
|
@@ -351,14 +371,14 @@ function resolveOverviewRef(ref, claimSpan, reportEvidence, forumThreads) {
|
|
|
351
371
|
if (!match)
|
|
352
372
|
return null;
|
|
353
373
|
const source = asRecord(reportEvidence[Number(match[1])]);
|
|
354
|
-
const
|
|
374
|
+
const link = receiptLink(source);
|
|
355
375
|
const quote = str(source?.quote);
|
|
356
|
-
if (!source || !
|
|
376
|
+
if (!source || !link.reachable || !quote || !isDisplaySuitableExcerpt(quote) || !overviewReceiptSupportsClaim(claimSpan, quote))
|
|
357
377
|
return null;
|
|
358
378
|
if (reportClaimNeedsIndependentMarketAuthority('summary', claimSpan) &&
|
|
359
379
|
!sourceAuthenticatesMarketEconomics(source))
|
|
360
380
|
return null;
|
|
361
|
-
return `${sourceId} -> ${href}`;
|
|
381
|
+
return `${sourceId} -> ${link.href ?? URL_WITHHELD}`;
|
|
362
382
|
}
|
|
363
383
|
if (kind === 'forum_receipt') {
|
|
364
384
|
const receiptId = safeId(record?.receiptId);
|
|
@@ -375,7 +395,7 @@ function resolveOverviewRef(ref, claimSpan, reportEvidence, forumThreads) {
|
|
|
375
395
|
!isDisplaySuitableExcerpt(excerpt) ||
|
|
376
396
|
!overviewReceiptSupportsClaim(claimSpan, excerpt))
|
|
377
397
|
return [];
|
|
378
|
-
const href =
|
|
398
|
+
const href = safeSourceUrl(thread?.url);
|
|
379
399
|
return [`${receiptId}${href
|
|
380
400
|
? ` -> ${href}`
|
|
381
401
|
: ` -> cached excerpt: "${safeInline(excerpt, OVERVIEW_ASSERTION_MAX) ?? '[unavailable]'}"`}`];
|
|
@@ -474,9 +494,10 @@ function resolvePersonaReceipt(rawClaim, personas) {
|
|
|
474
494
|
if (sourceIndex < 0)
|
|
475
495
|
return null;
|
|
476
496
|
const source = asRecord(sources[sourceIndex]);
|
|
477
|
-
const
|
|
478
|
-
if (!source || !
|
|
497
|
+
const link = receiptLink(source);
|
|
498
|
+
if (!source || !link.reachable)
|
|
479
499
|
return null;
|
|
500
|
+
const href = link.href;
|
|
480
501
|
// Branch on the RAW value, not on the resolved one: only `undefined` is "legacy claim, nothing
|
|
481
502
|
// to resolve" and every present value — including `null` — is a promise this parent must keep.
|
|
482
503
|
const claimedReceiptId = claim?.receiptId;
|
|
@@ -690,7 +711,7 @@ function resolveReportReceipt(rawClaim, reportEvidence) {
|
|
|
690
711
|
return null;
|
|
691
712
|
const source = asRecord(reportEvidence[sourceIndex]);
|
|
692
713
|
if (!source ||
|
|
693
|
-
!
|
|
714
|
+
!receiptRowHasSafeDestination(source) ||
|
|
694
715
|
(kind === 'grounded' && !isDisplaySuitableExcerpt(source.quote)))
|
|
695
716
|
return null;
|
|
696
717
|
if (reportClaimNeedsIndependentMarketAuthority(claim?.section, claim?.text) &&
|
|
@@ -737,7 +758,62 @@ function digestSpanOnRow(quote, span) {
|
|
|
737
758
|
const normalizedSpan = normalize(span);
|
|
738
759
|
return normalizedSpan.length > 0 && normalize(quote).includes(normalizedSpan);
|
|
739
760
|
}
|
|
740
|
-
|
|
761
|
+
/**
|
|
762
|
+
* FUL-1219: the unverified-source lines under one claim, or '' when there are none.
|
|
763
|
+
*
|
|
764
|
+
* Only a stored, non-attested `NO_RECEIPT` claim shows them: on a GROUNDED claim they would sit
|
|
765
|
+
* beside a receipt, on a model-attested one beside "cited", and a SPECULATION claim never expected
|
|
766
|
+
* evidence. `getReport` hands the RAW payload through after schema drift, so every check the
|
|
767
|
+
* projection makes is repeated here: the id resolves only through the anchored `RUVS-` parser (a
|
|
768
|
+
* receipt id never reaches the unverified pool), the pool row must be of the claim's section and
|
|
769
|
+
* have a plain, printable-ASCII http(s) URL not already listed under the claim, and the label must
|
|
770
|
+
* be one this client knows. Only the host is printed; the full URL stays in the structured channel.
|
|
771
|
+
*/
|
|
772
|
+
function unverifiedSourceLines(rawClaim, unverifiedEvidence) {
|
|
773
|
+
const claim = asRecord(rawClaim);
|
|
774
|
+
if (str(claim?.state) !== 'NO_RECEIPT')
|
|
775
|
+
return '';
|
|
776
|
+
// A model-attested claim renders `cited → RRCP-s{n}`; an unverified line beside "cited" would read
|
|
777
|
+
// as support (plan stop condition (d)). Same marker test as the projection and the app.
|
|
778
|
+
if (isModelAttestedReportClaim(claim))
|
|
779
|
+
return '';
|
|
780
|
+
const lines = [];
|
|
781
|
+
const seen = new Set();
|
|
782
|
+
let resolved = 0;
|
|
783
|
+
for (const raw of asArray(claim?.unverifiedSources)) {
|
|
784
|
+
const entry = asRecord(raw);
|
|
785
|
+
const index = parseUnverifiedSourceId(entry?.sourceId);
|
|
786
|
+
const word = typeof entry?.label === 'string' && Object.hasOwn(UNVERIFIED_SOURCE_WORDS, entry.label)
|
|
787
|
+
? UNVERIFIED_SOURCE_WORDS[entry.label]
|
|
788
|
+
: null;
|
|
789
|
+
const row = index === null ? null : asRecord(unverifiedEvidence[index]);
|
|
790
|
+
// Parity with the app: a pool row of another section is not this claim's page, and a URL
|
|
791
|
+
// already listed under this claim is not listed twice.
|
|
792
|
+
if (!row || row.section !== claim?.section)
|
|
793
|
+
continue;
|
|
794
|
+
const url = safeSourceUrl(row.url);
|
|
795
|
+
if (!word || !url || seen.has(url))
|
|
796
|
+
continue;
|
|
797
|
+
const host = safeInline(new URL(url).hostname.replace(/^www\./, ''), 120);
|
|
798
|
+
if (!host)
|
|
799
|
+
continue;
|
|
800
|
+
seen.add(url);
|
|
801
|
+
resolved += 1;
|
|
802
|
+
if (lines.length < UNVERIFIED_SOURCE_CAP) {
|
|
803
|
+
lines.push(`\n · ${UNVERIFIED_SOURCE_LABEL} RUVS-s${index} (${word}): ${host} (NOT a receipt)`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (lines.length === 0)
|
|
807
|
+
return '';
|
|
808
|
+
const stored = claim?.unverifiedSourcesOmitted;
|
|
809
|
+
const omitted = (Number.isSafeInteger(stored) && stored > 0 ? stored : 0) +
|
|
810
|
+
(resolved - lines.length);
|
|
811
|
+
if (omitted > 0) {
|
|
812
|
+
lines.push(`\n · +${omitted} more ${omitted === 1 ? UNVERIFIED_SOURCE_LABEL : `${UNVERIFIED_SOURCE_LABEL}s`} found`);
|
|
813
|
+
}
|
|
814
|
+
return lines.join('');
|
|
815
|
+
}
|
|
816
|
+
function renderReportClaim(raw, receipt, corroborating = [], unverified = '') {
|
|
741
817
|
const claim = asRecord(raw);
|
|
742
818
|
if (!claim)
|
|
743
819
|
return '- (unreadable claim entry)';
|
|
@@ -771,7 +847,7 @@ function renderReportClaim(raw, receipt, corroborating = []) {
|
|
|
771
847
|
provenance = ` · ${ATTESTED_LABEL} → ${receipt.sourceId} (NOT a receipt)`;
|
|
772
848
|
}
|
|
773
849
|
else if (rawState === 'NO_RECEIPT') {
|
|
774
|
-
provenance = ` · ${UNSOURCED_LABEL} — from model knowledge, unverified`;
|
|
850
|
+
provenance = ` · ${UNSOURCED_LABEL} — ${unverified ? NO_VERIFIED_RECEIPT : 'from model knowledge, unverified'}`;
|
|
775
851
|
}
|
|
776
852
|
// A pointer on a non-grounded claim is shown but explicitly disarmed, so it is
|
|
777
853
|
// neither hidden from the reader nor readable as grounding.
|
|
@@ -780,7 +856,7 @@ function renderReportClaim(raw, receipt, corroborating = []) {
|
|
|
780
856
|
}
|
|
781
857
|
const where = section ? ` (${section})` : '';
|
|
782
858
|
const body = text ? ` — "${text}"` : '';
|
|
783
|
-
return `- [${state}] ${id}${where}${provenance}${body}${attestedWindow(receipt)}`;
|
|
859
|
+
return `- [${state}] ${id}${where}${provenance}${body}${attestedWindow(receipt)}${unverified}`;
|
|
784
860
|
}
|
|
785
861
|
/**
|
|
786
862
|
* The attested claim's window: the captured snapshot text an independent judge read
|
|
@@ -862,6 +938,7 @@ function renderReportSpine(reportData, channel) {
|
|
|
862
938
|
'figure as unverified.');
|
|
863
939
|
}
|
|
864
940
|
const reportEvidence = asArray(reportData.reportEvidence);
|
|
941
|
+
const unverifiedEvidence = asArray(reportData.unverifiedEvidence);
|
|
865
942
|
const rows = claims.map((claim) => ({
|
|
866
943
|
claim,
|
|
867
944
|
receipt: resolveReportReceipt(claim, reportEvidence),
|
|
@@ -870,7 +947,21 @@ function renderReportSpine(reportData, channel) {
|
|
|
870
947
|
const attested = rows.filter(({ receipt }) => receipt?.kind === 'attested').length;
|
|
871
948
|
const attestedNote = attested > 0 ? ` (of which ${attested} ${ATTESTED_LABEL})` : '';
|
|
872
949
|
const shown = rows.slice(0, CLAIM_RENDER_CAP);
|
|
873
|
-
|
|
950
|
+
let anyUnverified = false;
|
|
951
|
+
const lines = shown.map(({ claim, receipt }) => {
|
|
952
|
+
const unverified = unverifiedSourceLines(claim, unverifiedEvidence);
|
|
953
|
+
if (unverified)
|
|
954
|
+
anyUnverified = true;
|
|
955
|
+
return renderReportClaim(claim, receipt, corroboratingReceiptIds(claim, receipt, reportEvidence), unverified);
|
|
956
|
+
});
|
|
957
|
+
// FUL-1219: said only when a row carries such a line, so a report without them renders
|
|
958
|
+
// byte-identically to before.
|
|
959
|
+
const unverifiedNote = anyUnverified
|
|
960
|
+
? `An indented \`· ${UNVERIFIED_SOURCE_LABEL} RUVS-s{n} (…)\` line under a NO_RECEIPT row is a page ` +
|
|
961
|
+
'the research found for that claim that did NOT verify, labelled with why (a paid market report, ' +
|
|
962
|
+
`the company's own page, or a related source); its full URL is \`${channel}.report_data.unverifiedEvidence[n]\`. It is NOT a ` +
|
|
963
|
+
'receipt: it changes no state and counts toward no tally above. '
|
|
964
|
+
: '';
|
|
874
965
|
// FUL-358 — SAY WHICH POPULATION THE HEADLINE COUNTED.
|
|
875
966
|
//
|
|
876
967
|
// The prose below has always explained that a `summary` claim usually restates a `market` or
|
|
@@ -902,6 +993,7 @@ function renderReportSpine(reportData, channel) {
|
|
|
902
993
|
'text as we captured it: read it as the context the judge weighed, never as a span checked against ' +
|
|
903
994
|
'the claim. Unmarked NO_RECEIPT is recalled model knowledge, unverified. A stored GROUNDED grade ' +
|
|
904
995
|
'whose receipt is unavailable is shown as neutral audit context and counted NO_RECEIPT. ' +
|
|
996
|
+
unverifiedNote +
|
|
905
997
|
'Key on the rendered state and receipt arrow, never on stored state or source-id presence alone.\n' +
|
|
906
998
|
`${lines.join('\n')}` +
|
|
907
999
|
capNote(shown.length, claims.length, channel, 'report_data.reportClaims'));
|
|
@@ -1017,7 +1109,7 @@ function renderReceiptPools(reportData, channel) {
|
|
|
1017
1109
|
// they sit on the SAME `- RCP-…` line, so a newline in either forges the
|
|
1018
1110
|
// receipt row the comment below says a forged quote would.
|
|
1019
1111
|
const platform = safeInline(source?.platform, 40) ?? 'unknown platform';
|
|
1020
|
-
const url = safeInline(receipt.href, 200) ?? '(no url)';
|
|
1112
|
+
const url = receipt.href === null ? URL_WITHHELD : safeInline(receipt.href, 200) ?? '(no url)';
|
|
1021
1113
|
// Indented under its own receipt line so the pool still scans as a list of
|
|
1022
1114
|
// pointers; a receipt with no cached quote simply has no second line, which
|
|
1023
1115
|
// is itself worth seeing — it is a pointer that grounds nothing until fetched.
|
|
@@ -1101,7 +1193,10 @@ function renderReceiptPools(reportData, channel) {
|
|
|
1101
1193
|
// claim's arrow with a source nobody fetched.
|
|
1102
1194
|
const section = safeInline(source?.section, 40) ?? 'unknown section';
|
|
1103
1195
|
const platform = safeInline(source?.platform, 40) ?? 'unknown platform';
|
|
1104
|
-
|
|
1196
|
+
// FUL-1225: the stored string is printed only when the display check accepts it; a withheld
|
|
1197
|
+
// or unsafe one never reaches the text channel.
|
|
1198
|
+
const link = receiptLink(source);
|
|
1199
|
+
const url = link.href ? safeInline(source?.url, 200) ?? '(no url)' : link.reachable ? URL_WITHHELD : '(no url)';
|
|
1105
1200
|
// FUL-148: `publishedDate` is the figure's recency; `retrievedAt` is only when
|
|
1106
1201
|
// WE fetched it. Rendering retrievedAt as recency would be a lie, so an absent
|
|
1107
1202
|
// publication date is shown honestly as unknown rather than substituted.
|
|
@@ -1252,7 +1347,7 @@ function renderCommunityPatterns(reportData, channel) {
|
|
|
1252
1347
|
const sentiment = safeInline(thread?.sentimentSummary, MODEL_FRAMING_TEXT_MAX);
|
|
1253
1348
|
const title = safeInline(thread?.title, MODEL_FRAMING_TEXT_MAX) ?? 'Untitled discussion';
|
|
1254
1349
|
const theme = readSemanticTheme(thread?.theme, title);
|
|
1255
|
-
const href =
|
|
1350
|
+
const href = safeSourceUrl(thread?.url);
|
|
1256
1351
|
return [
|
|
1257
1352
|
`- Theme: ${theme ?? 'not recorded'}`,
|
|
1258
1353
|
` Sentiment: ${sentiment ?? 'not recorded'}`,
|
|
@@ -1812,9 +1907,9 @@ function renderInsightCrossReferences(reportData) {
|
|
|
1812
1907
|
const source = Number.isSafeInteger(index) ? asRecord(evidence[index]) : null;
|
|
1813
1908
|
const authorityEligible = !reportClaimNeedsIndependentMarketAuthority('summary', statement) ||
|
|
1814
1909
|
sourceAuthenticatesMarketEconomics(source);
|
|
1815
|
-
const
|
|
1816
|
-
return sourceId &&
|
|
1817
|
-
? `${label} → ${sourceId} — ${href}`
|
|
1910
|
+
const link = authorityEligible ? receiptLink(source) : { reachable: false, href: null };
|
|
1911
|
+
return sourceId && link.reachable
|
|
1912
|
+
? `${label} → ${sourceId} — ${link.href ?? URL_WITHHELD}`
|
|
1818
1913
|
: `${label} (no resolvable receipt)`;
|
|
1819
1914
|
})
|
|
1820
1915
|
.filter((source) => Boolean(source));
|
|
@@ -1836,16 +1931,16 @@ function resolveCrossCuttingEvidence(reportData, ref, claim) {
|
|
|
1836
1931
|
!sourceAuthenticatesMarketEconomics(source))
|
|
1837
1932
|
return null;
|
|
1838
1933
|
const label = safeInline(source.platform, MODEL_FRAMING_TEXT_MAX) || 'Web source';
|
|
1839
|
-
const
|
|
1934
|
+
const link = receiptLink(source);
|
|
1840
1935
|
const hasQuote = Object.prototype.hasOwnProperty.call(source, 'quote');
|
|
1841
1936
|
if (hasQuote && !isDisplaySuitableExcerpt(source.quote))
|
|
1842
1937
|
return null;
|
|
1843
1938
|
const quote = hasQuote
|
|
1844
1939
|
? safeInline(source.quote, RECEIPT_QUOTE_MAX)
|
|
1845
1940
|
: null;
|
|
1846
|
-
if (!
|
|
1941
|
+
if (!link.reachable)
|
|
1847
1942
|
return null;
|
|
1848
|
-
return `${label}
|
|
1943
|
+
return `${label} — ${link.href ?? URL_WITHHELD}${quote ? `\n > ${quote}` : ''}`;
|
|
1849
1944
|
}
|
|
1850
1945
|
if (ref.kind === 'forum_receipt') {
|
|
1851
1946
|
const receiptId = safeId(ref.receiptId);
|
|
@@ -1862,7 +1957,7 @@ function resolveCrossCuttingEvidence(reportData, ref, claim) {
|
|
|
1862
1957
|
return null;
|
|
1863
1958
|
const match = matches[0];
|
|
1864
1959
|
const label = safeInline(match.thread?.platform, MODEL_FRAMING_TEXT_MAX) || 'Community source';
|
|
1865
|
-
const href =
|
|
1960
|
+
const href = safeSourceUrl(match.thread?.url);
|
|
1866
1961
|
const quote = isDisplaySuitableExcerpt(match.receipt?.excerpt)
|
|
1867
1962
|
? safeInline(match.receipt?.excerpt, RECEIPT_QUOTE_MAX)
|
|
1868
1963
|
: null;
|
|
@@ -1960,12 +2055,12 @@ function renderProjectedSourceBearingOutput(reportData, channel) {
|
|
|
1960
2055
|
else {
|
|
1961
2056
|
const evidenceRows = asArray(reportData.reportEvidence).slice(0, LIST_RENDER_CAP).flatMap((raw, index) => {
|
|
1962
2057
|
const source = asRecord(raw);
|
|
1963
|
-
const
|
|
1964
|
-
if (!source || !
|
|
2058
|
+
const link = receiptLink(source);
|
|
2059
|
+
if (!source || !link.reachable)
|
|
1965
2060
|
return [];
|
|
1966
2061
|
const section = safeInline(source.section, 40);
|
|
1967
2062
|
const platform = safeInline(source.platform, 40) ?? 'Source';
|
|
1968
|
-
return [`- RRCP-s${index}${section ? ` (${section})` : ''} — ${platform} — ${href}`];
|
|
2063
|
+
return [`- RRCP-s${index}${section ? ` (${section})` : ''} — ${platform} — ${link.href ?? URL_WITHHELD}`];
|
|
1969
2064
|
});
|
|
1970
2065
|
if (evidenceRows.length > 0)
|
|
1971
2066
|
sections.push(`## Report Sources\n\n${evidenceRows.join('\n')}`);
|
|
@@ -2145,7 +2240,7 @@ function renderMarketSizing(reportData) {
|
|
|
2145
2240
|
const lines = signals.map((signal) => {
|
|
2146
2241
|
const context = safeInline(signal.context, 360) ?? 'Context unavailable';
|
|
2147
2242
|
const sources = (signal.sources ?? (signal.source ? [signal.source] : []))
|
|
2148
|
-
.map((source) => safeInline(source.url, 240))
|
|
2243
|
+
.map((source) => safeSourceUrl(source.url) ? safeInline(source.url, 240) : URL_WITHHELD)
|
|
2149
2244
|
.filter((url) => Boolean(url));
|
|
2150
2245
|
const citations = sources.length > 0 ? ` ← ${sources.join(', ')}` : '';
|
|
2151
2246
|
if (signal.kind !== 'size')
|
|
@@ -2363,6 +2458,10 @@ function renderRedditRetrieval(reportData) {
|
|
|
2363
2458
|
* raw passthrough payload (see the module header). Returns the full digest, or
|
|
2364
2459
|
* the explicit "no trust data" banner — never an empty string, because an empty
|
|
2365
2460
|
* digest and a clean one must not look the same.
|
|
2461
|
+
*
|
|
2462
|
+
* ⚠️ FUL-1225: callers pass the output of exactly ONE `projectReportDataForMcp` (via
|
|
2463
|
+
* `prepareReportDataForMcp`). The projection strips stored `urlWithheld` / `urlWithheldHost` markers
|
|
2464
|
+
* before setting its own; on unprojected JSON a forged marker would be trusted as a reachable receipt.
|
|
2366
2465
|
*/
|
|
2367
2466
|
export function buildTrustDigest(reportData, channel) {
|
|
2368
2467
|
const rawData = asRecord(reportData);
|