@clien-ai/mcp 0.13.1 → 0.13.3

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.
@@ -12,7 +12,7 @@
12
12
  * — see how research.ts handles it.
13
13
  */
14
14
  import { z } from 'zod';
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, robustnessWorkingContradictsSummary, } from '../hypothesis-semantics.js';
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
16
  // ⚠️ A types module importing from `tools/` reads backwards, and it is deliberate.
17
17
  // `content-type-display.ts` imports NOTHING — it is the package's content-type vocabulary, which
18
18
  // happens to live beside the two renderers that were its first callers, and the FUL-341 coupling
@@ -21,6 +21,7 @@ import { CONFIDENCE_BASES, CONFIDENCE_LEVELS, EVIDENCE_SUFFICIENCY_LEVELS, HYPOT
21
21
  import { CONTENT_TYPE_EXCLUSIONS, CONTENT_TYPE_STATES } from '../tools/content-type-display.js';
22
22
  import { parseReceiptId } from '../tools/receipt-children.js';
23
23
  import { readSemanticTheme } from './semantic-theme.js';
24
+ import { isDisplaySuitableExcerpt } from '../lib/excerpt-suitability.js';
24
25
  export { CONFIDENCE_BASES, CONFIDENCE_LEVELS, EVIDENCE_SUFFICIENCY_LEVELS, HYPOTHESIS_SEMANTICS_VERSION, ROBUSTNESS_OUTCOMES, VERDICT_STATUSES, } from '../hypothesis-semantics.js';
25
26
  /**
26
27
  * A competitor discovered during the research run.
@@ -690,6 +691,7 @@ export const ReportEvidenceSchema = z
690
691
  'source type is genuinely unknown, NOT checked and found benign. ' +
691
692
  CONTENT_TYPE_EXCLUSIONS),
692
693
  publisherSubjectRelationship: z.enum(['independent', 'self', 'unknown']).optional(),
694
+ marketEconomicsAuthority: z.enum(['independent', 'commercial_report_seller', 'unknown']).optional(),
693
695
  })
694
696
  .passthrough();
695
697
  /**
@@ -1130,6 +1132,114 @@ export function sanitizeMcpReportTitle(value) {
1130
1132
  const words = latinWords.filter((word) => !/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(word)).length + cjkCharacters.length;
1131
1133
  return words >= 6 && words <= 12 ? title : 'Report';
1132
1134
  }
1135
+ export const MCP_REPORT_DECISION_SPINE_VERSION = 1;
1136
+ export const MCP_REPORT_DECISION_ACTION_METHODS = [
1137
+ 'target_user_test',
1138
+ 'independent_market_evidence',
1139
+ ];
1140
+ export const MCP_REPORT_DECISION_ACTION_PREFIXES = {
1141
+ target_user_test: 'Test ',
1142
+ independent_market_evidence: 'Collect independent market evidence for ',
1143
+ };
1144
+ export const MCP_REPORT_MIN_EXECUTIVE_SUMMARY_WORDS = 150;
1145
+ export const MCP_REPORT_MAX_EXECUTIVE_SUMMARY_WORDS = 250;
1146
+ export const MCP_REPORT_MIN_RECOMMENDATIONS = 2;
1147
+ export const MCP_REPORT_MAX_RECOMMENDATIONS = 8;
1148
+ /** Mirrors the producer's CJK-aware decision-summary counter at this package boundary. */
1149
+ export function countMcpReportWords(value) {
1150
+ const cjk = /[぀-ヿ㐀-䶿一-鿿豈-﫿ヲ-゚ᄀ-ᇿ㄰-㆏가-힣]/gu;
1151
+ const trimmed = value.trim();
1152
+ if (trimmed === '')
1153
+ return 0;
1154
+ const cjkCharacters = trimmed.match(cjk)?.length ?? 0;
1155
+ if (cjkCharacters === 0)
1156
+ return trimmed.split(/\s+/u).length;
1157
+ return cjkCharacters + trimmed.split(/\s+/u).filter((word) => word.replace(cjk, '') !== '').length;
1158
+ }
1159
+ export const MCP_REPORT_DECISION_SPINE_LABELS = [
1160
+ 'Decision:',
1161
+ 'Strongest support:',
1162
+ 'Strongest contradiction:',
1163
+ 'Market and community context:',
1164
+ 'Material uncertainty:',
1165
+ 'Next learning step:',
1166
+ ];
1167
+ function authenticatesMcpDecisionSpine(record, projectedRows, projectedRecommendations, reconciledIds) {
1168
+ if (record.reportDecisionSpineVersion !== MCP_REPORT_DECISION_SPINE_VERSION || reconciledIds.size === 0)
1169
+ return false;
1170
+ if (typeof record.executiveSummary !== 'string')
1171
+ return false;
1172
+ const summary = record.executiveSummary.trim();
1173
+ if (countMcpReportWords(summary) < MCP_REPORT_MIN_EXECUTIVE_SUMMARY_WORDS ||
1174
+ countMcpReportWords(summary) > MCP_REPORT_MAX_EXECUTIVE_SUMMARY_WORDS)
1175
+ return false;
1176
+ let labelOffset = -1;
1177
+ for (const label of MCP_REPORT_DECISION_SPINE_LABELS) {
1178
+ labelOffset = summary.indexOf(label, labelOffset + 1);
1179
+ if (labelOffset < 0)
1180
+ return false;
1181
+ }
1182
+ const knownIds = new Set();
1183
+ for (const rawRow of projectedRows) {
1184
+ if (rawRow === null || typeof rawRow !== 'object' || Array.isArray(rawRow))
1185
+ return false;
1186
+ const id = rawRow.hypothesisId;
1187
+ if (typeof id !== 'string' || !id.trim() || knownIds.has(id))
1188
+ return false;
1189
+ knownIds.add(id);
1190
+ }
1191
+ if (!Array.isArray(record.recommendations) ||
1192
+ projectedRecommendations !== record.recommendations ||
1193
+ record.recommendations.length < MCP_REPORT_MIN_RECOMMENDATIONS ||
1194
+ record.recommendations.length > MCP_REPORT_MAX_RECOMMENDATIONS)
1195
+ return false;
1196
+ const recommendations = [];
1197
+ for (const rawRecommendation of record.recommendations) {
1198
+ if (rawRecommendation === null || typeof rawRecommendation !== 'object' || Array.isArray(rawRecommendation))
1199
+ return false;
1200
+ const recommendation = rawRecommendation;
1201
+ if (typeof recommendation.action !== 'string' || !recommendation.action.trim())
1202
+ return false;
1203
+ if (!Array.isArray(recommendation.relatedHypotheses) || recommendation.relatedHypotheses.length === 0)
1204
+ return false;
1205
+ const relatedHypotheses = recommendation.relatedHypotheses;
1206
+ if (!relatedHypotheses.every((id) => typeof id === 'string' && knownIds.has(id)))
1207
+ return false;
1208
+ recommendations.push({ action: recommendation.action.trim(), relatedHypotheses });
1209
+ }
1210
+ const rawMethods = record.reportDecisionActionMethods;
1211
+ const rawPrimaryActions = record.reportDecisionPrimaryActions;
1212
+ if (rawMethods === null || typeof rawMethods !== 'object' || Array.isArray(rawMethods) ||
1213
+ rawPrimaryActions === null || typeof rawPrimaryActions !== 'object' || Array.isArray(rawPrimaryActions))
1214
+ return false;
1215
+ const methods = new Map();
1216
+ const primaryActions = new Map();
1217
+ for (const [id, rawMethod] of Object.entries(rawMethods)) {
1218
+ if (!knownIds.has(id) || !MCP_REPORT_DECISION_ACTION_METHODS.includes(rawMethod))
1219
+ return false;
1220
+ methods.set(id, rawMethod);
1221
+ }
1222
+ for (const [id, rawAction] of Object.entries(rawPrimaryActions)) {
1223
+ if (!knownIds.has(id) || typeof rawAction !== 'string' || !rawAction.trim())
1224
+ return false;
1225
+ primaryActions.set(id, rawAction);
1226
+ }
1227
+ if (methods.size !== reconciledIds.size || primaryActions.size !== reconciledIds.size ||
1228
+ [...methods.keys()].some((id) => !reconciledIds.has(id)) ||
1229
+ [...primaryActions.keys()].some((id) => !reconciledIds.has(id)))
1230
+ return false;
1231
+ for (const id of reconciledIds) {
1232
+ const method = methods.get(id);
1233
+ const primaryAction = primaryActions.get(id);
1234
+ const expectedPrefix = method ? MCP_REPORT_DECISION_ACTION_PREFIXES[method] : null;
1235
+ const matchingRecommendations = recommendations.filter((recommendation) => recommendation.relatedHypotheses.length === 1 &&
1236
+ recommendation.relatedHypotheses[0] === id &&
1237
+ recommendation.action === primaryAction);
1238
+ if (!expectedPrefix || !primaryAction?.startsWith(expectedPrefix) || matchingRecommendations.length !== 1)
1239
+ return false;
1240
+ }
1241
+ return true;
1242
+ }
1133
1243
  export function projectReportDataForMcp(raw) {
1134
1244
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
1135
1245
  return raw;
@@ -1153,6 +1263,9 @@ export function projectReportDataForMcp(raw) {
1153
1263
  value.replace(/[\p{C}\p{Z}\p{Default_Ignorable_Code_Point}]/gu, '').length > 0;
1154
1264
  const hasHighlights = Object.prototype.hasOwnProperty.call(record, 'interviewHighlights');
1155
1265
  const hasHypothesisResults = Object.prototype.hasOwnProperty.call(record, 'hypothesisResults');
1266
+ const hasClaims = Object.prototype.hasOwnProperty.call(record, 'claims');
1267
+ const hasReportClaims = Object.prototype.hasOwnProperty.call(record, 'reportClaims');
1268
+ const hasReportEvidence = Object.prototype.hasOwnProperty.call(record, 'reportEvidence');
1156
1269
  const hasReportTitle = Object.prototype.hasOwnProperty.call(record, 'reportTitle');
1157
1270
  const hasOverview = Object.prototype.hasOwnProperty.call(record, 'marketOverview') ||
1158
1271
  Object.prototype.hasOwnProperty.call(record, 'communityOverview');
@@ -1160,15 +1273,88 @@ export function projectReportDataForMcp(raw) {
1160
1273
  const hasPersonas = Object.prototype.hasOwnProperty.call(record, 'personas');
1161
1274
  const hasForumResearch = Object.prototype.hasOwnProperty.call(record, 'forumResearch');
1162
1275
  const hasHypothesisSemanticsVersion = Object.prototype.hasOwnProperty.call(record, 'hypothesisSemanticsVersion');
1276
+ const hasDecisionSpineMetadata = Object.prototype.hasOwnProperty.call(record, 'reportDecisionSpineVersion') ||
1277
+ Object.prototype.hasOwnProperty.call(record, 'reportDecisionActionMethods') ||
1278
+ Object.prototype.hasOwnProperty.call(record, 'reportDecisionPrimaryActions');
1163
1279
  const methodology = record.methodology;
1164
1280
  const hasRedditNote = methodology !== null &&
1165
1281
  typeof methodology === 'object' &&
1166
1282
  !Array.isArray(methodology) &&
1167
1283
  Object.prototype.hasOwnProperty.call(methodology, 'redditRetrieval');
1168
- if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion)
1284
+ if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasClaims && !hasReportClaims && !hasReportEvidence && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion && !hasDecisionSpineMetadata)
1169
1285
  return raw;
1170
1286
  const projected = { ...record };
1287
+ // These fields authenticate producer-owned repair, but are not report evidence and never cross
1288
+ // the public MCP boundary on either the typed or schema-drift exit.
1289
+ delete projected.reportDecisionSpineVersion;
1290
+ delete projected.reportDecisionActionMethods;
1291
+ delete projected.reportDecisionPrimaryActions;
1292
+ // Source excerpts are immutable evidence in storage, but every MCP channel is a display
1293
+ // surface. Rebuild only the containers which can carry verbatim source text, preserving safe
1294
+ // strings byte-for-byte and omitting an unsafe exact receipt rather than repairing its text.
1295
+ const projectReceipts = (value) => {
1296
+ if (!Array.isArray(value))
1297
+ return undefined;
1298
+ const safeReceipts = [];
1299
+ for (const rawReceipt of value) {
1300
+ const receipt = asProjectionRecord(rawReceipt);
1301
+ if (!receipt || typeof receipt.excerpt !== 'string')
1302
+ return undefined;
1303
+ if (isDisplaySuitableExcerpt(receipt.excerpt))
1304
+ safeReceipts.push({ ...receipt });
1305
+ }
1306
+ return safeReceipts;
1307
+ };
1308
+ const projectSource = (rawSource) => {
1309
+ const source = asProjectionRecord(rawSource);
1310
+ if (!source)
1311
+ return rawSource;
1312
+ const next = { ...source };
1313
+ if (Object.prototype.hasOwnProperty.call(source, 'quote')) {
1314
+ if (typeof source.quote === 'string' && !isDisplaySuitableExcerpt(source.quote)) {
1315
+ // `undefined` is absent on the JSON wire but remains distinguishable from a historical
1316
+ // source which never had a quote, so renderers can downgrade the dependent receipt.
1317
+ next.quote = undefined;
1318
+ }
1319
+ else if (typeof source.quote !== 'string') {
1320
+ delete next.quote;
1321
+ }
1322
+ }
1323
+ if (Object.prototype.hasOwnProperty.call(source, 'receipts')) {
1324
+ const receipts = projectReceipts(source.receipts);
1325
+ if (receipts === undefined)
1326
+ delete next.receipts;
1327
+ else
1328
+ next.receipts = receipts;
1329
+ }
1330
+ return next;
1331
+ };
1332
+ if (hasReportEvidence) {
1333
+ projected.reportEvidence = Array.isArray(record.reportEvidence)
1334
+ ? record.reportEvidence.map(projectSource)
1335
+ : [];
1336
+ }
1337
+ const reportEvidence = Array.isArray(projected.reportEvidence) ? projected.reportEvidence : [];
1338
+ const rejectedMarketEconomics = (Array.isArray(record.reportClaims) ? record.reportClaims : [])
1339
+ .some((rawClaim) => {
1340
+ const claim = asProjectionRecord(rawClaim);
1341
+ if (!claim || !reportClaimNeedsIndependentMarketAuthority(claim.section, claim.text))
1342
+ return false;
1343
+ const state = claim.state;
1344
+ const attestation = asProjectionRecord(claim.attestation);
1345
+ const sourceId = state === 'GROUNDED'
1346
+ ? claim.sourceId
1347
+ : state === 'NO_RECEIPT' && hasProjectionVisibleContent(attestation?.quote)
1348
+ ? attestation?.sourceId
1349
+ : null;
1350
+ const match = typeof sourceId === 'string' ? /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId) : null;
1351
+ const source = match ? asProjectionRecord(reportEvidence[Number(match[1])]) : null;
1352
+ return Boolean(source && !sourceAuthenticatesMarketEconomics(source));
1353
+ });
1354
+ if (rejectedMarketEconomics)
1355
+ delete projected.recommendations;
1171
1356
  let clauseSemanticsFailure = false;
1357
+ const reconciledClauseDowngradeIds = new Set();
1172
1358
  const outerVersion = record.hypothesisSemanticsVersion;
1173
1359
  const outerVersionPresent = hasHypothesisSemanticsVersion;
1174
1360
  const rawHypothesisRows = Array.isArray(record.hypothesisResults) ? record.hypothesisResults : [];
@@ -1192,6 +1378,10 @@ export function projectReportDataForMcp(raw) {
1192
1378
  if (!persona)
1193
1379
  return rawPersona;
1194
1380
  const nextPersona = { ...persona };
1381
+ if (Array.isArray(persona.sources))
1382
+ nextPersona.sources = persona.sources.map(projectSource);
1383
+ else if (Object.prototype.hasOwnProperty.call(persona, 'sources'))
1384
+ delete nextPersona.sources;
1195
1385
  const portrait = ReportPortraitSchema.safeParse(persona.portrait);
1196
1386
  if (portrait.success)
1197
1387
  nextPersona.portrait = portrait.data;
@@ -1219,6 +1409,19 @@ export function projectReportDataForMcp(raw) {
1219
1409
  if (!thread)
1220
1410
  return rawThread;
1221
1411
  const nextThread = { ...thread };
1412
+ if (Array.isArray(thread.relevantQuotes)) {
1413
+ nextThread.relevantQuotes = thread.relevantQuotes.filter(isDisplaySuitableExcerpt);
1414
+ }
1415
+ else if (Object.prototype.hasOwnProperty.call(thread, 'relevantQuotes')) {
1416
+ delete nextThread.relevantQuotes;
1417
+ }
1418
+ if (Object.prototype.hasOwnProperty.call(thread, 'receipts')) {
1419
+ const receipts = projectReceipts(thread.receipts);
1420
+ if (receipts === undefined)
1421
+ delete nextThread.receipts;
1422
+ else
1423
+ nextThread.receipts = receipts;
1424
+ }
1222
1425
  const theme = typeof thread.title === 'string'
1223
1426
  ? readSemanticTheme(thread.theme, thread.title)
1224
1427
  : null;
@@ -1240,6 +1443,105 @@ export function projectReportDataForMcp(raw) {
1240
1443
  delete projected.forumResearch;
1241
1444
  }
1242
1445
  }
1446
+ const projectionSupportsSpan = (excerpt, span) => {
1447
+ if (!isDisplaySuitableExcerpt(excerpt))
1448
+ return false;
1449
+ if (span === undefined || span === null)
1450
+ return true;
1451
+ if (typeof span !== 'string')
1452
+ return false;
1453
+ const normalize = (value) => value
1454
+ .normalize('NFC')
1455
+ .replace(/\s+/gu, ' ')
1456
+ .trim()
1457
+ .toLocaleLowerCase('en-US');
1458
+ const normalizedSpan = normalize(span);
1459
+ return normalizedSpan.length > 0 && normalize(excerpt).includes(normalizedSpan);
1460
+ };
1461
+ const omitClaimReceipt = (claim) => {
1462
+ const next = { ...claim };
1463
+ delete next.sourceId;
1464
+ delete next.receiptId;
1465
+ delete next.quoteSpan;
1466
+ delete next.attestation;
1467
+ return next;
1468
+ };
1469
+ if (hasReportClaims) {
1470
+ projected.reportClaims = Array.isArray(record.reportClaims)
1471
+ ? record.reportClaims.map((rawClaim) => {
1472
+ const claim = asProjectionRecord(rawClaim);
1473
+ if (!claim)
1474
+ return rawClaim;
1475
+ if (claim.state === 'GROUNDED') {
1476
+ const hasSourceId = Object.prototype.hasOwnProperty.call(claim, 'sourceId');
1477
+ const match = typeof claim.sourceId === 'string'
1478
+ ? /^RRCP-s(0|[1-9]\d*)$/.exec(claim.sourceId)
1479
+ : null;
1480
+ const rawSource = match
1481
+ ? asProjectionRecord(Array.isArray(record.reportEvidence)
1482
+ ? record.reportEvidence[Number(match[1])]
1483
+ : undefined)
1484
+ : null;
1485
+ if (!hasSourceId && Object.prototype.hasOwnProperty.call(claim, 'quoteSpan')) {
1486
+ return omitClaimReceipt(claim);
1487
+ }
1488
+ if (hasSourceId && (!rawSource || !projectionSupportsSpan(rawSource.quote, claim.quoteSpan))) {
1489
+ return omitClaimReceipt(claim);
1490
+ }
1491
+ }
1492
+ else if (claim.state === 'NO_RECEIPT') {
1493
+ const attestation = asProjectionRecord(claim.attestation);
1494
+ if (attestation && !isDisplaySuitableExcerpt(attestation.quote)) {
1495
+ const next = { ...claim };
1496
+ delete next.attestation;
1497
+ return next;
1498
+ }
1499
+ }
1500
+ return { ...claim };
1501
+ })
1502
+ : [];
1503
+ }
1504
+ if (Object.prototype.hasOwnProperty.call(record, 'claims')) {
1505
+ projected.claims = Array.isArray(record.claims)
1506
+ ? record.claims.map((rawClaim) => {
1507
+ const claim = asProjectionRecord(rawClaim);
1508
+ if (!claim || claim.state !== 'GROUNDED')
1509
+ return rawClaim;
1510
+ const hasSourceId = Object.prototype.hasOwnProperty.call(claim, 'sourceId');
1511
+ const match = typeof claim.sourceId === 'string'
1512
+ ? /^RCP-p(0|[1-9]\d*)-s(0|[1-9]\d*)$/.exec(claim.sourceId)
1513
+ : null;
1514
+ const personas = Array.isArray(record.personas) ? record.personas : [];
1515
+ const persona = match ? asProjectionRecord(personas[Number(match[1])]) : null;
1516
+ const sources = Array.isArray(persona?.sources) ? persona.sources : [];
1517
+ const source = match ? asProjectionRecord(sources[Number(match[2])]) : null;
1518
+ if (!hasSourceId && Object.prototype.hasOwnProperty.call(claim, 'quoteSpan')) {
1519
+ return omitClaimReceipt(claim);
1520
+ }
1521
+ if (hasSourceId && !source)
1522
+ return omitClaimReceipt(claim);
1523
+ if (!source)
1524
+ return { ...claim };
1525
+ const receipts = Array.isArray(source.receipts) ? source.receipts : [];
1526
+ const hasReceiptId = Object.prototype.hasOwnProperty.call(claim, 'receiptId');
1527
+ if (hasReceiptId) {
1528
+ const matches = receipts.filter((rawReceipt) => asProjectionRecord(rawReceipt)?.receiptId === claim.receiptId);
1529
+ const receipt = matches.length === 1 ? asProjectionRecord(matches[0]) : null;
1530
+ return receipt && projectionSupportsSpan(receipt.excerpt, claim.quoteSpan)
1531
+ ? { ...claim }
1532
+ : omitClaimReceipt(claim);
1533
+ }
1534
+ if (typeof source.quote === 'string' &&
1535
+ !projectionSupportsSpan(source.quote, claim.quoteSpan)) {
1536
+ return omitClaimReceipt(claim);
1537
+ }
1538
+ if (claim.quoteSpan !== undefined && typeof source.quote !== 'string') {
1539
+ return omitClaimReceipt(claim);
1540
+ }
1541
+ return { ...claim };
1542
+ })
1543
+ : [];
1544
+ }
1243
1545
  const projectOverviewRef = (rawRef) => {
1244
1546
  if (rawRef === null || typeof rawRef !== 'object' || Array.isArray(rawRef))
1245
1547
  return null;
@@ -1288,6 +1590,32 @@ export function projectReportDataForMcp(raw) {
1288
1590
  const safeRef = projectOverviewRef(rawRef);
1289
1591
  if (!safeRef)
1290
1592
  continue;
1593
+ if (safeRef.kind === 'report_evidence') {
1594
+ const match = typeof safeRef.sourceId === 'string'
1595
+ ? /^RRCP-s(0|[1-9]\d*)$/.exec(safeRef.sourceId)
1596
+ : null;
1597
+ const source = match
1598
+ ? asProjectionRecord(Array.isArray(projected.reportEvidence)
1599
+ ? projected.reportEvidence[Number(match[1])]
1600
+ : undefined)
1601
+ : null;
1602
+ if (!source || !projectionSupportsSpan(source.quote, claim.span))
1603
+ continue;
1604
+ }
1605
+ if (safeRef.kind === 'forum_receipt') {
1606
+ const matches = (Array.isArray(asProjectionRecord(projected.forumResearch)?.threads)
1607
+ ? asProjectionRecord(projected.forumResearch).threads
1608
+ : []).flatMap((rawThread) => {
1609
+ const thread = asProjectionRecord(rawThread);
1610
+ return (Array.isArray(thread?.receipts) ? thread.receipts : []).filter((rawReceipt) => {
1611
+ const receipt = asProjectionRecord(rawReceipt);
1612
+ return receipt !== null && receipt.receiptId === safeRef.receiptId &&
1613
+ projectionSupportsSpan(receipt.excerpt, claim.span);
1614
+ });
1615
+ });
1616
+ if (matches.length !== 1)
1617
+ continue;
1618
+ }
1291
1619
  const refKey = safeRef.kind === 'report_evidence'
1292
1620
  ? `${safeRef.kind}:${safeRef.sourceId}`
1293
1621
  : safeRef.kind === 'forum_receipt'
@@ -1303,13 +1631,13 @@ export function projectReportDataForMcp(raw) {
1303
1631
  return [{
1304
1632
  span: claim.span,
1305
1633
  claimType: claim.claimType,
1306
- evidenceRefsTruncated: rawEvidenceRefs.length > MCP_OVERVIEW_REF_CAP,
1634
+ evidenceRefsTruncated: claim.evidenceRefsTruncated === true || rawEvidenceRefs.length > MCP_OVERVIEW_REF_CAP,
1307
1635
  evidenceRefs,
1308
1636
  }];
1309
1637
  });
1310
1638
  projected[key] = {
1311
1639
  text: overviewRecord.text,
1312
- claimSpansTruncated: overviewRecord.claimSpans.length > MCP_OVERVIEW_SPAN_CAP,
1640
+ claimSpansTruncated: overviewRecord.claimSpansTruncated === true || overviewRecord.claimSpans.length > MCP_OVERVIEW_SPAN_CAP,
1313
1641
  claimSpans,
1314
1642
  };
1315
1643
  }
@@ -1330,22 +1658,24 @@ export function projectReportDataForMcp(raw) {
1330
1658
  const resolvesCrossCuttingRef = (ref) => {
1331
1659
  if (ref.kind === 'report_evidence') {
1332
1660
  const match = /^RRCP-s(0|[1-9]\d*)$/.exec(ref.sourceId ?? '');
1333
- const source = match && asProjectionRecord(Array.isArray(record.reportEvidence)
1334
- ? record.reportEvidence[Number(match[1])]
1661
+ const source = match && asProjectionRecord(Array.isArray(projected.reportEvidence)
1662
+ ? projected.reportEvidence[Number(match[1])]
1335
1663
  : undefined);
1336
1664
  return Boolean(source &&
1337
1665
  safeProjectionUrl(source.url) &&
1666
+ (!Object.prototype.hasOwnProperty.call(source, 'quote') ||
1667
+ isDisplaySuitableExcerpt(source.quote)) &&
1338
1668
  (source.section === 'market' || source.section === 'competitor' || source.section === 'summary'));
1339
1669
  }
1340
1670
  if (ref.kind === 'forum_receipt') {
1341
- const matches = (Array.isArray(asProjectionRecord(record.forumResearch)?.threads)
1342
- ? asProjectionRecord(record.forumResearch).threads
1671
+ const matches = (Array.isArray(asProjectionRecord(projected.forumResearch)?.threads)
1672
+ ? asProjectionRecord(projected.forumResearch).threads
1343
1673
  : []).flatMap((rawThread) => {
1344
1674
  const thread = asProjectionRecord(rawThread);
1345
1675
  return (Array.isArray(thread?.receipts) ? thread.receipts : []).filter((rawReceipt) => {
1346
1676
  const receipt = asProjectionRecord(rawReceipt);
1347
1677
  return Boolean(receipt && receipt.receiptId === ref.receiptId &&
1348
- hasProjectionVisibleContent(receipt.excerpt));
1678
+ isDisplaySuitableExcerpt(receipt.excerpt));
1349
1679
  });
1350
1680
  });
1351
1681
  return matches.length === 1;
@@ -1493,12 +1823,23 @@ export function projectReportDataForMcp(raw) {
1493
1823
  ? { ...safeRow, final: { version: HYPOTHESIS_SEMANTICS_VERSION } }
1494
1824
  : safeRow;
1495
1825
  const normalized = normalizeFinalHypothesisState(rowForNormalization);
1496
- if (semanticsMismatch ||
1826
+ const isCoherentIntentionalDowngrade = Boolean(!semanticsMismatch &&
1827
+ !clauseIntegrityMismatch &&
1828
+ normalized?.clauseCoverage !== undefined &&
1829
+ normalized.clauseCoverage !== 'invalid' &&
1830
+ normalized.clauseCoverage !== 'complete' &&
1831
+ normalized.originalVerdict !== normalized.verdict);
1832
+ if (isCoherentIntentionalDowngrade) {
1833
+ if (typeof safeRow.hypothesisId === 'string' && safeRow.hypothesisId.trim()) {
1834
+ reconciledClauseDowngradeIds.add(safeRow.hypothesisId);
1835
+ }
1836
+ else {
1837
+ clauseSemanticsFailure = true;
1838
+ }
1839
+ }
1840
+ else if (semanticsMismatch ||
1497
1841
  clauseIntegrityMismatch ||
1498
1842
  normalized?.clauseCoverage === 'invalid' ||
1499
- (normalized?.clauseCoverage !== undefined &&
1500
- normalized.clauseCoverage !== 'complete' &&
1501
- normalized.originalVerdict !== normalized.verdict) ||
1502
1843
  (requiresClauseIntegrity && normalized === null))
1503
1844
  clauseSemanticsFailure = true;
1504
1845
  if (normalized)
@@ -1552,6 +1893,9 @@ export function projectReportDataForMcp(raw) {
1552
1893
  return safeRow;
1553
1894
  });
1554
1895
  }
1896
+ if (reconciledClauseDowngradeIds.size > 0 &&
1897
+ !authenticatesMcpDecisionSpine(record, Array.isArray(projected.hypothesisResults) ? projected.hypothesisResults : [], projected.recommendations, reconciledClauseDowngradeIds))
1898
+ clauseSemanticsFailure = true;
1555
1899
  if (clauseSemanticsFailure) {
1556
1900
  projected.executiveSummary = 'This run did not establish every material part of at least one hypothesis. Treat the affected hypotheses as unresolved.';
1557
1901
  projected.verdictSummary = projected.executiveSummary;