@clien-ai/mcp 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hypothesis-semantics.js +334 -11
- package/dist/hypothesis-semantics.js.map +1 -1
- package/dist/lib/excerpt-suitability.js +46 -0
- package/dist/lib/excerpt-suitability.js.map +1 -0
- package/dist/tools/collections.js +7 -3
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/content-type-display.js +11 -3
- package/dist/tools/content-type-display.js.map +1 -1
- package/dist/tools/failure-guidance.js +53 -5
- package/dist/tools/failure-guidance.js.map +1 -1
- package/dist/tools/market-sizing-proof.js +3 -1
- package/dist/tools/market-sizing-proof.js.map +1 -1
- package/dist/tools/receipt-children.js +4 -1
- package/dist/tools/receipt-children.js.map +1 -1
- package/dist/tools/report-digest.js +145 -47
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/research.js +4 -0
- package/dist/tools/research.js.map +1 -1
- package/dist/tools/scoped-research.js +23 -4
- package/dist/tools/scoped-research.js.map +1 -1
- package/dist/tools/status.js +4 -0
- package/dist/tools/status.js.map +1 -1
- package/dist/types/report.js +372 -17
- package/dist/types/report.js.map +1 -1
- package/package.json +1 -1
package/dist/types/report.js
CHANGED
|
@@ -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, ROBUSTNESS_OUTCOMES, VERDICT_STATUSES, hasCoherentRobustnessWorking, 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.
|
|
@@ -264,13 +265,14 @@ export const RobustnessResultSchema = z
|
|
|
264
265
|
*/
|
|
265
266
|
export const FinalHypothesisStateSchema = z
|
|
266
267
|
.object({
|
|
267
|
-
version: z.literal(HYPOTHESIS_SEMANTICS_VERSION).describe('Which semantics contract this block was written under.'),
|
|
268
|
+
version: z.union([z.literal(1), z.literal(HYPOTHESIS_SEMANTICS_VERSION)]).describe('Which semantics contract this block was written under.'),
|
|
268
269
|
verdict: z.enum(VERDICT_STATUSES).describe('The EFFECTIVE verdict every surface shows. A robustness downgrade is applied only when it makes a strictly weaker claim, so a malformed re-ask block can never upgrade a row.'),
|
|
269
270
|
originalVerdict: z.enum(VERDICT_STATUSES).describe('The synthesiser\'s own verdict, preserved for audit even when the row was downgraded.'),
|
|
270
271
|
confidence: z.enum(CONFIDENCE_LEVELS).optional().describe('How reliable the final verdict is. ABSENT means withheld — see confidenceBasis. Never a probability that the hypothesis is true.'),
|
|
271
272
|
confidenceBasis: z.enum(CONFIDENCE_BASES).describe('Why confidence says what it says: read off the evidence, recomputed after the verdict flipped under rephrasing, or withheld because the run attached no evidence.'),
|
|
272
273
|
sufficiency: z.enum(EVIDENCE_SUFFICIENCY_LEVELS).describe('How much evidence sits behind the verdict, across how many research methods. A coverage axis, independent of which way the verdict pointed.'),
|
|
273
274
|
robustness: z.enum(ROBUSTNESS_OUTCOMES).describe('What the robustness pass did: held, flipped, or never re-asked. `not_retested` is explicit — it is not the same as held.'),
|
|
275
|
+
clauseCoverage: z.enum(['complete', 'uncovered', 'contradicted', 'invalid']).optional(),
|
|
274
276
|
priority: z.enum(HYPOTHESIS_PRIORITIES).optional().catch(undefined).describe('The product priority of the underlying hypothesis, for ranking what to go and ask about first.'),
|
|
275
277
|
})
|
|
276
278
|
.passthrough();
|
|
@@ -289,6 +291,8 @@ export const EvidenceSchema = z
|
|
|
289
291
|
summary: z.string(),
|
|
290
292
|
details: z.string().optional(),
|
|
291
293
|
url: z.string().optional(),
|
|
294
|
+
nominationId: z.string().regex(/^wh2:[a-f0-9]{16}$/).optional(),
|
|
295
|
+
receipt: z.object({ sourceId: z.string(), quoteSpan: z.string() }).optional(),
|
|
292
296
|
})
|
|
293
297
|
.passthrough();
|
|
294
298
|
/**
|
|
@@ -305,6 +309,20 @@ export const HypothesisResultSchema = z
|
|
|
305
309
|
confidence: z.number().describe('Legacy numeric confidence stored on either the retired 0-1 or 0-100 scale. Preserve and label it as legacy; never compare it with final.confidence, rank on it, or trend it.'),
|
|
306
310
|
supportingEvidence: z.array(EvidenceSchema).optional(),
|
|
307
311
|
contradictingEvidence: z.array(EvidenceSchema).optional(),
|
|
312
|
+
clauseCoverage: z.array(z.object({
|
|
313
|
+
clauseId: z.string(),
|
|
314
|
+
status: z.enum(['supported', 'contradicted', 'uncovered']),
|
|
315
|
+
evidenceRefs: z.array(z.object({ side: z.enum(['supporting', 'contradicting']), index: z.number().int().min(0) })),
|
|
316
|
+
})).optional(),
|
|
317
|
+
clauseIntegrity: z.object({
|
|
318
|
+
outcome: z.enum(['complete', 'uncovered', 'contradicted', 'invalid']),
|
|
319
|
+
clauses: z.array(z.object({
|
|
320
|
+
clauseId: z.string(),
|
|
321
|
+
text: z.string(),
|
|
322
|
+
status: z.enum(['supported', 'contradicted', 'uncovered']),
|
|
323
|
+
evidenceRefs: z.array(z.object({ side: z.enum(['supporting', 'contradicting']), index: z.number().int().min(0) })),
|
|
324
|
+
})),
|
|
325
|
+
}).optional(),
|
|
308
326
|
robustness: RobustnessResultSchema.optional().describe('Robustness re-ask survival count for this verdict (absent on legacy reports / when not re-tested).'),
|
|
309
327
|
scopeCaveat: z.string().optional().describe('Belief-adjacent scope-mismatch note (FUL-102): present when this hypothesis\'s persona support came from a persona speaking outside its credibility domain. Treat such support as directional, not grounded.'),
|
|
310
328
|
final: FinalHypothesisStateSchema.optional().describe('THE effective result (FUL-358) — verdict, reliability, evidence sufficiency and robustness outcome, reconciled once and read by every surface. Prefer `final.verdict` over `status` and `final.confidence` over the numeric `confidence` above. Absent on legacy reports, where the numeric `confidence` is the retired measure.'),
|
|
@@ -447,7 +465,7 @@ export const InsightSchema = z
|
|
|
447
465
|
* way), but every OTHER typed field on that report loses its typing too. Which is why
|
|
448
466
|
* this list grows in the SAME change as the agent's, never "when someone notices".
|
|
449
467
|
*/
|
|
450
|
-
export const OPEN_QUESTION_STATUSES = ['never_asked', 'nobody_answered', 'voices_disagreed'];
|
|
468
|
+
export const OPEN_QUESTION_STATUSES = ['never_asked', 'nobody_answered', 'voices_disagreed', 'clause_uncovered'];
|
|
451
469
|
/**
|
|
452
470
|
* One thing the run did not settle. MIRROR of the agent's `OpenQuestionSchema`
|
|
453
471
|
* (`agent/src/types.ts`); key set pinned by the coupling test.
|
|
@@ -468,6 +486,7 @@ export const OpenQuestionSchema = z
|
|
|
468
486
|
status: z
|
|
469
487
|
.enum(OPEN_QUESTION_STATUSES)
|
|
470
488
|
.describe('WHY it is still open: never_asked (the run never touched it), nobody_answered (put to personas, none took a position), voices_disagreed (the personas split).'),
|
|
489
|
+
clauseId: z.string().optional().describe('Stable clause ID for clause_uncovered provenance.'),
|
|
471
490
|
// Lenient `z.number()` against the agent's `z.number().int().min(0)`, deliberately.
|
|
472
491
|
// This is the one field on the row the app is willing to degrade: its parser clamps a
|
|
473
492
|
// count the run's own persona roster cannot support down to 0 and renders the
|
|
@@ -478,7 +497,12 @@ export const OpenQuestionSchema = z
|
|
|
478
497
|
.number()
|
|
479
498
|
.describe('How many DISTINCT personas were PUT this hypothesis — an interview counts when it returned a finding on it OR when the persona\'s brief claimed it, because a persona who was asked and said nothing was still asked. 0 for never_asked by construction; for nobody_answered it is what makes the row specific ("3 were asked; none took a position") rather than a vague claim of silence.'),
|
|
480
499
|
})
|
|
481
|
-
.passthrough()
|
|
500
|
+
.passthrough()
|
|
501
|
+
.superRefine((row, ctx) => {
|
|
502
|
+
if (row.status === 'clause_uncovered' && !row.clauseId) {
|
|
503
|
+
ctx.addIssue({ code: 'custom', path: ['clauseId'], message: 'Uncovered clauses require a clause ID' });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
482
506
|
/**
|
|
483
507
|
* Persona identity priors (Exa Rail M5b, FUL-150 / U10). The demographic shape of
|
|
484
508
|
* a persona's role — an explicit seniority RUNG + a career trajectory — force-filled
|
|
@@ -641,7 +665,7 @@ export const ReportClaimSchema = z
|
|
|
641
665
|
*/
|
|
642
666
|
export const ReportEvidenceSchema = z
|
|
643
667
|
.object({
|
|
644
|
-
section:
|
|
668
|
+
section: z.enum([...REPORT_CLAIM_SECTIONS, 'hypothesis']),
|
|
645
669
|
url: z.string().describe('Live URL the highlight was collected from.'),
|
|
646
670
|
platform: z.string().describe('The host the highlight came from.'),
|
|
647
671
|
quote: z.string().describe('Verbatim cached excerpt (≤600 chars) so a market/competitor figure renders even if the URL later 404s.'),
|
|
@@ -666,6 +690,8 @@ export const ReportEvidenceSchema = z
|
|
|
666
690
|
' Stamped from the source by code, never model-emitted. ABSENT means UNCLASSIFIED: the ' +
|
|
667
691
|
'source type is genuinely unknown, NOT checked and found benign. ' +
|
|
668
692
|
CONTENT_TYPE_EXCLUSIONS),
|
|
693
|
+
publisherSubjectRelationship: z.enum(['independent', 'self', 'unknown']).optional(),
|
|
694
|
+
marketEconomicsAuthority: z.enum(['independent', 'commercial_report_seller', 'unknown']).optional(),
|
|
669
695
|
})
|
|
670
696
|
.passthrough();
|
|
671
697
|
/**
|
|
@@ -837,6 +863,12 @@ export const MethodologyNoteSchema = z
|
|
|
837
863
|
totalTokensUsed: z.number(),
|
|
838
864
|
jobDurationSeconds: z.number(),
|
|
839
865
|
saturation: SaturationNoteSchema.optional(),
|
|
866
|
+
evidenceOutcome: z
|
|
867
|
+
.object({ code: z.literal('bounded_search_no_admissible_evidence') })
|
|
868
|
+
.strict()
|
|
869
|
+
.optional()
|
|
870
|
+
.catch(undefined)
|
|
871
|
+
.describe('Bounded acquisition completed without admissible external evidence.'),
|
|
840
872
|
// FUL-685 (T8 / D6). `.catch(undefined)` and not merely `.optional()`: a malformed or
|
|
841
873
|
// unrecognised value must degrade THIS FIELD to unrecorded, never reject an otherwise valid
|
|
842
874
|
// legacy report — the same fail-soft-per-field posture `personasSynthesis` takes.
|
|
@@ -928,6 +960,7 @@ export const ReportOverviewSchema = z.object({
|
|
|
928
960
|
*/
|
|
929
961
|
export const ResearchReportDataSchema = z
|
|
930
962
|
.object({
|
|
963
|
+
hypothesisSemanticsVersion: z.number().optional(),
|
|
931
964
|
competitors: z.array(CompetitorSchema).optional(),
|
|
932
965
|
hypotheses: z.array(z.unknown()).optional(),
|
|
933
966
|
/**
|
|
@@ -1122,20 +1155,101 @@ export function projectReportDataForMcp(raw) {
|
|
|
1122
1155
|
value.replace(/[\p{C}\p{Z}\p{Default_Ignorable_Code_Point}]/gu, '').length > 0;
|
|
1123
1156
|
const hasHighlights = Object.prototype.hasOwnProperty.call(record, 'interviewHighlights');
|
|
1124
1157
|
const hasHypothesisResults = Object.prototype.hasOwnProperty.call(record, 'hypothesisResults');
|
|
1158
|
+
const hasClaims = Object.prototype.hasOwnProperty.call(record, 'claims');
|
|
1159
|
+
const hasReportClaims = Object.prototype.hasOwnProperty.call(record, 'reportClaims');
|
|
1160
|
+
const hasReportEvidence = Object.prototype.hasOwnProperty.call(record, 'reportEvidence');
|
|
1125
1161
|
const hasReportTitle = Object.prototype.hasOwnProperty.call(record, 'reportTitle');
|
|
1126
1162
|
const hasOverview = Object.prototype.hasOwnProperty.call(record, 'marketOverview') ||
|
|
1127
1163
|
Object.prototype.hasOwnProperty.call(record, 'communityOverview');
|
|
1128
1164
|
const hasCrossCutting = Object.prototype.hasOwnProperty.call(record, 'crossCuttingFindings');
|
|
1129
1165
|
const hasPersonas = Object.prototype.hasOwnProperty.call(record, 'personas');
|
|
1130
1166
|
const hasForumResearch = Object.prototype.hasOwnProperty.call(record, 'forumResearch');
|
|
1167
|
+
const hasHypothesisSemanticsVersion = Object.prototype.hasOwnProperty.call(record, 'hypothesisSemanticsVersion');
|
|
1131
1168
|
const methodology = record.methodology;
|
|
1132
1169
|
const hasRedditNote = methodology !== null &&
|
|
1133
1170
|
typeof methodology === 'object' &&
|
|
1134
1171
|
!Array.isArray(methodology) &&
|
|
1135
1172
|
Object.prototype.hasOwnProperty.call(methodology, 'redditRetrieval');
|
|
1136
|
-
if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch)
|
|
1173
|
+
if (!hasHighlights && !hasRedditNote && !hasHypothesisResults && !hasClaims && !hasReportClaims && !hasReportEvidence && !hasOverview && !hasReportTitle && !hasCrossCutting && !hasPersonas && !hasForumResearch && !hasHypothesisSemanticsVersion)
|
|
1137
1174
|
return raw;
|
|
1138
1175
|
const projected = { ...record };
|
|
1176
|
+
// Source excerpts are immutable evidence in storage, but every MCP channel is a display
|
|
1177
|
+
// surface. Rebuild only the containers which can carry verbatim source text, preserving safe
|
|
1178
|
+
// strings byte-for-byte and omitting an unsafe exact receipt rather than repairing its text.
|
|
1179
|
+
const projectReceipts = (value) => {
|
|
1180
|
+
if (!Array.isArray(value))
|
|
1181
|
+
return undefined;
|
|
1182
|
+
const safeReceipts = [];
|
|
1183
|
+
for (const rawReceipt of value) {
|
|
1184
|
+
const receipt = asProjectionRecord(rawReceipt);
|
|
1185
|
+
if (!receipt || typeof receipt.excerpt !== 'string')
|
|
1186
|
+
return undefined;
|
|
1187
|
+
if (isDisplaySuitableExcerpt(receipt.excerpt))
|
|
1188
|
+
safeReceipts.push({ ...receipt });
|
|
1189
|
+
}
|
|
1190
|
+
return safeReceipts;
|
|
1191
|
+
};
|
|
1192
|
+
const projectSource = (rawSource) => {
|
|
1193
|
+
const source = asProjectionRecord(rawSource);
|
|
1194
|
+
if (!source)
|
|
1195
|
+
return rawSource;
|
|
1196
|
+
const next = { ...source };
|
|
1197
|
+
if (Object.prototype.hasOwnProperty.call(source, 'quote')) {
|
|
1198
|
+
if (typeof source.quote === 'string' && !isDisplaySuitableExcerpt(source.quote)) {
|
|
1199
|
+
// `undefined` is absent on the JSON wire but remains distinguishable from a historical
|
|
1200
|
+
// source which never had a quote, so renderers can downgrade the dependent receipt.
|
|
1201
|
+
next.quote = undefined;
|
|
1202
|
+
}
|
|
1203
|
+
else if (typeof source.quote !== 'string') {
|
|
1204
|
+
delete next.quote;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (Object.prototype.hasOwnProperty.call(source, 'receipts')) {
|
|
1208
|
+
const receipts = projectReceipts(source.receipts);
|
|
1209
|
+
if (receipts === undefined)
|
|
1210
|
+
delete next.receipts;
|
|
1211
|
+
else
|
|
1212
|
+
next.receipts = receipts;
|
|
1213
|
+
}
|
|
1214
|
+
return next;
|
|
1215
|
+
};
|
|
1216
|
+
if (hasReportEvidence) {
|
|
1217
|
+
projected.reportEvidence = Array.isArray(record.reportEvidence)
|
|
1218
|
+
? record.reportEvidence.map(projectSource)
|
|
1219
|
+
: [];
|
|
1220
|
+
}
|
|
1221
|
+
const reportEvidence = Array.isArray(projected.reportEvidence) ? projected.reportEvidence : [];
|
|
1222
|
+
const rejectedMarketEconomics = (Array.isArray(record.reportClaims) ? record.reportClaims : [])
|
|
1223
|
+
.some((rawClaim) => {
|
|
1224
|
+
const claim = asProjectionRecord(rawClaim);
|
|
1225
|
+
if (!claim || !reportClaimNeedsIndependentMarketAuthority(claim.section, claim.text))
|
|
1226
|
+
return false;
|
|
1227
|
+
const state = claim.state;
|
|
1228
|
+
const attestation = asProjectionRecord(claim.attestation);
|
|
1229
|
+
const sourceId = state === 'GROUNDED'
|
|
1230
|
+
? claim.sourceId
|
|
1231
|
+
: state === 'NO_RECEIPT' && hasProjectionVisibleContent(attestation?.quote)
|
|
1232
|
+
? attestation?.sourceId
|
|
1233
|
+
: null;
|
|
1234
|
+
const match = typeof sourceId === 'string' ? /^RRCP-s(0|[1-9]\d*)$/.exec(sourceId) : null;
|
|
1235
|
+
const source = match ? asProjectionRecord(reportEvidence[Number(match[1])]) : null;
|
|
1236
|
+
return Boolean(source && !sourceAuthenticatesMarketEconomics(source));
|
|
1237
|
+
});
|
|
1238
|
+
if (rejectedMarketEconomics)
|
|
1239
|
+
delete projected.recommendations;
|
|
1240
|
+
let clauseSemanticsFailure = false;
|
|
1241
|
+
const outerVersion = record.hypothesisSemanticsVersion;
|
|
1242
|
+
const outerVersionPresent = hasHypothesisSemanticsVersion;
|
|
1243
|
+
const rawHypothesisRows = Array.isArray(record.hypothesisResults) ? record.hypothesisResults : [];
|
|
1244
|
+
const rawRowsClaimV2 = rawHypothesisRows.some((rawRow) => {
|
|
1245
|
+
const row = asProjectionRecord(rawRow);
|
|
1246
|
+
return asProjectionRecord(row?.final)?.version === HYPOTHESIS_SEMANTICS_VERSION ||
|
|
1247
|
+
(row !== null && Object.prototype.hasOwnProperty.call(row, 'clauseIntegrity'));
|
|
1248
|
+
});
|
|
1249
|
+
const requiresClauseIntegrity = rawRowsClaimV2 || (outerVersionPresent && outerVersion !== LEGACY_HYPOTHESIS_SEMANTICS_VERSION);
|
|
1250
|
+
if (requiresClauseIntegrity && rawHypothesisRows.length === 0) {
|
|
1251
|
+
clauseSemanticsFailure = true;
|
|
1252
|
+
}
|
|
1139
1253
|
if (hasReportTitle)
|
|
1140
1254
|
projected.reportTitle = sanitizeMcpReportTitle(record.reportTitle);
|
|
1141
1255
|
// Validate the frozen decoration before either the typed parse or raw schema-drift exit.
|
|
@@ -1147,6 +1261,10 @@ export function projectReportDataForMcp(raw) {
|
|
|
1147
1261
|
if (!persona)
|
|
1148
1262
|
return rawPersona;
|
|
1149
1263
|
const nextPersona = { ...persona };
|
|
1264
|
+
if (Array.isArray(persona.sources))
|
|
1265
|
+
nextPersona.sources = persona.sources.map(projectSource);
|
|
1266
|
+
else if (Object.prototype.hasOwnProperty.call(persona, 'sources'))
|
|
1267
|
+
delete nextPersona.sources;
|
|
1150
1268
|
const portrait = ReportPortraitSchema.safeParse(persona.portrait);
|
|
1151
1269
|
if (portrait.success)
|
|
1152
1270
|
nextPersona.portrait = portrait.data;
|
|
@@ -1174,6 +1292,19 @@ export function projectReportDataForMcp(raw) {
|
|
|
1174
1292
|
if (!thread)
|
|
1175
1293
|
return rawThread;
|
|
1176
1294
|
const nextThread = { ...thread };
|
|
1295
|
+
if (Array.isArray(thread.relevantQuotes)) {
|
|
1296
|
+
nextThread.relevantQuotes = thread.relevantQuotes.filter(isDisplaySuitableExcerpt);
|
|
1297
|
+
}
|
|
1298
|
+
else if (Object.prototype.hasOwnProperty.call(thread, 'relevantQuotes')) {
|
|
1299
|
+
delete nextThread.relevantQuotes;
|
|
1300
|
+
}
|
|
1301
|
+
if (Object.prototype.hasOwnProperty.call(thread, 'receipts')) {
|
|
1302
|
+
const receipts = projectReceipts(thread.receipts);
|
|
1303
|
+
if (receipts === undefined)
|
|
1304
|
+
delete nextThread.receipts;
|
|
1305
|
+
else
|
|
1306
|
+
nextThread.receipts = receipts;
|
|
1307
|
+
}
|
|
1177
1308
|
const theme = typeof thread.title === 'string'
|
|
1178
1309
|
? readSemanticTheme(thread.theme, thread.title)
|
|
1179
1310
|
: null;
|
|
@@ -1195,6 +1326,105 @@ export function projectReportDataForMcp(raw) {
|
|
|
1195
1326
|
delete projected.forumResearch;
|
|
1196
1327
|
}
|
|
1197
1328
|
}
|
|
1329
|
+
const projectionSupportsSpan = (excerpt, span) => {
|
|
1330
|
+
if (!isDisplaySuitableExcerpt(excerpt))
|
|
1331
|
+
return false;
|
|
1332
|
+
if (span === undefined || span === null)
|
|
1333
|
+
return true;
|
|
1334
|
+
if (typeof span !== 'string')
|
|
1335
|
+
return false;
|
|
1336
|
+
const normalize = (value) => value
|
|
1337
|
+
.normalize('NFC')
|
|
1338
|
+
.replace(/\s+/gu, ' ')
|
|
1339
|
+
.trim()
|
|
1340
|
+
.toLocaleLowerCase('en-US');
|
|
1341
|
+
const normalizedSpan = normalize(span);
|
|
1342
|
+
return normalizedSpan.length > 0 && normalize(excerpt).includes(normalizedSpan);
|
|
1343
|
+
};
|
|
1344
|
+
const omitClaimReceipt = (claim) => {
|
|
1345
|
+
const next = { ...claim };
|
|
1346
|
+
delete next.sourceId;
|
|
1347
|
+
delete next.receiptId;
|
|
1348
|
+
delete next.quoteSpan;
|
|
1349
|
+
delete next.attestation;
|
|
1350
|
+
return next;
|
|
1351
|
+
};
|
|
1352
|
+
if (hasReportClaims) {
|
|
1353
|
+
projected.reportClaims = Array.isArray(record.reportClaims)
|
|
1354
|
+
? record.reportClaims.map((rawClaim) => {
|
|
1355
|
+
const claim = asProjectionRecord(rawClaim);
|
|
1356
|
+
if (!claim)
|
|
1357
|
+
return rawClaim;
|
|
1358
|
+
if (claim.state === 'GROUNDED') {
|
|
1359
|
+
const hasSourceId = Object.prototype.hasOwnProperty.call(claim, 'sourceId');
|
|
1360
|
+
const match = typeof claim.sourceId === 'string'
|
|
1361
|
+
? /^RRCP-s(0|[1-9]\d*)$/.exec(claim.sourceId)
|
|
1362
|
+
: null;
|
|
1363
|
+
const rawSource = match
|
|
1364
|
+
? asProjectionRecord(Array.isArray(record.reportEvidence)
|
|
1365
|
+
? record.reportEvidence[Number(match[1])]
|
|
1366
|
+
: undefined)
|
|
1367
|
+
: null;
|
|
1368
|
+
if (!hasSourceId && Object.prototype.hasOwnProperty.call(claim, 'quoteSpan')) {
|
|
1369
|
+
return omitClaimReceipt(claim);
|
|
1370
|
+
}
|
|
1371
|
+
if (hasSourceId && (!rawSource || !projectionSupportsSpan(rawSource.quote, claim.quoteSpan))) {
|
|
1372
|
+
return omitClaimReceipt(claim);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
else if (claim.state === 'NO_RECEIPT') {
|
|
1376
|
+
const attestation = asProjectionRecord(claim.attestation);
|
|
1377
|
+
if (attestation && !isDisplaySuitableExcerpt(attestation.quote)) {
|
|
1378
|
+
const next = { ...claim };
|
|
1379
|
+
delete next.attestation;
|
|
1380
|
+
return next;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return { ...claim };
|
|
1384
|
+
})
|
|
1385
|
+
: [];
|
|
1386
|
+
}
|
|
1387
|
+
if (Object.prototype.hasOwnProperty.call(record, 'claims')) {
|
|
1388
|
+
projected.claims = Array.isArray(record.claims)
|
|
1389
|
+
? record.claims.map((rawClaim) => {
|
|
1390
|
+
const claim = asProjectionRecord(rawClaim);
|
|
1391
|
+
if (!claim || claim.state !== 'GROUNDED')
|
|
1392
|
+
return rawClaim;
|
|
1393
|
+
const hasSourceId = Object.prototype.hasOwnProperty.call(claim, 'sourceId');
|
|
1394
|
+
const match = typeof claim.sourceId === 'string'
|
|
1395
|
+
? /^RCP-p(0|[1-9]\d*)-s(0|[1-9]\d*)$/.exec(claim.sourceId)
|
|
1396
|
+
: null;
|
|
1397
|
+
const personas = Array.isArray(record.personas) ? record.personas : [];
|
|
1398
|
+
const persona = match ? asProjectionRecord(personas[Number(match[1])]) : null;
|
|
1399
|
+
const sources = Array.isArray(persona?.sources) ? persona.sources : [];
|
|
1400
|
+
const source = match ? asProjectionRecord(sources[Number(match[2])]) : null;
|
|
1401
|
+
if (!hasSourceId && Object.prototype.hasOwnProperty.call(claim, 'quoteSpan')) {
|
|
1402
|
+
return omitClaimReceipt(claim);
|
|
1403
|
+
}
|
|
1404
|
+
if (hasSourceId && !source)
|
|
1405
|
+
return omitClaimReceipt(claim);
|
|
1406
|
+
if (!source)
|
|
1407
|
+
return { ...claim };
|
|
1408
|
+
const receipts = Array.isArray(source.receipts) ? source.receipts : [];
|
|
1409
|
+
const hasReceiptId = Object.prototype.hasOwnProperty.call(claim, 'receiptId');
|
|
1410
|
+
if (hasReceiptId) {
|
|
1411
|
+
const matches = receipts.filter((rawReceipt) => asProjectionRecord(rawReceipt)?.receiptId === claim.receiptId);
|
|
1412
|
+
const receipt = matches.length === 1 ? asProjectionRecord(matches[0]) : null;
|
|
1413
|
+
return receipt && projectionSupportsSpan(receipt.excerpt, claim.quoteSpan)
|
|
1414
|
+
? { ...claim }
|
|
1415
|
+
: omitClaimReceipt(claim);
|
|
1416
|
+
}
|
|
1417
|
+
if (typeof source.quote === 'string' &&
|
|
1418
|
+
!projectionSupportsSpan(source.quote, claim.quoteSpan)) {
|
|
1419
|
+
return omitClaimReceipt(claim);
|
|
1420
|
+
}
|
|
1421
|
+
if (claim.quoteSpan !== undefined && typeof source.quote !== 'string') {
|
|
1422
|
+
return omitClaimReceipt(claim);
|
|
1423
|
+
}
|
|
1424
|
+
return { ...claim };
|
|
1425
|
+
})
|
|
1426
|
+
: [];
|
|
1427
|
+
}
|
|
1198
1428
|
const projectOverviewRef = (rawRef) => {
|
|
1199
1429
|
if (rawRef === null || typeof rawRef !== 'object' || Array.isArray(rawRef))
|
|
1200
1430
|
return null;
|
|
@@ -1243,6 +1473,32 @@ export function projectReportDataForMcp(raw) {
|
|
|
1243
1473
|
const safeRef = projectOverviewRef(rawRef);
|
|
1244
1474
|
if (!safeRef)
|
|
1245
1475
|
continue;
|
|
1476
|
+
if (safeRef.kind === 'report_evidence') {
|
|
1477
|
+
const match = typeof safeRef.sourceId === 'string'
|
|
1478
|
+
? /^RRCP-s(0|[1-9]\d*)$/.exec(safeRef.sourceId)
|
|
1479
|
+
: null;
|
|
1480
|
+
const source = match
|
|
1481
|
+
? asProjectionRecord(Array.isArray(projected.reportEvidence)
|
|
1482
|
+
? projected.reportEvidence[Number(match[1])]
|
|
1483
|
+
: undefined)
|
|
1484
|
+
: null;
|
|
1485
|
+
if (!source || !projectionSupportsSpan(source.quote, claim.span))
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (safeRef.kind === 'forum_receipt') {
|
|
1489
|
+
const matches = (Array.isArray(asProjectionRecord(projected.forumResearch)?.threads)
|
|
1490
|
+
? asProjectionRecord(projected.forumResearch).threads
|
|
1491
|
+
: []).flatMap((rawThread) => {
|
|
1492
|
+
const thread = asProjectionRecord(rawThread);
|
|
1493
|
+
return (Array.isArray(thread?.receipts) ? thread.receipts : []).filter((rawReceipt) => {
|
|
1494
|
+
const receipt = asProjectionRecord(rawReceipt);
|
|
1495
|
+
return receipt !== null && receipt.receiptId === safeRef.receiptId &&
|
|
1496
|
+
projectionSupportsSpan(receipt.excerpt, claim.span);
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
if (matches.length !== 1)
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1246
1502
|
const refKey = safeRef.kind === 'report_evidence'
|
|
1247
1503
|
? `${safeRef.kind}:${safeRef.sourceId}`
|
|
1248
1504
|
: safeRef.kind === 'forum_receipt'
|
|
@@ -1258,13 +1514,13 @@ export function projectReportDataForMcp(raw) {
|
|
|
1258
1514
|
return [{
|
|
1259
1515
|
span: claim.span,
|
|
1260
1516
|
claimType: claim.claimType,
|
|
1261
|
-
evidenceRefsTruncated: rawEvidenceRefs.length > MCP_OVERVIEW_REF_CAP,
|
|
1517
|
+
evidenceRefsTruncated: claim.evidenceRefsTruncated === true || rawEvidenceRefs.length > MCP_OVERVIEW_REF_CAP,
|
|
1262
1518
|
evidenceRefs,
|
|
1263
1519
|
}];
|
|
1264
1520
|
});
|
|
1265
1521
|
projected[key] = {
|
|
1266
1522
|
text: overviewRecord.text,
|
|
1267
|
-
claimSpansTruncated: overviewRecord.claimSpans.length > MCP_OVERVIEW_SPAN_CAP,
|
|
1523
|
+
claimSpansTruncated: overviewRecord.claimSpansTruncated === true || overviewRecord.claimSpans.length > MCP_OVERVIEW_SPAN_CAP,
|
|
1268
1524
|
claimSpans,
|
|
1269
1525
|
};
|
|
1270
1526
|
}
|
|
@@ -1285,22 +1541,24 @@ export function projectReportDataForMcp(raw) {
|
|
|
1285
1541
|
const resolvesCrossCuttingRef = (ref) => {
|
|
1286
1542
|
if (ref.kind === 'report_evidence') {
|
|
1287
1543
|
const match = /^RRCP-s(0|[1-9]\d*)$/.exec(ref.sourceId ?? '');
|
|
1288
|
-
const source = match && asProjectionRecord(Array.isArray(
|
|
1289
|
-
?
|
|
1544
|
+
const source = match && asProjectionRecord(Array.isArray(projected.reportEvidence)
|
|
1545
|
+
? projected.reportEvidence[Number(match[1])]
|
|
1290
1546
|
: undefined);
|
|
1291
1547
|
return Boolean(source &&
|
|
1292
1548
|
safeProjectionUrl(source.url) &&
|
|
1549
|
+
(!Object.prototype.hasOwnProperty.call(source, 'quote') ||
|
|
1550
|
+
isDisplaySuitableExcerpt(source.quote)) &&
|
|
1293
1551
|
(source.section === 'market' || source.section === 'competitor' || source.section === 'summary'));
|
|
1294
1552
|
}
|
|
1295
1553
|
if (ref.kind === 'forum_receipt') {
|
|
1296
|
-
const matches = (Array.isArray(asProjectionRecord(
|
|
1297
|
-
? asProjectionRecord(
|
|
1554
|
+
const matches = (Array.isArray(asProjectionRecord(projected.forumResearch)?.threads)
|
|
1555
|
+
? asProjectionRecord(projected.forumResearch).threads
|
|
1298
1556
|
: []).flatMap((rawThread) => {
|
|
1299
1557
|
const thread = asProjectionRecord(rawThread);
|
|
1300
1558
|
return (Array.isArray(thread?.receipts) ? thread.receipts : []).filter((rawReceipt) => {
|
|
1301
1559
|
const receipt = asProjectionRecord(rawReceipt);
|
|
1302
1560
|
return Boolean(receipt && receipt.receiptId === ref.receiptId &&
|
|
1303
|
-
|
|
1561
|
+
isDisplaySuitableExcerpt(receipt.excerpt));
|
|
1304
1562
|
});
|
|
1305
1563
|
});
|
|
1306
1564
|
return matches.length === 1;
|
|
@@ -1357,18 +1615,110 @@ export function projectReportDataForMcp(raw) {
|
|
|
1357
1615
|
}
|
|
1358
1616
|
if (hasHypothesisResults && Array.isArray(record.hypothesisResults)) {
|
|
1359
1617
|
projected.hypothesisResults = record.hypothesisResults.map((rawRow) => {
|
|
1360
|
-
if (rawRow === null || typeof rawRow !== 'object' || Array.isArray(rawRow))
|
|
1618
|
+
if (rawRow === null || typeof rawRow !== 'object' || Array.isArray(rawRow)) {
|
|
1619
|
+
if (requiresClauseIntegrity)
|
|
1620
|
+
clauseSemanticsFailure = true;
|
|
1361
1621
|
return rawRow;
|
|
1622
|
+
}
|
|
1362
1623
|
const row = rawRow;
|
|
1363
1624
|
const safeRow = { ...row };
|
|
1364
1625
|
let changed = false;
|
|
1365
|
-
|
|
1366
|
-
|
|
1626
|
+
let evidenceSemanticsFailure = false;
|
|
1627
|
+
const integrityClauses = asProjectionRecord(row.clauseIntegrity)?.clauses;
|
|
1628
|
+
for (const key of ['supportingEvidence', 'contradictingEvidence']) {
|
|
1629
|
+
if (!Array.isArray(row[key])) {
|
|
1630
|
+
if (Object.prototype.hasOwnProperty.call(row, key)) {
|
|
1631
|
+
// A schema-drift fallback must not forward a malformed evidence container whose
|
|
1632
|
+
// nested shape bypasses the per-item raw nomination scrub below.
|
|
1633
|
+
delete safeRow[key];
|
|
1634
|
+
changed = true;
|
|
1635
|
+
}
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const side = key === 'supportingEvidence' ? 'supporting' : 'contradicting';
|
|
1639
|
+
safeRow[key] = row[key].flatMap((rawEvidence, evidenceIndex) => {
|
|
1640
|
+
const evidence = asProjectionRecord(rawEvidence);
|
|
1641
|
+
if (!evidence) {
|
|
1642
|
+
// Dropping the item also invalidates index-based clause refs; preserving the old
|
|
1643
|
+
// working could make the next evidence row inherit a removed row's authority.
|
|
1644
|
+
evidenceSemanticsFailure = true;
|
|
1645
|
+
return [];
|
|
1646
|
+
}
|
|
1647
|
+
const safeEvidence = { ...evidence };
|
|
1648
|
+
// These are model-owned nomination fields on every evidence shape, even when `source`
|
|
1649
|
+
// is absent, malformed, or lies about the method. They never cross the MCP boundary.
|
|
1650
|
+
delete safeEvidence.url;
|
|
1651
|
+
delete safeEvidence.nominationId;
|
|
1652
|
+
if (evidence.source !== 'web_research') {
|
|
1653
|
+
// Only web evidence can carry a producer-granted report receipt. A model-authored
|
|
1654
|
+
// pointer on any other method is inert and must not cross either MCP exit.
|
|
1655
|
+
delete safeEvidence.receipt;
|
|
1656
|
+
}
|
|
1657
|
+
else if (Object.prototype.hasOwnProperty.call(evidence, 'receipt')) {
|
|
1658
|
+
const linkedClauses = Array.isArray(integrityClauses)
|
|
1659
|
+
? integrityClauses.flatMap((rawClause) => {
|
|
1660
|
+
const clause = asProjectionRecord(rawClause);
|
|
1661
|
+
const text = typeof clause?.text === 'string' ? clause.text : null;
|
|
1662
|
+
const refs = Array.isArray(clause?.evidenceRefs) ? clause.evidenceRefs : [];
|
|
1663
|
+
const linked = refs.some((rawRef) => {
|
|
1664
|
+
const ref = asProjectionRecord(rawRef);
|
|
1665
|
+
return ref?.side === side && ref.index === evidenceIndex;
|
|
1666
|
+
});
|
|
1667
|
+
return linked && text ? [text] : [];
|
|
1668
|
+
})
|
|
1669
|
+
: [];
|
|
1670
|
+
const authenticatesEveryLinkedClause = linkedClauses.length > 0 && linkedClauses.every((text) => hypothesisWebReceiptAuthenticates(evidence, text, row.category, record));
|
|
1671
|
+
if (!authenticatesEveryLinkedClause) {
|
|
1672
|
+
delete safeEvidence.receipt;
|
|
1673
|
+
// An unreferenced receipt is inert model decoration. A referenced one is the
|
|
1674
|
+
// producer-owned basis of clauseIntegrity, so rejecting it invalidates that working.
|
|
1675
|
+
if (linkedClauses.length > 0)
|
|
1676
|
+
evidenceSemanticsFailure = true;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return [safeEvidence];
|
|
1680
|
+
});
|
|
1681
|
+
changed = true;
|
|
1682
|
+
}
|
|
1683
|
+
// A rejected source/span cannot leave a sibling producer-owned coverage block claiming that
|
|
1684
|
+
// same evidence completed a clause. Preserve the row and evidence prose, but remove the
|
|
1685
|
+
// unauthenticated working and force the final block through the current fail-closed state.
|
|
1686
|
+
if (evidenceSemanticsFailure) {
|
|
1687
|
+
delete safeRow.clauseIntegrity;
|
|
1688
|
+
changed = true;
|
|
1689
|
+
}
|
|
1690
|
+
const finalVersion = asProjectionRecord(row.final)?.version;
|
|
1691
|
+
const rowClaimsCurrentSemantics = finalVersion === HYPOTHESIS_SEMANTICS_VERSION ||
|
|
1692
|
+
Object.prototype.hasOwnProperty.call(row, 'clauseIntegrity');
|
|
1693
|
+
const clauseIntegrityMismatch = finalVersion === HYPOTHESIS_SEMANTICS_VERSION &&
|
|
1694
|
+
!finalClauseCoverageMatchesIntegrity(safeRow, record);
|
|
1695
|
+
const semanticsMismatch = (rowClaimsCurrentSemantics && outerVersion !== HYPOTHESIS_SEMANTICS_VERSION) || (outerVersionPresent && (outerVersion === HYPOTHESIS_SEMANTICS_VERSION
|
|
1696
|
+
? finalVersion !== HYPOTHESIS_SEMANTICS_VERSION
|
|
1697
|
+
: outerVersion === LEGACY_HYPOTHESIS_SEMANTICS_VERSION
|
|
1698
|
+
? finalVersion !== undefined && finalVersion !== LEGACY_HYPOTHESIS_SEMANTICS_VERSION
|
|
1699
|
+
: true));
|
|
1700
|
+
if (clauseIntegrityMismatch || semanticsMismatch) {
|
|
1701
|
+
delete safeRow.clauseIntegrity;
|
|
1702
|
+
changed = true;
|
|
1703
|
+
}
|
|
1704
|
+
if (Object.prototype.hasOwnProperty.call(row, 'final') || semanticsMismatch) {
|
|
1705
|
+
const rowForNormalization = semanticsMismatch || clauseIntegrityMismatch
|
|
1706
|
+
? { ...safeRow, final: { version: HYPOTHESIS_SEMANTICS_VERSION } }
|
|
1707
|
+
: safeRow;
|
|
1708
|
+
const normalized = normalizeFinalHypothesisState(rowForNormalization);
|
|
1709
|
+
if (semanticsMismatch ||
|
|
1710
|
+
clauseIntegrityMismatch ||
|
|
1711
|
+
normalized?.clauseCoverage === 'invalid' ||
|
|
1712
|
+
(normalized?.clauseCoverage !== undefined &&
|
|
1713
|
+
normalized.clauseCoverage !== 'complete' &&
|
|
1714
|
+
normalized.originalVerdict !== normalized.verdict) ||
|
|
1715
|
+
(requiresClauseIntegrity && normalized === null))
|
|
1716
|
+
clauseSemanticsFailure = true;
|
|
1367
1717
|
if (normalized)
|
|
1368
1718
|
safeRow.final = normalized;
|
|
1369
1719
|
else
|
|
1370
1720
|
delete safeRow.final;
|
|
1371
|
-
changed
|
|
1721
|
+
changed ||= safeRow.final !== row.final;
|
|
1372
1722
|
}
|
|
1373
1723
|
const rawRobustness = row.robustness;
|
|
1374
1724
|
const robustnessRecord = rawRobustness !== null &&
|
|
@@ -1415,6 +1765,11 @@ export function projectReportDataForMcp(raw) {
|
|
|
1415
1765
|
return safeRow;
|
|
1416
1766
|
});
|
|
1417
1767
|
}
|
|
1768
|
+
if (clauseSemanticsFailure) {
|
|
1769
|
+
projected.executiveSummary = 'This run did not establish every material part of at least one hypothesis. Treat the affected hypotheses as unresolved.';
|
|
1770
|
+
projected.verdictSummary = projected.executiveSummary;
|
|
1771
|
+
delete projected.recommendations;
|
|
1772
|
+
}
|
|
1418
1773
|
// ⚠️ FUL-685 — THE REDDIT NOTE IS PROJECTED HERE FOR THE SAME REASON `researchTaskId` IS:
|
|
1419
1774
|
// `prepareReportDataForMcp` returns this projection UNPARSED when the full schema drifts, so a
|
|
1420
1775
|
// field stripped only by `RedditRetrievalNoteSchema` would come straight back on that path — and
|