@clien-ai/mcp 0.13.8 → 0.15.0

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.
@@ -627,6 +627,98 @@ export const ReportClaimAttestationSchema = z
627
627
  quote: z.string().describe('The captured snapshot window an independent judge attested — verbatim from OUR snapshot, no exact claim-span pinned.'),
628
628
  })
629
629
  .passthrough();
630
+ /**
631
+ * FUL-1219: why an unverified source sits beside a claim. MIRROR of the agent's
632
+ * `UNVERIFIED_SOURCE_LABELS`; the coupling test pins the values.
633
+ */
634
+ export const UNVERIFIED_SOURCE_LABELS = ['paid_market_report', 'company_own_page', 'related_source'];
635
+ /** FUL-1219: MIRROR of the agent's `UNVERIFIED_SOURCE_REASONS` (drop reasons + relevance outcomes). */
636
+ export const UNVERIFIED_SOURCE_REASONS = [
637
+ 'no_snapshot',
638
+ 'self_serving_market_source',
639
+ 'market_source_not_independent',
640
+ 'span_unverified',
641
+ 'no_window',
642
+ 'evidence_gate',
643
+ 'unsupported_clause',
644
+ 'relevance_partial',
645
+ 'relevance_abstained',
646
+ ];
647
+ /**
648
+ * FUL-1219: one labelled page found for a NO_RECEIPT claim that did NOT verify. Never a receipt.
649
+ * MIRROR of the agent's `ReportClaimUnverifiedSourceSchema`. `label` and `reason` stay strict so an
650
+ * unknown value is never rendered as a guess; the claim-level `.catch(undefined)` absorbs it.
651
+ */
652
+ export const ReportClaimUnverifiedSourceSchema = z
653
+ .object({
654
+ sourceId: z
655
+ .string()
656
+ .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.'),
657
+ label: z
658
+ .enum(UNVERIFIED_SOURCE_LABELS)
659
+ .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).'),
660
+ reason: z
661
+ .enum(UNVERIFIED_SOURCE_REASONS)
662
+ .describe('The code-owned rejection behind the label (a verifier drop reason or a relevance outcome).'),
663
+ })
664
+ .passthrough();
665
+ /**
666
+ * FUL-1219: the `unverifiedEvidence` index an `RUVS-s{n}` id points at, or null. Anchored on
667
+ * `RUVS-`, so a receipt id (`RRCP-s0`) never resolves into the unverified pool. The schema types
668
+ * `sourceId` as any string, so every reader resolves through this, never a prefix-stripped index.
669
+ */
670
+ export function parseUnverifiedSourceId(sourceId) {
671
+ if (typeof sourceId !== 'string')
672
+ return null;
673
+ const match = /^RUVS-s(0|[1-9]\d*)$/.exec(sourceId);
674
+ if (!match)
675
+ return null;
676
+ const index = Number(match[1]);
677
+ return Number.isSafeInteger(index) ? index : null;
678
+ }
679
+ /**
680
+ * FUL-1219: whether a report claim is model-ATTESTED — stored `NO_RECEIPT` with an attestation
681
+ * marker whose `sourceId` and `quote` are both non-blank strings. MIRROR of the app's
682
+ * `isModelAttestedClaim` (`app/lib/validation/claim-states.ts`).
683
+ *
684
+ * ⚠️ Such a claim renders `cited → RRCP-s{n} (NOT a receipt)`. An unverified-source line beside
685
+ * "cited" would read as support (plan stop condition (d)), so every MCP reader of
686
+ * `unverifiedSources` skips an attested claim, exactly as the app's `claimUnverifiedSources` does.
687
+ * Keyed on the marker, not on whether the attested receipt resolves: the app does the same.
688
+ */
689
+ export function isModelAttestedReportClaim(claim) {
690
+ if (claim === null || typeof claim !== 'object' || Array.isArray(claim))
691
+ return false;
692
+ const { state, attestation } = claim;
693
+ if (state !== 'NO_RECEIPT' || attestation === null || typeof attestation !== 'object')
694
+ return false;
695
+ const { sourceId, quote } = attestation;
696
+ return typeof sourceId === 'string' && sourceId.trim().length > 0 &&
697
+ typeof quote === 'string' && quote.trim().length > 0;
698
+ }
699
+ /**
700
+ * FUL-1219: the `href` of an `unverifiedEvidence` row's URL, or null.
701
+ *
702
+ * The producer writes each URL through `normalizeUrl` (`agent/src/exa-fetch-tool.ts`), i.e. a WHATWG
703
+ * `URL` serialisation, which is always printable ASCII. So a stored URL with any other character —
704
+ * a newline, a tab, a bidi or zero-width mark, a `←` — was not written by the producer and is
705
+ * refused. `new URL()` alone would NOT refuse it: the parser silently strips tabs and newlines, so
706
+ * the raw string would pass and reach `structuredContent` unchanged. Also refuses non-http(s) and
707
+ * credential-carrying URLs.
708
+ */
709
+ export function safeUnverifiedEvidenceUrl(value) {
710
+ if (typeof value !== 'string' || !/^https?:\/\/[\x21-\x7e]+$/i.test(value))
711
+ return null;
712
+ try {
713
+ const url = new URL(value);
714
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password)
715
+ return null;
716
+ return url.href;
717
+ }
718
+ catch {
719
+ return null;
720
+ }
721
+ }
630
722
  /**
631
723
  * A single REPORT-level (market/competitor) claim. Shape mirrors {@link ClaimSchema}
632
724
  * but carries a `section` instead of a `personaId` — report claims are not attributed
@@ -657,6 +749,24 @@ export const ReportClaimSchema = z
657
749
  .optional()
658
750
  .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
751
  attestation: ReportClaimAttestationSchema.optional().describe('MODEL_ATTESTED marker on a NO_RECEIPT claim (forward-prep; presence never implies grounding).'),
752
+ // Per entry, like the app reader: a label a newer producer adds drops only its own entry, so an
753
+ // older published client keeps every label it knows on that claim.
754
+ unverifiedSources: z
755
+ .array(z.unknown())
756
+ .transform((items) => items.flatMap((item) => {
757
+ const parsed = ReportClaimUnverifiedSourceSchema.safeParse(item);
758
+ return parsed.success ? [parsed.data] : [];
759
+ }))
760
+ .optional()
761
+ .catch(undefined)
762
+ .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.'),
763
+ unverifiedSourcesOmitted: z
764
+ .number()
765
+ .int()
766
+ .positive()
767
+ .optional()
768
+ .catch(undefined)
769
+ .describe('How many further unverified sources were found beyond the three listed. Absent when none.'),
660
770
  })
661
771
  .passthrough();
662
772
  /**
@@ -698,6 +808,23 @@ export const ReportEvidenceSchema = z
698
808
  marketEconomicsAuthority: z.enum(['independent', 'commercial_report_seller', 'unknown']).optional(),
699
809
  })
700
810
  .passthrough();
811
+ /**
812
+ * FUL-1219: one row of `unverifiedEvidence`, the pool "RUVS-s{n}" indexes. MIRROR of the agent's
813
+ * `UnverifiedEvidenceSourceSchema`, with the same lenient scalars as {@link ReportEvidenceSchema}.
814
+ * Deliberately a separate pool from `reportEvidence`: nothing here is a receipt. No quote, no
815
+ * title.
816
+ */
817
+ export const UnverifiedEvidenceSchema = z
818
+ .object({
819
+ section: ReportClaimSectionSchema,
820
+ url: z.string().describe('The captured page URL (normalised by the producer).'),
821
+ platform: z.string().describe('The host the page came from.'),
822
+ retrievedAt: z.string().describe('ISO-8601 timestamp of when the page was captured (our run time, NOT a publication date).'),
823
+ contentType: z.string().optional().describe('Durable content-type stamped from the source by code; absent means unclassified.'),
824
+ publisherSubjectRelationship: z.enum(['independent', 'self', 'unknown']).optional(),
825
+ marketEconomicsAuthority: z.enum(['independent', 'commercial_report_seller', 'unknown']).optional(),
826
+ })
827
+ .passthrough();
701
828
  /**
702
829
  * FUL-586's completeness confession — see the field's doc comment on
703
830
  * {@link ResearchReportDataSchema} for what it means and why it crosses to this surface.
@@ -1082,6 +1209,15 @@ export const ResearchReportDataSchema = z
1082
1209
  .array(ReportEvidenceSchema)
1083
1210
  .optional()
1084
1211
  .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.'),
1212
+ /**
1213
+ * FUL-1219: the pool behind `reportClaims[].unverifiedSources`. Soft per row so one bad row
1214
+ * becomes a hole and every other "RUVS-s{n}" keeps its index.
1215
+ */
1216
+ unverifiedEvidence: z
1217
+ .array(UnverifiedEvidenceSchema.optional().catch(undefined))
1218
+ .optional()
1219
+ .catch(undefined)
1220
+ .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
1221
  /**
1086
1222
  * FUL-586 — what this run FAILED to deliver.
1087
1223
  *
@@ -1270,6 +1406,7 @@ export function projectReportDataForMcp(raw) {
1270
1406
  const hasClaims = Object.prototype.hasOwnProperty.call(record, 'claims');
1271
1407
  const hasReportClaims = Object.prototype.hasOwnProperty.call(record, 'reportClaims');
1272
1408
  const hasReportEvidence = Object.prototype.hasOwnProperty.call(record, 'reportEvidence');
1409
+ const hasUnverifiedEvidence = Object.prototype.hasOwnProperty.call(record, 'unverifiedEvidence');
1273
1410
  const hasReportTitle = Object.prototype.hasOwnProperty.call(record, 'reportTitle');
1274
1411
  const hasOverview = Object.prototype.hasOwnProperty.call(record, 'marketOverview') ||
1275
1412
  Object.prototype.hasOwnProperty.call(record, 'communityOverview');
@@ -1285,7 +1422,7 @@ export function projectReportDataForMcp(raw) {
1285
1422
  typeof methodology === 'object' &&
1286
1423
  !Array.isArray(methodology) &&
1287
1424
  Object.prototype.hasOwnProperty.call(methodology, 'redditRetrieval');
1288
- if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasClaims && !hasReportClaims && !hasReportEvidence && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion && !hasDecisionSpineMetadata)
1425
+ if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasClaims && !hasReportClaims && !hasReportEvidence && !hasUnverifiedEvidence && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion && !hasDecisionSpineMetadata)
1289
1426
  return raw;
1290
1427
  const projected = { ...record };
1291
1428
  // These fields authenticate producer-owned repair, but are not report evidence and never cross
@@ -1471,6 +1608,66 @@ export function projectReportDataForMcp(raw) {
1471
1608
  delete next.attestation;
1472
1609
  return next;
1473
1610
  };
1611
+ // FUL-1219: an unverified-evidence row whose URL is not a plain, credential-free, printable-ASCII
1612
+ // http(s) URL becomes a hole, so no channel carries it and every other RUVS-s{n} keeps its index. A
1613
+ // kept row carries only its declared keys: the pool is text-free by contract, and anything else is
1614
+ // untrusted JSON.
1615
+ const unverifiedEvidence = Array.isArray(record.unverifiedEvidence)
1616
+ ? record.unverifiedEvidence.map((rawRow) => {
1617
+ const row = asProjectionRecord(rawRow);
1618
+ if (!row || !safeUnverifiedEvidenceUrl(row.url))
1619
+ return undefined;
1620
+ return Object.fromEntries(Object.keys(UnverifiedEvidenceSchema.shape).filter((key) => key in row).map((key) => [key, row[key]]));
1621
+ })
1622
+ : [];
1623
+ if (hasUnverifiedEvidence) {
1624
+ projected.unverifiedEvidence = Array.isArray(record.unverifiedEvidence) ? unverifiedEvidence : undefined;
1625
+ }
1626
+ // FUL-1219: unverified sources belong only to a plain (non-attested) NO_RECEIPT claim, and each
1627
+ // entry must resolve through the anchored RUVS parser to a surviving pool row of the claim's own
1628
+ // section; a URL already kept for the claim is dropped (parity with the app's
1629
+ // `claimUnverifiedSources`). Only the three entry fields are copied: other keys are untrusted JSON
1630
+ // (FUL-1067). The omitted count survives only beside a kept entry: a bare "+N more" would point at
1631
+ // nothing. The attestation marker is read on the STORED claim as well, because the projection
1632
+ // above may have dropped an unsuitable attestation: the app still badges that claim "Cited".
1633
+ const projectUnverifiedSources = (value, storedClaim) => {
1634
+ const claim = asProjectionRecord(value);
1635
+ if (!claim)
1636
+ return value;
1637
+ const hasEntries = Object.prototype.hasOwnProperty.call(claim, 'unverifiedSources');
1638
+ const hasOmitted = Object.prototype.hasOwnProperty.call(claim, 'unverifiedSourcesOmitted');
1639
+ if (!hasEntries && !hasOmitted)
1640
+ return claim;
1641
+ const next = { ...claim };
1642
+ delete next.unverifiedSources;
1643
+ delete next.unverifiedSourcesOmitted;
1644
+ if (claim.state !== 'NO_RECEIPT')
1645
+ return next;
1646
+ if (isModelAttestedReportClaim(claim) || isModelAttestedReportClaim(storedClaim))
1647
+ return next;
1648
+ const seen = new Set();
1649
+ const kept = (Array.isArray(claim.unverifiedSources) ? claim.unverifiedSources : []).flatMap((raw) => {
1650
+ const entry = asProjectionRecord(raw);
1651
+ const index = parseUnverifiedSourceId(entry?.sourceId);
1652
+ const parsed = ReportClaimUnverifiedSourceSchema.safeParse(entry);
1653
+ const row = index === null ? undefined : unverifiedEvidence[index];
1654
+ if (!parsed.success || !row || row.section !== claim.section)
1655
+ return [];
1656
+ const href = safeUnverifiedEvidenceUrl(row.url);
1657
+ if (!href || seen.has(href))
1658
+ return [];
1659
+ seen.add(href);
1660
+ return [{ sourceId: parsed.data.sourceId, label: parsed.data.label, reason: parsed.data.reason }];
1661
+ });
1662
+ if (kept.length === 0)
1663
+ return next;
1664
+ next.unverifiedSources = kept;
1665
+ const omitted = claim.unverifiedSourcesOmitted;
1666
+ if (typeof omitted === 'number' && Number.isSafeInteger(omitted) && omitted > 0) {
1667
+ next.unverifiedSourcesOmitted = omitted;
1668
+ }
1669
+ return next;
1670
+ };
1474
1671
  if (hasReportClaims) {
1475
1672
  projected.reportClaims = Array.isArray(record.reportClaims)
1476
1673
  ? record.reportClaims.map((rawClaim) => {
@@ -1536,7 +1733,7 @@ export function projectReportDataForMcp(raw) {
1536
1733
  return next;
1537
1734
  }
1538
1735
  return { ...claim };
1539
- })
1736
+ }).map((claim, index) => projectUnverifiedSources(claim, record.reportClaims[index]))
1540
1737
  : [];
1541
1738
  }
1542
1739
  if (Object.prototype.hasOwnProperty.call(record, 'claims')) {
@@ -2046,6 +2243,7 @@ export const MCP_REDACTED_PIPELINE_KEYS = [
2046
2243
  'structuredOutputRejection',
2047
2244
  // FUL-1094: internal primary-synthesis diagnostics (hook denial codes, SDK result codes, bytes).
2048
2245
  'synthesisDiagnostics',
2246
+ 'personaDiagnostics',
2049
2247
  ];
2050
2248
  export function normalizeMcpPipelineKeyName(key) {
2051
2249
  return key.replace(/[_\-\s]/g, '').toLowerCase();