@certscore/mcp 0.2.15 → 0.2.16

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/tools.js CHANGED
@@ -4,7 +4,24 @@ export const MAX_EVIDENCE_PACKET_CHARS = 250_000;
4
4
  const EVIDENCE_STRING_CHARS = 4_000;
5
5
  const EVIDENCE_ARRAY_ITEMS = 40;
6
6
  const EVIDENCE_OBJECT_KEYS = 80;
7
- const OBSERVATION_ONLY_DISCLAIMER = "Observation only: no-go, not-observed, and limited-coverage results are not proof of compliance.";
7
+ const MAX_TOOL_TEXT_CHARS = 8_000;
8
+ const LEGAL_REVIEW_DISCLAIMER = "CertScore results are automated public-web observations for human and agentic review, not legal advice, certification, or a compliance determination.";
9
+ const SCAN_PROVENANCE_GROUNDING = "retrievalMode describes how the current tool response obtained the scan; creationDecision describes whether the original scan request created or reused a scan only when that decision is retained. Never infer an unknown creationDecision from scan_id_lookup. For a reused or retrieved existing scan, use only persisted scanFrom and timestamps. Never infer its original scan region from the current request, the user's location, or a default execution region. If persisted region or timestamps are unavailable, report them as unavailable.";
10
+ const INTERPRETATION_STATEMENT = "The CertScore score covers observable public-web scan signals only. Do not infer technologies that are not listed in the returned evidence or any legal compliance status.";
11
+ const SCAN_BUNDLE_RESPONSE_CONTRACT = `Response contract: Report only observed CertScore evidence and CertScore classifications. criticality, priority, and confidence are CertScore metadata; regulatory review lenses are non-determinative CertScore review context—not legal severity, legal exposure, or a compliance determination. Absence of captured consent-action evidence does not establish what happens after Accept, Reject, or Decline. A confirmed post-refusal observation with termination.kind=evidence_satisfied means the observer intentionally stopped after retaining qualifying evidence; do not treat that termination as uncertainty about the returned observation. Keep any separately returned coverage limitation scoped to what was not measured. Do not extrapolate an observed embed, vendor, or request into unobserved cookies, fingerprinting, tracking, or processing, and do not infer violations or compliance beyond what CertScore observed. ${SCAN_PROVENANCE_GROUNDING}`;
12
+ const SCAN_BUNDLE_INTERPRETATION_STATEMENT = "Report only observed CertScore evidence and persisted CertScore classifications. Without corresponding captured post-action evidence, do not infer what Accept, Reject, Decline, or another consent action would do; say the scan does not establish what happens after that action. When postRefusalObservation is confirmed and termination.kind is evidence_satisfied, state the returned post-refusal observation directly and explain that observation stopped intentionally after qualifying evidence was retained. Do not characterize that termination as uncertainty about the observation; mention unmeasured longer-term persistence only when relevant. Do not speculate that an observed embed, vendor, or request may cause additional cookies, fingerprinting, tracking, or processing unless CertScore observed that behavior. Treat returned priority or severity as a CertScore classification, not regulatory criticality or legal exposure; prefer ‘observed privacy risk signal’ or ‘CertScore finding’. Do not infer unobserved technologies, legal compliance, or a legal violation from scores or findings.";
13
+ const COMPACT_SCAN_BUNDLE_INTERPRETATION_STATEMENT = "Use only returned CertScore observations and classifications. Do not infer unobserved technologies, post-consent behavior, legal compliance, or violations. Treat priority and severity as CertScore metadata.";
14
+ const OBSERVATION_ONLY_DISCLAIMER = `${LEGAL_REVIEW_DISCLAIMER} No-go, not-observed, and limited-coverage results are not proof of compliance.`;
15
+ const COMPACT_OBSERVATION_ONLY_DISCLAIMER = "Automated public-web observation, not legal advice or a compliance determination; missing or limited evidence is not proof of compliance.";
16
+ function interpretationGuidance(statement = INTERPRETATION_STATEMENT) {
17
+ return {
18
+ scoreLabel: "CertScore score",
19
+ observableSignalsOnly: true,
20
+ doNotInferUnobservedTechnologies: true,
21
+ doNotInferLegalComplianceStatus: true,
22
+ statement
23
+ };
24
+ }
8
25
  function toolResultSummary(payload) {
9
26
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
10
27
  return "CertScore tool call completed. Read structuredContent for the result.";
@@ -13,10 +30,29 @@ function toolResultSummary(payload) {
13
30
  const type = typeof record.type === "string" ? record.type : "result";
14
31
  const status = typeof record.status === "string" ? `; status=${record.status}` : "";
15
32
  const scanId = typeof record.scanId === "string" ? `; scanId=${record.scanId}` : "";
16
- const score = typeof record.score === "number" ? `; score=${record.score}` : "";
17
- return `CertScore ${type}${status}${scanId}${score}. Full result is in structuredContent.`;
33
+ const score = typeof record.score === "number" ? `; CertScore score=${record.score}` : "";
34
+ const recordLinks = record.links && typeof record.links === "object" && !Array.isArray(record.links)
35
+ ? record.links
36
+ : null;
37
+ const reportUrl = typeof record.reportUrl === "string" && record.reportUrl.trim()
38
+ ? record.reportUrl.trim()
39
+ : typeof recordLinks?.report === "string" && recordLinks.report.trim()
40
+ ? recordLinks.report.trim()
41
+ : null;
42
+ const report = reportUrl ? `; full report=${reportUrl}` : "";
43
+ const provenanceRecord = record.provenance && typeof record.provenance === "object" && !Array.isArray(record.provenance)
44
+ ? record.provenance
45
+ : null;
46
+ const retrieval = typeof provenanceRecord?.retrievalMode === "string" ? `; retrieval=${provenanceRecord.retrievalMode}` : "";
47
+ const creation = typeof provenanceRecord?.creationDecision === "string" ? `; creation=${provenanceRecord.creationDecision}` : "";
48
+ const provenance = retrieval || creation
49
+ ? `${retrieval}${creation}`
50
+ : typeof provenanceRecord?.mode === "string"
51
+ ? `; provenance=${provenanceRecord.mode}`
52
+ : "";
53
+ return `CertScore ${type}${status}${scanId}${score}${provenance}${report}. Full result is in structuredContent.`;
18
54
  }
19
- export function toToolResult(payload) {
55
+ export function toToolResult(payload, text) {
20
56
  const structuredContent = payload !== null && typeof payload === "object" && !Array.isArray(payload)
21
57
  ? payload
22
58
  : { value: payload };
@@ -25,7 +61,7 @@ export function toToolResult(payload) {
25
61
  content: [
26
62
  {
27
63
  type: "text",
28
- text: toolResultSummary(payload)
64
+ text: text ?? toolResultSummary(payload)
29
65
  }
30
66
  ]
31
67
  };
@@ -50,11 +86,16 @@ export function toToolError(error) {
50
86
  : null;
51
87
  const code = error instanceof CertScoreError ? error.code : "internal_error";
52
88
  const message = error instanceof Error ? error.message : "Unknown CertScore MCP error.";
89
+ const creationRateLimit = terminalError?.creationRateLimit && typeof terminalError.creationRateLimit === "object" && !Array.isArray(terminalError.creationRateLimit)
90
+ ? terminalError.creationRateLimit
91
+ : null;
53
92
  const recommendedNextAction = typeof terminalError?.recommendedNextAction === "string"
54
93
  ? terminalError.recommendedNextAction
55
- : retryable
56
- ? `Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. Stop and contact CertScore support if the error repeats.`
57
- : "Correct the request using the error details, then retry only if the requested operation is still appropriate.";
94
+ : creationRateLimit
95
+ ? `No scan was created. Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. If the limit continues after that delay, contact support@certscore.ai.`
96
+ : retryable
97
+ ? `Wait ${retryAfterSeconds ?? 30} seconds, then retry the same request. Stop and contact support@certscore.ai if the error repeats.`
98
+ : "Correct the request using the error details, then retry only if the requested operation is still appropriate.";
58
99
  const payload = {
59
100
  error: {
60
101
  code,
@@ -62,6 +103,9 @@ export function toToolError(error) {
62
103
  retryable,
63
104
  retryAfterSeconds,
64
105
  recommendedNextAction,
106
+ ...(creationRateLimit
107
+ ? { creationRateLimit }
108
+ : {}),
65
109
  ...(error instanceof CertScoreError ? {
66
110
  name: error.name,
67
111
  status: error.status,
@@ -103,6 +147,9 @@ export function toInvalidArgumentsToolError(errorMessage) {
103
147
  type: "certscore_tool_error",
104
148
  status: "invalid_arguments",
105
149
  error,
150
+ scoreLabel: "CertScore score",
151
+ provenance: scanProvenance({}, "unknown"),
152
+ interpretationGuidance: interpretationGuidance(),
106
153
  recommendedNextTool: null,
107
154
  recommendedNextAction,
108
155
  observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
@@ -114,6 +161,21 @@ export function toInvalidArgumentsToolError(errorMessage) {
114
161
  isError: true
115
162
  };
116
163
  }
164
+ export function toInvalidScanIdToolError() {
165
+ const error = {
166
+ code: "invalid_scan_id",
167
+ field: "scanId",
168
+ message: "The scanId must be the canonical UUID returned by certscore_scan_site.",
169
+ retryable: false,
170
+ retryAfterSeconds: null,
171
+ recommendedNextAction: "Use the unchanged scanId returned by certscore_scan_site. Do not use placeholders, report URLs, domains, or job IDs.",
172
+ mcpCode: -32602
173
+ };
174
+ return {
175
+ content: [{ type: "text", text: JSON.stringify({ error }) }],
176
+ isError: true
177
+ };
178
+ }
117
179
  function terminalErrorForResult(value) {
118
180
  const existing = value.error && typeof value.error === "object" && !Array.isArray(value.error)
119
181
  ? value.error
@@ -169,14 +231,72 @@ function terminalErrorForResult(value) {
169
231
  };
170
232
  return fallback[String(value.status)] ?? null;
171
233
  }
172
- export function withMcpAgentGuidance(value) {
234
+ function scanProvenance(value, fallbackMode) {
235
+ const executionMode = value.executionMode === "new_scan" || value.executionMode === "reused_scan"
236
+ ? value.executionMode
237
+ : null;
238
+ const reused = typeof value.reused === "boolean" ? value.reused : executionMode === "reused_scan" ? true : executionMode === "new_scan" ? false : null;
239
+ const mode = executionMode === "new_scan"
240
+ ? "new_scan_started"
241
+ : executionMode === "reused_scan" || reused === true
242
+ ? "existing_completed_scan_reused"
243
+ : fallbackMode;
244
+ const retrievalMode = fallbackMode === "existing_scan_retrieved"
245
+ ? "scan_id_lookup"
246
+ : executionMode !== null || reused !== null
247
+ ? "creation_response"
248
+ : "unknown";
249
+ const creationDecision = executionMode === "new_scan" || reused === false
250
+ ? "new_scan"
251
+ : executionMode === "reused_scan" || reused === true
252
+ ? "reused_scan"
253
+ : "unknown";
254
+ const retainedAgeSeconds = typeof value.reusedScanAgeSeconds === "number" && Number.isFinite(value.reusedScanAgeSeconds) && value.reusedScanAgeSeconds >= 0
255
+ ? Math.floor(value.reusedScanAgeSeconds)
256
+ : null;
257
+ const completedAtMs = typeof value.completedAt === "string" ? Date.parse(value.completedAt) : Number.NaN;
258
+ const completedAgeSeconds = Number.isFinite(completedAtMs)
259
+ ? Math.max(0, Math.floor((Date.now() - completedAtMs) / 1_000))
260
+ : null;
261
+ const scanAgeSeconds = retrievalMode === "creation_response"
262
+ ? retainedAgeSeconds ?? completedAgeSeconds
263
+ : completedAgeSeconds;
264
+ return {
265
+ mode,
266
+ retrievalMode,
267
+ creationDecision,
268
+ scanAgeSeconds,
269
+ executionMode,
270
+ reused,
271
+ freshnessDecision: typeof value.freshnessDecision === "string" ? value.freshnessDecision : null
272
+ };
273
+ }
274
+ export function withMcpAgentGuidance(value, fallbackProvenanceMode = "unknown") {
173
275
  const status = String(value.status ?? "");
174
276
  const active = status === "queued" || status === "running" || status === "finalizing";
175
277
  const usable = status === "completed" || status === "completed_limited";
176
278
  const error = terminalErrorForResult(value);
279
+ const stableScanId = typeof value.scanId === "string" && value.scanId.trim()
280
+ ? value.scanId.trim()
281
+ : typeof value.scan_id === "string" && value.scan_id.trim()
282
+ ? value.scan_id.trim()
283
+ : null;
284
+ const reportUrl = usable
285
+ ? typeof value.reportUrl === "string" && value.reportUrl.trim()
286
+ ? value.reportUrl.trim()
287
+ : typeof value.links?.report === "string" && value.links.report.trim()
288
+ ? value.links.report.trim()
289
+ : stableScanId
290
+ ? `https://certscore.ai/scan/${encodeURIComponent(stableScanId)}`
291
+ : null
292
+ : null;
177
293
  return {
178
294
  ...value,
179
295
  error,
296
+ reportUrl,
297
+ scoreLabel: "CertScore score",
298
+ provenance: scanProvenance(value, fallbackProvenanceMode),
299
+ interpretationGuidance: interpretationGuidance(),
180
300
  recommendedNextTool: active ? "certscore_get_scan_status" : usable ? "certscore_get_scan_bundle" : null,
181
301
  recommendedNextAction: error?.recommendedNextAction ?? value.recommendedNextAction ?? (active
182
302
  ? `Poll certscore_get_scan_status with scanId ${value.scanId ?? value.jobId} after the recommended delay.`
@@ -186,6 +306,13 @@ export function withMcpAgentGuidance(value) {
186
306
  observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER
187
307
  };
188
308
  }
309
+ export function withMcpScanProvenanceGuidance(value, fallbackProvenanceMode) {
310
+ const guided = withMcpAgentGuidance(value, fallbackProvenanceMode);
311
+ return {
312
+ ...guided,
313
+ interpretationGuidance: interpretationGuidance(`${INTERPRETATION_STATEMENT} ${SCAN_PROVENANCE_GROUNDING}`)
314
+ };
315
+ }
189
316
  export function boundEvidencePacket(payload, maxSerializedChars = MAX_EVIDENCE_PACKET_CHARS) {
190
317
  const originalSerializedChars = measureSerializedChars(payload);
191
318
  if (originalSerializedChars <= maxSerializedChars) {
@@ -247,12 +374,51 @@ function boundedText(value, maxChars) {
247
374
  if (typeof value !== "string") {
248
375
  return value;
249
376
  }
250
- return value.length > maxChars ? `${value.slice(0, maxChars)}…[truncated]` : value;
377
+ if (value.length <= maxChars) {
378
+ return value;
379
+ }
380
+ const marker = "…[truncated]";
381
+ const contentLimit = Math.max(1, maxChars - marker.length);
382
+ const candidate = value.slice(0, contentLimit);
383
+ const boundary = candidate.search(/\s+\S*$/);
384
+ const bounded = boundary >= Math.floor(contentLimit * 0.6)
385
+ ? candidate.slice(0, boundary)
386
+ : candidate;
387
+ return `${bounded.trimEnd()}${marker}`;
251
388
  }
252
- function compactBundleFinding(finding) {
389
+ function compactFindingLink(finding) {
390
+ const self = typeof finding.links?.self === "string" && finding.links.self.trim()
391
+ ? finding.links.self.trim()
392
+ : typeof finding.evidence?.excerpt?.evidenceUrl === "string" && finding.evidence.excerpt.evidenceUrl.trim()
393
+ ? finding.evidence.excerpt.evidenceUrl.trim()
394
+ : null;
395
+ return self ? { self: self.slice(0, 2_048) } : undefined;
396
+ }
397
+ function withoutFindingDisclaimer(finding) {
398
+ const { disclaimer: _disclaimer, ...rest } = finding;
399
+ return rest;
400
+ }
401
+ function compactBundleFinding(finding, tier = "standard") {
253
402
  const evidence = finding.evidence && typeof finding.evidence === "object" && !Array.isArray(finding.evidence)
254
403
  ? finding.evidence
255
404
  : {};
405
+ const core = tier === "core";
406
+ const links = compactFindingLink(finding);
407
+ if (core) {
408
+ const compactNextStep = typeof finding.nextStep === "string" && finding.nextStep.trim().length <= 64
409
+ ? finding.nextStep.trim()
410
+ : null;
411
+ return {
412
+ type: finding.type,
413
+ id: finding.id,
414
+ label: boundedText(finding.label, 140),
415
+ criticality: finding.criticality,
416
+ confidence: finding.confidence,
417
+ plainEnglish: boundedText(finding.plainEnglish, 180),
418
+ ...(compactNextStep ? { nextStep: compactNextStep } : {}),
419
+ evidenceUrl: links?.self ?? null
420
+ };
421
+ }
256
422
  return {
257
423
  type: finding.type,
258
424
  id: finding.id,
@@ -275,9 +441,74 @@ function compactBundleFinding(finding) {
275
441
  ...(evidence.hasConsentContext !== undefined ? { hasConsentContext: evidence.hasConsentContext } : {}),
276
442
  ...(evidence.hasPolicyAnchor !== undefined ? { hasPolicyAnchor: evidence.hasPolicyAnchor } : {})
277
443
  },
278
- ...(finding.nextStep !== undefined ? { nextStep: boundedText(finding.nextStep, 180) } : {})
444
+ ...(finding.nextStep !== undefined ? { nextStep: boundedText(finding.nextStep, 180) } : {}),
445
+ ...(links ? { links } : {})
279
446
  };
280
447
  }
448
+ function deduplicatedFullReport(input) {
449
+ const report = input.report;
450
+ const residual = { ...report };
451
+ const deduplicatedSections = [];
452
+ const returnedFindings = new Map(input.findings.flatMap((finding) => typeof finding.id === "string" ? [[finding.id, finding]] : []));
453
+ for (const key of ["findings", "topFindings"]) {
454
+ const section = residual[key];
455
+ const sectionFindingIds = Array.isArray(section)
456
+ ? section.flatMap((finding) => finding && typeof finding === "object" && typeof finding.id === "string"
457
+ ? [finding.id]
458
+ : [])
459
+ : [];
460
+ const coveredByCanonicalProjection = sectionFindingIds.every((id) => {
461
+ const finding = returnedFindings.get(id);
462
+ return finding
463
+ && typeof finding.plainEnglish === "string"
464
+ && typeof finding.evidence?.summary === "string"
465
+ && compactFindingLink(finding) !== undefined;
466
+ });
467
+ if (Array.isArray(section) && sectionFindingIds.length === section.length && coveredByCanonicalProjection) {
468
+ delete residual[key];
469
+ deduplicatedSections.push(`fullReport.${key}`);
470
+ }
471
+ }
472
+ if ("transportSecurity" in residual && JSON.stringify(residual.transportSecurity) === JSON.stringify(input.transportSecurity)) {
473
+ delete residual.transportSecurity;
474
+ deduplicatedSections.push("fullReport.transportSecurity");
475
+ }
476
+ return { deduplicatedSections, residual };
477
+ }
478
+ function distinctSummaryFindings(findings) {
479
+ const labels = new Set();
480
+ return findings.filter((finding) => {
481
+ const key = typeof finding.label === "string" && finding.label.trim().length > 0
482
+ ? finding.label.trim().toLocaleLowerCase()
483
+ : typeof finding.id === "string"
484
+ ? finding.id
485
+ : JSON.stringify(finding);
486
+ if (labels.has(key))
487
+ return false;
488
+ labels.add(key);
489
+ return true;
490
+ });
491
+ }
492
+ const POST_REFUSAL_FINDING_IDS = new Set([
493
+ "post_refusal_non_essential_activity",
494
+ "pre_consent_storage_not_cleared",
495
+ "refusal_signal_contradicts_action",
496
+ ]);
497
+ function postRefusalTruncationPriority(finding) {
498
+ const id = typeof finding.id === "string" ? finding.id : "";
499
+ if (POST_REFUSAL_FINDING_IDS.has(id))
500
+ return 0;
501
+ if (/^pre_consent_|preconsent/i.test(id))
502
+ return 2;
503
+ return 1;
504
+ }
505
+ function prioritizeFindingsForBoundedBundle(findings) {
506
+ return findings
507
+ .map((finding, index) => ({ finding, index }))
508
+ .sort((left, right) => postRefusalTruncationPriority(left.finding) - postRefusalTruncationPriority(right.finding) ||
509
+ left.index - right.index)
510
+ .map(({ finding }) => finding);
511
+ }
281
512
  function compactPriorityEvidenceSummary(summary) {
282
513
  const firstDigest = Array.isArray(summary.digests) ? summary.digests[0] : null;
283
514
  const firstReference = summary.references && typeof summary.references === "object" && !Array.isArray(summary.references)
@@ -542,11 +773,21 @@ export function limitPreConsentRows(payload, options = {}) {
542
773
  return payload;
543
774
  }
544
775
  const maxRows = Math.min(200, Math.max(1, options.maxRows ?? 200));
545
- const total = rows.length;
546
- const truncated = total > maxRows;
776
+ const reportedTotal = payload.summary && typeof payload.summary === "object" && !Array.isArray(payload.summary)
777
+ && Number.isInteger(payload.summary.rowCount)
778
+ ? Number(payload.summary.rowCount)
779
+ : 0;
780
+ const total = Math.max(rows.length, reportedTotal);
781
+ const returnedRows = rows.slice(0, maxRows);
782
+ const truncated = total > returnedRows.length;
547
783
  return {
548
784
  ...payload,
549
- rows: rows.slice(0, maxRows),
785
+ rows: returnedRows,
786
+ evidenceMetadata: {
787
+ total,
788
+ returned: returnedRows.length,
789
+ truncated
790
+ },
550
791
  summary: {
551
792
  ...(payload.summary && typeof payload.summary === "object" && !Array.isArray(payload.summary) ? payload.summary : {}),
552
793
  totalRowCount: total,
@@ -554,24 +795,453 @@ export function limitPreConsentRows(payload, options = {}) {
554
795
  }
555
796
  };
556
797
  }
798
+ function uniqueBoundedStrings(values, maxItems, maxChars) {
799
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0).map((value) => value.slice(0, maxChars)))].slice(0, maxItems);
800
+ }
801
+ function compactPreConsentRow(row) {
802
+ const cookieDetails = Array.isArray(row.cookieDetails) ? row.cookieDetails : [];
803
+ const domains = uniqueBoundedStrings([
804
+ ...(Array.isArray(row.domains) ? row.domains : []),
805
+ row.host,
806
+ row.registrableDomain
807
+ ], 12, 253);
808
+ const purposes = Array.isArray(row.purposes) ? row.purposes : [];
809
+ return {
810
+ id: String(row.id ?? `${row.kind ?? "unknown"}:${row.name ?? row.vendor ?? row.host ?? "observed-item"}`).slice(0, 512),
811
+ kind: ["cookie", "tracker", "request", "storage"].includes(row.kind) ? row.kind : "unknown",
812
+ name: String(row.name ?? row.vendor ?? row.host ?? "Observed item").slice(0, 256),
813
+ cookieNames: uniqueBoundedStrings(cookieDetails.map((cookie) => cookie && typeof cookie === "object" ? cookie.name : null), 24, 256),
814
+ vendor: typeof row.vendor === "string" ? row.vendor.slice(0, 160) : null,
815
+ purpose: typeof row.purpose === "string" ? row.purpose.slice(0, 160) : typeof purposes[0] === "string" ? purposes[0].slice(0, 160) : null,
816
+ category: typeof row.category === "string" ? row.category.slice(0, 160) : null,
817
+ confidence: ["high", "medium", "low"].includes(row.confidence) ? row.confidence : "unknown",
818
+ firstObservedAtMs: Number.isInteger(row.firstObservedAtMs) && row.firstObservedAtMs >= 0 ? row.firstObservedAtMs : null,
819
+ domains,
820
+ requestCount: Number.isInteger(row.requestCount) && row.requestCount >= 0 ? row.requestCount : null,
821
+ evidenceClassification: {
822
+ basis: "public_report_projection",
823
+ phase: "pre_consent",
824
+ observedBeforeConsent: row.observedBeforeConsent !== false,
825
+ party: ["first_party", "third_party", "mixed"].includes(row.party) ? row.party : "unknown",
826
+ priority: ["high", "medium", "review_needed", "contextual"].includes(row.priority) ? row.priority : "unknown"
827
+ }
828
+ };
829
+ }
830
+ function preConsentBundleSection(payload, maxRows) {
831
+ const sourceRows = Array.isArray(payload.rows) ? payload.rows : [];
832
+ const total = Math.max(sourceRows.length, Number.isInteger(payload.summary?.rowCount) ? payload.summary.rowCount : 0);
833
+ const rows = sourceRows.slice(0, maxRows).map((row) => compactPreConsentRow(row));
834
+ return {
835
+ summary: {
836
+ rowCount: total,
837
+ trackerCount: Number.isInteger(payload.summary?.trackerCount) ? payload.summary.trackerCount : 0,
838
+ cookieCount: Number.isInteger(payload.summary?.cookieCount) ? payload.summary.cookieCount : 0,
839
+ requestCount: Number.isInteger(payload.summary?.requestCount) ? payload.summary.requestCount : 0,
840
+ vendorCount: Number.isInteger(payload.summary?.vendorCount) ? payload.summary.vendorCount : 0,
841
+ domainCount: Number.isInteger(payload.summary?.domainCount) ? payload.summary.domainCount : 0
842
+ },
843
+ rows,
844
+ total,
845
+ returned: rows.length,
846
+ truncated: total > rows.length
847
+ };
848
+ }
849
+ function neutralExecutiveSummary(value) {
850
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
851
+ return null;
852
+ }
853
+ const summary = value;
854
+ const score = typeof summary.score === "number" ? summary.score : null;
855
+ const scoreMetadata = summary.scoreMetadata && typeof summary.scoreMetadata === "object" && !Array.isArray(summary.scoreMetadata)
856
+ ? { ...summary.scoreMetadata, metricLabel: "CertScore score" }
857
+ : summary.scoreMetadata;
858
+ const trackerFootprint = summary.trackerFootprint && typeof summary.trackerFootprint === "object" && !Array.isArray(summary.trackerFootprint)
859
+ ? Object.fromEntries(Object.entries(summary.trackerFootprint).filter(([, entry]) => typeof entry === "number" || entry === null))
860
+ : undefined;
861
+ const policySurfaces = Array.isArray(summary.policySurfaces)
862
+ ? summary.policySurfaces.slice(0, 5).flatMap((surface) => {
863
+ if (!surface || typeof surface !== "object" || Array.isArray(surface))
864
+ return [];
865
+ const row = surface;
866
+ return [{
867
+ type: boundedText(row.type, 80),
868
+ title: boundedText(row.title, 160),
869
+ url: boundedText(row.url, 2_048)
870
+ }];
871
+ })
872
+ : undefined;
873
+ return {
874
+ completionSummary: boundedText(summary.completionSummary, 280),
875
+ domain: boundedText(summary.domain, 253),
876
+ score,
877
+ scoreLabel: score === null ? "CertScore score" : `CertScore score: ${score}/100`,
878
+ ...(scoreMetadata ? { scoreMetadata } : {}),
879
+ riskLevel: summary.riskLevel,
880
+ actionLabel: summary.actionLabel,
881
+ issuesToReview: summary.issuesToReview,
882
+ thirdPartyRequests: summary.thirdPartyRequests,
883
+ trackingClassifiedThirdPartyRequests: summary.trackingClassifiedThirdPartyRequests,
884
+ cookiesPreConsent: summary.cookiesPreConsent,
885
+ nonEssentialPreConsentStorage: summary.nonEssentialPreConsentStorage,
886
+ unclassifiedPreConsentStorageCount: summary.unclassifiedPreConsentStorageCount,
887
+ storageMetricLabel: boundedText(summary.storageMetricLabel, 120),
888
+ storageMetricScope: boundedText(summary.storageMetricScope, 120),
889
+ storageMetricStatus: boundedText(summary.storageMetricStatus, 120),
890
+ storageMetricExplanation: boundedText(summary.storageMetricExplanation, 280),
891
+ consentPlatform: boundedText(summary.consentPlatform, 160),
892
+ ...(trackerFootprint ? { trackerFootprint } : {}),
893
+ ...(policySurfaces ? { policySurfaces } : {}),
894
+ scanTimeSeconds: summary.scanTimeSeconds
895
+ };
896
+ }
897
+ function findingText(finding, priorityLabel = "criticality") {
898
+ const evidence = finding.evidence && typeof finding.evidence === "object" && !Array.isArray(finding.evidence)
899
+ ? finding.evidence
900
+ : {};
901
+ const lenses = Array.isArray(finding.reviewLenses) && finding.reviewLenses.length > 0
902
+ ? finding.reviewLenses.slice(0, 2).join(", ")
903
+ : "not classified";
904
+ const nextStep = typeof finding.nextStep === "string" && finding.nextStep.trim()
905
+ ? `; canonical next step=${boundedText(finding.nextStep.trim(), 180)}`
906
+ : "";
907
+ return `- ${finding.label ?? finding.id ?? "Projected finding"}; ${priorityLabel}=${finding.criticality ?? "unknown"}; confidence=${finding.confidence ?? "unknown"}; observation=${finding.plainEnglish ?? evidence.summary ?? "No compact description available"}; evidence=${evidence.basis ?? "unknown"}/${evidence.phase ?? "phase unknown"}: ${evidence.summary ?? "No compact evidence summary available"}; review lenses=${lenses}${nextStep}.`;
908
+ }
909
+ function executiveOverviewText(summary) {
910
+ if (!summary)
911
+ return null;
912
+ const tracker = summary.trackerFootprint && typeof summary.trackerFootprint === "object" && !Array.isArray(summary.trackerFootprint)
913
+ ? summary.trackerFootprint
914
+ : {};
915
+ const policies = Array.isArray(summary.policySurfaces)
916
+ ? summary.policySurfaces.flatMap((surface) => {
917
+ if (!surface || typeof surface !== "object" || Array.isArray(surface))
918
+ return [];
919
+ const row = surface;
920
+ return typeof row.title === "string" ? [row.title] : typeof row.type === "string" ? [row.type] : [];
921
+ }).slice(0, 5)
922
+ : [];
923
+ const values = [
924
+ `CMP/consent platform=${summary.consentPlatform ?? "not returned"}`,
925
+ `third-party requests=${summary.thirdPartyRequests ?? "unknown"}`,
926
+ `non-essential pre-consent storage=${summary.nonEssentialPreConsentStorage ?? summary.cookiesPreConsent ?? "unknown"}`,
927
+ `tracker vendors=${tracker.vendors ?? "unknown"}`,
928
+ `tracker domains=${tracker.domains ?? "unknown"}`,
929
+ `policy surfaces=${policies.length > 0 ? policies.join(", ") : "none returned"}`
930
+ ];
931
+ return `Canonical report overview: ${values.join("; ")}.`;
932
+ }
933
+ function preConsentRowText(row, priorityLabel = "priority") {
934
+ const cookieNames = Array.isArray(row.cookieNames) && row.cookieNames.length > 0
935
+ ? `; cookies=${row.cookieNames.slice(0, 8).join(", ")}${row.cookieNames.length > 8 ? ", …" : ""}`
936
+ : "";
937
+ const domains = Array.isArray(row.domains) && row.domains.length > 0
938
+ ? `; domains=${row.domains.slice(0, 4).join(", ")}${row.domains.length > 4 ? ", …" : ""}`
939
+ : "";
940
+ const timing = typeof row.firstObservedAtMs === "number" ? `${(row.firstObservedAtMs / 1_000).toFixed(3)}s` : "unknown";
941
+ const classification = row.evidenceClassification && typeof row.evidenceClassification === "object"
942
+ ? row.evidenceClassification
943
+ : {};
944
+ return `- ${row.kind}: ${row.name}${cookieNames}; vendor=${row.vendor ?? "unknown"}; purpose=${row.purpose ?? "unknown"}; category=${row.category ?? "unknown"}; first observed=${timing}${domains}; evidence=${classification.basis ?? "unknown"}/${classification.phase ?? "unknown"}/${classification.party ?? "unknown"}; observedBeforeConsent=${classification.observedBeforeConsent ?? "unknown"}; ${priorityLabel}=${classification.priority ?? "unknown"}; confidence=${row.confidence ?? "unknown"}.`;
945
+ }
946
+ function reportUrlFor(value) {
947
+ const scanId = extractScanId(value);
948
+ return typeof value.reportUrl === "string" && value.reportUrl.trim()
949
+ ? value.reportUrl.trim()
950
+ : typeof value.links?.report === "string" && value.links.report.trim()
951
+ ? value.links.report.trim()
952
+ : scanId
953
+ ? `https://certscore.ai/scan/${encodeURIComponent(scanId)}`
954
+ : null;
955
+ }
956
+ function canonicalScanProvenanceText(value) {
957
+ const present = (field) => typeof field === "string" && field.trim() ? field.trim() : "unavailable";
958
+ const numeric = (field) => typeof field === "number" && Number.isFinite(field) ? String(field) : "unavailable";
959
+ return `Canonical scan provenance: scanId=${present(extractScanId(value))}; scanFrom/execution region=${present(value.scanFrom)}; completedAt=${present(value.completedAt)}; startedAt=${present(value.startedAt)}; createdAt=${present(value.createdAt)}; retrieval mode=${present(value.provenance?.retrievalMode)}; original creation decision=${present(value.provenance?.creationDecision)}; scan age seconds=${numeric(value.provenance?.scanAgeSeconds)}; compatibility provenance mode=${present(value.provenance?.mode)}.`;
960
+ }
961
+ export function scanStatusText(value) {
962
+ const reportUrl = reportUrlFor(value);
963
+ return [
964
+ `CertScore scan status: status=${value.status ?? "unknown"}.`,
965
+ canonicalScanProvenanceText(value),
966
+ `Full report: ${reportUrl ?? "not available"}.`,
967
+ OBSERVATION_ONLY_DISCLAIMER,
968
+ INTERPRETATION_STATEMENT,
969
+ SCAN_PROVENANCE_GROUNDING
970
+ ].join("\n");
971
+ }
972
+ function boundedResultText(header, bodyLines, value) {
973
+ const footer = [OBSERVATION_ONLY_DISCLAIMER, INTERPRETATION_STATEMENT];
974
+ const reportUrl = reportUrlFor(value);
975
+ const lines = [
976
+ header,
977
+ `Provenance: retrieval=${value.provenance?.retrievalMode ?? "unknown"}; original creation=${value.provenance?.creationDecision ?? "unknown"}; scan age seconds=${value.provenance?.scanAgeSeconds ?? "unavailable"}.`,
978
+ `Full report: ${reportUrl ?? "not available"}.`
979
+ ];
980
+ let rendered = 0;
981
+ for (const line of bodyLines) {
982
+ if ([...lines, line, ...footer].join("\n").length > MAX_TOOL_TEXT_CHARS)
983
+ break;
984
+ lines.push(line);
985
+ rendered += 1;
986
+ }
987
+ if (rendered < bodyLines.length) {
988
+ const omitted = `${bodyLines.length - rendered} additional returned row${bodyLines.length - rendered === 1 ? " was" : "s were"} omitted from TextContent to preserve the size limit; use structuredContent or the report URL.`;
989
+ if ([...lines, omitted, ...footer].join("\n").length <= MAX_TOOL_TEXT_CHARS)
990
+ lines.push(omitted);
991
+ }
992
+ lines.push(...footer);
993
+ return lines.join("\n");
994
+ }
995
+ export function findingListText(value, label = "Canonical projected findings") {
996
+ const findings = Array.isArray(value.findings) ? value.findings : [];
997
+ const pagination = value.pagination && typeof value.pagination === "object" && !Array.isArray(value.pagination)
998
+ ? value.pagination
999
+ : {};
1000
+ const total = typeof pagination.total === "number" ? pagination.total : findings.length;
1001
+ const returned = typeof pagination.returned === "number" ? pagination.returned : findings.length;
1002
+ const truncated = pagination.truncated === true;
1003
+ const scanId = extractScanId(value) ?? "unknown";
1004
+ return boundedResultText(`${label} for scanId=${scanId}: ${returned} of ${total} returned${truncated ? " (truncated)" : ""}. These are canonical projected review signals, not MCP-derived findings.`, findings.map((finding) => findingText(finding)), value);
1005
+ }
1006
+ export function preConsentInventoryText(value) {
1007
+ const rows = Array.isArray(value.rows) ? value.rows : [];
1008
+ const metadata = value.evidenceMetadata && typeof value.evidenceMetadata === "object" && !Array.isArray(value.evidenceMetadata)
1009
+ ? value.evidenceMetadata
1010
+ : {};
1011
+ const total = typeof metadata.total === "number"
1012
+ ? metadata.total
1013
+ : typeof value.summary?.totalRowCount === "number"
1014
+ ? value.summary.totalRowCount
1015
+ : typeof value.summary?.rowCount === "number"
1016
+ ? value.summary.rowCount
1017
+ : rows.length;
1018
+ const returned = typeof metadata.returned === "number" ? metadata.returned : rows.length;
1019
+ const truncated = metadata.truncated === true || value.summary?.truncated === true;
1020
+ const scanId = extractScanId(value) ?? "unknown";
1021
+ const domain = typeof value.domain === "string" ? value.domain : "unknown domain";
1022
+ return boundedResultText(`Pre-consent cookie/tracker evidence for ${domain}; scanId=${scanId}; ${returned} of ${total} rows returned${truncated ? " (truncated)" : ""}. Enumerate only these observed rows.`, rows.map((row) => preConsentRowText(compactPreConsentRow(row))), value);
1023
+ }
1024
+ export function pulseReportText(value, label = "CertScore report") {
1025
+ const scanId = extractScanId(value) ?? "unknown";
1026
+ const domain = typeof value.domain === "string" ? value.domain : "unknown domain";
1027
+ const score = typeof value.summary?.score === "number"
1028
+ ? value.summary.score
1029
+ : typeof value.score === "number"
1030
+ ? value.score
1031
+ : null;
1032
+ const findings = findingsFromReport(value);
1033
+ const overview = executiveOverviewText(value.executiveSummary ?? value.summary?.executiveSummary);
1034
+ const body = [
1035
+ ...(overview ? [overview] : []),
1036
+ `Canonical projected findings returned in this ${label.toLocaleLowerCase()}: ${findings.length}.`,
1037
+ ...findings.map((finding) => findingText(finding))
1038
+ ];
1039
+ return boundedResultText(`${label} for ${domain}; scanId=${scanId}${score === null ? "" : `; CertScore score=${score}`}.`, body, value);
1040
+ }
1041
+ export function markdownReportText(value) {
1042
+ const markdown = typeof value.value === "string" ? value.value : "";
1043
+ const excerptLimit = Math.max(0, MAX_TOOL_TEXT_CHARS - 1_500);
1044
+ const excerpt = markdown.length > excerptLimit ? `${markdown.slice(0, excerptLimit)}\n…[markdown truncated for MCP TextContent]` : markdown;
1045
+ return boundedResultText(`CertScore report for scanId=${extractScanId(value) ?? "unknown"}. The following is a bounded canonical report excerpt.`, excerpt ? [excerpt] : ["No Markdown report body was returned."], value);
1046
+ }
1047
+ export function scanBundleText(bundle) {
1048
+ const score = typeof bundle.score === "number" ? `; CertScore score=${bundle.score}` : "";
1049
+ const footer = [OBSERVATION_ONLY_DISCLAIMER, SCAN_BUNDLE_INTERPRETATION_STATEMENT];
1050
+ const lines = [
1051
+ SCAN_BUNDLE_RESPONSE_CONTRACT,
1052
+ `CertScore scan bundle for ${bundle.domain ?? "unknown domain"}; status=${bundle.status ?? "unknown"}${score}; scanId=${bundle.scanId ?? "unknown"}.`,
1053
+ canonicalScanProvenanceText(bundle),
1054
+ `Full report: ${bundle.reportUrl ?? (bundle.scanId ? `https://certscore.ai/scan/${encodeURIComponent(String(bundle.scanId))}` : "not available")}.`
1055
+ ];
1056
+ const canAppend = (line) => [...lines, line, ...footer].join("\n").length <= MAX_TOOL_TEXT_CHARS;
1057
+ const append = (line) => {
1058
+ if (!canAppend(line))
1059
+ return false;
1060
+ lines.push(line);
1061
+ return true;
1062
+ };
1063
+ const coverage = bundle.coverage && typeof bundle.coverage === "object" && !Array.isArray(bundle.coverage)
1064
+ ? bundle.coverage
1065
+ : null;
1066
+ if (coverage) {
1067
+ append(`Coverage: status=${coverage.status ?? "unknown"}; ${coverage.summary ?? "Review limitations before interpreting absence."}`);
1068
+ }
1069
+ const postRefusal = bundle.postRefusalObservation && typeof bundle.postRefusalObservation === "object" && !Array.isArray(bundle.postRefusalObservation)
1070
+ ? bundle.postRefusalObservation
1071
+ : null;
1072
+ if (postRefusal && typeof postRefusal.interpretation === "string") {
1073
+ const termination = postRefusal.termination && typeof postRefusal.termination === "object" && !Array.isArray(postRefusal.termination)
1074
+ ? postRefusal.termination
1075
+ : null;
1076
+ const intentionalEvidenceStop = termination?.kind === "evidence_satisfied" && termination.intentional === true
1077
+ ? " The observation then stopped intentionally because qualifying evidence had been captured."
1078
+ : "";
1079
+ append(`Reject Path: ${postRefusal.interpretation}${intentionalEvidenceStop}`);
1080
+ for (const limitation of Array.isArray(postRefusal.coverageLimitations)
1081
+ ? postRefusal.coverageLimitations.slice(0, 3)
1082
+ : []) {
1083
+ append(`Reject Path coverage limitation: ${limitation}`);
1084
+ }
1085
+ }
1086
+ const overview = executiveOverviewText(bundle.summary?.executiveSummary);
1087
+ if (overview)
1088
+ append(overview);
1089
+ const transportSecurity = bundle.transportSecurity && typeof bundle.transportSecurity === "object" && !Array.isArray(bundle.transportSecurity)
1090
+ ? bundle.transportSecurity
1091
+ : null;
1092
+ if (transportSecurity) {
1093
+ const counts = transportSecurity.observationCounts && typeof transportSecurity.observationCounts === "object"
1094
+ ? transportSecurity.observationCounts
1095
+ : {};
1096
+ append(`Transport security: status=${transportSecurity.status ?? "unavailable"}; retained evidence=${transportSecurity.evidenceRetained === true ? "yes" : "no"}; canonical observations=${counts.total ?? 0}; concerns/review=${counts.concernOrReview ?? 0}; unavailable=${counts.unavailable ?? 0}.`);
1097
+ for (const observation of Array.isArray(transportSecurity.observations) ? transportSecurity.observations : []) {
1098
+ append(`Transport observation: ${observation.label ?? observation.id ?? "unknown"}; status=${observation.status ?? "unknown"}; ${observation.summary ?? "Review the structured transport evidence."}`);
1099
+ }
1100
+ for (const limitation of Array.isArray(transportSecurity.limitations) ? transportSecurity.limitations.slice(0, 3) : []) {
1101
+ append(`Transport limitation: ${limitation}`);
1102
+ }
1103
+ }
1104
+ const findings = Array.isArray(bundle.findings) ? bundle.findings : [];
1105
+ const findingTotal = bundle.findingsMetadata?.total ?? findings.length;
1106
+ const findingReturned = bundle.findingsMetadata?.returned ?? bundle.findingsMetadata?.shown ?? findings.length;
1107
+ append(`Canonical projected findings: ${findingReturned} of ${findingTotal} returned${bundle.findingsMetadata?.truncated ? " (truncated)" : ""}. These are already-projected review signals, not inferred technologies or legal conclusions.`);
1108
+ let findingsRendered = 0;
1109
+ for (const [index, finding] of findings.entries()) {
1110
+ const next = findingText(finding, "CertScore priority/classification");
1111
+ const remaining = findings.length - index - 1;
1112
+ const reserve = remaining > 0
1113
+ ? `${remaining} additional returned finding${remaining === 1 ? " was" : "s were"} omitted from TextContent to preserve the size limit; see structuredContent or the report URL.`
1114
+ : null;
1115
+ if ([...lines, next, ...(reserve ? [reserve] : []), ...footer].join("\n").length > MAX_TOOL_TEXT_CHARS)
1116
+ break;
1117
+ lines.push(next);
1118
+ findingsRendered += 1;
1119
+ }
1120
+ if (findingsRendered < findings.length) {
1121
+ append(`${findings.length - findingsRendered} additional returned finding${findings.length - findingsRendered === 1 ? " was" : "s were"} omitted from TextContent to preserve the size limit; see structuredContent or the report URL.`);
1122
+ }
1123
+ const inventory = bundle.preConsentCookiesTrackers;
1124
+ if (inventory && typeof inventory === "object") {
1125
+ append(`Pre-consent cookie/tracker evidence: ${inventory.returned ?? 0} of ${inventory.total ?? 0} rows returned${inventory.truncated ? " (truncated)" : ""}.`);
1126
+ const rows = Array.isArray(inventory.rows) ? inventory.rows : [];
1127
+ let rowsRendered = 0;
1128
+ for (const [index, row] of rows.entries()) {
1129
+ const next = preConsentRowText(row, "CertScore priority");
1130
+ const remaining = rows.length - index - 1;
1131
+ const reserve = remaining > 0
1132
+ ? `${remaining} additional returned pre-consent row${remaining === 1 ? " was" : "s were"} omitted from TextContent to preserve the size limit; see structuredContent or the report URL.`
1133
+ : null;
1134
+ if ([...lines, next, ...(reserve ? [reserve] : []), ...footer].join("\n").length > MAX_TOOL_TEXT_CHARS)
1135
+ break;
1136
+ lines.push(next);
1137
+ rowsRendered += 1;
1138
+ }
1139
+ if (rowsRendered < rows.length) {
1140
+ append(`${rows.length - rowsRendered} additional returned pre-consent row${rows.length - rowsRendered === 1 ? " was" : "s were"} omitted from TextContent to preserve the size limit; see structuredContent or the report URL.`);
1141
+ }
1142
+ }
1143
+ else {
1144
+ append("No row-level pre-consent inventory was available for this result; review coverage and limitations before interpreting absence.");
1145
+ }
1146
+ lines.push(...footer);
1147
+ return lines.join("\n");
1148
+ }
1149
+ function scanBundleTransportSecurity(report, detail) {
1150
+ const source = report.transportSecurity && typeof report.transportSecurity === "object" && !Array.isArray(report.transportSecurity)
1151
+ ? report.transportSecurity
1152
+ : null;
1153
+ if (!source) {
1154
+ return {
1155
+ status: "unavailable",
1156
+ evidenceRetained: false,
1157
+ observationCounts: {
1158
+ total: 0,
1159
+ observedPositive: 0,
1160
+ concernOrReview: 0,
1161
+ notObserved: 0,
1162
+ unavailable: 0
1163
+ },
1164
+ observations: [],
1165
+ limitations: [
1166
+ "No canonical transport-security projection was available in this scan response. Do not infer a positive transport result."
1167
+ ]
1168
+ };
1169
+ }
1170
+ const observations = Array.isArray(source.observations) ? source.observations : [];
1171
+ const compactObservations = observations.slice(0, 8).map((observation) => {
1172
+ const row = observation && typeof observation === "object" && !Array.isArray(observation)
1173
+ ? observation
1174
+ : {};
1175
+ return {
1176
+ id: row.id,
1177
+ label: boundedText(row.label, 140),
1178
+ status: row.status,
1179
+ assessmentStatus: row.assessmentStatus,
1180
+ evidenceState: row.evidenceState,
1181
+ summary: boundedText(row.summary, 180),
1182
+ evidenceRefs: []
1183
+ };
1184
+ });
1185
+ return {
1186
+ status: source.status === "available" || source.status === "limited" ? source.status : "unavailable",
1187
+ evidenceRetained: source.evidenceRetained === true,
1188
+ observationCounts: source.observationCounts && typeof source.observationCounts === "object" && !Array.isArray(source.observationCounts)
1189
+ ? source.observationCounts
1190
+ : {
1191
+ total: observations.length,
1192
+ observedPositive: 0,
1193
+ concernOrReview: 0,
1194
+ notObserved: 0,
1195
+ unavailable: observations.length
1196
+ },
1197
+ observations: detail === "summary" || detail === "findings"
1198
+ ? compactObservations
1199
+ : observations.map((observation) => compactEvidenceValue(observation, {
1200
+ arrayItems: 6,
1201
+ depth: 4,
1202
+ objectKeys: 12,
1203
+ stringChars: 1_200
1204
+ })),
1205
+ limitations: Array.isArray(source.limitations)
1206
+ ? source.limitations.filter((value) => typeof value === "string").slice(0, 8)
1207
+ : [],
1208
+ ...(detail === "full" && source.retainedSummary && typeof source.retainedSummary === "object" && !Array.isArray(source.retainedSummary)
1209
+ ? {
1210
+ retainedSummary: compactEvidenceValue(source.retainedSummary, {
1211
+ arrayItems: 20,
1212
+ depth: 4,
1213
+ objectKeys: 24,
1214
+ stringChars: 1_200
1215
+ })
1216
+ }
1217
+ : {})
1218
+ };
1219
+ }
557
1220
  export function buildScanBundle(input) {
558
1221
  const detail = input.detail ?? "summary";
559
1222
  const maxFindings = Math.min(50, Math.max(1, input.maxFindings ?? (detail === "summary" ? 5 : 20)));
560
1223
  const maxPreConsentRows = Math.min(50, Math.max(1, input.maxPreConsentRows ?? 20));
561
1224
  const evidence = (input.evidence ?? {});
562
1225
  const report = (input.report ?? {});
563
- const allFindings = Array.isArray(input.findings.findings) ? input.findings.findings : [];
564
- const findings = detail === "summary"
565
- ? []
566
- : allFindings.slice(0, maxFindings).map((finding) => detail === "full" ? finding : compactBundleFinding(finding));
567
- const preConsentRows = Array.isArray(input.preConsentCookiesTrackers?.rows)
568
- ? input.preConsentCookiesTrackers.rows.slice(0, maxPreConsentRows)
569
- : [];
1226
+ const transportSecurity = scanBundleTransportSecurity(report, detail);
1227
+ const allFindings = prioritizeFindingsForBoundedBundle(Array.isArray(input.findings.findings) ? input.findings.findings : []);
1228
+ const selectedFindings = detail === "summary" ? distinctSummaryFindings(allFindings) : allFindings;
1229
+ const selectedFindingRows = selectedFindings.slice(0, maxFindings);
1230
+ const findingDisclaimersDeduplicated = detail === "full"
1231
+ && selectedFindingRows.some((finding) => typeof finding.disclaimer === "string");
1232
+ const findings = selectedFindingRows
1233
+ .map((finding) => detail === "full" ? withoutFindingDisclaimer(finding) : compactBundleFinding(finding));
1234
+ const deduplicatedReport = deduplicatedFullReport({ findings, report, transportSecurity });
1235
+ const preConsentCookiesTrackers = input.preConsentCookiesTrackers
1236
+ ? preConsentBundleSection(input.preConsentCookiesTrackers, maxPreConsentRows)
1237
+ : null;
570
1238
  const links = {
571
1239
  ...(input.scan.links ?? {}),
572
1240
  ...(report.links && typeof report.links === "object" && !Array.isArray(report.links) ? report.links : {})
573
1241
  };
574
- const maxBytes = Math.min(200_000, Math.max(5_000, input.maxBytes ?? 50_000));
1242
+ const requestedMaxBytes = Math.min(200_000, Math.max(5_000, input.requestedMaxBytes ?? input.maxBytes ?? 50_000));
1243
+ const responseCeilingBytes = Math.min(200_000, Math.max(5_000, input.responseCeilingBytes ?? 200_000));
1244
+ const maxBytes = Math.min(requestedMaxBytes, responseCeilingBytes);
575
1245
  const contentUrls = Object.fromEntries(Object.entries({
576
1246
  report: input.scan.links?.report ?? links.report,
577
1247
  findings: links.findings,
@@ -579,8 +1249,8 @@ export function buildScanBundle(input) {
579
1249
  preConsentCookiesTrackers: links.preConsentCookiesTrackers
580
1250
  }).filter(([, value]) => typeof value === "string"));
581
1251
  const intentionallyOmitted = new Set();
582
- if (detail === "summary" && allFindings.length > 0)
583
- intentionallyOmitted.add("findings");
1252
+ if (allFindings.length > findings.length)
1253
+ intentionallyOmitted.add("additionalFindings");
584
1254
  if ((detail === "summary" || detail === "findings") && (Object.keys(evidence).length > 0 || allFindings.length > 0))
585
1255
  intentionallyOmitted.add("evidence");
586
1256
  if (detail !== "full" && Object.keys(report).length > 0)
@@ -592,12 +1262,17 @@ export function buildScanBundle(input) {
592
1262
  scanId: input.scan.scanId,
593
1263
  domain: input.scan.domain,
594
1264
  url: input.scan.url ?? null,
1265
+ scanFrom: input.scan.scanFrom ?? null,
595
1266
  status: input.scan.status,
596
1267
  score: input.scan.score ?? null,
1268
+ scoreLabel: "CertScore score",
597
1269
  scoreStatus: input.scan.scoreStatus ?? "final",
598
1270
  scoreVersion: input.scan.scoreVersion ?? null,
599
1271
  scoreUpdatedAt: input.scan.scoreUpdatedAt ?? null,
600
1272
  riskLevel: input.scan.riskLevel ?? null,
1273
+ postRefusalObservation: input.scan.postRefusalObservation ?? null,
1274
+ provenance: scanProvenance(input.scan, "existing_scan_retrieved"),
1275
+ interpretationGuidance: interpretationGuidance(SCAN_BUNDLE_INTERPRETATION_STATEMENT),
601
1276
  resultDisposition: input.scan.resultDisposition ?? null,
602
1277
  noGo: input.scan.noGo ?? null,
603
1278
  coverage: input.scan.coverage ?? null,
@@ -615,39 +1290,35 @@ export function buildScanBundle(input) {
615
1290
  headline: report.summary && typeof report.summary === "object" && !Array.isArray(report.summary)
616
1291
  ? report.summary.headline ?? null
617
1292
  : null,
618
- executiveSummary: report.executiveSummary ?? null,
1293
+ executiveSummary: neutralExecutiveSummary(report.executiveSummary),
619
1294
  counts: report.counts ?? null,
620
1295
  agentInterpretation: report.agentInterpretation ?? null
621
1296
  },
622
1297
  findings,
623
1298
  findingsMetadata: {
624
1299
  shown: findings.length,
1300
+ returned: findings.length,
625
1301
  total: allFindings.length,
626
1302
  truncated: allFindings.length > findings.length
627
1303
  },
1304
+ transportSecurity,
628
1305
  ...(detail === "evidence" || detail === "full"
629
1306
  ? { evidenceSummary: bundleEvidenceSummary(evidence, allFindings, links, detail === "full") }
630
1307
  : {}),
631
1308
  ...(detail === "full" ? {
632
- fullReport: compactEvidenceValue(report, {
1309
+ fullReport: compactEvidenceValue(deduplicatedReport.residual, {
633
1310
  arrayItems: 50,
634
1311
  depth: 8,
635
1312
  objectKeys: 100,
636
1313
  stringChars: 4_000
637
1314
  })
638
1315
  } : {}),
639
- ...(input.preConsentCookiesTrackers ? { preConsentCookiesTrackers: {
640
- summary: input.preConsentCookiesTrackers.summary,
641
- rows: preConsentRows,
642
- shown: preConsentRows.length,
643
- total: Array.isArray(input.preConsentCookiesTrackers.rows) ? input.preConsentCookiesTrackers.rows.length : 0,
644
- truncated: Array.isArray(input.preConsentCookiesTrackers.rows) && input.preConsentCookiesTrackers.rows.length > preConsentRows.length
645
- } } : {}),
1316
+ ...(preConsentCookiesTrackers ? { preConsentCookiesTrackers } : {}),
646
1317
  links,
647
- reportUrl: input.scan.links?.report ?? (typeof links.report === "string" ? links.report : null),
1318
+ reportUrl: input.scan.links?.report ?? (typeof links.report === "string" ? links.report : `https://certscore.ai/scan/${encodeURIComponent(input.scan.scanId)}`),
648
1319
  recommendedNextTool: null,
649
- recommendedNextAction: guidedScan.error?.recommendedNextAction ?? (detail === "summary" && allFindings.length > 0
650
- ? "Review this overview, then request detail=findings for bounded finding objects or open the report URL."
1320
+ recommendedNextAction: guidedScan.error?.recommendedNextAction ?? (detail === "summary" && allFindings.length > findings.length
1321
+ ? "Review the returned overview and findings, then request detail=findings or a larger maxFindings value for more projected findings."
651
1322
  : findings.length > 0
652
1323
  ? "Review the returned findings and follow their evidence references. Use detail=evidence only when deeper retained context is needed."
653
1324
  : "Review coverage and limitations before interpreting the absence of findings."),
@@ -656,30 +1327,64 @@ export function buildScanBundle(input) {
656
1327
  detail,
657
1328
  heavyEvidenceIncluded: detail === "evidence" || detail === "full",
658
1329
  findingsTruncated: allFindings.length > findings.length,
659
- requestedMaxBytes: maxBytes,
1330
+ requestedMaxBytes,
1331
+ effectiveMaxBytes: maxBytes,
1332
+ responseCeilingBytes,
1333
+ responseBudgetClamped: requestedMaxBytes > maxBytes,
660
1334
  actualBytes: 0,
1335
+ fullPayloadBytes: 0,
661
1336
  truncated: false,
1337
+ canonicalFindingsComplete: allFindings.length === findings.length,
662
1338
  truncationReason: null,
663
1339
  omittedSections: [...intentionallyOmitted],
1340
+ deduplicatedSections: detail === "full"
1341
+ ? [
1342
+ ...deduplicatedReport.deduplicatedSections,
1343
+ ...(findingDisclaimersDeduplicated ? ["findings[].disclaimer"] : [])
1344
+ ]
1345
+ : [],
664
1346
  nextRecommendedMaxBytes: null,
665
1347
  omittedContentAvailableViaUrl: intentionallyOmitted.size > 0 && Object.keys(contentUrls).length > 0,
666
1348
  contentUrls
667
1349
  },
668
1350
  observationOnlyDisclaimer: OBSERVATION_ONLY_DISCLAIMER,
669
- disclaimer: input.scan.disclaimer ?? input.report?.disclaimer ?? OBSERVATION_ONLY_DISCLAIMER
1351
+ disclaimer: LEGAL_REVIEW_DISCLAIMER
670
1352
  };
671
- const recommendationCandidates = [];
672
- const markBudgetOmitted = (section, reason, requiredBytes = bundle.mcpMetadata.actualBytes) => {
1353
+ const markBudgetOmitted = (section, reason) => {
673
1354
  bundle.mcpMetadata.truncated = true;
674
1355
  bundle.mcpMetadata.truncationReason ??= reason;
675
1356
  if (!bundle.mcpMetadata.omittedSections.includes(section)) {
676
1357
  bundle.mcpMetadata.omittedSections.push(section);
677
1358
  }
678
- recommendationCandidates.push(requiredBytes);
679
1359
  bundle.mcpMetadata.omittedContentAvailableViaUrl = Object.keys(contentUrls).length > 0;
680
1360
  };
681
1361
  const refresh = () => updateBundleActualBytes(bundle);
682
- refresh();
1362
+ const captureFullPayloadBytes = () => {
1363
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1364
+ refresh();
1365
+ const measured = bundle.mcpMetadata.actualBytes;
1366
+ if (bundle.mcpMetadata.fullPayloadBytes === measured)
1367
+ break;
1368
+ bundle.mcpMetadata.fullPayloadBytes = measured;
1369
+ }
1370
+ refresh();
1371
+ };
1372
+ const refreshTruncationGuidance = () => {
1373
+ const completeBytes = bundle.mcpMetadata.fullPayloadBytes;
1374
+ const canonicalFindingsComplete = bundle.findingsMetadata.returned === bundle.findingsMetadata.total &&
1375
+ bundle.findingsMetadata.truncated === false;
1376
+ bundle.mcpMetadata.canonicalFindingsComplete = canonicalFindingsComplete;
1377
+ bundle.mcpMetadata.nextRecommendedMaxBytes = completeBytes <= responseCeilingBytes
1378
+ ? Math.max(5_000, Math.ceil(completeBytes / 1_000) * 1_000)
1379
+ : null;
1380
+ bundle.recommendedNextAction = canonicalFindingsComplete
1381
+ ? "Canonical findings complete; retry only for omitted envelope detail."
1382
+ : bundle.mcpMetadata.nextRecommendedMaxBytes
1383
+ ? `Retry with maxBytes=${bundle.mcpMetadata.nextRecommendedMaxBytes} to retrieve the complete requested tier, or open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`
1384
+ : `The complete requested tier exceeds the MCP byte ceiling; open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`;
1385
+ refresh();
1386
+ };
1387
+ captureFullPayloadBytes();
683
1388
  if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.fullReport) {
684
1389
  markBudgetOmitted("fullReport", "full_report_omitted_to_byte_limit");
685
1390
  delete bundle.fullReport;
@@ -691,25 +1396,21 @@ export function buildScanBundle(input) {
691
1396
  bundle.evidenceSummary = compactSummary;
692
1397
  refresh();
693
1398
  }
694
- const inventoryRows = bundle.preConsentCookiesTrackers?.rows;
695
- while (bundle.mcpMetadata.actualBytes > maxBytes && Array.isArray(inventoryRows) && inventoryRows.length > 0) {
696
- markBudgetOmitted("preConsentCookiesTrackers", "evidence_inventory_reduced_to_byte_limit");
697
- inventoryRows.pop();
698
- bundle.preConsentCookiesTrackers.shown = inventoryRows.length;
699
- bundle.preConsentCookiesTrackers.truncated = true;
700
- refresh();
701
- }
702
- if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.preConsentCookiesTrackers) {
703
- markBudgetOmitted("preConsentCookiesTrackers", "evidence_inventory_omitted_to_byte_limit");
704
- delete bundle.preConsentCookiesTrackers;
1399
+ if (bundle.mcpMetadata.actualBytes > maxBytes &&
1400
+ (bundle.transportSecurity?.observations?.length > 0 || bundle.transportSecurity?.retainedSummary)) {
1401
+ markBudgetOmitted("transportSecurityDetail", "transport_security_detail_omitted_to_byte_limit");
1402
+ bundle.transportSecurity = {
1403
+ status: bundle.transportSecurity.status,
1404
+ evidenceRetained: bundle.transportSecurity.evidenceRetained,
1405
+ observationCounts: bundle.transportSecurity.observationCounts,
1406
+ observations: [],
1407
+ limitations: bundle.transportSecurity.limitations
1408
+ };
705
1409
  refresh();
706
1410
  }
707
- while (bundle.mcpMetadata.actualBytes > maxBytes && bundle.findings.length > 1) {
708
- markBudgetOmitted("additionalFindings", "findings_reduced_to_byte_limit");
709
- bundle.findings.pop();
710
- bundle.findingsMetadata.shown = bundle.findings.length;
711
- bundle.findingsMetadata.truncated = true;
712
- bundle.mcpMetadata.findingsTruncated = true;
1411
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.findings.length > 0) {
1412
+ markBudgetOmitted("findingDetail", "findings_compacted_to_preserve_core_rows");
1413
+ bundle.findings = bundle.findings.map((finding) => compactBundleFinding(finding, "core"));
713
1414
  refresh();
714
1415
  }
715
1416
  if (bundle.mcpMetadata.actualBytes > maxBytes) {
@@ -737,36 +1438,78 @@ export function buildScanBundle(input) {
737
1438
  bundle.links = Object.fromEntries(Object.entries(bundle.links).filter(([key]) => ["self", "report"].includes(key)));
738
1439
  refresh();
739
1440
  }
1441
+ if (bundle.mcpMetadata.actualBytes > maxBytes) {
1442
+ markBudgetOmitted("duplicateGuidance", "guidance_compacted_to_preserve_priority_content");
1443
+ bundle.interpretationGuidance = interpretationGuidance(COMPACT_SCAN_BUNDLE_INTERPRETATION_STATEMENT);
1444
+ bundle.observationOnlyDisclaimer = COMPACT_OBSERVATION_ONLY_DISCLAIMER;
1445
+ bundle.disclaimer = null;
1446
+ refresh();
1447
+ }
740
1448
  if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.evidenceSummary) {
741
1449
  markBudgetOmitted("evidenceDetail", "evidence_compacted_to_preserve_priority_content");
742
1450
  bundle.evidenceSummary = compactPriorityEvidenceSummary(bundle.evidenceSummary);
743
1451
  refresh();
744
1452
  }
745
- if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.disclaimer === OBSERVATION_ONLY_DISCLAIMER) {
746
- markBudgetOmitted("duplicateDisclaimer", "duplicate_disclaimer_omitted_to_byte_limit");
747
- bundle.disclaimer = null;
1453
+ const inventoryRows = bundle.preConsentCookiesTrackers?.rows;
1454
+ while (bundle.mcpMetadata.actualBytes > maxBytes && Array.isArray(inventoryRows) && inventoryRows.length > 0) {
1455
+ markBudgetOmitted("additionalPreConsentRows", "evidence_inventory_reduced_to_preserve_findings");
1456
+ inventoryRows.pop();
1457
+ bundle.preConsentCookiesTrackers.returned = inventoryRows.length;
1458
+ bundle.preConsentCookiesTrackers.truncated = true;
748
1459
  refresh();
749
1460
  }
750
- if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.findings.length > 0) {
751
- markBudgetOmitted("findings", "finding_omitted_to_byte_limit");
752
- bundle.findings = [];
753
- bundle.findingsMetadata.shown = 0;
754
- bundle.findingsMetadata.truncated = bundle.findingsMetadata.total > 0;
755
- bundle.mcpMetadata.findingsTruncated = bundle.findingsMetadata.total > 0;
1461
+ if (bundle.mcpMetadata.actualBytes > maxBytes &&
1462
+ bundle.findings.length > 1 &&
1463
+ typeof contentUrls.findings === "string") {
1464
+ markBudgetOmitted("findingEvidenceUrls", "finding_evidence_urls_replaced_by_template");
1465
+ bundle.evidenceUrlTemplate = "{contentUrls.findings}/{findingId}";
1466
+ bundle.findings = bundle.findings.map((finding) => {
1467
+ const { evidenceUrl: _evidenceUrl, ...rest } = finding;
1468
+ return rest;
1469
+ });
756
1470
  refresh();
757
1471
  }
1472
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.preConsentCookiesTrackers) {
1473
+ markBudgetOmitted("preConsentCookiesTrackers", "pre_consent_summary_omitted_to_preserve_findings");
1474
+ delete bundle.preConsentCookiesTrackers;
1475
+ refresh();
1476
+ }
1477
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.links) {
1478
+ markBudgetOmitted("links", "duplicate_links_omitted_to_preserve_findings");
1479
+ delete bundle.links;
1480
+ refresh();
1481
+ }
1482
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.timing) {
1483
+ markBudgetOmitted("timing", "duplicate_timing_envelope_omitted_to_preserve_findings");
1484
+ delete bundle.timing;
1485
+ refresh();
1486
+ }
1487
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.summary) {
1488
+ markBudgetOmitted("summary", "duplicate_summary_envelope_omitted_to_preserve_findings");
1489
+ delete bundle.summary;
1490
+ refresh();
1491
+ }
1492
+ if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.mcpMetadata.truncated) {
1493
+ refreshTruncationGuidance();
1494
+ }
758
1495
  if (bundle.mcpMetadata.actualBytes > maxBytes && bundle.evidenceSummary) {
759
1496
  markBudgetOmitted("evidence", "evidence_digest_omitted_to_byte_limit");
760
1497
  delete bundle.evidenceSummary;
761
1498
  bundle.mcpMetadata.heavyEvidenceIncluded = false;
762
1499
  refresh();
763
1500
  }
764
- if (bundle.mcpMetadata.truncated) {
765
- const required = Math.min(...recommendationCandidates.filter((value) => Number.isFinite(value) && value > maxBytes));
766
- bundle.mcpMetadata.nextRecommendedMaxBytes = Math.min(200_000, Math.max(maxBytes + 1_000, Math.ceil((Number.isFinite(required) ? required : maxBytes + 1_000) / 1_000) * 1_000));
767
- bundle.recommendedNextAction = `Retry with maxBytes=${bundle.mcpMetadata.nextRecommendedMaxBytes} or open ${bundle.reportUrl ? "the report URL" : "an available content URL"}.`;
1501
+ while (bundle.mcpMetadata.actualBytes > maxBytes && bundle.findings.length > 1) {
1502
+ markBudgetOmitted("additionalFindings", "findings_reduced_to_byte_limit");
1503
+ bundle.findings.pop();
1504
+ bundle.findingsMetadata.shown = bundle.findings.length;
1505
+ bundle.findingsMetadata.returned = bundle.findings.length;
1506
+ bundle.findingsMetadata.truncated = true;
1507
+ bundle.mcpMetadata.findingsTruncated = true;
768
1508
  refresh();
769
1509
  }
1510
+ if (bundle.mcpMetadata.truncated) {
1511
+ refreshTruncationGuidance();
1512
+ }
770
1513
  if (bundle.mcpMetadata.actualBytes > maxBytes) {
771
1514
  const minimal = {
772
1515
  type: bundle.type,
@@ -776,10 +1519,14 @@ export function buildScanBundle(input) {
776
1519
  url: bundle.url,
777
1520
  status: bundle.status,
778
1521
  score: bundle.score,
1522
+ scoreLabel: bundle.scoreLabel,
779
1523
  scoreStatus: bundle.scoreStatus,
780
1524
  scoreVersion: bundle.scoreVersion,
781
1525
  scoreUpdatedAt: bundle.scoreUpdatedAt,
782
1526
  riskLevel: bundle.riskLevel,
1527
+ postRefusalObservation: bundle.postRefusalObservation,
1528
+ provenance: bundle.provenance,
1529
+ interpretationGuidance: bundle.interpretationGuidance,
783
1530
  resultDisposition: bundle.resultDisposition,
784
1531
  noGo: bundle.noGo,
785
1532
  coverage: null,
@@ -794,12 +1541,29 @@ export function buildScanBundle(input) {
794
1541
  counts: bundle.summary?.counts ?? null,
795
1542
  agentInterpretation: null
796
1543
  },
797
- findings: [],
1544
+ findings: bundle.findings.slice(0, 1),
798
1545
  findingsMetadata: {
799
- shown: 0,
1546
+ shown: Math.min(1, bundle.findings.length),
1547
+ returned: Math.min(1, bundle.findings.length),
800
1548
  total: bundle.findingsMetadata.total,
801
- truncated: bundle.findingsMetadata.total > 0
1549
+ truncated: bundle.findingsMetadata.total > Math.min(1, bundle.findings.length)
1550
+ },
1551
+ transportSecurity: {
1552
+ status: bundle.transportSecurity.status,
1553
+ evidenceRetained: bundle.transportSecurity.evidenceRetained,
1554
+ observationCounts: bundle.transportSecurity.observationCounts,
1555
+ observations: [],
1556
+ limitations: bundle.transportSecurity.limitations.slice(0, 2)
802
1557
  },
1558
+ ...(bundle.preConsentCookiesTrackers ? {
1559
+ preConsentCookiesTrackers: {
1560
+ summary: bundle.preConsentCookiesTrackers.summary,
1561
+ rows: [],
1562
+ total: bundle.preConsentCookiesTrackers.total,
1563
+ returned: 0,
1564
+ truncated: bundle.preConsentCookiesTrackers.total > 0
1565
+ }
1566
+ } : {}),
803
1567
  links: Object.fromEntries(Object.entries(bundle.links ?? {}).filter(([key]) => ["docs", "report", "self"].includes(key))),
804
1568
  reportUrl: bundle.reportUrl,
805
1569
  recommendedNextTool: null,
@@ -808,13 +1572,25 @@ export function buildScanBundle(input) {
808
1572
  mcpMetadata: {
809
1573
  detail,
810
1574
  heavyEvidenceIncluded: false,
811
- findingsTruncated: bundle.findingsMetadata.total > 0,
812
- requestedMaxBytes: maxBytes,
1575
+ findingsTruncated: bundle.findingsMetadata.total > Math.min(1, bundle.findings.length),
1576
+ requestedMaxBytes,
1577
+ effectiveMaxBytes: maxBytes,
1578
+ responseCeilingBytes,
1579
+ responseBudgetClamped: requestedMaxBytes > maxBytes,
813
1580
  actualBytes: 0,
1581
+ fullPayloadBytes: bundle.mcpMetadata.fullPayloadBytes,
814
1582
  truncated: true,
1583
+ canonicalFindingsComplete: bundle.findingsMetadata.total === Math.min(1, bundle.findings.length),
815
1584
  truncationReason: "minimal_canonical_result_returned_to_byte_limit",
816
- omittedSections: [...new Set([...bundle.mcpMetadata.omittedSections, "summaryDetail", "findings", "evidence"])],
817
- nextRecommendedMaxBytes: bundle.mcpMetadata.nextRecommendedMaxBytes ?? Math.min(200_000, maxBytes + 1_000),
1585
+ omittedSections: [...new Set([
1586
+ ...bundle.mcpMetadata.omittedSections,
1587
+ "summaryDetail",
1588
+ "evidence",
1589
+ ...(bundle.findingsMetadata.total > Math.min(1, bundle.findings.length) ? ["additionalFindings"] : []),
1590
+ ...(bundle.preConsentCookiesTrackers?.total > 0 ? ["additionalPreConsentRows"] : [])
1591
+ ])],
1592
+ deduplicatedSections: bundle.mcpMetadata.deduplicatedSections,
1593
+ nextRecommendedMaxBytes: bundle.mcpMetadata.nextRecommendedMaxBytes,
818
1594
  omittedContentAvailableViaUrl: Object.keys(contentUrls).length > 0,
819
1595
  contentUrls
820
1596
  },