@tangle-network/agent-app 0.44.12 → 0.44.14

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.
@@ -215,6 +215,12 @@ declare const EVIDENCE_COVERAGE_CHECK = "evidence_coverage";
215
215
  * a reviewer reads the strength of the lineage off the row itself rather than
216
216
  * trusting that a quote was checked. */
217
217
  declare const QUOTE_VERIFICATION_CHECK = "quote_verification";
218
+ /** Platform check: how many quoted evidence entries anchor to text that
219
+ * actually CARRIES the figure the entry claims. Distinct from
220
+ * `quote_verification`, which only proves the text came from the document —
221
+ * production row `7256ef49` passed that one on all four entries while
222
+ * supporting none of them. */
223
+ declare const CLAIM_SUPPORT_CHECK = "claim_support";
218
224
  /** Domain seams for the three work-product tools — every domain word is a
219
225
  * parameter; the shell bakes none. */
220
226
  interface WorkProductToolConfig {
@@ -258,6 +264,17 @@ interface WorkProductToolConfig {
258
264
  * document-derived targets, and coverage stops being satisfiable by
259
265
  * assertion. */
260
266
  requireAnchoredEvidence?: boolean;
267
+ /** Verify that each anchored quote CARRIES the figure its entry claims.
268
+ * ON by default — an anchor that does not support its claim is the one
269
+ * failure mode every other gate here passes, and it reads to a reviewer as
270
+ * the most authoritative citation on the row.
271
+ *
272
+ * Only claims that assert a figure are checked, so a filing status, a name
273
+ * or a date is unaffected; see `./claim-support` for exactly where the line
274
+ * is drawn and why it is drawn to stay satisfiable. Set `false` only for a
275
+ * product whose claims are figures the source states in a form no numeric
276
+ * comparison can reach. */
277
+ verifyClaimSupport?: boolean;
261
278
  /** Per-turn provenance closure the ROUTE supplies (profileHash + runId are
262
279
  * known at dispatch; trusted, never read from model args). */
263
280
  provenance: (ctx: AppToolContext) => WorkProductProvenanceBase;
@@ -386,6 +403,10 @@ type SourceFindFailure = {
386
403
  } | {
387
404
  reason: 'occurrence_out_of_range';
388
405
  found: number;
406
+ } | {
407
+ reason: 'not_distinctive';
408
+ needle: string;
409
+ found: number;
389
410
  };
390
411
  type SourceFindResult = {
391
412
  ok: true;
@@ -408,6 +429,130 @@ declare function sliceSourceSpan(sourceText: string, span: {
408
429
  end: number;
409
430
  }): SourceSpanResult;
410
431
 
432
+ /**
433
+ * Claim support — does the anchored text actually say what the entry claims?
434
+ *
435
+ * The two gates before this one answer different questions. `sourceContainsQuote`
436
+ * asks whether the quote is really in the document; `findSourceLine` /
437
+ * `sliceSourceSpan` make the platform produce the quote so it cannot be typed
438
+ * wrong. Both are about the TEXT's provenance. Neither one looks at `claim`.
439
+ *
440
+ * Production row `7256ef49` is what that gap costs. Four evidence entries, all
441
+ * `quoteBasis:'span'`, every quote a genuine slice of the document it named —
442
+ * and every one landing on the employer/payer line about 200 characters above
443
+ * the figure:
444
+ *
445
+ * claim 128450.00 -> "tics LLC EIN 84-2213907\nEmployee: Dana"
446
+ * claim 812.44 -> "nt Savings Bank TIN 22-5510983\nRecip"
447
+ * claim 2204.18 -> "ndex Fund Trust TIN 47-3320115\nRec"
448
+ * claim 1955.02 -> "pient: Dana R. Whitfield\n------------"
449
+ *
450
+ * That is strictly worse than a fabricated quote. A fabricated quote fails the
451
+ * verbatim gate; this one passes every gate, is real text from the right
452
+ * document, and reads to a reviewer as an authoritative citation while
453
+ * supporting nothing. `locator.find` (the platform locating a value the model
454
+ * names) prevents the model from ADDRESSING the wrong line, and it is the right
455
+ * primary fix. This module is the independent check underneath it: whatever
456
+ * anchoring form produced the text, the text has to contain the figure.
457
+ *
458
+ * That also settles what a raw `locator.span` is worth. Hand-computed offsets
459
+ * stay accepted, but only when they independently verify — which is exactly
460
+ * "the slice contains the claimed value".
461
+ *
462
+ * ── The rule, and why it is drawn here ──────────────────────────────────────
463
+ *
464
+ * Strict on figures, silent on everything else. An unsatisfiable gate does not
465
+ * stop a bad submit, it SELECTS for one — that is the measured mechanism behind
466
+ * the 38 fabricated quotes on row `a68b1943`, where a coverage gate demanded
467
+ * document lineage for computed values and got thirteen invented citations
468
+ * forty seconds later. So the rule fires only where an honest citation can
469
+ * always satisfy it:
470
+ *
471
+ * - The claim IS a value (`"128450.00"`, `"$30,000"`, `"3"`) — the anchored
472
+ * text MUST contain that value. No latitude. This is the shape every tax
473
+ * figure takes and the shape row `7256ef49` failed.
474
+ * - The claim is prose naming figures (`"indemnity capped at $5,000,000"`) —
475
+ * at least one of its currency-shaped figures must occur. Not all of them:
476
+ * a claim may legitimately narrate a computation over several lines while
477
+ * anchoring to the one line under discussion, and refusing that would make
478
+ * an honest citation unrepresentable for the sake of a stricter-sounding
479
+ * rule. One is enough to keep the anchor tethered to the claim's subject.
480
+ * - The claim carries no figure at all (`"Married Filing Jointly"`,
481
+ * `"Dana R. Whitfield"`, `"2025-04-15"`) — nothing to check, and the entry
482
+ * passes. A filing status has no number to find, and inventing a
483
+ * word-overlap score here would re-open the hole the verbatim gate closes.
484
+ * - The entry has no anchored text at all — nothing to check. A computed
485
+ * value cites its computation, not a document.
486
+ *
487
+ * A bare year, a form number and a box number are deliberately NOT figures:
488
+ * `2025`, `1040` and `Box 1` have no thousands separator, no cent pair and no
489
+ * currency symbol, so prose mentioning them does not trip the rule.
490
+ *
491
+ * ── Matching is value-wise, never substring ─────────────────────────────────
492
+ *
493
+ * `text.includes(claim)` would be the obvious implementation and it is wrong in
494
+ * the direction that matters: it passes `"450.00"` against `"128,450.00"`, so a
495
+ * claim citing the wrong figure survives whenever its digits happen to be a tail
496
+ * of a real one. Both sides are tokenized into numbers and compared as VALUES,
497
+ * so `450` and `128450` are simply different.
498
+ */
499
+ /**
500
+ * Reduce a numeric token to the form both sides are compared in: no currency,
501
+ * no grouping, no trailing zeros in the fraction, no leading zeros.
502
+ *
503
+ * Sign and accounting parentheses are stripped rather than preserved, so
504
+ * `(1,234.00)`, `-1,234.00` and `1,234.00` all reduce to `1234`. A document
505
+ * renders the same deduction all three ways depending on the form and the
506
+ * extractor, and the sign is a property of the TARGET LINE's semantics, not of
507
+ * the document's typography. Comparing magnitudes keeps those honest citations
508
+ * working, and it concedes nothing to fabrication: the digits still have to be
509
+ * the document's digits.
510
+ *
511
+ * Returns `null` for anything that is not a plain number.
512
+ */
513
+ declare function canonicalizeValue(token: string): string | null;
514
+ /** Every distinct value appearing in `text`, canonicalized. */
515
+ declare function valuesInText(text: string): string[];
516
+ /**
517
+ * The values a claim asserts, canonicalized — empty when the claim asserts no
518
+ * figure, which is the "nothing to check" case.
519
+ *
520
+ * A claim that is ENTIRELY one value yields that value (strict path). Otherwise
521
+ * only currency-shaped figures inside the prose count, so an assertion that
522
+ * merely mentions a year or a form number yields nothing.
523
+ */
524
+ declare function claimValues(claim: string): string[];
525
+ type ClaimSupport =
526
+ /** No figure to check: a non-numeric claim, or no anchored text. */
527
+ {
528
+ status: 'not_applicable';
529
+ } | {
530
+ status: 'supported';
531
+ matched: string;
532
+ } | {
533
+ status: 'unsupported';
534
+ claimed: string[];
535
+ present: string[];
536
+ };
537
+ /**
538
+ * Does `quote` carry the figure `claim` asserts?
539
+ *
540
+ * `supported` requires ONE claimed value to occur, which is exact for the
541
+ * single-value claim (there is only one) and deliberate latitude for prose (see
542
+ * the rule note at the top of the file).
543
+ */
544
+ declare function verifyClaimSupport(quote: string, claim: string): ClaimSupport;
545
+ /**
546
+ * The sentence a model can act on without re-reading the document: what it
547
+ * claimed, what the line it cited actually says, what figures that line does
548
+ * carry, and the two ways out — cite the value, or drop the locator because the
549
+ * figure was computed. Naming both keeps the gate satisfiable, which is the
550
+ * property that stops it manufacturing the citation it screens for.
551
+ */
552
+ declare function claimSupportErrorDetail(failure: Extract<ClaimSupport, {
553
+ status: 'unsupported';
554
+ }>, quote: string): string;
555
+
411
556
  /**
412
557
  * Framework-neutral work-product review endpoints — the
413
558
  * `createInteractionAnswerRoute` factory pattern: web-standard
@@ -497,4 +642,4 @@ interface WorkProductRoutes {
497
642
  /** Create the work-product review endpoints over the store port and product seams */
498
643
  declare function createWorkProductRoutes(options: WorkProductRoutesOptions): WorkProductRoutes;
499
644
 
500
- export { type CreateWorkProductInput, EVIDENCE_COVERAGE_CHECK, EvidenceEntry, ExceptionEntry, type FinalizeWorkProductProvenanceInput, type InMemoryWorkProductStore, MAX_WORK_PRODUCT_BATCH, QUOTE_VERIFICATION_CHECK, QualityCheck, type SourceFindFailure, type SourceFindResult, type SourceSpanFailure, type SourceSpanResult, type SubmitWorkProductInput, TrustItem, WorkProductArtifact, WorkProductAuditEvent, type WorkProductAuthorizeArgs, type WorkProductOutcome, WorkProductPersistedPart, WorkProductProvenance, type WorkProductProvenanceBase, WorkProductRecord, type WorkProductRouteAuthorization, type WorkProductRoutes, type WorkProductRoutesOptions, type WorkProductService, type WorkProductServiceOptions, WorkProductStatus, WorkProductStorePort, type WorkProductToolConfig, type WorkProductVerdictBody, type WorkProductVerdictInput, buildWorkProductTools, canTransitionWorkProduct, createInMemoryWorkProductStore, createWorkProductRoutes, createWorkProductService, finalizeWorkProductProvenance, findSourceLine, isWorkProductTerminal, normalizeQuoteText, sliceSourceSpan, sourceContainsQuote, stampProvenance, validateWorkProductVerdictBody, workProductTrustInputs };
645
+ export { CLAIM_SUPPORT_CHECK, type ClaimSupport, type CreateWorkProductInput, EVIDENCE_COVERAGE_CHECK, EvidenceEntry, ExceptionEntry, type FinalizeWorkProductProvenanceInput, type InMemoryWorkProductStore, MAX_WORK_PRODUCT_BATCH, QUOTE_VERIFICATION_CHECK, QualityCheck, type SourceFindFailure, type SourceFindResult, type SourceSpanFailure, type SourceSpanResult, type SubmitWorkProductInput, TrustItem, WorkProductArtifact, WorkProductAuditEvent, type WorkProductAuthorizeArgs, type WorkProductOutcome, WorkProductPersistedPart, WorkProductProvenance, type WorkProductProvenanceBase, WorkProductRecord, type WorkProductRouteAuthorization, type WorkProductRoutes, type WorkProductRoutesOptions, type WorkProductService, type WorkProductServiceOptions, WorkProductStatus, WorkProductStorePort, type WorkProductToolConfig, type WorkProductVerdictBody, type WorkProductVerdictInput, buildWorkProductTools, canTransitionWorkProduct, canonicalizeValue, claimSupportErrorDetail, claimValues, createInMemoryWorkProductStore, createWorkProductRoutes, createWorkProductService, finalizeWorkProductProvenance, findSourceLine, isWorkProductTerminal, normalizeQuoteText, sliceSourceSpan, sourceContainsQuote, stampProvenance, validateWorkProductVerdictBody, valuesInText, verifyClaimSupport, workProductTrustInputs };
@@ -387,6 +387,64 @@ function workProductTrustInputs(records, verdictsFor) {
387
387
  return items;
388
388
  }
389
389
 
390
+ // src/work-product/claim-support.ts
391
+ var CURRENCY = /[$€£¥₹]/gu;
392
+ var NUMBER_IN_TEXT = /[$€£¥₹]?\s*\d{1,3}(?:,\d{3})+(?:\.\d+)?|[$€£¥₹]?\s*\d+(?:\.\d+)?/gu;
393
+ var FIGURE_IN_PROSE = /[$€£¥₹]\s*[-+]?\d[\d,]*(?:\.\d+)?|[-+]?\d{1,3}(?:,\d{3})+(?:\.\d+)?|[-+]?\d+\.\d{2}(?!\d)/gu;
394
+ var WHOLE_VALUE = /^[$€£¥₹]?\s*[-+]?\s*(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?\s*%?$/u;
395
+ function canonicalizeValue(token) {
396
+ let text = token.trim();
397
+ if (text.length === 0) return null;
398
+ if (/^\(.*\)$/u.test(text)) text = text.slice(1, -1).trim();
399
+ text = text.replace(CURRENCY, "").trim();
400
+ text = text.replace(/^[-+]\s*/u, "").trim();
401
+ text = text.replace(/%$/u, "").trim();
402
+ if (!/^(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?$/u.test(text)) return null;
403
+ text = text.replace(/,/gu, "");
404
+ if (text.includes(".")) text = text.replace(/0+$/u, "").replace(/\.$/u, "");
405
+ text = text.replace(/^0+(?=\d)/u, "");
406
+ return text.length === 0 ? null : text;
407
+ }
408
+ function valuesInText(text) {
409
+ const seen = /* @__PURE__ */ new Set();
410
+ for (const match of text.matchAll(NUMBER_IN_TEXT)) {
411
+ const canonical = canonicalizeValue(match[0]);
412
+ if (canonical !== null) seen.add(canonical);
413
+ }
414
+ return [...seen];
415
+ }
416
+ function claimValues(claim) {
417
+ const trimmed = claim.trim();
418
+ if (WHOLE_VALUE.test(trimmed)) {
419
+ const whole = canonicalizeValue(trimmed);
420
+ if (whole !== null) return [whole];
421
+ }
422
+ const seen = /* @__PURE__ */ new Set();
423
+ for (const match of trimmed.matchAll(FIGURE_IN_PROSE)) {
424
+ const canonical = canonicalizeValue(match[0]);
425
+ if (canonical !== null) seen.add(canonical);
426
+ }
427
+ return [...seen];
428
+ }
429
+ function verifyClaimSupport(quote, claim) {
430
+ if (quote.trim().length === 0) return { status: "not_applicable" };
431
+ const claimed = claimValues(claim);
432
+ if (claimed.length === 0) return { status: "not_applicable" };
433
+ const present = valuesInText(quote);
434
+ const matched = claimed.find((value) => present.includes(value));
435
+ if (matched !== void 0) return { status: "supported", matched };
436
+ return { status: "unsupported", claimed, present };
437
+ }
438
+ function excerpt(quote, limit = 120) {
439
+ const flat = quote.replace(/\s+/gu, " ").trim();
440
+ return flat.length <= limit ? flat : `${flat.slice(0, limit)}\u2026`;
441
+ }
442
+ function claimSupportErrorDetail(failure, quote) {
443
+ const wanted = failure.claimed.length === 1 ? failure.claimed[0] : `any of ${failure.claimed.join(", ")}`;
444
+ const carries = failure.present.length === 0 ? "that line carries no figure at all" : `the only figures on it are ${failure.present.join(", ")}`;
445
+ return `the cited text does not contain ${wanted}. It reads "${excerpt(quote)}", and ${carries}. Cite locator.find with the value exactly as it appears in the document and the platform will locate the right line for you. If this figure was COMPUTED rather than read from the document, omit the locator entirely and state the computation in claim.`;
446
+ }
447
+
390
448
  // src/work-product/quote.ts
391
449
  var WHITESPACE = /[\s\p{Zs}\u2028\u2029\u200b-\u200d\ufeff]+/gu;
392
450
  var DASHES = /[\u2010-\u2015\u2212\ufe58\ufe63\uff0d]/gu;
@@ -403,6 +461,8 @@ function sourceContainsQuote(sourceText, quote) {
403
461
  if (normalizedQuote.length === 0) return false;
404
462
  return normalizeQuoteText(sourceText).includes(normalizedQuote);
405
463
  }
464
+ var MIN_NEEDLE_LENGTH = 3;
465
+ var MAX_AMBIGUOUS_MATCHES = 8;
406
466
  function lineAround(text, index) {
407
467
  let start = text.lastIndexOf("\n", index);
408
468
  start = start < 0 ? 0 : start + 1;
@@ -414,26 +474,41 @@ function lineAround(text, index) {
414
474
  function findSourceLine(sourceText, needle, occurrence = 1) {
415
475
  const trimmed = needle.trim();
416
476
  if (trimmed.length === 0) return { ok: false, failure: { reason: "blank_needle" } };
417
- const positions = [];
418
- for (let at = sourceText.indexOf(trimmed); at >= 0; at = sourceText.indexOf(trimmed, at + 1)) {
419
- positions.push(at);
477
+ if (trimmed.length < MIN_NEEDLE_LENGTH) {
478
+ return { ok: false, failure: { reason: "not_distinctive", needle: trimmed, found: 0 } };
420
479
  }
421
- if (positions.length === 0) {
422
- const wanted = normalizeQuoteText(trimmed);
423
- if (wanted.length > 0) {
424
- let cursor = 0;
425
- while (cursor <= sourceText.length) {
426
- const bound2 = lineAround(sourceText, cursor);
427
- const line = sourceText.slice(bound2.start, bound2.end);
428
- if (normalizeQuoteText(line).includes(wanted) || normalizeQuoteText(line.replace(/,/gu, "")).includes(wanted)) {
429
- positions.push(bound2.start);
430
- }
431
- if (bound2.end >= sourceText.length) break;
432
- cursor = bound2.end + 1;
480
+ const eachLine = (visit) => {
481
+ let cursor = 0;
482
+ while (cursor <= sourceText.length) {
483
+ const bound2 = lineAround(sourceText, cursor);
484
+ visit(bound2, sourceText.slice(bound2.start, bound2.end));
485
+ if (bound2.end >= sourceText.length) break;
486
+ cursor = bound2.end + 1;
487
+ }
488
+ };
489
+ const positions = [];
490
+ const wantedValue = canonicalizeValue(trimmed);
491
+ if (wantedValue !== null) {
492
+ eachLine((bound2, line) => {
493
+ if (valuesInText(line).includes(wantedValue)) positions.push(bound2.start);
494
+ });
495
+ } else {
496
+ for (let at = sourceText.indexOf(trimmed); at >= 0; at = sourceText.indexOf(trimmed, at + 1)) {
497
+ positions.push(at);
498
+ }
499
+ if (positions.length === 0) {
500
+ const wanted = normalizeQuoteText(trimmed);
501
+ if (wanted.length > 0) {
502
+ eachLine((bound2, line) => {
503
+ if (normalizeQuoteText(line).includes(wanted)) positions.push(bound2.start);
504
+ });
433
505
  }
434
506
  }
435
507
  }
436
508
  if (positions.length === 0) return { ok: false, failure: { reason: "not_found" } };
509
+ if (occurrence === 1 && positions.length > MAX_AMBIGUOUS_MATCHES) {
510
+ return { ok: false, failure: { reason: "not_distinctive", needle: trimmed, found: positions.length } };
511
+ }
437
512
  if (occurrence < 1 || occurrence > positions.length) {
438
513
  return { ok: false, failure: { reason: "occurrence_out_of_range", found: positions.length } };
439
514
  }
@@ -461,6 +536,7 @@ function sliceSourceSpan(sourceText, span) {
461
536
  var MAX_WORK_PRODUCT_BATCH = 50;
462
537
  var EVIDENCE_COVERAGE_CHECK = "evidence_coverage";
463
538
  var QUOTE_VERIFICATION_CHECK = "quote_verification";
539
+ var CLAIM_SUPPORT_CHECK = "claim_support";
464
540
  async function unwrap(run, code) {
465
541
  let outcome = await run();
466
542
  if (!outcome.succeeded && outcome.conflict) outcome = await run();
@@ -527,6 +603,8 @@ function findErrorDetail(failure, needle) {
527
603
  return `${JSON.stringify(needle)} does not occur in that document. Read it again and cite a value it actually contains, or \u2014 if this figure is COMPUTED rather than read \u2014 omit the locator and state the computation in claim.`;
528
604
  case "occurrence_out_of_range":
529
605
  return `that value occurs ${failure.found} time(s) in the document; findOccurrence is out of range.`;
606
+ case "not_distinctive":
607
+ return failure.found === 0 ? `${JSON.stringify(failure.needle)} is too short to identify a place in the document \u2014 a digit or two matches somewhere in almost any text. Cite the labelled line instead (for example "Box 1 Wages, tips, other compensation ......... 128,450.00"). If the document does not state this value at all, omit the locator and say so in claim rather than pointing at an unrelated line.` : `${JSON.stringify(failure.needle)} occurs ${failure.found} times, so it names no particular place. Cite a longer stretch of the supporting line, or pass findOccurrence to say which one you mean.`;
530
608
  }
531
609
  }
532
610
  async function resolveEvidenceQuotes(config, entries, ctx) {
@@ -612,6 +690,20 @@ async function resolveEvidenceQuotes(config, entries, ctx) {
612
690
  entry.locator.quoteBasis = "model";
613
691
  }
614
692
  }
693
+ function assertClaimsSupported(config, entries) {
694
+ if (config.verifyClaimSupport === false) return;
695
+ for (let index = 0; index < entries.length; index += 1) {
696
+ const entry = entries[index];
697
+ const quote = entry.locator.quote;
698
+ if (quote === void 0) continue;
699
+ const support = verifyClaimSupport(quote, entry.claim);
700
+ if (support.status !== "unsupported") continue;
701
+ throw new ToolInputError(
702
+ "claim_not_supported",
703
+ `entries[${index}].claim ${JSON.stringify(entry.claim)} is not supported by the text it cites in "${entry.sourceRef}": ${claimSupportErrorDetail(support, quote)}`
704
+ );
705
+ }
706
+ }
615
707
  async function summarizeQuoteVerification(config, evidence, ctx) {
616
708
  const readSourceText = config.readSourceText;
617
709
  if (!readSourceText) return void 0;
@@ -646,6 +738,21 @@ async function summarizeQuoteVerification(config, evidence, ctx) {
646
738
  }
647
739
  return { verified, spanAnchored, withoutQuote, failed };
648
740
  }
741
+ function summarizeClaimSupport(evidence) {
742
+ let supported = 0;
743
+ let checkable = 0;
744
+ const unsupported = [];
745
+ for (const entry of evidence) {
746
+ const quote = entry.locator.quote;
747
+ if (quote === void 0) continue;
748
+ const support = verifyClaimSupport(quote, entry.claim);
749
+ if (support.status === "not_applicable") continue;
750
+ checkable += 1;
751
+ if (support.status === "supported") supported += 1;
752
+ else unsupported.push(entry.id);
753
+ }
754
+ return { supported, checkable, unsupported };
755
+ }
649
756
  function buildWorkProductTools(config) {
650
757
  const service = createWorkProductService({
651
758
  store: config.store,
@@ -726,6 +833,7 @@ function buildWorkProductTools(config) {
726
833
  }
727
834
  }
728
835
  await resolveEvidenceQuotes(config, entries, ctx);
836
+ assertClaimsSupported(config, entries);
729
837
  const draft = await resolveDraft(service, config, scopeKey, ctx);
730
838
  const record = await unwrap(() => service.upsertEvidence(draft.id, entries), "evidence_rejected");
731
839
  return {
@@ -885,6 +993,28 @@ function buildWorkProductTools(config) {
885
993
  );
886
994
  }
887
995
  }
996
+ if (config.verifyClaimSupport !== false) {
997
+ const support = summarizeClaimSupport(draft.evidence);
998
+ checks.unshift({
999
+ id: CLAIM_SUPPORT_CHECK,
1000
+ name: CLAIM_SUPPORT_CHECK,
1001
+ passed: support.unsupported.length === 0,
1002
+ detail: support.unsupported.length > 0 ? `Cited text does not contain the claimed figure on: ${support.unsupported.join(", ")}` : support.checkable === 0 ? (
1003
+ // Honest about a vacuous pass: no entry paired a quote with a
1004
+ // figure, so nothing was checked. Reporting "0/0 verified"
1005
+ // would read to a reviewer as assurance that was never earned.
1006
+ "No citation pairs a quote with a claimed figure \u2014 nothing to check"
1007
+ ) : `${support.supported}/${support.checkable} value-bearing citations anchor to text containing the claimed figure`,
1008
+ source: "platform"
1009
+ });
1010
+ if (support.unsupported.length > 0) {
1011
+ await unwrap(() => service.recordChecks(draft.id, checks), "checks_rejected");
1012
+ throw new ToolInputError(
1013
+ "claim_not_supported",
1014
+ `Cannot submit: ${support.unsupported.length} evidence entr${support.unsupported.length === 1 ? "y cites" : "ies cite"} text that does not contain the figure claimed (${support.unsupported.join(", ")}). Re-emit each with locator.find set to the value exactly as it appears in the document, or without a locator if the figure was computed rather than read.`
1015
+ );
1016
+ }
1017
+ }
888
1018
  if (config.materialTargets) {
889
1019
  const targets = config.materialTargets(artifact);
890
1020
  const covered = new Set(draft.evidence.map((entry) => entry.target));
@@ -1031,11 +1161,15 @@ function createWorkProductRoutes(options) {
1031
1161
  return { list, detail, verdict };
1032
1162
  }
1033
1163
  export {
1164
+ CLAIM_SUPPORT_CHECK,
1034
1165
  EVIDENCE_COVERAGE_CHECK,
1035
1166
  MAX_WORK_PRODUCT_BATCH,
1036
1167
  QUOTE_VERIFICATION_CHECK,
1037
1168
  buildWorkProductTools,
1038
1169
  canTransitionWorkProduct,
1170
+ canonicalizeValue,
1171
+ claimSupportErrorDetail,
1172
+ claimValues,
1039
1173
  createInMemoryWorkProductStore,
1040
1174
  createWorkProductRoutes,
1041
1175
  createWorkProductService,
@@ -1056,6 +1190,8 @@ export {
1056
1190
  stampProvenance,
1057
1191
  unresolvedBlockingExceptions,
1058
1192
  validateWorkProductVerdictBody,
1193
+ valuesInText,
1194
+ verifyClaimSupport,
1059
1195
  workProductToPersistedPart,
1060
1196
  workProductTrustInputs
1061
1197
  };