@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -0,0 +1,9 @@
1
+ // Stable case-record facade. Core validation, evidence history/comparison,
2
+ // and analyst-record operations remain framework-neutral and independently
3
+ // testable while existing consumers keep one compatibility import.
4
+ export * from "./case-record-contracts.mjs";
5
+ export * from "./case-record-core.mjs";
6
+ export * from "./case-brand-profile-references.mjs";
7
+ export * from "./case-evidence-model.mjs";
8
+ export * from "./case-record-operations.mjs";
9
+ export * from "./case-investigation-branch-model.mjs";
@@ -1,9 +1,11 @@
1
1
  // Pure, framework-neutral analyst-case records, evidence histories, bounded
2
2
  // record normalization, and analyst updates.
3
- import { appendCaseAction, appendCaseAssertion, appendCaseDecision, appendCaseEvidencePin, appendCaseEvidencePins, appendCaseManualTrailEvent, appendCaseSighting, normalizeCaseActions, normalizeCaseAssertions, normalizeCaseDecisions, normalizeCaseEvidencePins, normalizeCaseManualTrail, normalizeCaseSightings, updateCaseAction, updateCaseAssertion, } from "./case-response-model.js";
4
- import { MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.js";
5
- import { DEFAULT_EVIDENCE_SOURCE, EVIDENCE_SOURCE_SET, deterministicId, isoOrNow, makeId, normalizeDisposition, normalizeDomain, normalizeNoteBody, normalizeReviewReasonCode, normalizeNotes, normalizeSource, normalizeStatus, normalizeTags, objectRecord, safeId, } from "./case-record-core.js";
6
- import { normalizeEvidenceHistory, } from "./case-evidence-model.js";
3
+ 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";
4
+ import { CASE_SCHEMA_VERSION, MAX_CASES, MAX_NOTES_PER_CASE, } from "./case-record-contracts.mjs";
5
+ 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";
6
+ import { assertCaseBrandProfileIds, normalizeCaseBrandProfileIds, } from "./case-brand-profile-references.mjs";
7
+ import { normalizeEvidenceHistory, } from "./case-evidence-model.mjs";
8
+ import { appendCaseInvestigationBranch, caseInvestigationBranchReferences, normalizeCaseInvestigationBranches, updateCaseInvestigationBranch, } from "./case-investigation-branch-model.mjs";
7
9
  // ---------------------------------------------------------------------------
8
10
  // Case normalization
9
11
  // ---------------------------------------------------------------------------
@@ -16,10 +18,15 @@ function inferCaptureSource(caseSource) {
16
18
  }
17
19
  // Builds a case's bounded current-schema evidence history. Uses a lenient
18
20
  // local fallback timestamp so recoverable current data always loads.
19
- function normalizeCaseEvidence(record, createdAt, updatedAt, now) {
21
+ function normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion) {
20
22
  const localFallback = updatedAt || createdAt || now;
21
23
  if (Array.isArray(record.evidenceHistory)) {
22
- return normalizeEvidenceHistory(record.evidenceHistory, { source: DEFAULT_EVIDENCE_SOURCE, fallback: localFallback });
24
+ return normalizeEvidenceHistory(record.evidenceHistory, {
25
+ source: DEFAULT_EVIDENCE_SOURCE,
26
+ fallback: localFallback,
27
+ caseDomain: domain,
28
+ ...(sourceVersion === undefined ? {} : { sourceVersion }),
29
+ });
23
30
  }
24
31
  return [];
25
32
  }
@@ -34,32 +41,51 @@ function normalizeCaseEvidence(record, createdAt, updatedAt, now) {
34
41
  * @param {string} [nowIso]
35
42
  * @returns {CaseRecord | null}
36
43
  */
37
- export function normalizeCase(raw, existing, nowIso) {
38
- const now = nowIso || new Date().toISOString();
44
+ export function normalizeCase(raw, existing, nowIso, sourceVersion) {
45
+ const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
39
46
  const record = objectRecord(raw);
40
47
  const domain = normalizeDomain(existing ? existing.domain : record.domain);
41
48
  if (!domain)
42
49
  return null;
43
- const createdAt = existing ? existing.createdAt : isoOrNow(record.createdAt, now);
44
- const updatedAt = isoOrNow(record.updatedAt, now);
45
- const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt);
50
+ const createdAt = existing ? existing.createdAt : caseTimestampOrNull(record.createdAt, sourceVersion) || now;
51
+ const updatedAt = caseTimestampOrNull(record.updatedAt, sourceVersion) || createdAt;
52
+ const timestampOptions = {
53
+ legacyTimestamps: sourceVersion != null && sourceVersion < CASE_SCHEMA_VERSION,
54
+ ...(sourceVersion === undefined ? {} : { sourceVersion }),
55
+ };
56
+ const evidencePins = normalizeCaseEvidencePins(record.evidencePins, updatedAt, timestampOptions);
46
57
  const pinIds = new Set(evidencePins.map((item) => item.id));
58
+ const actions = normalizeCaseActions(record.actions, updatedAt, { ...timestampOptions, validEvidencePinIds: pinIds });
59
+ const assertions = normalizeCaseAssertions(record.assertions, updatedAt, pinIds, timestampOptions);
60
+ const sightings = normalizeCaseSightings(record.sightings, updatedAt, pinIds, timestampOptions);
61
+ const sightingIds = new Set(sightings.map((item) => item.id));
62
+ const observedEffects = normalizeCaseObservedEffectHistory(sourceVersion != null && sourceVersion < 13 ? undefined : record.observedEffects, updatedAt, pinIds, sightingIds, timestampOptions);
63
+ 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));
64
+ const normalizedStatus = normalizeStatus(record.status);
65
+ const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
47
66
  return {
48
67
  id: existing ? existing.id : safeId(record.id) || deterministicId(domain),
49
68
  domain,
50
- status: normalizeStatus(record.status),
69
+ status: normalizedStatus === 'resolved'
70
+ && closures.records.length === 0 && !closures.preV13HistoryUnavailable
71
+ ? 'reviewing'
72
+ : normalizedStatus,
51
73
  disposition: normalizeDisposition(record.disposition),
52
74
  reviewReasonCode: normalizeReviewReasonCode(record.reviewReasonCode),
75
+ brandProfileIds: normalizeCaseBrandProfileIds(record.brandProfileIds),
53
76
  tags: normalizeTags(record.tags),
54
- notes: normalizeNotes(record.notes, now),
77
+ notes: normalizeNotes(record.notes, now, sourceVersion),
55
78
  source: normalizeSource(record.source),
56
- evidenceHistory: normalizeCaseEvidence(record, createdAt, updatedAt, now),
79
+ evidenceHistory: normalizeCaseEvidence(record, domain, createdAt, updatedAt, now, sourceVersion),
57
80
  evidencePins,
58
- decisions: normalizeCaseDecisions(record.decisions, updatedAt, pinIds),
59
- actions: normalizeCaseActions(record.actions, updatedAt),
60
- assertions: normalizeCaseAssertions(record.assertions, updatedAt, pinIds),
61
- manualTrail: normalizeCaseManualTrail(record.manualTrail, updatedAt),
62
- sightings: normalizeCaseSightings(record.sightings, updatedAt, pinIds),
81
+ decisions: normalizeCaseDecisions(record.decisions, updatedAt, pinIds, timestampOptions),
82
+ actions,
83
+ assertions,
84
+ manualTrail: normalizeCaseManualTrail(record.manualTrail, updatedAt, timestampOptions),
85
+ sightings,
86
+ observedEffects,
87
+ closures,
88
+ branches: normalizeCaseInvestigationBranches(record.branches, updatedAt, branchReferences, timestampOptions),
63
89
  createdAt,
64
90
  updatedAt,
65
91
  };
@@ -70,7 +96,7 @@ export function normalizeCase(raw, existing, nowIso) {
70
96
  * @returns {CaseRecord}
71
97
  */
72
98
  export function createCase(input, nowIso) {
73
- const now = nowIso || new Date().toISOString();
99
+ const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
74
100
  const domain = normalizeDomain(input.domain);
75
101
  if (!domain)
76
102
  throw new Error('A valid domain is required to open a case.');
@@ -82,34 +108,54 @@ export function createCase(input, nowIso) {
82
108
  ? appendCaseEvidencePin([], input.evidencePin, now)
83
109
  : [];
84
110
  const pinIds = new Set(evidencePins.map((item) => item.id));
111
+ const actions = input.action !== undefined
112
+ ? appendCaseAction([], input.action, now)
113
+ : [];
114
+ const assertions = input.assertion !== undefined
115
+ ? appendCaseAssertion([], input.assertion, now)
116
+ : [];
117
+ const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
118
+ const sightings = input.sighting !== undefined
119
+ ? appendCaseSighting([], input.sighting, now, pinIds)
120
+ : [];
121
+ const observedEffects = input.observedEffectReview !== undefined
122
+ ? appendCaseObservedEffectReview(normalizeCaseObservedEffectHistory(undefined, now), input.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)))
123
+ : normalizeCaseObservedEffectHistory(undefined, now);
124
+ const closures = input.closure !== undefined
125
+ ? appendCaseClosure(normalizeCaseClosureHistory(undefined, now), input.closure, now, observedEffects, actions)
126
+ : normalizeCaseClosureHistory(undefined, now);
127
+ if (normalizeStatus(input.status) === 'resolved' && input.closure === undefined) {
128
+ throw new Error('Opening a resolved case requires a deliberate closure reason and its linked review context.');
129
+ }
85
130
  return {
86
131
  id: makeId(),
87
132
  domain,
88
- status: normalizeStatus(input.status),
133
+ status: input.closure !== undefined ? 'resolved' : normalizeStatus(input.status),
89
134
  disposition: normalizeDisposition(input.disposition),
90
135
  reviewReasonCode: normalizeReviewReasonCode(input.reviewReasonCode),
136
+ brandProfileIds: input.brandProfileIds === undefined ? [] : assertCaseBrandProfileIds(input.brandProfileIds),
91
137
  tags: normalizeTags(input.tags),
92
138
  notes: noteBody ? [{ id: makeId(), body: noteBody, createdAt: now }] : [],
93
139
  source,
94
140
  evidenceHistory: normalizeEvidenceHistory(input.evidence ? [input.evidence] : [], {
95
141
  source: inferCaptureSource(source),
96
142
  fallback: now,
143
+ caseDomain: domain,
97
144
  }),
98
145
  evidencePins,
99
146
  decisions: input.decision !== undefined
100
147
  ? appendCaseDecision([], input.decision, now)
101
148
  : [],
102
- actions: input.action !== undefined
103
- ? appendCaseAction([], input.action, now)
104
- : [],
105
- assertions: input.assertion !== undefined
106
- ? appendCaseAssertion([], input.assertion, now)
107
- : [],
149
+ actions,
150
+ assertions,
108
151
  manualTrail: input.trailEvent !== undefined
109
152
  ? appendCaseManualTrailEvent([], input.trailEvent, now)
110
153
  : [],
111
- sightings: input.sighting !== undefined
112
- ? appendCaseSighting([], input.sighting, now, pinIds)
154
+ sightings,
155
+ observedEffects,
156
+ closures,
157
+ branches: input.branch !== undefined
158
+ ? appendCaseInvestigationBranch([], input.branch, now, branchReferences)
113
159
  : [],
114
160
  createdAt: now,
115
161
  updatedAt: now,
@@ -148,7 +194,7 @@ export function openOrCreateCase(cases, input, nowIso) {
148
194
  * @returns {{ cases: CaseRecord[], record: CaseRecord }}
149
195
  */
150
196
  export function updateCase(cases, id, patch, nowIso) {
151
- const now = nowIso || new Date().toISOString();
197
+ const now = caseTimestampOrNull(nowIso) || new Date().toISOString();
152
198
  const index = cases.findIndex((item) => item.id === id);
153
199
  if (index < 0)
154
200
  throw new Error('That case no longer exists.');
@@ -168,7 +214,7 @@ export function updateCase(cases, id, patch, nowIso) {
168
214
  const source = patch.source !== undefined ? normalizeSource(patch.source) : current.source;
169
215
  let evidenceHistory = current.evidenceHistory;
170
216
  if (patch.evidence !== undefined) {
171
- evidenceHistory = normalizeEvidenceHistory([...current.evidenceHistory, ...(patch.evidence ? [patch.evidence] : [])], { source: inferCaptureSource(source), fallback: now });
217
+ evidenceHistory = normalizeEvidenceHistory([...current.evidenceHistory, ...(patch.evidence ? [patch.evidence] : [])], { source: inferCaptureSource(source), fallback: now, caseDomain: current.domain });
172
218
  }
173
219
  let evidencePins = current.evidencePins;
174
220
  if (patch.evidencePins !== undefined) {
@@ -187,7 +233,7 @@ export function updateCase(cases, id, patch, nowIso) {
187
233
  actions = appendCaseAction(current.actions, patch.action, now);
188
234
  }
189
235
  if (patch.actionUpdate !== undefined) {
190
- actions = updateCaseAction(actions, patch.actionUpdate, now);
236
+ actions = updateCaseAction(actions, patch.actionUpdate, now, pinIds);
191
237
  }
192
238
  let assertions = current.assertions;
193
239
  if (patch.assertion !== undefined) {
@@ -196,6 +242,14 @@ export function updateCase(cases, id, patch, nowIso) {
196
242
  if (patch.assertionUpdate !== undefined) {
197
243
  assertions = updateCaseAssertion(assertions, patch.assertionUpdate, now, pinIds);
198
244
  }
245
+ const branchReferences = caseInvestigationBranchReferences({ evidencePins, actions, assertions });
246
+ let branches = normalizeCaseInvestigationBranches(current.branches ?? [], current.updatedAt, branchReferences);
247
+ if (patch.branch !== undefined) {
248
+ branches = appendCaseInvestigationBranch(branches, patch.branch, now, branchReferences);
249
+ }
250
+ if (patch.branchUpdate !== undefined) {
251
+ branches = updateCaseInvestigationBranch(branches, patch.branchUpdate, now, branchReferences);
252
+ }
199
253
  let manualTrail = current.manualTrail;
200
254
  if (patch.trailEvent !== undefined) {
201
255
  manualTrail = appendCaseManualTrailEvent(current.manualTrail, patch.trailEvent, now);
@@ -204,13 +258,29 @@ export function updateCase(cases, id, patch, nowIso) {
204
258
  if (patch.sighting !== undefined) {
205
259
  sightings = appendCaseSighting(current.sightings, patch.sighting, now, pinIds);
206
260
  }
261
+ let observedEffects = normalizeCaseObservedEffectHistory(current.observedEffects, current.updatedAt, pinIds, new Set(sightings.map((item) => item.id)));
262
+ if (patch.observedEffectReview !== undefined) {
263
+ observedEffects = appendCaseObservedEffectReview(observedEffects, patch.observedEffectReview, now, pinIds, new Set(sightings.map((item) => item.id)));
264
+ }
265
+ let closures = normalizeCaseClosureHistory(current.closures, current.updatedAt, new Set(observedEffects.reviews.map((item) => item.id)), new Set(actions.map((item) => item.id)), {}, buildCaseClosureLinkContext(observedEffects, actions));
266
+ if (patch.closure !== undefined) {
267
+ closures = appendCaseClosure(closures, patch.closure, now, observedEffects, actions);
268
+ }
269
+ if (patch.status === 'resolved' && patch.closure === undefined) {
270
+ throw new Error('Resolve this case through the deliberate closure review so the reason and evidence state remain explicit.');
271
+ }
207
272
  const record = {
208
273
  ...current,
209
- status: patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
274
+ status: patch.closure !== undefined
275
+ ? 'resolved'
276
+ : patch.status !== undefined ? normalizeStatus(patch.status) : current.status,
210
277
  disposition: patch.disposition !== undefined ? normalizeDisposition(patch.disposition) : current.disposition,
211
278
  reviewReasonCode: patch.reviewReasonCode !== undefined
212
279
  ? normalizeReviewReasonCode(patch.reviewReasonCode)
213
280
  : current.reviewReasonCode ?? null,
281
+ brandProfileIds: patch.brandProfileIds !== undefined
282
+ ? assertCaseBrandProfileIds(patch.brandProfileIds)
283
+ : current.brandProfileIds,
214
284
  tags: patch.tags !== undefined ? normalizeTags(patch.tags) : current.tags,
215
285
  source,
216
286
  evidenceHistory,
@@ -220,6 +290,9 @@ export function updateCase(cases, id, patch, nowIso) {
220
290
  assertions,
221
291
  manualTrail,
222
292
  sightings,
293
+ observedEffects,
294
+ closures,
295
+ branches,
223
296
  notes,
224
297
  updatedAt: now,
225
298
  };
@@ -8,22 +8,25 @@
8
8
  // contains raw RDAP/WHOIS material. This module deliberately excludes all raw
9
9
  // registry/web responses, contacts, cookies, screenshots, and authentication
10
10
  // data. Reports contain only the normalized case record.
11
- import { caseEvidenceIncomparableReasons, compareCaseEvidence, latestCaseEvidence } from "./case-model.js";
12
- import { httpSecurityHeaderLabel } from "./http-summary.js";
13
- import { analystInteroperabilityTags } from "../../../../lib/analyst-taxonomy.mjs";
14
- import { CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.js";
11
+ import { caseEvidenceIncomparableReasons, compareCaseEvidence, latestCaseEvidence } from "./case-model.mjs";
12
+ import { httpSecurityHeaderLabel } from "./http-summary.mjs";
13
+ import { analystInteroperabilityTags } from "../../lib/analyst-taxonomy.mjs";
14
+ import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../../lib/portable-generator.mjs";
15
+ import { buildCaseResponseLifecycleSummary, CASE_EVIDENCE_RELATION_STANCES } from "./case-response-model.mjs";
16
+ import { normalizeCaseBrandProfileIds } from "./case-brand-profile-references.mjs";
17
+ import { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION, } from "../contracts/case-portability.mjs";
15
18
  // ---------------------------------------------------------------------------
16
19
  // Constants
17
20
  // ---------------------------------------------------------------------------
18
- export const CASE_REPORT_SCHEMA = 'whoisleuth.case-report';
19
- export const CASE_REPORT_SCHEMA_VERSION = 6;
20
- const APPLICATION_NAME = 'WHOISleuth';
21
+ export { CASE_REPORT_SCHEMA, CASE_REPORT_SCHEMA_VERSION };
21
22
  const LIMITATIONS_TEXT = [
22
23
  'This report contains normalised browser-local observations from WHOISleuth analyst cases.',
23
24
  'It is not a live lookup and does not contain raw WHOIS, RDAP, DNS, HTML, or responses collected during website checks.',
24
25
  'Absence of a signal (e.g. no MX record observed) does not prove nonexistence. It may not have been evaluated.',
25
26
  'Snapshot fingerprints are deduplication identifiers, not cryptographic evidence hashes.',
26
27
  'Scan-depth and scoring-model gates prevent misleading comparisons; "incomparable" means observations differ materially but one or more fields cannot be compared reliably.',
28
+ 'Brand Profile references record an explicit analyst-selected association only; they do not establish ownership, attribution, intent, safety, or maliciousness.',
29
+ 'Provider workflow outcomes and independently observed technical effects remain separately attributed. A provider outcome does not establish independent remediation, absence, or safety; neither evidence family establishes legal sufficiency or provider performance.',
27
30
  'Generated locally in the browser. Review the package before sharing it.',
28
31
  ].join(' ');
29
32
  function escapeMarkdownInline(text) {
@@ -139,6 +142,8 @@ function pickKnownSnapshotFields(snapshot) {
139
142
  idnReferenceMatch: snapshot.idnReferenceMatch ?? null,
140
143
  pageBaselineMatch: snapshot.pageBaselineMatch ?? null,
141
144
  hasActiveBrandProfile: snapshot.hasActiveBrandProfile ?? null,
145
+ profileContextState: snapshot.profileContextState ?? null,
146
+ profileContextLimitation: snapshot.profileContextLimitation ?? null,
142
147
  mutationTypes: Array.isArray(snapshot.mutationTypes) ? [...snapshot.mutationTypes] : [],
143
148
  };
144
149
  }
@@ -158,6 +163,7 @@ export function buildCaseReport(caseRecord, options = {}) {
158
163
  const { generatedAt } = options;
159
164
  const includeNotes = options.includeNotes === true;
160
165
  const now = generatedAt || new Date().toISOString();
166
+ const generator = buildPortableGeneratorMetadata(options.applicationVersion);
161
167
  // --- Build JSON report ---
162
168
  const timelineEntries = [];
163
169
  if (Array.isArray(caseRecord.evidenceHistory) && caseRecord.evidenceHistory.length > 0) {
@@ -176,7 +182,9 @@ export function buildCaseReport(caseRecord, options = {}) {
176
182
  if (!previous)
177
183
  continue;
178
184
  const rawChanges = compareCaseEvidence(previous, snapshot);
179
- incomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
185
+ const allIncomparableReasons = caseEvidenceIncomparableReasons(previous, snapshot);
186
+ const hostnameContextChanged = allIncomparableReasons.includes('observation-context');
187
+ incomparableReasons = allIncomparableReasons.filter((reason) => reason !== 'observation-context');
180
188
  if (rawChanges.length > 0) {
181
189
  changes = rawChanges.map((change) => ({
182
190
  field: change.field,
@@ -186,7 +194,9 @@ export function buildCaseReport(caseRecord, options = {}) {
186
194
  tone: change.tone,
187
195
  }));
188
196
  }
189
- else if (snapshot.fingerprint !== previous.fingerprint && incomparableReasons.length === 0) {
197
+ else if (snapshot.fingerprint !== previous.fingerprint
198
+ && incomparableReasons.length === 0
199
+ && !hostnameContextChanged) {
190
200
  incomparableReasons = ['other'];
191
201
  }
192
202
  hasIncomparableChange = incomparableReasons.length > 0;
@@ -203,17 +213,31 @@ export function buildCaseReport(caseRecord, options = {}) {
203
213
  }
204
214
  const latest = latestCaseEvidence({ evidenceHistory: caseRecord.evidenceHistory ?? undefined });
205
215
  const currentAssessment = latest ? pickKnownSnapshotFields(latest) : null;
216
+ const responseLifecycle = buildCaseResponseLifecycleSummary(caseRecord);
217
+ const observedEffects = caseRecord.observedEffects ?? {
218
+ reviews: [],
219
+ omitted: 0,
220
+ preV13HistoryUnavailable: false,
221
+ limitations: ['Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.'],
222
+ };
223
+ const closures = caseRecord.closures ?? {
224
+ records: [],
225
+ omitted: 0,
226
+ preV13HistoryUnavailable: false,
227
+ limitations: ['Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.'],
228
+ };
206
229
  const json = {
207
230
  schema: CASE_REPORT_SCHEMA,
208
231
  schemaVersion: CASE_REPORT_SCHEMA_VERSION,
209
232
  generatedAt: now,
210
- application: { name: APPLICATION_NAME },
233
+ application: generator,
211
234
  case: {
212
235
  id: caseRecord.id,
213
236
  domain: caseRecord.domain,
214
237
  status: caseRecord.status,
215
238
  disposition: caseRecord.disposition,
216
239
  reviewReasonCode: caseRecord.reviewReasonCode ?? null,
240
+ brandProfileIds: normalizeCaseBrandProfileIds(caseRecord.brandProfileIds),
217
241
  interoperabilityTags: analystInteroperabilityTags(caseRecord.disposition, caseRecord.reviewReasonCode),
218
242
  tags: Array.isArray(caseRecord.tags) ? [...caseRecord.tags] : [],
219
243
  source: caseRecord.source,
@@ -227,7 +251,12 @@ export function buildCaseReport(caseRecord, options = {}) {
227
251
  analystResponse: {
228
252
  evidencePins: caseRecord.evidencePins.map((item) => ({ ...item, limitations: [...item.limitations] })),
229
253
  decisions: caseRecord.decisions.map((item) => ({ ...item, evidencePinIds: [...item.evidencePinIds] })),
230
- actions: caseRecord.actions.map((item) => ({ ...item, contactLimitations: [...item.contactLimitations] })),
254
+ actions: caseRecord.actions.map((item) => ({
255
+ ...item,
256
+ contactLimitations: [...item.contactLimitations],
257
+ history: item.history.map((event) => ({ ...event, limitations: [...event.limitations] })),
258
+ historyLimitations: [...item.historyLimitations],
259
+ })),
231
260
  assertions: caseRecord.assertions.map((item) => ({
232
261
  ...item,
233
262
  evidencePinIds: [...item.evidencePinIds],
@@ -242,11 +271,31 @@ export function buildCaseReport(caseRecord, options = {}) {
242
271
  })),
243
272
  manualTrail: caseRecord.manualTrail.map((item) => ({ ...item })),
244
273
  sightings: caseRecord.sightings.map((item) => ({ ...item, limitations: [...item.limitations] })),
274
+ observedEffects: {
275
+ reviews: observedEffects.reviews.map((item) => ({ ...item, limitations: [...item.limitations] })),
276
+ omitted: observedEffects.omitted,
277
+ preV13HistoryUnavailable: observedEffects.preV13HistoryUnavailable,
278
+ limitations: [...observedEffects.limitations],
279
+ },
280
+ closures: {
281
+ records: closures.records.map((item) => ({ ...item, limitations: [...item.limitations] })),
282
+ omitted: closures.omitted,
283
+ preV13HistoryUnavailable: closures.preV13HistoryUnavailable,
284
+ limitations: [...closures.limitations],
285
+ },
286
+ branches: (caseRecord.branches ?? []).map((item) => ({
287
+ ...item,
288
+ evidencePinIds: [...item.evidencePinIds],
289
+ checkpointIds: [...item.checkpointIds],
290
+ assertionIds: [...item.assertionIds],
291
+ actionIds: [...item.actionIds],
292
+ })),
245
293
  },
294
+ responseLifecycle,
246
295
  limitations: LIMITATIONS_TEXT,
247
296
  };
248
297
  // --- Build Markdown ---
249
- const md = buildMarkdown(json);
298
+ const md = buildMarkdown(json, options.includeAttribution !== false);
250
299
  return { json, markdown: md };
251
300
  }
252
301
  // ---------------------------------------------------------------------------
@@ -257,7 +306,7 @@ export function buildCaseReport(caseRecord, options = {}) {
257
306
  * @param {object} report
258
307
  * @returns {string}
259
308
  */
260
- function buildMarkdown(report) {
309
+ function buildMarkdown(report, includeAttribution) {
261
310
  const lines = [];
262
311
  // Title
263
312
  const domain = escapeMarkdownInline(report.case.domain || 'unknown');
@@ -276,6 +325,7 @@ function buildMarkdown(report) {
276
325
  lines.push(`| Disposition | ${escapeMarkdownInline(report.case.disposition)} |`);
277
326
  if (report.case.reviewReasonCode)
278
327
  lines.push(`| Review reason | ${escapeMarkdownInline(report.case.reviewReasonCode.replaceAll('_', ' '))} |`);
328
+ lines.push(`| Brand Profile references | ${report.case.brandProfileIds.length ? escapeMarkdownInline(report.case.brandProfileIds.join(', ')) : 'None'} |`);
279
329
  lines.push(`| Source | ${escapeMarkdownInline(report.case.source)} |`);
280
330
  lines.push(`| Opened | ${escapeMarkdownInline(report.case.openedAt)} |`);
281
331
  lines.push(`| Updated | ${escapeMarkdownInline(report.case.updatedAt)} |`);
@@ -292,6 +342,9 @@ function buildMarkdown(report) {
292
342
  lines.push(`- **Availability:** ${escapeMarkdownInline(formatReportValue(a.availability))}`);
293
343
  lines.push(`- **Risk score:** ${escapeMarkdownInline(formatReportValue(a.riskScore))}`);
294
344
  lines.push(`- **Risk model:** ${a.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.riskModelVersion))}`}`);
345
+ lines.push(`- **Brand Profile context:** ${escapeMarkdownInline(formatReportValue(a.profileContextState))}`);
346
+ if (a.profileContextLimitation)
347
+ lines.push(`- **Profile-context limitation:** ${escapeMarkdownInline(a.profileContextLimitation)}`);
295
348
  lines.push(`- **Opportunity model:** ${a.opportunityModelVersion == null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(a.opportunityModelVersion))}`}`);
296
349
  lines.push(`- **Registrar:** ${escapeMarkdownInline(formatReportValue(a.registrar))}`);
297
350
  lines.push(`- **Website activity:** ${escapeMarkdownInline(formatReportValue(a.activityStatus))}`);
@@ -342,6 +395,9 @@ function buildMarkdown(report) {
342
395
  lines.push(`- Availability: ${escapeMarkdownInline(formatReportValue(snap.availability))}`);
343
396
  lines.push(`- Risk score: ${escapeMarkdownInline(formatReportValue(snap.riskScore))}`);
344
397
  lines.push(`- Risk model: ${snap.riskModelVersion === null ? 'Unversioned' : `v${escapeMarkdownInline(formatReportValue(snap.riskModelVersion))}`}`);
398
+ lines.push(`- Brand Profile context: ${escapeMarkdownInline(formatReportValue(snap.profileContextState))}`);
399
+ if (snap.profileContextLimitation)
400
+ lines.push(`- Profile-context limitation: ${escapeMarkdownInline(snap.profileContextLimitation)}`);
345
401
  lines.push(`- Registrar: ${escapeMarkdownInline(formatReportValue(snap.registrar))}`);
346
402
  lines.push(`- Website activity: ${escapeMarkdownInline(formatReportValue(snap.activityStatus))}`);
347
403
  if (snap.httpResponseStatus != null)
@@ -411,8 +467,8 @@ function buildMarkdown(report) {
411
467
  lines.push('## Analyst Decision Packet');
412
468
  lines.push('');
413
469
  const response = report.analystResponse;
414
- if (!response.evidencePins.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length) {
415
- lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, case actions, or manual investigation steps recorded.');
470
+ if (!response.evidencePins.length && !response.sightings.length && !response.decisions.length && !response.actions.length && !response.assertions.length && !response.manualTrail.length && !response.observedEffects.reviews.length && !response.closures.records.length && !response.branches.length) {
471
+ lines.push('No evidence pins, source-qualified sightings, structured assertions, decision records, response actions, independent effect reviews, closures, or manual investigation steps recorded.');
416
472
  lines.push('');
417
473
  }
418
474
  else {
@@ -499,16 +555,44 @@ function buildMarkdown(report) {
499
555
  for (const action of response.actions) {
500
556
  lines.push(`- **${escapeMarkdownInline(action.type.replaceAll('_', ' '))}:** ${escapeMarkdownInline(action.recipient)} (${escapeMarkdownInline(action.state)})`);
501
557
  lines.push(` Contact source: ${escapeMarkdownInline(action.contactSource)}; updated ${escapeMarkdownInline(action.updatedAt)}.`);
558
+ if (action.originActionId)
559
+ lines.push(` Originating action: ${escapeMarkdownInline(action.originActionId)}`);
502
560
  if (action.dueAt)
503
561
  lines.push(` Due: ${escapeMarkdownInline(action.dueAt)}`);
504
562
  if (action.followUpAt)
505
563
  lines.push(` Follow-up: ${escapeMarkdownInline(action.followUpAt)}`);
506
564
  if (action.reference)
507
565
  lines.push(` Reference: ${escapeMarkdownInline(action.reference)}`);
566
+ if (action.providerOutcome)
567
+ lines.push(` Latest typed provider outcome: ${escapeMarkdownInline(action.providerOutcome.replaceAll('_', ' '))}`);
508
568
  if (action.outcome)
509
- lines.push(` Outcome: ${escapeMarkdownInline(action.outcome)}`);
569
+ lines.push(` ${action.providerOutcome ? 'Provider' : 'Legacy recorded'} outcome detail: ${escapeMarkdownInline(action.outcome)}`);
510
570
  if (action.contactLimitations.length)
511
571
  lines.push(` Contact limitations: ${escapeMarkdownInline(action.contactLimitations.join('; '))}`);
572
+ for (const event of action.history) {
573
+ lines.push(` - ${escapeMarkdownInline(event.occurredAt)} · ${escapeMarkdownInline(event.previousState ?? 'none')} → ${escapeMarkdownInline(event.nextState)} · ${escapeMarkdownInline(event.sourceClass)} · ${event.applied ? 'applied' : 'retained conflict'}`);
574
+ if (event.providerOutcome)
575
+ lines.push(` Provider outcome at this time: ${escapeMarkdownInline(event.providerOutcome.replaceAll('_', ' '))}`);
576
+ if (event.reference)
577
+ lines.push(` Event reference: ${escapeMarkdownInline(event.reference)}`);
578
+ if (event.evidencePinId)
579
+ lines.push(` Evidence pin: ${escapeMarkdownInline(event.evidencePinId)}`);
580
+ if (event.limitations.length)
581
+ lines.push(` Limitations: ${escapeMarkdownInline(event.limitations.join('; '))}`);
582
+ }
583
+ if (action.historyOmitted)
584
+ lines.push(` Earlier transition events omitted by bound: ${action.historyOmitted}`);
585
+ if (action.historyLimitations.length)
586
+ lines.push(` History limitations: ${escapeMarkdownInline(action.historyLimitations.join('; '))}`);
587
+ }
588
+ lines.push('');
589
+ lines.push('### Investigation branches');
590
+ lines.push('');
591
+ if (!response.branches.length)
592
+ lines.push('No named investigation branches recorded.');
593
+ for (const branch of response.branches) {
594
+ lines.push(`- **${escapeMarkdownInline(branch.name)}** (${escapeMarkdownInline(branch.state)}; updated ${escapeMarkdownInline(branch.updatedAt)})`);
595
+ lines.push(` References: ${branch.evidencePinIds.length} evidence pins; ${branch.checkpointIds.length} checkpoints; ${branch.assertionIds.length} assertions; ${branch.actionIds.length} actions.`);
512
596
  }
513
597
  lines.push('');
514
598
  lines.push('### Explicit investigation trail');
@@ -522,6 +606,45 @@ function buildMarkdown(report) {
522
606
  }
523
607
  lines.push('');
524
608
  }
609
+ lines.push('### Independent remediation review and closure');
610
+ lines.push('');
611
+ const providerOutcome = report.responseLifecycle.latestProviderOutcome;
612
+ const observedEffect = report.responseLifecycle.latestObservedEffect;
613
+ lines.push(providerOutcome
614
+ ? `- Latest provider outcome time: ${escapeMarkdownInline(providerOutcome.occurredAt)} (${escapeMarkdownInline(providerOutcome.outcome.replaceAll('_', ' '))})`
615
+ : `- Latest provider outcome time: Withheld because the typed event state is ${escapeMarkdownInline(report.responseLifecycle.providerOutcomeState)}.`);
616
+ lines.push(report.responseLifecycle.latestObservedChangeAt
617
+ ? `- Latest independently observed change time: ${escapeMarkdownInline(report.responseLifecycle.latestObservedChangeAt)}`
618
+ : `- Latest independently observed change time: Withheld because the independent change state is ${escapeMarkdownInline(report.responseLifecycle.observedChangeState)}.`);
619
+ if (observedEffect)
620
+ lines.push(`- Latest independent review: ${escapeMarkdownInline(observedEffect.state.replaceAll('_', ' '))} at ${escapeMarkdownInline(observedEffect.observedAt)} from ${escapeMarkdownInline(observedEffect.source)}.`);
621
+ if (!response.observedEffects.reviews.length)
622
+ lines.push('- No independent observed-effect review recorded.');
623
+ for (const review of response.observedEffects.reviews) {
624
+ lines.push(`- **${escapeMarkdownInline(review.state.replaceAll('_', ' '))}** (${escapeMarkdownInline(review.observedAt)}): ${escapeMarkdownInline(review.source)}; class ${escapeMarkdownInline(review.sourceClass)}; completeness ${escapeMarkdownInline(review.completeness)}.`);
625
+ if (review.evidencePinId)
626
+ lines.push(` Evidence pin: ${escapeMarkdownInline(review.evidencePinId)}`);
627
+ if (review.sightingId)
628
+ lines.push(` Sighting: ${escapeMarkdownInline(review.sightingId)}`);
629
+ if (review.followUpAt)
630
+ lines.push(` Scheduled local follow-up: ${escapeMarkdownInline(review.followUpAt)}`);
631
+ if (review.limitations.length)
632
+ lines.push(` Limitations: ${escapeMarkdownInline(review.limitations.join('; '))}`);
633
+ }
634
+ if (response.observedEffects.omitted)
635
+ lines.push(`- Earlier independent reviews omitted by bound: ${response.observedEffects.omitted}`);
636
+ if (!response.closures.records.length)
637
+ lines.push('- No deliberate analyst closure recorded.');
638
+ for (const closure of response.closures.records) {
639
+ lines.push(`- **Closure · ${escapeMarkdownInline(closure.reason.replaceAll('_', ' '))}** (${escapeMarkdownInline(closure.createdAt)}): ${escapeMarkdownInline(closure.summary)}`);
640
+ if (closure.observedEffectReviewId)
641
+ lines.push(` Independent review: ${escapeMarkdownInline(closure.observedEffectReviewId)}`);
642
+ if (closure.actionId)
643
+ lines.push(` Provider action: ${escapeMarkdownInline(closure.actionId)}`);
644
+ if (closure.limitations.length)
645
+ lines.push(` Limitations: ${escapeMarkdownInline(closure.limitations.join('; '))}`);
646
+ }
647
+ lines.push('');
525
648
  // Analyst notes (only when included)
526
649
  if (report.case.notesIncluded && Array.isArray(report.case.notes) && report.case.notes.length > 0) {
527
650
  lines.push('## Analyst Notes');
@@ -542,6 +665,10 @@ function buildMarkdown(report) {
542
665
  lines.push('');
543
666
  lines.push(LIMITATIONS_TEXT);
544
667
  lines.push('');
668
+ if (includeAttribution) {
669
+ lines.push(portableGeneratorAttribution(report.application));
670
+ lines.push('');
671
+ }
545
672
  return lines.join('\n');
546
673
  }
547
674
  // ---------------------------------------------------------------------------