@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FUL-1219 / FUL-1225: which stored source URLs and platform labels an MCP channel may SHOW, and how a
|
|
3
|
+
* receipt whose URL the projection withheld still answers the questions its URL used to answer.
|
|
4
|
+
*
|
|
5
|
+
* Imports nothing, so `hypothesis-semantics.ts` (which `report.ts` imports) can read it too.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ THE MARKERS BELOW ARE TRUSTED ONLY ON PROJECTED DATA. `projectReportDataForMcp` deletes any
|
|
8
|
+
* stored copy before it sets them, so a reader must be handed the output of exactly one projection:
|
|
9
|
+
* on unprojected report JSON a forged `urlWithheld` would read as a reachable receipt.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The `href` of a stored source URL an MCP channel may show, or null.
|
|
13
|
+
*
|
|
14
|
+
* The producer writes each URL through `normalizeUrl` (`agent/src/exa-fetch-tool.ts`), i.e. a WHATWG
|
|
15
|
+
* `URL` serialisation, which is always printable ASCII. So a stored URL with any other character —
|
|
16
|
+
* a newline, a tab, a bidi or zero-width mark, a `←` — was not written by the producer and is
|
|
17
|
+
* refused. `new URL()` alone would NOT refuse it: the parser silently strips tabs and newlines, so
|
|
18
|
+
* the raw string would pass and reach `structuredContent` unchanged. Also refuses non-http(s) and
|
|
19
|
+
* credential-carrying URLs.
|
|
20
|
+
*/
|
|
21
|
+
export function safeSourceUrl(value) {
|
|
22
|
+
if (typeof value !== 'string' || !/^https?:\/\/[\x21-\x7e]+$/i.test(value))
|
|
23
|
+
return null;
|
|
24
|
+
return isSafeHttpDestination(value) ? new URL(value).href : null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* FUL-1225: a stored `platform` label an MCP channel may show: printable ASCII on one line (space
|
|
28
|
+
* allowed), else null. Anything else (a newline, a bidi or zero-width mark) was not a host label.
|
|
29
|
+
*/
|
|
30
|
+
export function safeSourcePlatform(value) {
|
|
31
|
+
return typeof value === 'string' && /^[\x20-\x7e]*$/.test(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* FUL-534's reachability test: a non-credentialed http(s) URL the WHATWG parser accepts. Decides
|
|
35
|
+
* whether a receipt earns its GROUNDED / Cited presentation. It is deliberately NOT the display
|
|
36
|
+
* test ({@link safeSourceUrl}): tightening it would downgrade claims, which FUL-1225 must not do.
|
|
37
|
+
*/
|
|
38
|
+
export function isSafeHttpDestination(value) {
|
|
39
|
+
if (typeof value !== 'string' || !/^https?:\/\//i.test(value))
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(value);
|
|
43
|
+
return (url.protocol === 'http:' || url.protocol === 'https:') && !url.username && !url.password;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Set by the projection on a `reportEvidence` or persona-source row whose URL it blanked
|
|
51
|
+
* (`url: ''`) because the URL is unsafe to show, while the stored URL still passed
|
|
52
|
+
* {@link isSafeHttpDestination}. It means "still a receipt, link withheld".
|
|
53
|
+
*/
|
|
54
|
+
export const RECEIPT_URL_WITHHELD_KEY = 'urlWithheld';
|
|
55
|
+
/**
|
|
56
|
+
* Set beside {@link RECEIPT_URL_WITHHELD_KEY}: the withheld URL's hostname (ASCII by WHATWG
|
|
57
|
+
* construction, and re-checked). Integrity checks that compare hosts (`sameNamedVendor`) read it,
|
|
58
|
+
* so withholding a URL for display never changes an integrity or coverage decision.
|
|
59
|
+
*/
|
|
60
|
+
export const RECEIPT_URL_WITHHELD_HOST_KEY = 'urlWithheldHost';
|
|
61
|
+
/** The hostname the projection preserves for a withheld URL, or null. */
|
|
62
|
+
export function withheldUrlHost(value) {
|
|
63
|
+
if (!isSafeHttpDestination(value))
|
|
64
|
+
return null;
|
|
65
|
+
const host = new URL(value).hostname;
|
|
66
|
+
return /^[a-z0-9.-]+$/.test(host) ? host : null;
|
|
67
|
+
}
|
|
68
|
+
function asRow(row) {
|
|
69
|
+
return row !== null && typeof row === 'object' && !Array.isArray(row) ? row : null;
|
|
70
|
+
}
|
|
71
|
+
function isWithheld(record) {
|
|
72
|
+
return record[RECEIPT_URL_WITHHELD_KEY] === true && record.url === '';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* FUL-534 + FUL-1225: does a receipt row have a safe destination, shown or withheld? Every
|
|
76
|
+
* receipt-granting reader calls this instead of testing `row.url`, so blanking an unprintable URL
|
|
77
|
+
* never changes a claim's state or tally.
|
|
78
|
+
*/
|
|
79
|
+
export function receiptRowHasSafeDestination(row) {
|
|
80
|
+
const record = asRow(row);
|
|
81
|
+
if (!record)
|
|
82
|
+
return false;
|
|
83
|
+
if (isWithheld(record))
|
|
84
|
+
return true;
|
|
85
|
+
return isSafeHttpDestination(record.url);
|
|
86
|
+
}
|
|
87
|
+
/** The hostname behind a receipt row's URL, shown or withheld, or null. */
|
|
88
|
+
export function receiptRowHostname(row) {
|
|
89
|
+
const record = asRow(row);
|
|
90
|
+
if (!record)
|
|
91
|
+
return null;
|
|
92
|
+
if (isWithheld(record)) {
|
|
93
|
+
const host = record[RECEIPT_URL_WITHHELD_HOST_KEY];
|
|
94
|
+
return typeof host === 'string' && /^[a-z0-9.-]+$/.test(host) ? host : null;
|
|
95
|
+
}
|
|
96
|
+
if (typeof record.url !== 'string')
|
|
97
|
+
return null;
|
|
98
|
+
try {
|
|
99
|
+
return new URL(record.url).hostname;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=receipt-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receipt-url.js","sourceRoot":"","sources":["../../src/types/receipt-url.ts"],"names":[],"mappings":"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"}
|
package/dist/types/report.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { CONFIDENCE_BASES, CONFIDENCE_LEVELS, EVIDENCE_SUFFICIENCY_LEVELS, HYPOTHESIS_PRIORITIES, HYPOTHESIS_SEMANTICS_VERSION, LEGACY_HYPOTHESIS_SEMANTICS_VERSION, ROBUSTNESS_OUTCOMES, VERDICT_STATUSES, finalClauseCoverageMatchesIntegrity, hasCoherentRobustnessWorking, hypothesisWebReceiptAuthenticates, normalizeFinalHypothesisState, readRobustnessCounts, reportClaimNeedsIndependentMarketAuthority, robustnessWorkingContradictsSummary, sourceAuthenticatesMarketEconomics, } from '../hypothesis-semantics.js';
|
|
16
|
+
import { RECEIPT_URL_WITHHELD_HOST_KEY, RECEIPT_URL_WITHHELD_KEY, receiptRowHasSafeDestination, safeSourcePlatform, safeSourceUrl, withheldUrlHost, } from './receipt-url.js';
|
|
16
17
|
// ⚠️ A types module importing from `tools/` reads backwards, and it is deliberate.
|
|
17
18
|
// `content-type-display.ts` imports NOTHING — it is the package's content-type vocabulary, which
|
|
18
19
|
// happens to live beside the two renderers that were its first callers, and the FUL-341 coupling
|
|
@@ -627,6 +628,78 @@ export const ReportClaimAttestationSchema = z
|
|
|
627
628
|
quote: z.string().describe('The captured snapshot window an independent judge attested — verbatim from OUR snapshot, no exact claim-span pinned.'),
|
|
628
629
|
})
|
|
629
630
|
.passthrough();
|
|
631
|
+
/**
|
|
632
|
+
* FUL-1219: why an unverified source sits beside a claim. MIRROR of the agent's
|
|
633
|
+
* `UNVERIFIED_SOURCE_LABELS`; the coupling test pins the values.
|
|
634
|
+
*/
|
|
635
|
+
export const UNVERIFIED_SOURCE_LABELS = ['paid_market_report', 'company_own_page', 'related_source'];
|
|
636
|
+
/** FUL-1219: MIRROR of the agent's `UNVERIFIED_SOURCE_REASONS` (drop reasons + relevance outcomes). */
|
|
637
|
+
export const UNVERIFIED_SOURCE_REASONS = [
|
|
638
|
+
'no_snapshot',
|
|
639
|
+
'self_serving_market_source',
|
|
640
|
+
'market_source_not_independent',
|
|
641
|
+
'span_unverified',
|
|
642
|
+
'no_window',
|
|
643
|
+
'evidence_gate',
|
|
644
|
+
'unsupported_clause',
|
|
645
|
+
'relevance_partial',
|
|
646
|
+
'relevance_abstained',
|
|
647
|
+
];
|
|
648
|
+
/**
|
|
649
|
+
* FUL-1219: one labelled page found for a NO_RECEIPT claim that did NOT verify. Never a receipt.
|
|
650
|
+
* MIRROR of the agent's `ReportClaimUnverifiedSourceSchema`. `label` and `reason` stay strict so an
|
|
651
|
+
* unknown value is never rendered as a guess; the claim-level `.catch(undefined)` absorbs it.
|
|
652
|
+
*/
|
|
653
|
+
export const ReportClaimUnverifiedSourceSchema = z
|
|
654
|
+
.object({
|
|
655
|
+
sourceId: z
|
|
656
|
+
.string()
|
|
657
|
+
.describe('Pointer ("RUVS-s{n}") into `unverifiedEvidence`. NOT a receipt and never an "RRCP-" id: the page did not verify, and the claim\'s `state` is unchanged by it.'),
|
|
658
|
+
label: z
|
|
659
|
+
.enum(UNVERIFIED_SOURCE_LABELS)
|
|
660
|
+
.describe('Why the page is shown although it did not verify: paid_market_report (a report seller), company_own_page (the subject\'s own site), related_source (on-topic but does not fully support the claim).'),
|
|
661
|
+
reason: z
|
|
662
|
+
.enum(UNVERIFIED_SOURCE_REASONS)
|
|
663
|
+
.describe('The code-owned rejection behind the label (a verifier drop reason or a relevance outcome).'),
|
|
664
|
+
})
|
|
665
|
+
.passthrough();
|
|
666
|
+
/**
|
|
667
|
+
* FUL-1219: the `unverifiedEvidence` index an `RUVS-s{n}` id points at, or null. Anchored on
|
|
668
|
+
* `RUVS-`, so a receipt id (`RRCP-s0`) never resolves into the unverified pool. The schema types
|
|
669
|
+
* `sourceId` as any string, so every reader resolves through this, never a prefix-stripped index.
|
|
670
|
+
*/
|
|
671
|
+
export function parseUnverifiedSourceId(sourceId) {
|
|
672
|
+
if (typeof sourceId !== 'string')
|
|
673
|
+
return null;
|
|
674
|
+
const match = /^RUVS-s(0|[1-9]\d*)$/.exec(sourceId);
|
|
675
|
+
if (!match)
|
|
676
|
+
return null;
|
|
677
|
+
const index = Number(match[1]);
|
|
678
|
+
return Number.isSafeInteger(index) ? index : null;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* FUL-1219: whether a report claim is model-ATTESTED — stored `NO_RECEIPT` with an attestation
|
|
682
|
+
* marker whose `sourceId` and `quote` are both non-blank strings. MIRROR of the app's
|
|
683
|
+
* `isModelAttestedClaim` (`app/lib/validation/claim-states.ts`).
|
|
684
|
+
*
|
|
685
|
+
* ⚠️ Such a claim renders `cited → RRCP-s{n} (NOT a receipt)`. An unverified-source line beside
|
|
686
|
+
* "cited" would read as support (plan stop condition (d)), so every MCP reader of
|
|
687
|
+
* `unverifiedSources` skips an attested claim, exactly as the app's `claimUnverifiedSources` does.
|
|
688
|
+
* Keyed on the marker, not on whether the attested receipt resolves: the app does the same.
|
|
689
|
+
*/
|
|
690
|
+
export function isModelAttestedReportClaim(claim) {
|
|
691
|
+
if (claim === null || typeof claim !== 'object' || Array.isArray(claim))
|
|
692
|
+
return false;
|
|
693
|
+
const { state, attestation } = claim;
|
|
694
|
+
if (state !== 'NO_RECEIPT' || attestation === null || typeof attestation !== 'object')
|
|
695
|
+
return false;
|
|
696
|
+
const { sourceId, quote } = attestation;
|
|
697
|
+
return typeof sourceId === 'string' && sourceId.trim().length > 0 &&
|
|
698
|
+
typeof quote === 'string' && quote.trim().length > 0;
|
|
699
|
+
}
|
|
700
|
+
// FUL-1219 / FUL-1225: the URL display and reachability helpers live in `receipt-url.ts` (it imports
|
|
701
|
+
// nothing, so `hypothesis-semantics.ts` can read them too); re-exported for this module's callers.
|
|
702
|
+
export { RECEIPT_URL_WITHHELD_HOST_KEY, RECEIPT_URL_WITHHELD_KEY, isSafeHttpDestination, receiptRowHasSafeDestination, safeSourcePlatform, safeSourceUrl, } from './receipt-url.js';
|
|
630
703
|
/**
|
|
631
704
|
* A single REPORT-level (market/competitor) claim. Shape mirrors {@link ClaimSchema}
|
|
632
705
|
* but carries a `section` instead of a `personaId` — report claims are not attributed
|
|
@@ -657,6 +730,24 @@ export const ReportClaimSchema = z
|
|
|
657
730
|
.optional()
|
|
658
731
|
.describe('Further receipts for this same GROUNDED claim ("RRCP-s{n}" + verbatim span), each independently code-verified and judged relevant. At most 2; absent on older reports.'),
|
|
659
732
|
attestation: ReportClaimAttestationSchema.optional().describe('MODEL_ATTESTED marker on a NO_RECEIPT claim (forward-prep; presence never implies grounding).'),
|
|
733
|
+
// Per entry, like the app reader: a label a newer producer adds drops only its own entry, so an
|
|
734
|
+
// older published client keeps every label it knows on that claim.
|
|
735
|
+
unverifiedSources: z
|
|
736
|
+
.array(z.unknown())
|
|
737
|
+
.transform((items) => items.flatMap((item) => {
|
|
738
|
+
const parsed = ReportClaimUnverifiedSourceSchema.safeParse(item);
|
|
739
|
+
return parsed.success ? [parsed.data] : [];
|
|
740
|
+
}))
|
|
741
|
+
.optional()
|
|
742
|
+
.catch(undefined)
|
|
743
|
+
.describe('Pages the research found for a NO_RECEIPT claim that did NOT verify, each labelled with why ("RUVS-s{n}" into unverifiedEvidence). Never receipts: they change no state and count toward no grounded number. Absent on older reports.'),
|
|
744
|
+
unverifiedSourcesOmitted: z
|
|
745
|
+
.number()
|
|
746
|
+
.int()
|
|
747
|
+
.positive()
|
|
748
|
+
.optional()
|
|
749
|
+
.catch(undefined)
|
|
750
|
+
.describe('How many further unverified sources were found beyond the three listed. Absent when none.'),
|
|
660
751
|
})
|
|
661
752
|
.passthrough();
|
|
662
753
|
/**
|
|
@@ -698,6 +789,23 @@ export const ReportEvidenceSchema = z
|
|
|
698
789
|
marketEconomicsAuthority: z.enum(['independent', 'commercial_report_seller', 'unknown']).optional(),
|
|
699
790
|
})
|
|
700
791
|
.passthrough();
|
|
792
|
+
/**
|
|
793
|
+
* FUL-1219: one row of `unverifiedEvidence`, the pool "RUVS-s{n}" indexes. MIRROR of the agent's
|
|
794
|
+
* `UnverifiedEvidenceSourceSchema`, with the same lenient scalars as {@link ReportEvidenceSchema}.
|
|
795
|
+
* Deliberately a separate pool from `reportEvidence`: nothing here is a receipt. No quote, no
|
|
796
|
+
* title.
|
|
797
|
+
*/
|
|
798
|
+
export const UnverifiedEvidenceSchema = z
|
|
799
|
+
.object({
|
|
800
|
+
section: ReportClaimSectionSchema,
|
|
801
|
+
url: z.string().describe('The captured page URL (normalised by the producer).'),
|
|
802
|
+
platform: z.string().describe('The host the page came from.'),
|
|
803
|
+
retrievedAt: z.string().describe('ISO-8601 timestamp of when the page was captured (our run time, NOT a publication date).'),
|
|
804
|
+
contentType: z.string().optional().describe('Durable content-type stamped from the source by code; absent means unclassified.'),
|
|
805
|
+
publisherSubjectRelationship: z.enum(['independent', 'self', 'unknown']).optional(),
|
|
806
|
+
marketEconomicsAuthority: z.enum(['independent', 'commercial_report_seller', 'unknown']).optional(),
|
|
807
|
+
})
|
|
808
|
+
.passthrough();
|
|
701
809
|
/**
|
|
702
810
|
* FUL-586's completeness confession — see the field's doc comment on
|
|
703
811
|
* {@link ResearchReportDataSchema} for what it means and why it crosses to this surface.
|
|
@@ -1081,7 +1189,16 @@ export const ResearchReportDataSchema = z
|
|
|
1081
1189
|
reportEvidence: z
|
|
1082
1190
|
.array(ReportEvidenceSchema)
|
|
1083
1191
|
.optional()
|
|
1084
|
-
.describe('The reportClaims receipt pool: cached market/competitor evidence pages ("RRCP-s{n}" indexes this array) with the verbatim highlight a claim\'s span was matched against.'),
|
|
1192
|
+
.describe('The reportClaims receipt pool: cached market/competitor evidence pages ("RRCP-s{n}" indexes this array) with the verbatim highlight a claim\'s span was matched against. A row with `url: ""` and `urlWithheld: true` (plus `urlWithheldHost`) is still a receipt whose stored URL was unsafe to show; its claims keep their state.'),
|
|
1193
|
+
/**
|
|
1194
|
+
* FUL-1219: the pool behind `reportClaims[].unverifiedSources`. Soft per row so one bad row
|
|
1195
|
+
* becomes a hole and every other "RUVS-s{n}" keeps its index.
|
|
1196
|
+
*/
|
|
1197
|
+
unverifiedEvidence: z
|
|
1198
|
+
.array(UnverifiedEvidenceSchema.optional().catch(undefined))
|
|
1199
|
+
.optional()
|
|
1200
|
+
.catch(undefined)
|
|
1201
|
+
.describe('Pages found but NOT verified ("RUVS-s{n}" indexes this array). Never receipts; kept apart from reportEvidence so no receipt pointer resolves here. Absent on older reports.'),
|
|
1085
1202
|
/**
|
|
1086
1203
|
* FUL-586 — what this run FAILED to deliver.
|
|
1087
1204
|
*
|
|
@@ -1251,18 +1368,6 @@ export function projectReportDataForMcp(raw) {
|
|
|
1251
1368
|
const asProjectionRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
1252
1369
|
? value
|
|
1253
1370
|
: null;
|
|
1254
|
-
const safeProjectionUrl = (value) => {
|
|
1255
|
-
if (typeof value !== 'string' || !/^https?:\/\//i.test(value))
|
|
1256
|
-
return false;
|
|
1257
|
-
try {
|
|
1258
|
-
const url = new URL(value);
|
|
1259
|
-
return (url.protocol === 'http:' || url.protocol === 'https:') &&
|
|
1260
|
-
!url.username && !url.password;
|
|
1261
|
-
}
|
|
1262
|
-
catch {
|
|
1263
|
-
return false;
|
|
1264
|
-
}
|
|
1265
|
-
};
|
|
1266
1371
|
const hasProjectionVisibleContent = (value) => typeof value === 'string' &&
|
|
1267
1372
|
value.replace(/[\p{C}\p{Z}\p{Default_Ignorable_Code_Point}]/gu, '').length > 0;
|
|
1268
1373
|
const hasHighlights = Object.prototype.hasOwnProperty.call(record, 'interviewHighlights');
|
|
@@ -1270,6 +1375,7 @@ export function projectReportDataForMcp(raw) {
|
|
|
1270
1375
|
const hasClaims = Object.prototype.hasOwnProperty.call(record, 'claims');
|
|
1271
1376
|
const hasReportClaims = Object.prototype.hasOwnProperty.call(record, 'reportClaims');
|
|
1272
1377
|
const hasReportEvidence = Object.prototype.hasOwnProperty.call(record, 'reportEvidence');
|
|
1378
|
+
const hasUnverifiedEvidence = Object.prototype.hasOwnProperty.call(record, 'unverifiedEvidence');
|
|
1273
1379
|
const hasReportTitle = Object.prototype.hasOwnProperty.call(record, 'reportTitle');
|
|
1274
1380
|
const hasOverview = Object.prototype.hasOwnProperty.call(record, 'marketOverview') ||
|
|
1275
1381
|
Object.prototype.hasOwnProperty.call(record, 'communityOverview');
|
|
@@ -1285,7 +1391,7 @@ export function projectReportDataForMcp(raw) {
|
|
|
1285
1391
|
typeof methodology === 'object' &&
|
|
1286
1392
|
!Array.isArray(methodology) &&
|
|
1287
1393
|
Object.prototype.hasOwnProperty.call(methodology, 'redditRetrieval');
|
|
1288
|
-
if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasClaims && !hasReportClaims && !hasReportEvidence && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion && !hasDecisionSpineMetadata)
|
|
1394
|
+
if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasClaims && !hasReportClaims && !hasReportEvidence && !hasUnverifiedEvidence && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion && !hasDecisionSpineMetadata)
|
|
1289
1395
|
return raw;
|
|
1290
1396
|
const projected = { ...record };
|
|
1291
1397
|
// These fields authenticate producer-owned repair, but are not report evidence and never cross
|
|
@@ -1314,6 +1420,27 @@ export function projectReportDataForMcp(raw) {
|
|
|
1314
1420
|
if (!source)
|
|
1315
1421
|
return rawSource;
|
|
1316
1422
|
const next = { ...source };
|
|
1423
|
+
// FUL-1225: a URL the display check refuses is blanked, with the host-derived `platform`, so
|
|
1424
|
+
// no channel carries it. The row itself stays, so every RRCP-s{n} / RCP-p{i}-s{j} keeps its
|
|
1425
|
+
// index, and the markers keep FUL-534's reachability answer and the hostname for every
|
|
1426
|
+
// receipt-granting or host-comparing reader (`receiptRowHasSafeDestination`,
|
|
1427
|
+
// `receiptRowHostname`): no claim or integrity decision changes. Stored markers are untrusted.
|
|
1428
|
+
delete next[RECEIPT_URL_WITHHELD_KEY];
|
|
1429
|
+
delete next[RECEIPT_URL_WITHHELD_HOST_KEY];
|
|
1430
|
+
if (Object.prototype.hasOwnProperty.call(source, 'url') && source.url !== '' && !safeSourceUrl(source.url)) {
|
|
1431
|
+
next.url = '';
|
|
1432
|
+
if (Object.prototype.hasOwnProperty.call(source, 'platform'))
|
|
1433
|
+
next.platform = '';
|
|
1434
|
+
const host = withheldUrlHost(source.url);
|
|
1435
|
+
if (host !== null) {
|
|
1436
|
+
next[RECEIPT_URL_WITHHELD_KEY] = true;
|
|
1437
|
+
next[RECEIPT_URL_WITHHELD_HOST_KEY] = host;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
else if (Object.prototype.hasOwnProperty.call(source, 'platform') && safeSourcePlatform(source.platform) === null) {
|
|
1441
|
+
// A platform label that is not printable ASCII on one line reaches no channel either.
|
|
1442
|
+
next.platform = '';
|
|
1443
|
+
}
|
|
1317
1444
|
if (Object.prototype.hasOwnProperty.call(source, 'quote')) {
|
|
1318
1445
|
if (typeof source.quote === 'string' && !isDisplaySuitableExcerpt(source.quote)) {
|
|
1319
1446
|
// `undefined` is absent on the JSON wire but remains distinguishable from a historical
|
|
@@ -1413,6 +1540,14 @@ export function projectReportDataForMcp(raw) {
|
|
|
1413
1540
|
if (!thread)
|
|
1414
1541
|
return rawThread;
|
|
1415
1542
|
const nextThread = { ...thread };
|
|
1543
|
+
// FUL-1225: a thread URL is display-only (every forum receipt renders from its cached
|
|
1544
|
+
// excerpt without one), so an unsafe one is simply dropped.
|
|
1545
|
+
if (Object.prototype.hasOwnProperty.call(thread, 'url') && thread.url !== '' && !safeSourceUrl(thread.url)) {
|
|
1546
|
+
delete nextThread.url;
|
|
1547
|
+
}
|
|
1548
|
+
if (Object.prototype.hasOwnProperty.call(thread, 'platform') && safeSourcePlatform(thread.platform) === null) {
|
|
1549
|
+
delete nextThread.platform;
|
|
1550
|
+
}
|
|
1416
1551
|
if (Array.isArray(thread.relevantQuotes)) {
|
|
1417
1552
|
nextThread.relevantQuotes = thread.relevantQuotes.filter(isDisplaySuitableExcerpt);
|
|
1418
1553
|
}
|
|
@@ -1471,6 +1606,66 @@ export function projectReportDataForMcp(raw) {
|
|
|
1471
1606
|
delete next.attestation;
|
|
1472
1607
|
return next;
|
|
1473
1608
|
};
|
|
1609
|
+
// FUL-1219: an unverified-evidence row whose URL is not a plain, credential-free, printable-ASCII
|
|
1610
|
+
// http(s) URL becomes a hole, so no channel carries it and every other RUVS-s{n} keeps its index. A
|
|
1611
|
+
// kept row carries only its declared keys: the pool is text-free by contract, and anything else is
|
|
1612
|
+
// untrusted JSON.
|
|
1613
|
+
const unverifiedEvidence = Array.isArray(record.unverifiedEvidence)
|
|
1614
|
+
? record.unverifiedEvidence.map((rawRow) => {
|
|
1615
|
+
const row = asProjectionRecord(rawRow);
|
|
1616
|
+
if (!row || !safeSourceUrl(row.url))
|
|
1617
|
+
return undefined;
|
|
1618
|
+
return Object.fromEntries(Object.keys(UnverifiedEvidenceSchema.shape).filter((key) => key in row).map((key) => [key, key === 'platform' ? safeSourcePlatform(row[key]) ?? '' : row[key]]));
|
|
1619
|
+
})
|
|
1620
|
+
: [];
|
|
1621
|
+
if (hasUnverifiedEvidence) {
|
|
1622
|
+
projected.unverifiedEvidence = Array.isArray(record.unverifiedEvidence) ? unverifiedEvidence : undefined;
|
|
1623
|
+
}
|
|
1624
|
+
// FUL-1219: unverified sources belong only to a plain (non-attested) NO_RECEIPT claim, and each
|
|
1625
|
+
// entry must resolve through the anchored RUVS parser to a surviving pool row of the claim's own
|
|
1626
|
+
// section; a URL already kept for the claim is dropped (parity with the app's
|
|
1627
|
+
// `claimUnverifiedSources`). Only the three entry fields are copied: other keys are untrusted JSON
|
|
1628
|
+
// (FUL-1067). The omitted count survives only beside a kept entry: a bare "+N more" would point at
|
|
1629
|
+
// nothing. The attestation marker is read on the STORED claim as well, because the projection
|
|
1630
|
+
// above may have dropped an unsuitable attestation: the app still badges that claim "Cited".
|
|
1631
|
+
const projectUnverifiedSources = (value, storedClaim) => {
|
|
1632
|
+
const claim = asProjectionRecord(value);
|
|
1633
|
+
if (!claim)
|
|
1634
|
+
return value;
|
|
1635
|
+
const hasEntries = Object.prototype.hasOwnProperty.call(claim, 'unverifiedSources');
|
|
1636
|
+
const hasOmitted = Object.prototype.hasOwnProperty.call(claim, 'unverifiedSourcesOmitted');
|
|
1637
|
+
if (!hasEntries && !hasOmitted)
|
|
1638
|
+
return claim;
|
|
1639
|
+
const next = { ...claim };
|
|
1640
|
+
delete next.unverifiedSources;
|
|
1641
|
+
delete next.unverifiedSourcesOmitted;
|
|
1642
|
+
if (claim.state !== 'NO_RECEIPT')
|
|
1643
|
+
return next;
|
|
1644
|
+
if (isModelAttestedReportClaim(claim) || isModelAttestedReportClaim(storedClaim))
|
|
1645
|
+
return next;
|
|
1646
|
+
const seen = new Set();
|
|
1647
|
+
const kept = (Array.isArray(claim.unverifiedSources) ? claim.unverifiedSources : []).flatMap((raw) => {
|
|
1648
|
+
const entry = asProjectionRecord(raw);
|
|
1649
|
+
const index = parseUnverifiedSourceId(entry?.sourceId);
|
|
1650
|
+
const parsed = ReportClaimUnverifiedSourceSchema.safeParse(entry);
|
|
1651
|
+
const row = index === null ? undefined : unverifiedEvidence[index];
|
|
1652
|
+
if (!parsed.success || !row || row.section !== claim.section)
|
|
1653
|
+
return [];
|
|
1654
|
+
const href = safeSourceUrl(row.url);
|
|
1655
|
+
if (!href || seen.has(href))
|
|
1656
|
+
return [];
|
|
1657
|
+
seen.add(href);
|
|
1658
|
+
return [{ sourceId: parsed.data.sourceId, label: parsed.data.label, reason: parsed.data.reason }];
|
|
1659
|
+
});
|
|
1660
|
+
if (kept.length === 0)
|
|
1661
|
+
return next;
|
|
1662
|
+
next.unverifiedSources = kept;
|
|
1663
|
+
const omitted = claim.unverifiedSourcesOmitted;
|
|
1664
|
+
if (typeof omitted === 'number' && Number.isSafeInteger(omitted) && omitted > 0) {
|
|
1665
|
+
next.unverifiedSourcesOmitted = omitted;
|
|
1666
|
+
}
|
|
1667
|
+
return next;
|
|
1668
|
+
};
|
|
1474
1669
|
if (hasReportClaims) {
|
|
1475
1670
|
projected.reportClaims = Array.isArray(record.reportClaims)
|
|
1476
1671
|
? record.reportClaims.map((rawClaim) => {
|
|
@@ -1536,7 +1731,7 @@ export function projectReportDataForMcp(raw) {
|
|
|
1536
1731
|
return next;
|
|
1537
1732
|
}
|
|
1538
1733
|
return { ...claim };
|
|
1539
|
-
})
|
|
1734
|
+
}).map((claim, index) => projectUnverifiedSources(claim, record.reportClaims[index]))
|
|
1540
1735
|
: [];
|
|
1541
1736
|
}
|
|
1542
1737
|
if (Object.prototype.hasOwnProperty.call(record, 'claims')) {
|
|
@@ -1700,7 +1895,7 @@ export function projectReportDataForMcp(raw) {
|
|
|
1700
1895
|
? projected.reportEvidence[Number(match[1])]
|
|
1701
1896
|
: undefined);
|
|
1702
1897
|
return Boolean(source &&
|
|
1703
|
-
|
|
1898
|
+
receiptRowHasSafeDestination(source) &&
|
|
1704
1899
|
(!Object.prototype.hasOwnProperty.call(source, 'quote') ||
|
|
1705
1900
|
isDisplaySuitableExcerpt(source.quote)) &&
|
|
1706
1901
|
(source.section === 'market' || source.section === 'competitor' || source.section === 'summary'));
|