@slicedearth/whoisleuth-cli 2.3.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.
- package/NOTICE +8 -2
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -408
- package/cli/artifact-validation/case-response.mjs +65 -44
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +15 -9
- package/cli/artifact-validation/signed-review.mjs +136 -68
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +92 -24
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +40 -5
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +4 -2
- package/cli/bulk-output.mjs +29 -3
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +212 -0
- package/cli/case-pack.mjs +50 -32
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -206
- package/cli/command-definition.mjs +329 -0
- package/cli/command-reference.mjs +94 -1357
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +8 -6
- package/cli/ct-event-intake.mjs +16 -16
- package/cli/discover.mjs +5 -4
- package/cli/discovery-observation-snapshot.mjs +46 -18
- package/cli/discovery-scan-command-runner.mjs +5 -3
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +5 -5
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +151 -103
- package/cli/errors.mjs +37 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +18 -8
- package/cli/evidence-signing.mjs +5 -4
- package/cli/external-observation-mapping.mjs +9 -10
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +24 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +33 -0
- package/cli/inline-command-families.mjs +6 -46
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +107 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +8 -10
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +38 -147
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +256 -35
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/local-mmdb-review.mjs +197 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +33 -39
- package/cli/lookup-diff.mjs +47 -12
- package/cli/lookup-plan.mjs +16 -14
- package/cli/lookup-reconcile.mjs +15 -12
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +75 -21
- package/cli/manual.mjs +1 -0
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +45 -31
- package/cli/offline-evidence-review.mjs +39 -6
- package/cli/output-file.mjs +31 -5
- package/cli/posture.mjs +1 -1
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +10 -9
- package/cli/review-arguments.mjs +131 -0
- package/cli/review-command-definitions.mjs +273 -0
- package/cli/review-command-runner.mjs +104 -149
- package/cli/risk-calibration.mjs +8 -8
- package/cli/runner.mjs +74 -37
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +63 -0
- package/cli/sharing-review.mjs +8 -80
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +75 -84
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +2 -2
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-arguments.mjs +86 -0
- package/cli/workflow-command-definitions.mjs +78 -0
- package/cli/workflow-command-runner.mjs +75 -13
- package/docs/cli-reference.md +103 -30
- package/docs/cli.md +510 -25
- package/docs/contextual-reviews.md +155 -0
- package/lib/abort.mjs +4 -1
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +27 -754
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +2 -290
- package/lib/browser-library-profile.mjs +99 -130
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +89 -46
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +36 -22
- package/lib/dns-inheritance-review.mjs +207 -0
- package/lib/dns-intelligence.mjs +44 -29
- package/lib/dns-mx.mjs +1 -1
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +18 -14
- package/lib/domain-change-review.mjs +40 -16
- package/lib/domain-control-flight-recorder.mjs +301 -124
- package/lib/domain-control-manifest.mjs +126 -58
- package/lib/domain-posture-analysis.mjs +41 -11
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +120 -32
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +568 -0
- package/lib/evidence-export.mjs +12 -593
- package/lib/evidence-report.mjs +7 -2
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +50 -4
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +2742 -1034
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +87 -85
- package/lib/http-evidence-bounds.mjs +2 -7
- package/lib/http-intelligence.mjs +52 -27
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +79 -13
- package/lib/lookup-response-contract.mjs +1077 -0
- package/lib/lookup-response.mjs +11 -24
- package/lib/lookup-source-progress.mjs +24 -35
- package/lib/lookup.mjs +49 -28
- package/lib/observed-network-context.mjs +43 -27
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +87 -134
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +79 -24
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +26 -14
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +2 -36
- package/lib/registrar-standing-contract.mjs +2 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +7 -3
- package/lib/registry-capability-catalogue.mjs +29 -20
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +2 -338
- package/lib/risk-scoring.mjs +2 -345
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/security-txt.mjs +10 -7
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +35 -0
- package/lib/smtp-transport-review.mjs +75 -34
- package/lib/sslbl-certificates.generated.mjs +141 -138
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +2 -1
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +2 -36
- package/lib/typosquat-generator.mjs +29 -16
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-enrichment.mjs +388 -0
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +55 -5
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-parser.mjs +5 -3
- package/lib/whois-transport.mjs +23 -7
- package/package.json +14 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +345 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-links.mjs +135 -0
- package/packages/cases/case-evidence-model.mjs +229 -69
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +55 -0
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +98 -48
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +149 -0
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +4 -3
- package/packages/cases/case-record-operations.mjs +123 -133
- package/packages/cases/case-record-projection.mjs +30 -20
- package/packages/cases/case-record-version-input.mjs +19 -0
- package/packages/cases/case-report.mjs +114 -67
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +756 -0
- package/packages/cases/case-response-model.mjs +49 -1129
- package/packages/cases/case-response-outcomes.mjs +338 -0
- package/packages/cases/case-response-packet-render.mjs +157 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +58 -593
- package/packages/cases/case-response-records.mjs +93 -0
- package/packages/cases/case-response-review-inputs.mjs +382 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +26 -1
- package/packages/cases/case-workflow-metadata.mjs +225 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +3 -2
- package/packages/comparison/relationship-evidence.mjs +103 -14
- package/packages/comparison/relationship-provenance.mjs +99 -0
- package/packages/contracts/analyst-interchange.mjs +234 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +370 -114
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +3 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/context-review.mjs +24 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
- package/packages/contracts/domain-control-manifest.mjs +128 -74
- package/packages/contracts/domain-control-monitor.mjs +59 -45
- package/packages/contracts/domain-control-review.mjs +200 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +18 -0
- package/packages/contracts/ip-address.mjs +44 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +31 -0
- package/packages/contracts/offline-comparison.mjs +4 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +219 -238
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +108 -0
- package/packages/evidence/observation.mjs +18 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +93 -61
- package/packages/interchange/managed-indicator-export.mjs +35 -0
- package/packages/interchange/managed-indicator-set.mjs +210 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +389 -0
- package/packages/investigation/acquisition-decision-packet.mjs +1 -1
- package/packages/investigation/bulk-mail-exposure.mjs +9 -14
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +81 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +100 -0
- package/packages/investigation/domain-transition-review.mjs +47 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/intake-report.mjs +19 -0
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/link-intake.mjs +179 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +195 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +160 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +39 -31
- package/packages/workspace/bulk-session-model.mjs +112 -27
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +1 -1
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +45 -6
- package/packages/workspace/page-baseline.mjs +20 -17
- package/packages/workspace/relationship-observation-model.mjs +54 -35
- package/packages/workspace/shortlist-model.mjs +14 -5
- package/packages/workspace/watchlist-history.mjs +30 -14
- package/packages/workspace/watchlist-store.mjs +24 -6
- package/packages/workspace/website-snapshot-model.mjs +98 -33
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +90 -25
- package/third-party-notices.txt +577 -17
- package/lib/hostname.mjs +0 -38
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Independent observed effects, linked closures and lifecycle summaries.
|
|
2
|
+
import { MAX_CASE_CLOSURES, MAX_CASE_OBSERVED_EFFECT_REVIEWS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, } from "../contracts/case-portability.mjs";
|
|
3
|
+
import { latestObservationCohort, } from "../evidence/latest-observations.mjs";
|
|
4
|
+
import { readCaseRecheckAnswerContext } from "./case-recheck-model.mjs";
|
|
5
|
+
import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
|
|
6
|
+
import { CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, } from "./case-response-records.mjs";
|
|
7
|
+
import { COMPLETENESS, SAFE_ID_RE, boundedCounter, compareCodeUnits, freshId, iso, lifecycleLimitations, limitations, optionalIso, record, safeId, text, } from "./case-response-values.mjs";
|
|
8
|
+
const OBSERVED_EFFECT_STATES = new Set(CASE_OBSERVED_EFFECT_STATES);
|
|
9
|
+
const OBSERVED_EFFECT_SOURCE_CLASSES = new Set(CASE_OBSERVED_EFFECT_SOURCE_CLASSES);
|
|
10
|
+
const CLOSURE_REASONS = new Set(CASE_CLOSURE_REASONS);
|
|
11
|
+
function normalizeObservedEffectReview(raw, fallback, validPinIds, validSightingIds, options = {}) {
|
|
12
|
+
const item = record(raw);
|
|
13
|
+
if (typeof item.state !== 'string' || !OBSERVED_EFFECT_STATES.has(item.state))
|
|
14
|
+
return null;
|
|
15
|
+
if (typeof item.sourceClass !== 'string' || !OBSERVED_EFFECT_SOURCE_CLASSES.has(item.sourceClass))
|
|
16
|
+
return null;
|
|
17
|
+
const source = text(item.source, MAX_RESPONSE_LABEL_LENGTH);
|
|
18
|
+
const observedAt = optionalIso(item.observedAt, options);
|
|
19
|
+
if (!source || !observedAt)
|
|
20
|
+
return null;
|
|
21
|
+
const createdAt = iso(item.createdAt, observedAt || fallback, options);
|
|
22
|
+
const recheck = readCaseRecheckAnswerContext(item.recheck, options.sourceVersion);
|
|
23
|
+
if (recheck && item.state === 'not_reproduced' && (item.completeness !== 'complete' || recheck.conditionsMatch !== 'comparable')) {
|
|
24
|
+
throw new TypeError('A question cannot be marked not reproduced from incomplete evidence or unconfirmed comparison conditions.');
|
|
25
|
+
}
|
|
26
|
+
const evidencePinId = typeof item.evidencePinId === 'string'
|
|
27
|
+
&& SAFE_ID_RE.test(item.evidencePinId)
|
|
28
|
+
&& (!validPinIds || validPinIds.has(item.evidencePinId))
|
|
29
|
+
? item.evidencePinId
|
|
30
|
+
: null;
|
|
31
|
+
const sightingId = typeof item.sightingId === 'string'
|
|
32
|
+
&& SAFE_ID_RE.test(item.sightingId)
|
|
33
|
+
&& (!validSightingIds || validSightingIds.has(item.sightingId))
|
|
34
|
+
? item.sightingId
|
|
35
|
+
: null;
|
|
36
|
+
const linkLimitations = [
|
|
37
|
+
...(item.evidencePinId != null && !evidencePinId ? ['A malformed or dangling evidence-pin reference was omitted from this independent review.'] : []),
|
|
38
|
+
...(item.sightingId != null && !sightingId ? ['A malformed or dangling sighting reference was omitted from this independent review.'] : []),
|
|
39
|
+
];
|
|
40
|
+
return {
|
|
41
|
+
id: safeId(item.id, 'effect-review', { state: item.state, source, observedAt, createdAt }),
|
|
42
|
+
state: item.state,
|
|
43
|
+
observedAt,
|
|
44
|
+
sourceClass: item.sourceClass,
|
|
45
|
+
source,
|
|
46
|
+
completeness: typeof item.completeness === 'string' && COMPLETENESS.has(item.completeness)
|
|
47
|
+
? item.completeness
|
|
48
|
+
: 'unknown',
|
|
49
|
+
limitations: limitations([...linkLimitations, ...limitations(item.limitations)]),
|
|
50
|
+
evidencePinId,
|
|
51
|
+
sightingId,
|
|
52
|
+
followUpAt: optionalIso(item.followUpAt, options),
|
|
53
|
+
...(recheck ? { recheck } : {}),
|
|
54
|
+
createdAt,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function normalizeCaseObservedEffectHistory(raw, fallback, validPinIds, validSightingIds, options = {}) {
|
|
58
|
+
const root = record(raw);
|
|
59
|
+
const source = Array.isArray(raw) ? raw : Array.isArray(root.reviews) ? root.reviews : [];
|
|
60
|
+
const byId = new Map();
|
|
61
|
+
let invalid = 0;
|
|
62
|
+
let duplicateConflict = 0;
|
|
63
|
+
for (const item of source.slice(0, MAX_CASE_OBSERVED_EFFECT_REVIEWS * 4)) {
|
|
64
|
+
const normalized = normalizeObservedEffectReview(item, fallback, validPinIds, validSightingIds, options);
|
|
65
|
+
if (!normalized) {
|
|
66
|
+
invalid += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const existing = byId.get(normalized.id);
|
|
70
|
+
if (!existing)
|
|
71
|
+
byId.set(normalized.id, normalized);
|
|
72
|
+
else if (JSON.stringify(normalized) !== JSON.stringify(existing)) {
|
|
73
|
+
duplicateConflict += 1;
|
|
74
|
+
if (JSON.stringify(normalized) < JSON.stringify(existing))
|
|
75
|
+
byId.set(normalized.id, normalized);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const all = [...byId.values()].sort((left, right) => Date.parse(left.observedAt) - Date.parse(right.observedAt) || compareCodeUnits(left.id, right.id));
|
|
79
|
+
invalid += Math.max(0, source.length - MAX_CASE_OBSERVED_EFFECT_REVIEWS * 4);
|
|
80
|
+
const omitted = boundedCounter(root.omitted)
|
|
81
|
+
+ invalid
|
|
82
|
+
+ duplicateConflict
|
|
83
|
+
+ Math.max(0, all.length - MAX_CASE_OBSERVED_EFFECT_REVIEWS);
|
|
84
|
+
const preV13HistoryUnavailable = options.sourceVersion != null && options.sourceVersion <= 12
|
|
85
|
+
? true
|
|
86
|
+
: root.preV13HistoryUnavailable === true;
|
|
87
|
+
const retainedLimitations = limitations(root.limitations).filter((item) => !/^\d+ earlier observed-effect reviews? omitted by bounded retention\.$/u.test(item));
|
|
88
|
+
return {
|
|
89
|
+
reviews: all.slice(-MAX_CASE_OBSERVED_EFFECT_REVIEWS),
|
|
90
|
+
omitted,
|
|
91
|
+
preV13HistoryUnavailable,
|
|
92
|
+
limitations: lifecycleLimitations([
|
|
93
|
+
...(omitted ? [`${omitted} earlier observed-effect review${omitted === 1 ? '' : 's'} omitted by bounded retention.`] : []),
|
|
94
|
+
...(invalid ? [`${invalid} malformed or unlinked observed-effect review${invalid === 1 ? '' : 's'} omitted during normalisation.`] : []),
|
|
95
|
+
...(duplicateConflict ? [`${duplicateConflict} conflicting observed-effect review identit${duplicateConflict === 1 ? 'y was' : 'ies were'} reconciled deterministically.`] : []),
|
|
96
|
+
...(preV13HistoryUnavailable ? ['Migrated from a pre-v13 Case; earlier independent observed-effect review history is unavailable.'] : []),
|
|
97
|
+
'Observed-effect reviews are independent point-in-time records; provider workflow events do not create or replace them.',
|
|
98
|
+
...retainedLimitations,
|
|
99
|
+
]),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function appendCaseObservedEffectReview(current, raw, now, validPinIds, validSightingIds) {
|
|
103
|
+
const item = record(raw);
|
|
104
|
+
const created = normalizeObservedEffectReview({ ...item, id: freshId('effect-review'), createdAt: now }, now, validPinIds, validSightingIds);
|
|
105
|
+
if (!created)
|
|
106
|
+
throw new Error('An independent observed-effect review requires a valid state, time, source class, and source.');
|
|
107
|
+
return normalizeCaseObservedEffectHistory({
|
|
108
|
+
reviews: [...current.reviews, created],
|
|
109
|
+
omitted: current.omitted,
|
|
110
|
+
preV13HistoryUnavailable: current.preV13HistoryUnavailable,
|
|
111
|
+
limitations: current.limitations,
|
|
112
|
+
}, now, validPinIds, validSightingIds);
|
|
113
|
+
}
|
|
114
|
+
export function mergeCaseObservedEffectHistories(local, imported, fallback, validPinIds, validSightingIds) {
|
|
115
|
+
return normalizeCaseObservedEffectHistory({
|
|
116
|
+
reviews: [...local.reviews, ...imported.reviews],
|
|
117
|
+
omitted: Math.max(local.omitted, imported.omitted),
|
|
118
|
+
preV13HistoryUnavailable: local.preV13HistoryUnavailable || imported.preV13HistoryUnavailable,
|
|
119
|
+
limitations: [...local.limitations, ...imported.limitations],
|
|
120
|
+
}, fallback, validPinIds, validSightingIds);
|
|
121
|
+
}
|
|
122
|
+
function normalizeClosure(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
|
|
123
|
+
const item = record(raw);
|
|
124
|
+
if (typeof item.reason !== 'string' || !CLOSURE_REASONS.has(item.reason))
|
|
125
|
+
return null;
|
|
126
|
+
const reason = item.reason;
|
|
127
|
+
const summary = text(item.summary, MAX_RESPONSE_RATIONALE_LENGTH);
|
|
128
|
+
if (!summary)
|
|
129
|
+
return null;
|
|
130
|
+
const createdAt = iso(item.createdAt, fallback, options);
|
|
131
|
+
const observedEffectReviewId = typeof item.observedEffectReviewId === 'string'
|
|
132
|
+
&& SAFE_ID_RE.test(item.observedEffectReviewId)
|
|
133
|
+
&& (!validReviewIds || validReviewIds.has(item.observedEffectReviewId))
|
|
134
|
+
? item.observedEffectReviewId
|
|
135
|
+
: null;
|
|
136
|
+
const actionId = typeof item.actionId === 'string'
|
|
137
|
+
&& SAFE_ID_RE.test(item.actionId)
|
|
138
|
+
&& (!validActionIds || validActionIds.has(item.actionId))
|
|
139
|
+
? item.actionId
|
|
140
|
+
: null;
|
|
141
|
+
const linkedReview = observedEffectReviewId ? linkContext.reviewEvidence?.get(observedEffectReviewId) : undefined;
|
|
142
|
+
const reviewPredatesClosure = linkedReview !== undefined
|
|
143
|
+
&& Date.parse(linkedReview.observedAt) <= Date.parse(createdAt)
|
|
144
|
+
&& Date.parse(linkedReview.createdAt) <= Date.parse(createdAt);
|
|
145
|
+
if (linkContext.reviewEvidence && reason === 'independently_not_reproduced'
|
|
146
|
+
&& (!reviewPredatesClosure || linkedReview?.state !== 'not_reproduced'))
|
|
147
|
+
return null;
|
|
148
|
+
if (linkContext.reviewEvidence && reason === 'infrastructure_changed'
|
|
149
|
+
&& (!reviewPredatesClosure || linkedReview?.state !== 'changed'))
|
|
150
|
+
return null;
|
|
151
|
+
const linkedProviderEvents = actionId ? linkContext.providerResolutionEvents?.get(actionId) ?? [] : [];
|
|
152
|
+
if (linkContext.providerResolutionEvents && reason === 'provider_reported_resolution_not_independently_checked'
|
|
153
|
+
&& !linkedProviderEvents.some((event) => Date.parse(event.occurredAt) <= Date.parse(createdAt)))
|
|
154
|
+
return null;
|
|
155
|
+
const linkLimitations = [
|
|
156
|
+
...(item.observedEffectReviewId != null && !observedEffectReviewId ? ['A malformed or dangling observed-effect review reference was omitted from this closure.'] : []),
|
|
157
|
+
...(item.actionId != null && !actionId ? ['A malformed or dangling response-action reference was omitted from this closure.'] : []),
|
|
158
|
+
];
|
|
159
|
+
return {
|
|
160
|
+
id: safeId(item.id, 'case-closure', { reason: item.reason, summary, createdAt }),
|
|
161
|
+
reason,
|
|
162
|
+
summary,
|
|
163
|
+
observedEffectReviewId,
|
|
164
|
+
actionId,
|
|
165
|
+
limitations: limitations([...linkLimitations, ...limitations(item.limitations)]),
|
|
166
|
+
createdAt,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function buildCaseClosureLinkContext(observedEffects, actions) {
|
|
170
|
+
return {
|
|
171
|
+
reviewEvidence: new Map(observedEffects.reviews.map((review) => [review.id, {
|
|
172
|
+
state: review.state,
|
|
173
|
+
observedAt: review.observedAt,
|
|
174
|
+
createdAt: review.createdAt,
|
|
175
|
+
}])),
|
|
176
|
+
providerResolutionEvents: new Map(actions.map((action) => [action.id, action.history
|
|
177
|
+
.filter((event) => event.applied && event.providerOutcome === 'provider_reports_resolved')
|
|
178
|
+
.map((event) => ({ eventId: event.id, occurredAt: event.occurredAt }))])),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function normalizeCaseClosureHistory(raw, fallback, validReviewIds, validActionIds, options = {}, linkContext = {}) {
|
|
182
|
+
const root = record(raw);
|
|
183
|
+
const source = Array.isArray(raw) ? raw : Array.isArray(root.records) ? root.records : [];
|
|
184
|
+
const byId = new Map();
|
|
185
|
+
let invalid = 0;
|
|
186
|
+
let duplicateConflict = 0;
|
|
187
|
+
for (const item of source.slice(0, MAX_CASE_CLOSURES * 4)) {
|
|
188
|
+
const normalized = normalizeClosure(item, fallback, validReviewIds, validActionIds, options, linkContext);
|
|
189
|
+
if (!normalized) {
|
|
190
|
+
invalid += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const existing = byId.get(normalized.id);
|
|
194
|
+
if (!existing)
|
|
195
|
+
byId.set(normalized.id, normalized);
|
|
196
|
+
else if (JSON.stringify(normalized) !== JSON.stringify(existing)) {
|
|
197
|
+
duplicateConflict += 1;
|
|
198
|
+
if (JSON.stringify(normalized) < JSON.stringify(existing))
|
|
199
|
+
byId.set(normalized.id, normalized);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const all = [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt) || compareCodeUnits(left.id, right.id));
|
|
203
|
+
invalid += Math.max(0, source.length - MAX_CASE_CLOSURES * 4);
|
|
204
|
+
const omitted = boundedCounter(root.omitted)
|
|
205
|
+
+ invalid
|
|
206
|
+
+ duplicateConflict
|
|
207
|
+
+ Math.max(0, all.length - MAX_CASE_CLOSURES);
|
|
208
|
+
const preV13HistoryUnavailable = options.sourceVersion != null && options.sourceVersion <= 12
|
|
209
|
+
? true
|
|
210
|
+
: root.preV13HistoryUnavailable === true;
|
|
211
|
+
const retainedLimitations = limitations(root.limitations).filter((item) => !/^\d+ earlier closure records? omitted by bounded retention\.$/u.test(item));
|
|
212
|
+
return {
|
|
213
|
+
records: all.slice(-MAX_CASE_CLOSURES),
|
|
214
|
+
omitted,
|
|
215
|
+
preV13HistoryUnavailable,
|
|
216
|
+
limitations: lifecycleLimitations([
|
|
217
|
+
...(omitted ? [`${omitted} earlier closure record${omitted === 1 ? '' : 's'} omitted by bounded retention.`] : []),
|
|
218
|
+
...(invalid ? [`${invalid} malformed, unsupported, or unlinked closure record${invalid === 1 ? '' : 's'} omitted during normalisation.`] : []),
|
|
219
|
+
...(duplicateConflict ? [`${duplicateConflict} conflicting closure record identit${duplicateConflict === 1 ? 'y was' : 'ies were'} reconciled deterministically.`] : []),
|
|
220
|
+
...(preV13HistoryUnavailable ? ['Migrated from a pre-v13 Case; earlier deliberate closure history is unavailable.'] : []),
|
|
221
|
+
'Closure records are deliberate analyst actions and do not establish absence, safety, provider performance, or legal sufficiency.',
|
|
222
|
+
...retainedLimitations,
|
|
223
|
+
]),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** New-write policy only. Historical normalisation must not re-adjudicate a closure. */
|
|
227
|
+
export function caseClosureReviewBlocker(reason, review, now) {
|
|
228
|
+
if (reason !== 'independently_not_reproduced' && reason !== 'infrastructure_changed')
|
|
229
|
+
return null;
|
|
230
|
+
const expected = reason === 'independently_not_reproduced' ? 'not_reproduced' : 'changed';
|
|
231
|
+
const invalidLink = reason === 'independently_not_reproduced'
|
|
232
|
+
? 'This closure reason requires a linked independent not-reproduced review.'
|
|
233
|
+
: 'This closure reason requires a linked independent changed review.';
|
|
234
|
+
if (!review || review.state !== expected)
|
|
235
|
+
return invalidLink;
|
|
236
|
+
const observedAt = normalizeExplicitIsoTimestamp(review.observedAt);
|
|
237
|
+
const createdAt = normalizeExplicitIsoTimestamp(review.createdAt);
|
|
238
|
+
const closedAt = normalizeExplicitIsoTimestamp(now);
|
|
239
|
+
if (!observedAt || !createdAt || !closedAt
|
|
240
|
+
|| Date.parse(observedAt) > Date.parse(closedAt)
|
|
241
|
+
|| Date.parse(createdAt) > Date.parse(closedAt))
|
|
242
|
+
return invalidLink;
|
|
243
|
+
if (reason === 'independently_not_reproduced') {
|
|
244
|
+
if (review.completeness !== 'complete')
|
|
245
|
+
return 'Independent non-reproduction closure requires a complete observation. Keep this limited review and collect or record a complete recheck.';
|
|
246
|
+
if (review.recheck && review.recheck.conditionsMatch !== 'comparable')
|
|
247
|
+
return 'Independent non-reproduction closure requires comparable recheck conditions.';
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
export function appendCaseClosure(current, raw, now, observedEffects, actions) {
|
|
252
|
+
const item = record(raw);
|
|
253
|
+
const reason = typeof item.reason === 'string' && CLOSURE_REASONS.has(item.reason)
|
|
254
|
+
? item.reason
|
|
255
|
+
: null;
|
|
256
|
+
const review = typeof item.observedEffectReviewId === 'string'
|
|
257
|
+
? observedEffects.reviews.find((candidate) => candidate.id === item.observedEffectReviewId) ?? null
|
|
258
|
+
: null;
|
|
259
|
+
const action = typeof item.actionId === 'string'
|
|
260
|
+
? actions.find((candidate) => candidate.id === item.actionId) ?? null
|
|
261
|
+
: null;
|
|
262
|
+
const reviewBlocker = caseClosureReviewBlocker(reason, review, now);
|
|
263
|
+
if (reviewBlocker)
|
|
264
|
+
throw new Error(reviewBlocker);
|
|
265
|
+
if (reason === 'provider_reported_resolution_not_independently_checked'
|
|
266
|
+
&& (action?.providerOutcome !== 'provider_reports_resolved'
|
|
267
|
+
|| !action.history.some((event) => event.applied
|
|
268
|
+
&& event.providerOutcome === 'provider_reports_resolved'
|
|
269
|
+
&& Date.parse(event.occurredAt) <= Date.parse(now)))) {
|
|
270
|
+
throw new Error('This closure reason requires a linked typed provider-reported-resolution outcome.');
|
|
271
|
+
}
|
|
272
|
+
const linkContext = buildCaseClosureLinkContext(observedEffects, actions);
|
|
273
|
+
const created = normalizeClosure({ ...item, id: freshId('case-closure'), createdAt: now }, now, new Set(observedEffects.reviews.map((candidate) => candidate.id)), new Set(actions.map((candidate) => candidate.id)), {}, linkContext);
|
|
274
|
+
if (!created || !reason)
|
|
275
|
+
throw new Error('A deliberate closure requires a typed reason and summary.');
|
|
276
|
+
return normalizeCaseClosureHistory({
|
|
277
|
+
records: [...current.records, created],
|
|
278
|
+
omitted: current.omitted,
|
|
279
|
+
preV13HistoryUnavailable: current.preV13HistoryUnavailable,
|
|
280
|
+
limitations: current.limitations,
|
|
281
|
+
}, now, new Set(observedEffects.reviews.map((candidate) => candidate.id)), new Set(actions.map((candidate) => candidate.id)), {}, linkContext);
|
|
282
|
+
}
|
|
283
|
+
export function mergeCaseClosureHistories(local, imported, fallback, validReviewIds, validActionIds, linkContext = {}) {
|
|
284
|
+
return normalizeCaseClosureHistory({
|
|
285
|
+
records: [...local.records, ...imported.records],
|
|
286
|
+
omitted: Math.max(local.omitted, imported.omitted),
|
|
287
|
+
preV13HistoryUnavailable: local.preV13HistoryUnavailable || imported.preV13HistoryUnavailable,
|
|
288
|
+
limitations: [...local.limitations, ...imported.limitations],
|
|
289
|
+
}, fallback, validReviewIds, validActionIds, {}, linkContext);
|
|
290
|
+
}
|
|
291
|
+
export function buildCaseResponseLifecycleSummary(input) {
|
|
292
|
+
const providerEvents = (input.actions ?? []).flatMap((action) => action.history
|
|
293
|
+
.filter((event) => event.providerOutcome)
|
|
294
|
+
.map((event) => ({ action, event })));
|
|
295
|
+
const providerCohort = latestObservationCohort(providerEvents, ({ event }) => event.occurredAt);
|
|
296
|
+
const providerEventsAtLatestTime = providerCohort.latest;
|
|
297
|
+
const latestProvider = providerEventsAtLatestTime.length === 1 && !providerCohort.undated.length && providerEventsAtLatestTime[0].event.applied
|
|
298
|
+
? providerEventsAtLatestTime[0]
|
|
299
|
+
: null;
|
|
300
|
+
const providerOutcomeState = providerEvents.length === 0
|
|
301
|
+
? 'missing'
|
|
302
|
+
: latestProvider ? 'available' : 'ambiguous';
|
|
303
|
+
const reviews = input.observedEffects?.reviews ?? [];
|
|
304
|
+
const observedCohort = latestObservationCohort(reviews, (review) => review.observedAt);
|
|
305
|
+
const latestObserved = observedCohort.latest.length === 1 && !observedCohort.undated.length
|
|
306
|
+
? observedCohort.latest[0]
|
|
307
|
+
: null;
|
|
308
|
+
const changedReviews = reviews.filter((review) => review.state === 'changed');
|
|
309
|
+
const changedCohort = latestObservationCohort(changedReviews, (review) => review.observedAt);
|
|
310
|
+
const latestObservedChangeAt = changedCohort.latest.length === 1 && !changedCohort.undated.length
|
|
311
|
+
? changedCohort.observedAt
|
|
312
|
+
: null;
|
|
313
|
+
const observedChangeState = changedReviews.length === 0
|
|
314
|
+
? 'missing'
|
|
315
|
+
: latestObservedChangeAt ? 'available' : 'ambiguous';
|
|
316
|
+
const latestClosure = [...(input.closures?.records ?? [])]
|
|
317
|
+
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt) || compareCodeUnits(right.id, left.id))[0] ?? null;
|
|
318
|
+
return {
|
|
319
|
+
providerOutcomeState,
|
|
320
|
+
latestProviderOutcome: latestProvider ? {
|
|
321
|
+
actionId: latestProvider.action.id,
|
|
322
|
+
eventId: latestProvider.event.id,
|
|
323
|
+
outcome: latestProvider.event.providerOutcome,
|
|
324
|
+
occurredAt: latestProvider.event.occurredAt,
|
|
325
|
+
reference: latestProvider.event.reference,
|
|
326
|
+
} : null,
|
|
327
|
+
observedChangeState,
|
|
328
|
+
latestObservedEffect: latestObserved ? {
|
|
329
|
+
reviewId: latestObserved.id,
|
|
330
|
+
state: latestObserved.state,
|
|
331
|
+
observedAt: latestObserved.observedAt,
|
|
332
|
+
sourceClass: latestObserved.sourceClass,
|
|
333
|
+
source: latestObserved.source,
|
|
334
|
+
} : null,
|
|
335
|
+
latestObservedChangeAt,
|
|
336
|
+
latestClosure,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { RESPONSE_AUTHORISATION_CONFIRMATION_IDS, responseContactLabel as contactLabel } from "./case-response-packet-vocabulary.mjs";
|
|
2
|
+
import { escapeCaseMarkdownInline as escapeMarkdown } from "./case-markdown.mjs";
|
|
3
|
+
/** Render only the projected packet, never the source Case or private inputs. */
|
|
4
|
+
export function renderCaseResponsePacket(packet) {
|
|
5
|
+
const { profile, case: caseRecord, incident: { category, affectedParty, abusiveUrls, observedHarm, observedAt }, generatedAt: normalizedGeneratedAt, recipientRoute, escalationHistory, escalationHistoryOmitted, escalationHistoryLimitations, authorisation, readiness, selectedEvidence, artefactReferences, responseLifecycle, preflight, provenance: { limitations, evidencePinCount, decisionCount, assertionCount, observationAge: age }, integrity: { digestSha256 }, } = packet;
|
|
6
|
+
const lines = [
|
|
7
|
+
`# ${escapeMarkdown(profile.label)} packet`,
|
|
8
|
+
'',
|
|
9
|
+
`**Domain:** ${escapeMarkdown(caseRecord.domain)}`,
|
|
10
|
+
`**Category:** ${escapeMarkdown(category)}`,
|
|
11
|
+
`**Affected party:** ${escapeMarkdown(affectedParty)}`,
|
|
12
|
+
`**Observed at (UTC):** ${observedAt}`,
|
|
13
|
+
`**Generated at (UTC):** ${normalizedGeneratedAt}`,
|
|
14
|
+
`**Audience:** ${escapeMarkdown(profile.audience)}`,
|
|
15
|
+
`**Suggested subject:** ${escapeMarkdown(profile.subject)}`,
|
|
16
|
+
'',
|
|
17
|
+
'## Observed harm',
|
|
18
|
+
'',
|
|
19
|
+
escapeMarkdown(observedHarm),
|
|
20
|
+
'',
|
|
21
|
+
'## Exact abusive URLs',
|
|
22
|
+
'',
|
|
23
|
+
...abusiveUrls.map((url) => `- ${escapeMarkdown(url)}`),
|
|
24
|
+
'',
|
|
25
|
+
'## Selected response route',
|
|
26
|
+
'',
|
|
27
|
+
...(recipientRoute
|
|
28
|
+
? [
|
|
29
|
+
`### ${contactLabel(recipientRoute.kind)}`,
|
|
30
|
+
'',
|
|
31
|
+
`- Case action: ${escapeMarkdown(recipientRoute.actionId)}`,
|
|
32
|
+
`- Contact: ${escapeMarkdown(recipientRoute.contact)}`,
|
|
33
|
+
`- Source: ${escapeMarkdown(recipientRoute.source)}`,
|
|
34
|
+
`- Route observed: ${recipientRoute.observedAt ?? 'Not provided'} (${recipientRoute.freshness})`,
|
|
35
|
+
`- Route review after: ${recipientRoute.reviewAfter ?? 'Not provided'}`,
|
|
36
|
+
`- Limitations: ${recipientRoute.limitations.length ? recipientRoute.limitations.map(escapeMarkdown).join('; ') : 'None recorded'}`,
|
|
37
|
+
'',
|
|
38
|
+
]
|
|
39
|
+
: ['No profile-appropriate response route was bound to this packet.', '']),
|
|
40
|
+
'## Selected action lineage',
|
|
41
|
+
'',
|
|
42
|
+
...(escalationHistory.length
|
|
43
|
+
? escalationHistory.flatMap((action) => [
|
|
44
|
+
`- ${escapeMarkdown(action.type.replaceAll('_', ' '))} to ${escapeMarkdown(action.recipient)} · ${escapeMarkdown(action.state.replaceAll('_', ' '))} · updated ${action.updatedAt}`,
|
|
45
|
+
` - Route observed: ${action.routeObservedAt ?? 'Not provided'}`,
|
|
46
|
+
...(action.reference ? [` - Reference: ${escapeMarkdown(action.reference)}`] : []),
|
|
47
|
+
...(action.providerOutcome ? [` - Typed provider outcome: ${escapeMarkdown(action.providerOutcome.replaceAll('_', ' '))}`] : []),
|
|
48
|
+
...(action.outcomeDetail ? [` - Outcome detail: ${escapeMarkdown(action.outcomeDetail)}`] : []),
|
|
49
|
+
...(action.originActionId ? [` - Originating action: ${escapeMarkdown(action.originActionId)}`] : []),
|
|
50
|
+
...(action.amendment ? [` - Amendment of submitted packet SHA-256: ${action.amendment.packetDigestSha256}`, ` - Prepared request events: ${action.amendment.requestEventIds.map(escapeMarkdown).join(', ')}`] : []),
|
|
51
|
+
...action.transitions.flatMap((event) => [
|
|
52
|
+
` - ${event.occurredAt}: ${escapeMarkdown(event.previousState ?? 'none')} → ${escapeMarkdown(event.nextState)} · ${escapeMarkdown(event.sourceClass)} · ${escapeMarkdown(event.provenance)}${event.providerOutcome ? ` · ${escapeMarkdown(event.providerOutcome.replaceAll('_', ' '))}` : ''}${event.applied ? '' : ' · retained conflict'}`,
|
|
53
|
+
...(event.reference ? [` - Reference: ${escapeMarkdown(event.reference)}`] : []),
|
|
54
|
+
...(event.evidencePinId ? [` - Evidence pin: ${escapeMarkdown(event.evidencePinId)}`] : []),
|
|
55
|
+
...(event.evidenceRequest ? [
|
|
56
|
+
` - Requested evidence (${event.evidenceRequest.state}): ${escapeMarkdown(event.evidenceRequest.summary)}`,
|
|
57
|
+
` - Original packet SHA-256: ${event.evidenceRequest.packetDigestSha256}; deadline: ${event.evidenceRequest.dueAt ?? 'not provided'}`,
|
|
58
|
+
` - Prepared pins: ${event.evidenceRequest.evidencePinIds.map(escapeMarkdown).join(', ') || 'none'}${event.evidenceRequest.rationale ? `; ${escapeMarkdown(event.evidenceRequest.rationale)}` : ''}`,
|
|
59
|
+
] : []),
|
|
60
|
+
...(event.originActionId ? [` - Originating action: ${escapeMarkdown(event.originActionId)}`] : []),
|
|
61
|
+
...event.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
|
|
62
|
+
]),
|
|
63
|
+
...(action.historyOmitted ? [` - ${action.historyOmitted} earlier transition event${action.historyOmitted === 1 ? '' : 's'} omitted by bound.`] : []),
|
|
64
|
+
...action.historyLimitations.map((limitation) => ` - History limitation: ${escapeMarkdown(limitation)}`),
|
|
65
|
+
])
|
|
66
|
+
: ['No Case action was selected.']),
|
|
67
|
+
...(escalationHistoryOmitted ? [`- Unrelated Case actions excluded from packet: ${escalationHistoryOmitted}`] : []),
|
|
68
|
+
...escalationHistoryLimitations.map((limitation) => `- Packet history limitation: ${escapeMarkdown(limitation)}`),
|
|
69
|
+
'',
|
|
70
|
+
'## Readiness and authorisation',
|
|
71
|
+
'',
|
|
72
|
+
`- Packet state: ${authorisation.status}`,
|
|
73
|
+
`- Reviewed-input SHA-256: ${authorisation.reviewedInputDigestSha256}`,
|
|
74
|
+
`- Supplied review digest matches: ${authorisation.digestMatches ? 'yes' : 'no'}`,
|
|
75
|
+
...RESPONSE_AUTHORISATION_CONFIRMATION_IDS.map((id) => `- Confirmation ${id}: ${authorisation.confirmations[id] ? 'yes' : 'no'}`),
|
|
76
|
+
...readiness.rows.flatMap((row) => [
|
|
77
|
+
`- ${escapeMarkdown(row.label)} [${row.state}]: ${escapeMarkdown(row.detail)}`,
|
|
78
|
+
...row.limitations.map((limitation) => ` - Limitation: ${escapeMarkdown(limitation)}`),
|
|
79
|
+
]),
|
|
80
|
+
...authorisation.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
81
|
+
'',
|
|
82
|
+
'## Selected evidence and integrity references',
|
|
83
|
+
'',
|
|
84
|
+
...(selectedEvidence.length ? selectedEvidence.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · ${escapeMarkdown(item.source)}${item.observationHostname ? ` · ${escapeMarkdown(item.observationHostname)}` : ''}${item.webObservationMode ? ' · selected URL' : ''} · ${item.observedAt ?? 'Observation time unavailable'} · ${escapeMarkdown(item.completeness)}`) : ['- No evidence pin was explicitly selected.']),
|
|
85
|
+
...artefactReferences.map((item) => `- ${escapeMarkdown(item.id)} · ${escapeMarkdown(item.label)} · SHA-256 ${item.digestSha256} · captured ${item.capturedAt}`),
|
|
86
|
+
'',
|
|
87
|
+
'## Provider outcome and independent effect',
|
|
88
|
+
'',
|
|
89
|
+
responseLifecycle.latestProviderOutcome
|
|
90
|
+
? `- Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${escapeMarkdown(responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' '))})`
|
|
91
|
+
: `- Provider outcome time: Withheld because the typed event state is ${responseLifecycle.providerOutcomeState}.`,
|
|
92
|
+
responseLifecycle.latestObservedChangeAt
|
|
93
|
+
? `- Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`
|
|
94
|
+
: `- Independently observed change time: Withheld because the independent change state is ${responseLifecycle.observedChangeState}.`,
|
|
95
|
+
...(responseLifecycle.latestObservedEffect ? [`- Latest independent review: ${escapeMarkdown(responseLifecycle.latestObservedEffect.state.replaceAll('_', ' '))} · ${responseLifecycle.latestObservedEffect.observedAt} · ${escapeMarkdown(responseLifecycle.latestObservedEffect.source)}`] : []),
|
|
96
|
+
...responseLifecycle.limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
97
|
+
'',
|
|
98
|
+
'## Review and provenance',
|
|
99
|
+
'',
|
|
100
|
+
`- Preflight: ${preflight.status.replaceAll('_', ' ')} (${preflight.counts.pass} pass, ${preflight.counts.caution} caution, ${preflight.counts.block} block)`,
|
|
101
|
+
...preflight.checks.map((check) => `- ${escapeMarkdown(check.label)} [${check.state}]: ${escapeMarkdown(check.detail)}`),
|
|
102
|
+
...limitations.map((limitation) => `- ${escapeMarkdown(limitation)}`),
|
|
103
|
+
`- Case evidence pins: ${evidencePinCount}`,
|
|
104
|
+
`- Case decision records: ${decisionCount}`,
|
|
105
|
+
`- Case structured assertions: ${assertionCount}`,
|
|
106
|
+
`- Observation-age band at export: ${age.band.replaceAll('_', ' ')}`,
|
|
107
|
+
`- Canonical packet SHA-256: ${digestSha256}`,
|
|
108
|
+
'- Digest scope: canonical sorted JSON packet excluding the integrity object',
|
|
109
|
+
'',
|
|
110
|
+
'## Audience profile',
|
|
111
|
+
'',
|
|
112
|
+
...profile.checklist.map((item) => `- Checklist: ${escapeMarkdown(item)}`),
|
|
113
|
+
...profile.includedEvidence.map((item) => `- Included: ${escapeMarkdown(item)}`),
|
|
114
|
+
...profile.excludedEvidence.map((item) => `- Excluded: ${escapeMarkdown(item)}`),
|
|
115
|
+
...profile.redactions.map((item) => `- Redaction to confirm: ${escapeMarkdown(item)}`),
|
|
116
|
+
...profile.attachments.map((item) => `- Attachment expectation: ${escapeMarkdown(item)}`),
|
|
117
|
+
...profile.followUpFields.map((item) => `- Follow-up field: ${escapeMarkdown(item)}`),
|
|
118
|
+
];
|
|
119
|
+
const markdown = `${lines.join('\n').trim()}\n`;
|
|
120
|
+
const email = [
|
|
121
|
+
`Subject: ${profile.subject}`,
|
|
122
|
+
'',
|
|
123
|
+
'Hello,',
|
|
124
|
+
'',
|
|
125
|
+
`I am reporting observed ${category} activity involving ${caseRecord.domain}.`,
|
|
126
|
+
`Affected party: ${affectedParty}`,
|
|
127
|
+
`Observed at (UTC): ${observedAt}`,
|
|
128
|
+
'',
|
|
129
|
+
'Observed harm:',
|
|
130
|
+
observedHarm,
|
|
131
|
+
'',
|
|
132
|
+
'Exact URLs:',
|
|
133
|
+
...abusiveUrls.map((url) => `- ${url}`),
|
|
134
|
+
'',
|
|
135
|
+
'Selected evidence:',
|
|
136
|
+
...(selectedEvidence.length
|
|
137
|
+
? selectedEvidence.map((item) => `- ${item.label} — ${item.source}${item.observationHostname ? ` for ${item.observationHostname}` : ''}${item.webObservationMode ? ' · selected URL' : ''}, observed ${item.observedAt ?? 'time unavailable'} (${item.completeness})`)
|
|
138
|
+
: ['- No Case evidence pin was selected.']),
|
|
139
|
+
'',
|
|
140
|
+
`Reviewed packet SHA-256: ${digestSha256}`,
|
|
141
|
+
'Attach the reviewed packet (and any separately reviewed evidence files) through the recipient’s approved submission channel; this message does not embed or transmit attachments.',
|
|
142
|
+
'',
|
|
143
|
+
...(responseLifecycle.latestProviderOutcome
|
|
144
|
+
? [`Provider outcome time: ${responseLifecycle.latestProviderOutcome.occurredAt} (${responseLifecycle.latestProviderOutcome.outcome.replaceAll('_', ' ')})`]
|
|
145
|
+
: []),
|
|
146
|
+
...(responseLifecycle.latestObservedChangeAt
|
|
147
|
+
? [`Independently observed change time: ${responseLifecycle.latestObservedChangeAt}`]
|
|
148
|
+
: []),
|
|
149
|
+
'',
|
|
150
|
+
'Please review this report under the applicable abuse and acceptable-use policies.',
|
|
151
|
+
'',
|
|
152
|
+
authorisation.status === 'authorised'
|
|
153
|
+
? 'This locally prepared packet is bound to explicit review confirmations. It was not submitted automatically and does not promise any provider outcome.'
|
|
154
|
+
: 'This is an unauthorised local draft with cautions. It was not submitted automatically and does not promise any provider outcome.',
|
|
155
|
+
].join('\n');
|
|
156
|
+
return { markdown, email: `${email}\n` };
|
|
157
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Response packet vocabulary shared by construction and independent validation.
|
|
2
|
+
export const RESPONSE_CONTACT_KINDS = [
|
|
3
|
+
'registrar',
|
|
4
|
+
'registry',
|
|
5
|
+
'network_hosting',
|
|
6
|
+
'security_txt',
|
|
7
|
+
'application_platform',
|
|
8
|
+
];
|
|
9
|
+
export const RESPONSE_PACKET_PROFILE_IDS = [
|
|
10
|
+
'registrar',
|
|
11
|
+
'registry',
|
|
12
|
+
'network_hosting',
|
|
13
|
+
'security_contact',
|
|
14
|
+
'application_platform',
|
|
15
|
+
'browser_blocklist',
|
|
16
|
+
'internal_soc',
|
|
17
|
+
];
|
|
18
|
+
export const RESPONSE_READINESS_STATES = ['complete', 'partial', 'stale', 'unavailable', 'not_provided'];
|
|
19
|
+
export const RESPONSE_READINESS_ROW_IDS = [
|
|
20
|
+
'observed_behaviour',
|
|
21
|
+
'exact_url',
|
|
22
|
+
'observation_time',
|
|
23
|
+
'capture_provenance',
|
|
24
|
+
'infrastructure_responsibility',
|
|
25
|
+
'recipient_route',
|
|
26
|
+
'authority_review',
|
|
27
|
+
'selected_evidence',
|
|
28
|
+
'contradictions',
|
|
29
|
+
'source_limitations',
|
|
30
|
+
];
|
|
31
|
+
export const RESPONSE_AUTHORISATION_CONFIRMATION_IDS = [
|
|
32
|
+
'selectedEvidence',
|
|
33
|
+
'recipientScope',
|
|
34
|
+
'privacyRedactions',
|
|
35
|
+
'analystAuthority',
|
|
36
|
+
'evidenceFreshness',
|
|
37
|
+
];
|
|
38
|
+
export function responseContactLabel(value) {
|
|
39
|
+
if (value === 'network_hosting')
|
|
40
|
+
return 'Observed endpoint network registration';
|
|
41
|
+
if (value === 'security_txt')
|
|
42
|
+
return 'security.txt';
|
|
43
|
+
if (value === 'application_platform')
|
|
44
|
+
return 'Application platform';
|
|
45
|
+
if (value === 'manual')
|
|
46
|
+
return 'Manual submission';
|
|
47
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
48
|
+
}
|