@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

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.
Files changed (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -1,7 +1,12 @@
1
- import { isValidAsciiHostname } from "../../lib/hostname.mjs";
2
- import { INCIDENT_CASE_SCHEMA_VERSION, MAX_RESPONSE_RATIONALE_LENGTH } from "../contracts/case-portability.mjs";
1
+ import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
2
+ import { INCIDENT_CASE_SCHEMA_VERSION, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
3
3
  import { enumeration, exact, text } from "../evidence/artifact-structure.mjs";
4
- export const CASE_RECHECK_CONDITIONS = Object.freeze({ unknown: 'Not established', comparable: 'Comparable for this question', different: 'Different conditions' });
4
+ import { casePinHasCompleteObservation } from "./case-evidence-quality.mjs";
5
+ export const CASE_RECHECK_CONDITIONS = Object.freeze({
6
+ unknown: 'Not established',
7
+ comparable: 'Comparable for this question',
8
+ different: 'Different conditions',
9
+ });
5
10
  function reference(value, label) {
6
11
  const id = text(value, label, 64);
7
12
  if (!/^[A-Za-z0-9_-]+$/u.test(id))
@@ -16,8 +21,11 @@ function contextFields(item) {
16
21
  const conditions = text(item.conditions, 'Recheck comparison conditions', MAX_RESPONSE_RATIONALE_LENGTH);
17
22
  if (!conditions.trim())
18
23
  throw new TypeError('Describe the conditions needed to compare the observations.');
19
- return Object.freeze({ targetHostname, conditions,
20
- baselinePinId: item.baselinePinId === null ? null : reference(item.baselinePinId, 'Recheck baseline pin') });
24
+ return Object.freeze({
25
+ targetHostname,
26
+ conditions,
27
+ baselinePinId: item.baselinePinId === null ? null : reference(item.baselinePinId, 'Recheck baseline pin'),
28
+ });
21
29
  }
22
30
  /** Optional current-format context; historical records have no invented question. */
23
31
  export function readCaseRecheckContext(value, sourceVersion) {
@@ -36,56 +44,106 @@ export function readCaseRecheckAnswerContext(value, sourceVersion) {
36
44
  const question = text(item.question, 'Recheck question', MAX_RESPONSE_RATIONALE_LENGTH);
37
45
  if (!question.trim())
38
46
  throw new TypeError('A recheck answer requires its original question.');
39
- return Object.freeze({ ...contextFields(item), questionId: reference(item.questionId, 'Recheck question ID'),
47
+ return Object.freeze({
48
+ ...contextFields(item),
49
+ questionId: reference(item.questionId, 'Recheck question ID'),
40
50
  question,
41
- conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison') });
51
+ conditionsMatch: enumeration(item.conditionsMatch, Object.keys(CASE_RECHECK_CONDITIONS), 'Recheck condition comparison'),
52
+ });
42
53
  }
43
54
  export function caseRecheckQuestions(assertions) {
44
- return assertions.filter(item => item.kind === 'next_step' && item.recheck && item.state === 'open');
55
+ return assertions.filter((item) => item.kind === 'next_step' && item.recheck && item.state === 'open');
45
56
  }
46
57
  export function caseRecheckAnswerContext(question, conditionsMatch) {
47
58
  if (question.kind !== 'next_step' || !question.recheck)
48
59
  throw new TypeError('Select a saved recheck question.');
49
- return readCaseRecheckAnswerContext({ ...question.recheck, questionId: question.id, question: question.statement, conditionsMatch });
60
+ return readCaseRecheckAnswerContext({
61
+ ...question.recheck,
62
+ questionId: question.id,
63
+ question: question.statement,
64
+ conditionsMatch,
65
+ });
66
+ }
67
+ /** Shared read projection; each form retains its draft and existing save coordinator. */
68
+ export function selectCaseRecheckQuestion(assertions, selection) {
69
+ const questions = caseRecheckQuestions(assertions);
70
+ const question = questions.find((item) => item.id === selection.questionId);
71
+ return {
72
+ questions,
73
+ context: question ? caseRecheckAnswerContext(question, selection.conditionsMatch) : undefined,
74
+ stale: Boolean(selection.questionId) && (!question || question.updatedAt !== selection.questionUpdatedAt),
75
+ };
50
76
  }
51
77
  /** A saved answer is a snapshot, not a live reference to an editable plan. */
52
78
  export function assertCurrentRecheckQuestion(answer, assertions) {
53
- const question = caseRecheckQuestions(assertions).find(item => item.id === answer.questionId);
54
- if (!question || JSON.stringify(caseRecheckAnswerContext(question, answer.conditionsMatch)) !== JSON.stringify(answer)) {
79
+ const question = caseRecheckQuestions(assertions).find((item) => item.id === answer.questionId);
80
+ if (!question ||
81
+ JSON.stringify(caseRecheckAnswerContext(question, answer.conditionsMatch)) !==
82
+ JSON.stringify(answer)) {
55
83
  throw new Error('The recheck question changed or was resolved. Refresh and review its current conditions before saving this answer.');
56
84
  }
57
85
  }
58
- export function caseRecheckComparisonWarnings(context, pins, current) {
59
- const warnings = [];
60
- if (context.conditionsMatch !== 'comparable')
61
- warnings.push(context.conditionsMatch === 'different'
62
- ? 'The comparison conditions differ.' : 'Comparable conditions have not been confirmed.');
63
- const baseline = pins.find(pin => pin.id === context.baselinePinId);
86
+ const RECHECK_COMPARISON_MESSAGES = {
87
+ different_conditions: 'The comparison conditions differ.',
88
+ unconfirmed_conditions: 'Comparable conditions have not been confirmed.',
89
+ missing_baseline: 'The baseline evidence is no longer available.',
90
+ current_target_mismatch: 'The current evidence concerns a different or unknown hostname.',
91
+ incomplete_current: 'The current source does not establish a complete observation.',
92
+ later_observation_needed: 'A later source observation is needed; reviewing the baseline again is not a recheck.',
93
+ baseline_target_mismatch: 'The baseline concerns a different hostname.',
94
+ different_field_or_source: 'The observations use different fields or sources.',
95
+ };
96
+ /** Admissibility depends on these reasons, not on presentation warnings. */
97
+ export function caseRecheckComparisonBlockers(context, pins, current) {
98
+ const blockers = [];
99
+ if (context.conditionsMatch !== 'comparable') {
100
+ blockers.push(context.conditionsMatch === 'different'
101
+ ? 'different_conditions'
102
+ : 'unconfirmed_conditions');
103
+ }
104
+ const baseline = pins.find((pin) => pin.id === context.baselinePinId);
64
105
  if (context.baselinePinId && !baseline)
65
- warnings.push('The baseline evidence is no longer available.');
106
+ blockers.push('missing_baseline');
66
107
  if (current) {
67
108
  if (current.observationHostname !== context.targetHostname)
68
- warnings.push('The current evidence concerns a different or unknown hostname.');
69
- if (current.completeness !== 'complete' || current.truncated || current.sourceState !== 'complete' && current.sourceState !== 'success' && current.sourceState !== 'reviewed' && current.sourceState !== 'not_found')
70
- warnings.push('The current source does not establish a complete observation.');
109
+ blockers.push('current_target_mismatch');
110
+ if (!casePinHasCompleteObservation(current))
111
+ blockers.push('incomplete_current');
71
112
  if (baseline) {
72
- if (baseline.id === current.id || !baseline.observedAt || !current.observedAt || Date.parse(current.observedAt) <= Date.parse(baseline.observedAt))
73
- warnings.push('A later source observation is needed; reviewing the baseline again is not a recheck.');
113
+ const sameObservation = baseline.id === current.id;
114
+ const unknownObservationTime = !baseline.observedAt || !current.observedAt;
115
+ const nonLaterObservation = !unknownObservationTime &&
116
+ Date.parse(current.observedAt) <= Date.parse(baseline.observedAt);
117
+ if (sameObservation || unknownObservationTime || nonLaterObservation) {
118
+ blockers.push('later_observation_needed');
119
+ }
74
120
  if (baseline.observationHostname && baseline.observationHostname !== context.targetHostname)
75
- warnings.push('The baseline concerns a different hostname.');
76
- if (baseline.field && current.field !== baseline.field || baseline.source !== current.source)
77
- warnings.push('The observations use different fields or sources.');
121
+ blockers.push('baseline_target_mismatch');
122
+ const differentField = Boolean(baseline.field) && current.field !== baseline.field;
123
+ const differentSource = baseline.source !== current.source;
124
+ if (differentField || differentSource)
125
+ blockers.push('different_field_or_source');
78
126
  }
79
127
  }
80
- return warnings;
128
+ return blockers;
129
+ }
130
+ export function caseRecheckComparisonWarnings(context, pins, current) {
131
+ return caseRecheckComparisonBlockers(context, pins, current)
132
+ .map((reason) => RECHECK_COMPARISON_MESSAGES[reason]);
81
133
  }
82
134
  export function assertRecheckNonReproduction(state, context, completeness, pins, current, observedAt) {
83
135
  if (state !== 'not_reproduced')
84
136
  return;
85
- const warnings = caseRecheckComparisonWarnings(context, pins, current);
86
- const baseline = pins.find(pin => pin.id === context.baselinePinId);
87
- if (!current && baseline && (!baseline.observedAt || !observedAt || !Number.isFinite(Date.parse(observedAt)) || Date.parse(observedAt) <= Date.parse(baseline.observedAt)))
88
- warnings.push('A later independent observation is needed.');
89
- if (completeness !== 'complete' || warnings.length)
137
+ const blockers = caseRecheckComparisonBlockers(context, pins, current);
138
+ const baseline = pins.find((pin) => pin.id === context.baselinePinId);
139
+ let laterIndependentObservationNeeded = false;
140
+ if (!current && baseline) {
141
+ const missingObservationTime = !baseline.observedAt || !observedAt;
142
+ const invalidObservationTime = observedAt ? !Number.isFinite(Date.parse(observedAt)) : false;
143
+ const nonLaterObservation = !missingObservationTime && Date.parse(observedAt) <= Date.parse(baseline.observedAt);
144
+ laterIndependentObservationNeeded = missingObservationTime || invalidObservationTime || nonLaterObservation;
145
+ }
146
+ if (completeness !== 'complete' || blockers.length || laterIndependentObservationNeeded) {
90
147
  throw new Error('Not reproduced requires a complete observation under comparable conditions. Record unavailable or describe the limited observation instead.');
148
+ }
91
149
  }
@@ -1,4 +1,4 @@
1
- import { ANALYST_REVIEW_REASONS } from "../../lib/analyst-taxonomy.mjs";
1
+ import { ANALYST_REVIEW_REASONS } from "../analysis/analyst-taxonomy.mjs";
2
2
  import { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, } from "../contracts/case-portability.mjs";
3
3
  import { CASE_DISPOSITIONS, CASE_STATUSES, DEFAULT_DISPOSITION, DEFAULT_STATUS, } from "./case-record-decisions.mjs";
4
4
  export { CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, PUBLIC_CASE_SCHEMA_VERSION, MAX_CASE_BRAND_PROFILE_ID_CANDIDATES, MAX_CASE_BRAND_PROFILE_IDS, MAX_CASE_IMPORT_BYTES, MAX_CASE_STORE_BYTES, MAX_CASES, MAX_DOMAIN_LENGTH, MAX_EVIDENCE_CHANGES, MAX_EVIDENCE_DETAIL_LENGTH, MAX_EVIDENCE_FACTORS, MAX_EVIDENCE_MUTATIONS, MAX_EVIDENCE_NAMESERVERS, MAX_EVIDENCE_SNAPSHOTS_PER_CASE, MAX_EVIDENCE_STRING_LENGTH, MAX_EVIDENCE_TITLE_LENGTH, MAX_NOTE_LENGTH, MAX_NOTES_PER_CASE, MAX_TAG_LENGTH, MAX_TAGS_PER_CASE, };
@@ -4,7 +4,7 @@ import { CASE_REVIEW_REASONS, CASE_SOURCES, DEFAULT_DISPOSITION, DEFAULT_SOURCE,
4
4
  import { dispositionLabel, isValidDisposition, isValidStatus, statusLabel, } from "./case-record-decisions.mjs";
5
5
  import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "../evidence/observation.mjs";
6
6
  import { PUBLISHED_V2_3_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
7
- import { canonicalRegistrableDomain } from "../../lib/registrable-domain.mjs";
7
+ import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
8
8
  // Forward-version policy (two distinct guarantees):
9
9
  // - A locally-stored envelope that declares a version greater than this is
10
10
  // still read best-effort on load (known fields kept, unknown dropped), but
@@ -1,12 +1,15 @@
1
1
  // Pure, framework-neutral analyst-case records, evidence histories, bounded
2
2
  // record normalization, and analyst updates.
3
- import { canonicalRegistrableDomain } from "../../lib/registrable-domain.mjs";
4
- import { parseCredentialFreeHttpUrl } from "../evidence/lookup-target.mjs";
3
+ import { normalizeCaseObjective, parseIncidentUrlContext, } from "./case-incident-context.mjs";
5
4
  import { selectExistingCase } from "./case-selection.mjs";
5
+ import { emptyCaseWorkflowMetadata, updateCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
6
+ import { readCaseWorkflowFields } from "./case-workflow-migration.mjs";
6
7
  import { appendCaseAction, appendCaseAssertion, appendCaseClosure, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseObservedEffectReview, appendCaseSighting, buildCaseClosureLinkContext, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseClosureHistory, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseObservedEffectHistory, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.mjs";
7
8
  import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
8
- import { PUBLISHED_V2_3_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, MAX_CASE_OBJECTIVE_LENGTH } from "../contracts/case-portability.mjs";
9
+ import { MAX_CASE_OBJECTIVE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../contracts/case-portability.mjs";
10
+ import { caseRecordVersionInput } from "./case-record-version-input.mjs";
9
11
  import { readCaseAttachments } from "./case-attachment-model.mjs";
12
+ import { readCaseEvidenceLinks, appendCaseEvidenceLink, withdrawCaseEvidenceLink } from "./case-evidence-links.mjs";
10
13
  import { assertCurrentRecheckQuestion, assertRecheckNonReproduction, readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
11
14
  import { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, } from "./case-record-decisions.mjs";
12
15
  import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, caseTimestampOrNull, deterministicId, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.mjs";
@@ -15,84 +18,7 @@ import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
15
18
  import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
16
19
  export { caseDispositionSupportsDefensiveResponse, caseStatusIsClosed, caseStatusOptionsForDirectEdit, caseStatusRequiresClosure, isReviewedCaseDisposition, };
17
20
  export { MAX_CASE_OBJECTIVE_LENGTH };
18
- export const MAX_CASE_INCIDENT_URL_LENGTH = 1_850;
19
- export const INCIDENT_CONTEXT_STATEMENT_PREFIX = 'Investigate incident URL: ';
20
- const OBJECTIVE_PREFIX = 'Objective: ';
21
- const RETENTION_SEPARATOR = ' | URL retained: ';
22
- function boundedContextText(value, maximum) {
23
- return typeof value === 'string'
24
- ? value.replace(/[\u0000-\u001f\u007f]/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maximum)
25
- : '';
26
- }
27
- export function normalizeCaseObjective(value) {
28
- return boundedContextText(value, MAX_CASE_OBJECTIVE_LENGTH);
29
- }
30
- export function parseIncidentUrlContext(value) {
31
- const parsed = parseCredentialFreeHttpUrl(value, MAX_CASE_INCIDENT_URL_LENGTH);
32
- if (!parsed)
33
- return null;
34
- const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
35
- const registrableDomain = canonicalRegistrableDomain(hostname);
36
- if (!registrableDomain)
37
- return null;
38
- const exactUrl = parsed.toString();
39
- if (exactUrl.length > MAX_CASE_INCIDENT_URL_LENGTH)
40
- return null;
41
- return Object.freeze({
42
- exactUrl,
43
- hostname,
44
- registrableDomain,
45
- originUrl: parsed.origin,
46
- hasPath: parsed.pathname !== '/',
47
- hasQuery: Boolean(parsed.search),
48
- hasFragment: Boolean(parsed.hash),
49
- });
50
- }
51
- export function caseInvestigationContext(record) {
52
- if (!record)
53
- return null;
54
- for (const assertion of [...record.assertions].reverse()) {
55
- if (assertion.kind !== 'next_step' || assertion.state !== 'open'
56
- || !assertion.statement.startsWith(INCIDENT_CONTEXT_STATEMENT_PREFIX))
57
- continue;
58
- const incidentUrl = assertion.statement.slice(INCIDENT_CONTEXT_STATEMENT_PREFIX.length);
59
- const parsed = parseIncidentUrlContext(incidentUrl);
60
- if (!parsed || parsed.registrableDomain !== record.domain)
61
- continue;
62
- const rationale = assertion.rationale ?? '';
63
- const separatorIndex = rationale.lastIndexOf(RETENTION_SEPARATOR);
64
- const objective = separatorIndex > OBJECTIVE_PREFIX.length && rationale.startsWith(OBJECTIVE_PREFIX)
65
- ? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, separatorIndex))
66
- : '';
67
- const retention = separatorIndex >= 0 ? rationale.slice(separatorIndex + RETENTION_SEPARATOR.length) : '';
68
- if (!objective || (retention !== 'exact' && retention !== 'origin_only'))
69
- continue;
70
- return Object.freeze({
71
- objective,
72
- incidentUrl: parsed.exactUrl,
73
- urlRetention: retention,
74
- assertionId: assertion.id,
75
- updatedAt: assertion.updatedAt,
76
- });
77
- }
78
- return null;
79
- }
80
- export function caseInvestigationContextAssertion(input) {
81
- const objective = normalizeCaseObjective(input.objective);
82
- if (!objective)
83
- throw new Error('Enter the investigation objective before retaining Incident context.');
84
- const parsed = parseIncidentUrlContext(input.incidentUrl);
85
- if (!parsed)
86
- throw new Error(`Enter one absolute HTTP(S) Incident URL of at most ${MAX_CASE_INCIDENT_URL_LENGTH} characters without credentials.`);
87
- const retention = input.retainExactUrl ? 'exact' : 'origin_only';
88
- const retainedUrl = input.retainExactUrl ? parsed.exactUrl : parsed.originUrl;
89
- return Object.freeze({
90
- statement: `${INCIDENT_CONTEXT_STATEMENT_PREFIX}${retainedUrl}`,
91
- rationale: `${OBJECTIVE_PREFIX}${objective}${RETENTION_SEPARATOR}${retention}`,
92
- retainedUrl,
93
- retention,
94
- });
95
- }
21
+ export * from "./case-incident-context.mjs";
96
22
  // ---------------------------------------------------------------------------
97
23
  // Case normalization
98
24
  // ---------------------------------------------------------------------------
@@ -130,31 +56,29 @@ function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, source
130
56
  */
131
57
  export function normalizeCase(raw, existing, nowIso, sourceVersion) {
132
58
  const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
133
- const record = objectRecord(raw);
134
- const domain = normalizeDomain(existing ? existing.domain : record.domain);
59
+ const input = objectRecord(raw);
60
+ const domain = normalizeDomain(existing ? existing.domain : input.domain);
135
61
  if (!domain)
136
62
  return null;
63
+ const { record, timestampOptions } = caseRecordVersionInput(input, sourceVersion);
137
64
  const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
138
65
  const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
139
- const timestampOptions = {
140
- legacyTimestamps: sourceVersion != null && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
141
- ...(sourceVersion === undefined ? {} : { sourceVersion }),
142
- };
143
66
  const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
144
67
  const pinIds = new Set(evidencePins.map((item) => item.id));
145
68
  const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
146
69
  const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
147
70
  const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
148
71
  const sightingIds = new Set(sightings.map((item) => item.id));
149
- const observedEffects = normalizeCaseObservedEffectHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
150
- const closures = normalizeCaseClosureHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
72
+ const observedEffects = normalizeCaseObservedEffectHistory(record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
73
+ const closures = normalizeCaseClosureHistory(record.closures, updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), timestampOptions, buildCaseClosureLinkContext(observedEffects, actions));
151
74
  const normalizedStatus = normalizeStatus(record.status);
152
75
  const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
153
76
  const attachments = readCaseAttachments(record.attachments);
77
+ const evidenceLinks = readCaseEvidenceLinks(record.evidenceLinks);
154
78
  return {
155
79
  id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
156
80
  domain,
157
- title: sourceVersion != null && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION ? '' : normalizeCaseObjective(record.title),
81
+ title: normalizeCaseObjective(record.title),
158
82
  status: caseStatusRequiresClosure(normalizedStatus)
159
83
  && closures.records.length === 0 && !closures.preV13HistoryUnavailable
160
84
  ? 'reviewing'
@@ -162,7 +86,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
162
86
  disposition: normalizeDisposition(record.disposition),
163
87
  reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
164
88
  brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
165
- tags: normalizeTags(record.tags),
89
+ ...readCaseWorkflowFields(record, domain, assertions, sourceVersion),
166
90
  notes: normalizeNotes(record.notes, now, sourceVersion),
167
91
  source: normalizeSource(record.source),
168
92
  evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
@@ -176,6 +100,7 @@ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
176
100
  closures,
177
101
  branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
178
102
  ...(attachments === undefined ? {} : { attachments }),
103
+ ...(evidenceLinks === undefined ? {} : { evidenceLinks }),
179
104
  createdAt,
180
105
  updatedAt,
181
106
  };
@@ -232,6 +157,7 @@ export function createCase(input, nowIso) {
232
157
  reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
233
158
  brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
234
159
  tags: normalizeTags(input.tags),
160
+ workflowMetadata: updateCaseWorkflowMetadata(emptyCaseWorkflowMetadata(), input, domain, now),
235
161
  notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
236
162
  source,
237
163
  evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
@@ -243,6 +169,7 @@ export function createCase(input, nowIso) {
243
169
  decisions: input.decision !== undefined
244
170
  ? appendCaseDecision([], input.decision, now)
245
171
  : [],
172
+ ...(input.evidenceLink === undefined ? {} : { evidenceLinks: appendCaseEvidenceLink([], input.evidenceLink, evidencePins, now) }),
246
173
  actions,
247
174
  assertions,
248
175
  manualTrail: input.trailEvent !== undefined
@@ -319,9 +246,21 @@ export function updateCase(cases, id, patch, nowIso) {
319
246
  const current = cases[index];
320
247
  if (!current)
321
248
  throw new Error('That case no longer exists.');
249
+ if (patch.evidenceLink !== undefined && patch.evidenceLinkWithdrawal !== undefined)
250
+ throw new TypeError('Record or withdraw one evidence relationship at a time.');
322
251
  if (patch.title !== undefined && patch.expectedTitle !== undefined && patch.expectedTitle !== (current.title ?? '')) {
323
252
  throw new Error('The incident title changed after this draft was started. Reload and review the current title before saving; your draft has not been applied.');
324
253
  }
254
+ for (const [value, expected, actual, label] of [
255
+ [patch.status, patch.expectedStatus, current.status, 'status'],
256
+ [patch.disposition, patch.expectedDisposition, current.disposition, 'disposition'],
257
+ [patch.reviewReasonCode, patch.expectedReviewReasonCode, current.reviewReasonCode ?? null, 'review reason'],
258
+ [patch.tags, patch.expectedTags, current.tags, 'tags'],
259
+ ]) {
260
+ if (value !== undefined && expected !== undefined && JSON.stringify(expected) !== JSON.stringify(actual)) {
261
+ throw new Error(`The Case ${label} changed after this edit was started. Reopen the Case to review the current value; your edit has not been applied.`);
262
+ }
263
+ }
325
264
  let notes = current.notes;
326
265
  if (patch.note !== undefined) {
327
266
  const body = normalizeNoteBody(patch.note);
@@ -398,6 +337,8 @@ export function updateCase(cases, id, patch, nowIso) {
398
337
  }
399
338
  const record = {
400
339
  ...current,
340
+ workflowMetadata: updateCaseWorkflowMetadata(current.workflowMetadata
341
+ ?? readCaseWorkflowFields(current, current.domain, current.assertions).workflowMetadata, patch, current.domain, now),
401
342
  title: patch.title === undefined ? current.title ?? '' : normalizeCaseObjective(patch.title),
402
343
  status: patch.closure !== undefined
403
344
  ? 'resolved'
@@ -413,6 +354,11 @@ export function updateCase(cases, id, patch, nowIso) {
413
354
  source,
414
355
  evidenceHistory,
415
356
  evidencePins,
357
+ ...((patch.evidenceLink !== undefined || patch.evidenceLinkWithdrawal !== undefined) ? {
358
+ evidenceLinks: patch.evidenceLink !== undefined
359
+ ? appendCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLink, evidencePins, now)
360
+ : withdrawCaseEvidenceLink(current.evidenceLinks ?? [], patch.evidenceLinkWithdrawal, now),
361
+ } : {}),
416
362
  decisions,
417
363
  actions,
418
364
  assertions,
@@ -537,26 +483,7 @@ export function recordCaseInvestigationContext(cases, id, input, nowIso) {
537
483
  if (!parsed || parsed.registrableDomain !== current.domain) {
538
484
  throw new Error(`The Incident URL must belong to the Case domain ${current.domain}.`);
539
485
  }
540
- const context = caseInvestigationContextAssertion(input);
541
- const existing = caseInvestigationContext(current);
542
- return updateCase(cases, id, existing
543
- ? {
544
- assertionUpdate: {
545
- id: existing.assertionId,
546
- statement: context.statement,
547
- rationale: context.rationale,
548
- state: 'open',
549
- },
550
- }
551
- : {
552
- assertion: {
553
- kind: 'next_step',
554
- statement: context.statement,
555
- rationale: context.rationale,
556
- evidenceRelations: [],
557
- state: 'open',
558
- },
559
- }, nowIso);
486
+ return updateCase(cases, id, { investigationContext: input }, nowIso);
560
487
  }
561
488
  export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
562
489
  const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
@@ -578,7 +505,8 @@ export function recordCaseRecheckOutcome(cases, id, input, nowIso) {
578
505
  collectionDepth: input.collectionDepth,
579
506
  observationHostname: input.observationHostname,
580
507
  completeness: input.completeness,
581
- truncated: false,
508
+ truncated: input.comparisonTruncated === true
509
+ || (typeof input.comparisonSummary === 'string' && input.comparisonSummary.length > MAX_RESPONSE_VALUE_LENGTH),
582
510
  limitations: input.limitations,
583
511
  },
584
512
  }, now);
@@ -1,4 +1,4 @@
1
- import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
1
+ import { PUBLIC_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
2
2
  import { caseStatusIsClosed } from "./case-record-decisions.mjs";
3
3
  const PRESERVE = Object.freeze({
4
4
  durable: 'preserve', internal: 'preserve', trusted: 'preserve', public: 'preserve',
@@ -86,6 +86,12 @@ const CASE_FIELD_RULES = Object.freeze({
86
86
  audienceExclusions: { public: { label: 'Brand Profile references', order: 2 } },
87
87
  }),
88
88
  tags: fieldRule('tags', PRESERVE, (record) => [...record.tags]),
89
+ workflowMetadata: fieldRule('workflowMetadata', PUBLIC_REDACT, (record, profile) => (record.workflowMetadata === undefined ? undefined : profile === 'public'
90
+ ? { types: [...record.workflowMetadata.types], incidentTargets: [], investigationContext: null }
91
+ : structuredClone(record.workflowMetadata)), {
92
+ nestedSensitiveFields: ['incidentTargets', 'investigationContext'],
93
+ audienceExclusions: { public: { label: 'Private incident links and investigation context', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } },
94
+ }),
89
95
  notes: fieldRule('notes', SHARED_EXCLUDE, (record, profile) => (profile === 'trusted' || profile === 'public'
90
96
  ? []
91
97
  : record.notes.map((item) => ({ ...item }))), {
@@ -97,6 +103,7 @@ const CASE_FIELD_RULES = Object.freeze({
97
103
  source: preservedField('source'),
98
104
  evidenceHistory: fieldRule('evidenceHistory', PRESERVE, (record) => structuredClone(record.evidenceHistory)),
99
105
  evidencePins: fieldRule('evidencePins', PRESERVE, (record) => structuredClone(record.evidencePins)),
106
+ evidenceLinks: fieldRule('evidenceLinks', PUBLIC_EXCLUDE, (record, profile) => (profile === 'public' ? undefined : structuredClone(record.evidenceLinks)), { audienceExclusions: { public: { label: 'Analyst evidence relationships', order: 4, sinceVersion: EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } } }),
100
107
  decisions: fieldRule('decisions', PRESERVE, (record) => structuredClone(record.decisions)),
101
108
  actions: fieldRule('actions', TRUSTED_REDACT_PUBLIC_EXCLUDE, (record, profile) => (profile === 'public'
102
109
  ? []
@@ -0,0 +1,19 @@
1
+ import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, PUBLISHED_V2_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
2
+ /** Adapt declared historical fields before ordinary current-record recovery. */
3
+ export function caseRecordVersionInput(record, sourceVersion) {
4
+ const declared = sourceVersion != null;
5
+ if (record.evidenceLinks !== undefined && declared && sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION) {
6
+ throw new TypeError('Evidence relationships require the current Case schema.');
7
+ }
8
+ return {
9
+ record: declared && sourceVersion < INCIDENT_CASE_SCHEMA_VERSION ? {
10
+ ...record,
11
+ ...(sourceVersion < PUBLISHED_V2_CASE_SCHEMA_VERSION ? { observedEffects: undefined, closures: undefined } : {}),
12
+ title: '',
13
+ } : record,
14
+ timestampOptions: {
15
+ legacyTimestamps: declared && sourceVersion < PUBLISHED_V2_3_CASE_SCHEMA_VERSION,
16
+ ...(sourceVersion === undefined ? {} : { sourceVersion }),
17
+ },
18
+ };
19
+ }