@tangle-network/agent-app 0.44.16 → 0.44.18

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.
@@ -17,6 +17,124 @@ import {
17
17
  defineAppTool
18
18
  } from "../chunk-YEFFHORB.js";
19
19
 
20
+ // src/work-product/claim-support.ts
21
+ var CURRENCY = /[$€£¥₹]/gu;
22
+ var NUMBER_IN_TEXT = /[$€£¥₹]?\s*\d{1,3}(?:,\d{3})+(?:\.\d+)?|[$€£¥₹]?\s*\d+(?:\.\d+)?/gu;
23
+ var FIGURE_IN_PROSE = /[$€£¥₹]\s*[-+]?\d[\d,]*(?:\.\d+)?|[-+]?\d{1,3}(?:,\d{3})+(?:\.\d+)?|[-+]?\d+\.\d{2}(?!\d)/gu;
24
+ var WHOLE_VALUE = /^[$€£¥₹]?\s*[-+]?\s*(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?\s*%?$/u;
25
+ function canonicalizeValue(token) {
26
+ let text = token.trim();
27
+ if (text.length === 0) return null;
28
+ if (/^\(.*\)$/u.test(text)) text = text.slice(1, -1).trim();
29
+ text = text.replace(CURRENCY, "").trim();
30
+ text = text.replace(/^[-+]\s*/u, "").trim();
31
+ text = text.replace(/%$/u, "").trim();
32
+ if (!/^(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?$/u.test(text)) return null;
33
+ text = text.replace(/,/gu, "");
34
+ if (text.includes(".")) text = text.replace(/0+$/u, "").replace(/\.$/u, "");
35
+ text = text.replace(/^0+(?=\d)/u, "");
36
+ return text.length === 0 ? null : text;
37
+ }
38
+ function valuesInText(text) {
39
+ const seen = /* @__PURE__ */ new Set();
40
+ for (const match of text.matchAll(NUMBER_IN_TEXT)) {
41
+ const canonical = canonicalizeValue(match[0]);
42
+ if (canonical !== null) seen.add(canonical);
43
+ }
44
+ return [...seen];
45
+ }
46
+ function claimValues(claim) {
47
+ const trimmed = claim.trim();
48
+ if (WHOLE_VALUE.test(trimmed)) {
49
+ const whole = canonicalizeValue(trimmed);
50
+ if (whole !== null) return [whole];
51
+ }
52
+ const seen = /* @__PURE__ */ new Set();
53
+ for (const match of trimmed.matchAll(FIGURE_IN_PROSE)) {
54
+ const canonical = canonicalizeValue(match[0]);
55
+ if (canonical !== null) seen.add(canonical);
56
+ }
57
+ return [...seen];
58
+ }
59
+ function verifyClaimSupport(quote, claim) {
60
+ if (quote.trim().length === 0) return { status: "not_applicable" };
61
+ const claimed = claimValues(claim);
62
+ if (claimed.length === 0) return { status: "not_applicable" };
63
+ const present = valuesInText(quote);
64
+ const matched = claimed.find((value) => present.includes(value));
65
+ if (matched !== void 0) return { status: "supported", matched };
66
+ return { status: "unsupported", claimed, present };
67
+ }
68
+ function excerpt(quote, limit = 120) {
69
+ const flat = quote.replace(/\s+/gu, " ").trim();
70
+ return flat.length <= limit ? flat : `${flat.slice(0, limit)}\u2026`;
71
+ }
72
+ function claimSupportErrorDetail(failure, quote) {
73
+ const wanted = failure.claimed.length === 1 ? failure.claimed[0] : `any of ${failure.claimed.join(", ")}`;
74
+ const carries = failure.present.length === 0 ? "that line carries no figure at all" : `the only figures on it are ${failure.present.join(", ")}`;
75
+ 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.`;
76
+ }
77
+ function foldLabel(value) {
78
+ return value.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
79
+ }
80
+ function verifyTargetLabel(quote, target, groups) {
81
+ if (quote.trim().length === 0) return { status: "not_applicable" };
82
+ const group = groups.find((candidate) => Object.hasOwn(candidate.labels, target));
83
+ if (!group) return { status: "not_applicable" };
84
+ const line = ` ${foldLabel(quote)} `;
85
+ const own = group.labels[target] ?? [];
86
+ for (const label of own) {
87
+ const folded = foldLabel(label);
88
+ if (folded.length > 0 && line.includes(folded)) return { status: "identified", label };
89
+ }
90
+ for (const [rival, labels] of Object.entries(group.labels)) {
91
+ if (rival === target) continue;
92
+ for (const label of labels) {
93
+ const folded = foldLabel(label);
94
+ if (folded.length === 0 || !line.includes(folded)) continue;
95
+ return {
96
+ status: "crossed",
97
+ rival,
98
+ rivalLabel: label,
99
+ expected: own,
100
+ ...group.note === void 0 ? {} : { note: group.note }
101
+ };
102
+ }
103
+ }
104
+ return { status: "not_applicable" };
105
+ }
106
+ function targetLabelErrorDetail(failure, target, quote) {
107
+ const ownLooks = failure.expected.length === 0 ? "" : ` A citation for ${target} should land on the line naming ${failure.expected.map((label) => JSON.stringify(label)).join(" or ")}.`;
108
+ const note = failure.note === void 0 ? "" : ` (${failure.note})`;
109
+ return `the line it cites reads "${excerpt(quote)}", which is the line for ${failure.rival} \u2014 it names ${JSON.stringify(failure.rivalLabel)}${note}.${ownLooks} Cite the line that belongs to this target, or attach this citation to ${failure.rival} instead. The figure is real; it is on the wrong line.`;
110
+ }
111
+ function indexArtifactValues(fields, normalizeTarget) {
112
+ const index = /* @__PURE__ */ new Map();
113
+ for (const [key, raw] of Object.entries(fields ?? {})) {
114
+ const value = typeof raw === "number" && Number.isFinite(raw) ? canonicalizeValue(String(raw)) : typeof raw === "string" ? canonicalizeValue(raw) : null;
115
+ if (value === null) continue;
116
+ index.set(normalizeTarget ? normalizeTarget(key) : key, value);
117
+ }
118
+ return index;
119
+ }
120
+ function verifyArtifactAgreement(target, claim, fieldValues) {
121
+ const expected = fieldValues.get(target);
122
+ if (expected === void 0) return { status: "not_applicable" };
123
+ const claimed = claimValues(claim);
124
+ if (claimed.length === 0) return { status: "not_applicable" };
125
+ if (claimed.includes(expected)) return { status: "agrees", value: expected };
126
+ for (const value of claimed) {
127
+ for (const [other, otherValue] of fieldValues) {
128
+ if (other === target || otherValue !== value) continue;
129
+ return { status: "contradicts", claimed: value, expected, belongsTo: other };
130
+ }
131
+ }
132
+ return { status: "not_applicable" };
133
+ }
134
+ function artifactAgreementErrorDetail(failure, target) {
135
+ return `the artifact reports ${failure.expected} on ${target} and ${failure.claimed} on ${failure.belongsTo}, so a citation claiming ${failure.claimed} does not support ${target} \u2014 it supports ${failure.belongsTo}. Attach this citation to ${failure.belongsTo}, or correct the artifact if ${target} really is ${failure.claimed}. The package cannot state both.`;
136
+ }
137
+
20
138
  // src/work-product/service.ts
21
139
  var WORK_PRODUCT_TRANSITIONS = {
22
140
  draft: /* @__PURE__ */ new Set(["blocked", "ready"]),
@@ -52,6 +170,29 @@ function mergeById(existing, incoming) {
52
170
  }
53
171
  return merged;
54
172
  }
173
+ function evidenceIdentity(entry) {
174
+ const whole = canonicalizeValue(entry.claim);
175
+ const claimKey = whole ?? entry.claim.normalize("NFKC").toLowerCase().replace(/\s+/gu, " ").trim();
176
+ return `${entry.target}\0${entry.sourceRef}\0${claimKey}`;
177
+ }
178
+ function mergeEvidence(existing, incoming) {
179
+ const merged = existing.slice();
180
+ for (const entry of incoming) {
181
+ const identity = evidenceIdentity(entry);
182
+ const hits = [];
183
+ for (let index = 0; index < merged.length; index += 1) {
184
+ const candidate = merged[index];
185
+ if (candidate.id === entry.id || evidenceIdentity(candidate) === identity) hits.push(index);
186
+ }
187
+ if (hits.length === 0) {
188
+ merged.push(entry);
189
+ continue;
190
+ }
191
+ merged[hits[0]] = entry;
192
+ for (const index of hits.slice(1).reverse()) merged.splice(index, 1);
193
+ }
194
+ return merged;
195
+ }
55
196
  function createWorkProductService(options) {
56
197
  const { store } = options;
57
198
  const now = options.now ?? (() => Date.now());
@@ -158,7 +299,7 @@ function createWorkProductService(options) {
158
299
  const upsertEvidence = (id, entries) => guardedMerge(
159
300
  id,
160
301
  ["draft", "blocked"],
161
- (record) => ({ evidence: mergeById(record.evidence, entries) }),
302
+ (record) => ({ evidence: mergeEvidence(record.evidence, entries) }),
162
303
  {
163
304
  step: "wp.evidence",
164
305
  message: (record) => `Evidence upserted (${entries.length} entries, ${record.evidence.length} total)`,
@@ -387,64 +528,6 @@ function workProductTrustInputs(records, verdictsFor) {
387
528
  return items;
388
529
  }
389
530
 
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
-
448
531
  // src/work-product/quote.ts
449
532
  var WHITESPACE = /[\s\p{Zs}\u2028\u2029\u200b-\u200d\ufeff]+/gu;
450
533
  var DASHES = /[\u2010-\u2015\u2212\ufe58\ufe63\uff0d]/gu;
@@ -537,6 +620,8 @@ var MAX_WORK_PRODUCT_BATCH = 50;
537
620
  var EVIDENCE_COVERAGE_CHECK = "evidence_coverage";
538
621
  var QUOTE_VERIFICATION_CHECK = "quote_verification";
539
622
  var CLAIM_SUPPORT_CHECK = "claim_support";
623
+ var TARGET_CORRECTNESS_CHECK = "target_correctness";
624
+ var ARTIFACT_AGREEMENT_CHECK = "artifact_agreement";
540
625
  async function unwrap(run, code) {
541
626
  let outcome = await run();
542
627
  if (!outcome.succeeded && outcome.conflict) outcome = await run();
@@ -704,6 +789,21 @@ function assertClaimsSupported(config, entries) {
704
789
  );
705
790
  }
706
791
  }
792
+ function assertTargetsNotCrossed(config, entries) {
793
+ const groups = config.confusableTargets;
794
+ if (!groups || groups.length === 0) return;
795
+ for (let index = 0; index < entries.length; index += 1) {
796
+ const entry = entries[index];
797
+ const quote = entry.locator.quote;
798
+ if (quote === void 0) continue;
799
+ const verdict = verifyTargetLabel(quote, entry.target, groups);
800
+ if (verdict.status !== "crossed") continue;
801
+ throw new ToolInputError(
802
+ "target_crossed",
803
+ `entries[${index}] is attached to ${entry.target} but ${targetLabelErrorDetail(verdict, entry.target, quote)}`
804
+ );
805
+ }
806
+ }
707
807
  async function summarizeQuoteVerification(config, evidence, ctx) {
708
808
  const readSourceText = config.readSourceText;
709
809
  if (!readSourceText) return void 0;
@@ -753,6 +853,38 @@ function summarizeClaimSupport(evidence) {
753
853
  }
754
854
  return { supported, checkable, unsupported };
755
855
  }
856
+ function summarizeTargetCorrectness(evidence, groups, targetOf) {
857
+ let correct = 0;
858
+ let checkable = 0;
859
+ const crossed = [];
860
+ for (const entry of evidence) {
861
+ const quote = entry.locator.quote;
862
+ if (quote === void 0) continue;
863
+ const target = targetOf(entry);
864
+ const verdict = verifyTargetLabel(quote, target, groups);
865
+ if (verdict.status === "not_applicable") continue;
866
+ checkable += 1;
867
+ if (verdict.status === "identified") correct += 1;
868
+ else crossed.push({ id: entry.id, detail: `${entry.id} (${target} cites the ${verdict.rival} line)` });
869
+ }
870
+ return { correct, checkable, crossed };
871
+ }
872
+ function summarizeArtifactAgreement(evidence, fieldValues, targetOf) {
873
+ let agreeing = 0;
874
+ let checkable = 0;
875
+ const contradicting = [];
876
+ for (const entry of evidence) {
877
+ const target = targetOf(entry);
878
+ const agreement = verifyArtifactAgreement(target, entry.claim, fieldValues);
879
+ if (agreement.status === "not_applicable") continue;
880
+ checkable += 1;
881
+ if (agreement.status === "agrees") agreeing += 1;
882
+ else {
883
+ contradicting.push({ id: entry.id, detail: `${entry.id}: ${artifactAgreementErrorDetail(agreement, target)}` });
884
+ }
885
+ }
886
+ return { agreeing, checkable, contradicting };
887
+ }
756
888
  function buildWorkProductTools(config) {
757
889
  const service = createWorkProductService({
758
890
  store: config.store,
@@ -821,6 +953,7 @@ function buildWorkProductTools(config) {
821
953
  for (let index = 0; index < raw.length; index += 1) {
822
954
  const parsed = parseEvidenceInput(raw[index], `entries[${index}]`);
823
955
  if (!parsed.ok) throw new ToolInputError("invalid_evidence", `${parsed.field}: ${parsed.error}`);
956
+ if (config.normalizeTarget) parsed.value.target = config.normalizeTarget(parsed.value.target);
824
957
  entries.push(parsed.value);
825
958
  }
826
959
  for (let index = 0; index < entries.length; index += 1) {
@@ -834,6 +967,7 @@ function buildWorkProductTools(config) {
834
967
  }
835
968
  await resolveEvidenceQuotes(config, entries, ctx);
836
969
  assertClaimsSupported(config, entries);
970
+ assertTargetsNotCrossed(config, entries);
837
971
  const draft = await resolveDraft(service, config, scopeKey, ctx);
838
972
  const record = await unwrap(() => service.upsertEvidence(draft.id, entries), "evidence_rejected");
839
973
  return {
@@ -1015,15 +1149,55 @@ function buildWorkProductTools(config) {
1015
1149
  );
1016
1150
  }
1017
1151
  }
1152
+ const targetOf = (entry) => config.normalizeTarget ? config.normalizeTarget(entry.target) : entry.target;
1153
+ if (config.confusableTargets && config.confusableTargets.length > 0) {
1154
+ const crossing = summarizeTargetCorrectness(draft.evidence, config.confusableTargets, targetOf);
1155
+ checks.unshift({
1156
+ id: TARGET_CORRECTNESS_CHECK,
1157
+ name: TARGET_CORRECTNESS_CHECK,
1158
+ passed: crossing.crossed.length === 0,
1159
+ detail: crossing.crossed.length > 0 ? `Citations attached to the wrong target: ${crossing.crossed.map((item) => item.detail).join("; ")}` : crossing.checkable === 0 ? "No citation lands on a line this product can tell apart from a sibling target \u2014 nothing to check" : `${crossing.correct}/${crossing.checkable} citations land on a line belonging to their own target`,
1160
+ source: "platform"
1161
+ });
1162
+ if (crossing.crossed.length > 0) {
1163
+ await unwrap(() => service.recordChecks(draft.id, checks), "checks_rejected");
1164
+ throw new ToolInputError(
1165
+ "target_crossed",
1166
+ `Cannot submit: ${crossing.crossed.length} evidence entr${crossing.crossed.length === 1 ? "y cites" : "ies cite"} a line belonging to a different target \u2014 ${crossing.crossed.map((item) => item.detail).join("; ")}. Re-emit each against the target whose line it actually cites, or cite the line that belongs to the target it is attached to.`
1167
+ );
1168
+ }
1169
+ }
1170
+ const artifactValues = indexArtifactValues(artifact.fields, config.normalizeTarget);
1171
+ if (config.verifyArtifactAgreement !== false && artifactValues.size > 0) {
1172
+ const agreement = summarizeArtifactAgreement(draft.evidence, artifactValues, targetOf);
1173
+ checks.unshift({
1174
+ id: ARTIFACT_AGREEMENT_CHECK,
1175
+ name: ARTIFACT_AGREEMENT_CHECK,
1176
+ passed: agreement.contradicting.length === 0,
1177
+ detail: agreement.contradicting.length > 0 ? `Evidence contradicts the artifact on: ${agreement.contradicting.map((item) => item.detail).join("; ")}` : agreement.checkable === 0 ? "No evidence claim states a figure the artifact also states \u2014 nothing to check" : `${agreement.agreeing}/${agreement.checkable} evidence claims agree with the artifact field they support`,
1178
+ source: "platform"
1179
+ });
1180
+ if (agreement.contradicting.length > 0) {
1181
+ await unwrap(() => service.recordChecks(draft.id, checks), "checks_rejected");
1182
+ throw new ToolInputError(
1183
+ "contradicts_artifact",
1184
+ `Cannot submit: ${agreement.contradicting.length} evidence entr${agreement.contradicting.length === 1 ? "y contradicts" : "ies contradict"} the artifact they support \u2014 ${agreement.contradicting.map((item) => item.detail).join("; ")}. Move each citation to the target it actually supports, or correct the artifact. The package cannot state both.`
1185
+ );
1186
+ }
1187
+ }
1018
1188
  if (config.materialTargets) {
1019
- const targets = config.materialTargets(artifact);
1020
- const covered = new Set(draft.evidence.map((entry) => entry.target));
1189
+ const targets = [
1190
+ ...new Set(
1191
+ config.materialTargets(artifact).map((target) => config.normalizeTarget ? config.normalizeTarget(target) : target)
1192
+ )
1193
+ ];
1194
+ const covered = new Set(draft.evidence.map(targetOf));
1021
1195
  const missing = targets.filter((target) => !covered.has(target));
1022
1196
  const anchoredTargets = new Set(
1023
- draft.evidence.filter((entry) => entry.locator.quoteBasis !== void 0).map((entry) => entry.target)
1197
+ draft.evidence.filter((entry) => entry.locator.quoteBasis !== void 0).map(targetOf)
1024
1198
  );
1025
1199
  const spanTargets = new Set(
1026
- draft.evidence.filter((entry) => entry.locator.quoteBasis === "span").map((entry) => entry.target)
1200
+ draft.evidence.filter((entry) => entry.locator.quoteBasis === "span").map(targetOf)
1027
1201
  );
1028
1202
  const present = targets.filter((target) => covered.has(target));
1029
1203
  const unanchored = present.filter((target) => !anchoredTargets.has(target));
@@ -1161,10 +1335,13 @@ function createWorkProductRoutes(options) {
1161
1335
  return { list, detail, verdict };
1162
1336
  }
1163
1337
  export {
1338
+ ARTIFACT_AGREEMENT_CHECK,
1164
1339
  CLAIM_SUPPORT_CHECK,
1165
1340
  EVIDENCE_COVERAGE_CHECK,
1166
1341
  MAX_WORK_PRODUCT_BATCH,
1167
1342
  QUOTE_VERIFICATION_CHECK,
1343
+ TARGET_CORRECTNESS_CHECK,
1344
+ artifactAgreementErrorDetail,
1168
1345
  buildWorkProductTools,
1169
1346
  canTransitionWorkProduct,
1170
1347
  canonicalizeValue,
@@ -1175,6 +1352,7 @@ export {
1175
1352
  createWorkProductService,
1176
1353
  finalizeWorkProductProvenance,
1177
1354
  findSourceLine,
1355
+ indexArtifactValues,
1178
1356
  isWorkProductStatus,
1179
1357
  isWorkProductTerminal,
1180
1358
  normalizeQuoteText,
@@ -1188,10 +1366,13 @@ export {
1188
1366
  sliceSourceSpan,
1189
1367
  sourceContainsQuote,
1190
1368
  stampProvenance,
1369
+ targetLabelErrorDetail,
1191
1370
  unresolvedBlockingExceptions,
1192
1371
  validateWorkProductVerdictBody,
1193
1372
  valuesInText,
1373
+ verifyArtifactAgreement,
1194
1374
  verifyClaimSupport,
1375
+ verifyTargetLabel,
1195
1376
  workProductToPersistedPart,
1196
1377
  workProductTrustInputs
1197
1378
  };