@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,177 @@
|
|
|
1
|
+
import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
|
|
2
|
+
import { DOMAIN_CONTROL_REVIEW_SCHEMA } from "../packages/contracts/domain-control-review.mjs";
|
|
3
|
+
import { EXTERNAL_FINDINGS_SCHEMA } from "../packages/contracts/external-observation-interchange.mjs";
|
|
4
|
+
import { CLI_LOOKUP_BRIEF_SCHEMA } from "../packages/contracts/cli-lookup-brief.mjs";
|
|
5
|
+
export const INVESTIGATION_RECIPE_DEFINITIONS = Object.freeze({
|
|
6
|
+
'domain-triage': Object.freeze({
|
|
7
|
+
label: 'New domain triage',
|
|
8
|
+
standardInputs: Object.freeze([
|
|
9
|
+
{ stepId: 'export', input: 1, sourceStepId: 'collect' },
|
|
10
|
+
{ stepId: 'verify', input: 1, sourceStepId: 'export' },
|
|
11
|
+
]),
|
|
12
|
+
subjectRequirement: 'domain',
|
|
13
|
+
objective: 'Collect and preserve separately attributed registration, DNS, HTTP, TLS, page, and network-context evidence.',
|
|
14
|
+
limitations: Object.freeze([
|
|
15
|
+
'Collection remains analyst-triggered and source limitations remain explicit.',
|
|
16
|
+
'Disposition, reviewed response actions, monitoring, and closure continue in the saved Case workspace; this CLI recipe does not submit reports.',
|
|
17
|
+
]),
|
|
18
|
+
steps: (domain) => Object.freeze([
|
|
19
|
+
step('collect', 'Collect a Deep lookup', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Review source health and limitations before using missing fields.'),
|
|
20
|
+
step('export', 'Create a portable evidence report', 'export', ['<saved-lookup.json>'], 'offline', 'analyst_selection', 'whoisleuth.lookup-evidence', 'Reuse the collected Lookup by default, or select a different reviewed Lookup file.'),
|
|
21
|
+
step('verify', 'Verify the exported artefact', 'verify-artifact', ['<evidence.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.offline-artifact-verification', 'Keep verification distinct from a claim that the observations are correct or current.'),
|
|
22
|
+
]),
|
|
23
|
+
}),
|
|
24
|
+
'lookalike-review': Object.freeze({
|
|
25
|
+
label: 'Lookalike candidate review',
|
|
26
|
+
subjectRequirement: 'brand_or_domain',
|
|
27
|
+
objective: 'Generate a bounded candidate queue, collect only the selected scope, and retain a reviewed candidate lookup.',
|
|
28
|
+
limitations: Object.freeze([
|
|
29
|
+
'Candidate generation does not establish registration, control, intent, or maliciousness.',
|
|
30
|
+
'Official-reference collection and page comparison require analyst-selected saved evidence; use page-compare after retaining the reference and candidate observations.',
|
|
31
|
+
]),
|
|
32
|
+
steps: (subject) => Object.freeze([
|
|
33
|
+
step('generate', 'Generate candidates offline', 'discover', [subject, '--preset', 'all', '--json'], 'offline', 'none', 'whoisleuth.cli.discover', 'Review mutation families and suppressions before collection.'),
|
|
34
|
+
step('scan', 'Collect a bounded candidate queue', 'discover-scan', [subject, '--fast', '--scan-limit', '50', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.discovery-scan', 'Fast collection is a triage boundary; partial or inconclusive authority evidence remains explicit.'),
|
|
35
|
+
step('inspect', 'Deep-review one selected candidate', 'lookup', ['<selected-domain>', '--deep', '--json'], 'network', 'analyst_selection', 'whoisleuth.cli.lookup', 'Select a candidate deliberately; generation does not prove registration, control, intent, or maliciousness.'),
|
|
36
|
+
]),
|
|
37
|
+
}),
|
|
38
|
+
'owned-domain-review': Object.freeze({
|
|
39
|
+
label: 'Owned domain posture review',
|
|
40
|
+
subjectRequirement: 'domain',
|
|
41
|
+
objective: 'Review current passive posture and compare supplied observations with an analyst-authored control manifest.',
|
|
42
|
+
limitations: Object.freeze([
|
|
43
|
+
'Use only for a domain the analyst owns or is authorised to review.',
|
|
44
|
+
]),
|
|
45
|
+
steps: (domain) => Object.freeze([
|
|
46
|
+
step('posture', 'Collect bounded DNS posture', 'posture', [domain, '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.posture', 'Review mail profile and delegation evidence before interpreting missing records.'),
|
|
47
|
+
step('lookup', 'Collect supporting Deep evidence', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Retain separately attributed registration, DNS, TLS, and page observations.'),
|
|
48
|
+
step('manifest', 'Review the domain control manifest', 'domain-control', ['<review-input.json>', '--json'], 'offline', 'analyst_selection', DOMAIN_CONTROL_REVIEW_SCHEMA, 'Only complete supplied observations may produce drift.'),
|
|
49
|
+
]),
|
|
50
|
+
}),
|
|
51
|
+
'historical-comparison': Object.freeze({
|
|
52
|
+
label: 'Historical observation comparison',
|
|
53
|
+
standardInputs: Object.freeze([
|
|
54
|
+
{ stepId: 'diff', input: 2, sourceStepId: 'current' },
|
|
55
|
+
{ stepId: 'timeline', input: 3, sourceStepId: 'current' },
|
|
56
|
+
]),
|
|
57
|
+
subjectRequirement: 'domain',
|
|
58
|
+
objective: 'Collect a current observation and compare it with analyst-selected saved observations without merging source states.',
|
|
59
|
+
limitations: Object.freeze([
|
|
60
|
+
'A later observation does not retroactively refresh retained evidence.',
|
|
61
|
+
]),
|
|
62
|
+
steps: (domain) => Object.freeze([
|
|
63
|
+
step('current', 'Collect the current lookup', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'A current request does not refresh or validate older provider-reported history.'),
|
|
64
|
+
step('diff', 'Compare two selected observations', 'diff', ['<previous.json>', '<current.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.lookup-diff', 'Equal, different, conflicting, and unavailable evidence remain separate.'),
|
|
65
|
+
step('timeline', 'Build a bounded local timeline', 'timeline', ['<oldest.json>', '<newer.json>', '<current.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.lookup-timeline', 'Choose two to twenty same-domain files in chronological scope.'),
|
|
66
|
+
]),
|
|
67
|
+
}),
|
|
68
|
+
'campaign-review': Object.freeze({
|
|
69
|
+
label: 'Campaign candidate review',
|
|
70
|
+
subjectRequirement: 'brand_or_domain',
|
|
71
|
+
objective: 'Prepare a bounded candidate set, collect a deliberately selected queue, and review retained evidence without asserting campaign attribution.',
|
|
72
|
+
limitations: Object.freeze([
|
|
73
|
+
'Grouping candidates is analyst triage and does not prove common ownership, control, infrastructure, or intent.',
|
|
74
|
+
]),
|
|
75
|
+
steps: (subject) => Object.freeze([
|
|
76
|
+
step('prepare', 'Prepare candidates offline', 'discover', [subject, '--preset', 'all', '--json'], 'offline', 'none', 'whoisleuth.cli.discover', 'Review mutation families and bounded omissions before selecting a collection scope.'),
|
|
77
|
+
step('collect', 'Collect the selected candidate queue', 'discover-scan', [subject, '--fast', '--scan-limit', '50', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.discovery-scan', 'Treat partial and inconclusive authority results as explicit outcomes.'),
|
|
78
|
+
step('review', 'Prepare a selected candidate brief', 'brief', ['<saved-lookup.json>', '--json'], 'offline', 'analyst_selection', CLI_LOOKUP_BRIEF_SCHEMA, 'Select a saved candidate Lookup; keep any campaign grouping analyst-authored.'),
|
|
79
|
+
]),
|
|
80
|
+
}),
|
|
81
|
+
'certificate-anomaly': Object.freeze({
|
|
82
|
+
label: 'Certificate anomaly review',
|
|
83
|
+
subjectRequirement: 'domain',
|
|
84
|
+
objective: 'Review bounded certificate observations alongside current source-qualified domain evidence without treating issuance as proof of control or intent.',
|
|
85
|
+
limitations: Object.freeze([
|
|
86
|
+
'Certificate observations are separately attributed and do not establish current service control.',
|
|
87
|
+
]),
|
|
88
|
+
steps: (domain) => Object.freeze([
|
|
89
|
+
step('search', 'Collect bounded certificate observations', 'ct-search', [domain, '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.ct-search', 'Review source availability, truncation, and observation timing.'),
|
|
90
|
+
step('intake', 'Normalise the selected observations', 'ct-intake', ['<certificate-events.json>', '--json'], 'offline', 'analyst_selection', EXTERNAL_FINDINGS_SCHEMA, 'Select a certificate-event batch; a certificate-search report is not interchangeable with that input.'),
|
|
91
|
+
step('corroborate', 'Collect supporting domain evidence', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Compare evidence families without collapsing certificate and registration identities.'),
|
|
92
|
+
]),
|
|
93
|
+
}),
|
|
94
|
+
'registry-disagreement': Object.freeze({
|
|
95
|
+
label: 'Registry disagreement review',
|
|
96
|
+
standardInputs: Object.freeze([
|
|
97
|
+
{ stepId: 'compare', input: 1, sourceStepId: 'collect' },
|
|
98
|
+
{ stepId: 'report', input: 1, sourceStepId: 'collect' },
|
|
99
|
+
]),
|
|
100
|
+
subjectRequirement: 'domain',
|
|
101
|
+
objective: 'Collect separately attributed registration evidence and review conflicting publications without selecting an arbitrary source as truth.',
|
|
102
|
+
limitations: Object.freeze([
|
|
103
|
+
'Only authority-aware registration evidence may decide availability; disagreement remains explicit.',
|
|
104
|
+
]),
|
|
105
|
+
steps: (domain) => Object.freeze([
|
|
106
|
+
step('collect', 'Collect source-qualified registration evidence', 'lookup', [domain, '--deep', '--json'], 'network', 'network_disclosure', 'whoisleuth.cli.lookup', 'Retain RDAP, registrar RDAP, WHOIS, and authority states separately.'),
|
|
107
|
+
step('compare', 'Compare registry publications offline', 'compare', ['<saved-lookup.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.compare', 'Do not convert conflicting or unavailable publications into equivalence.'),
|
|
108
|
+
step('report', 'Prepare a target-free source report', 'source-report', ['<saved-lookup.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.source-reliability-report', 'The report describes source behaviour, not ownership, safety, or legal status.'),
|
|
109
|
+
]),
|
|
110
|
+
}),
|
|
111
|
+
'evidence-handoff': Object.freeze({
|
|
112
|
+
label: 'Reviewed evidence handoff',
|
|
113
|
+
standardInputs: Object.freeze([{ stepId: 'lint', input: 1, sourceStepId: 'package' }]),
|
|
114
|
+
subjectRequirement: 'review_label',
|
|
115
|
+
objective: 'Verify, minimise, and package analyst-selected evidence for a deliberate handoff without transmitting or submitting it.',
|
|
116
|
+
limitations: Object.freeze([
|
|
117
|
+
'The recipe prepares local material only; sharing remains a separate deliberate action.',
|
|
118
|
+
]),
|
|
119
|
+
steps: () => Object.freeze([
|
|
120
|
+
step('verify', 'Verify the selected artefact', 'verify-artifact', ['<evidence.json>', '--json', '--strict-exit'], 'offline', 'analyst_selection', 'whoisleuth.offline-artifact-verification', 'Verification checks structure and integrity, not the truth or currency of observations.'),
|
|
121
|
+
step('package', 'Build a reviewed public Case-pack', 'case-pack', ['<cases.json>', '--audience', 'public', '--reviewed', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.case-pack', 'Review minimisation and audience projection before retaining the separate package.'),
|
|
122
|
+
step('lint', 'Review deliberate-sharing metadata', 'sharing-review', [
|
|
123
|
+
'<package.json>',
|
|
124
|
+
'--marking',
|
|
125
|
+
'clear',
|
|
126
|
+
'--recipient-scope',
|
|
127
|
+
'public',
|
|
128
|
+
'--purpose',
|
|
129
|
+
'reviewed evidence handoff',
|
|
130
|
+
'--human-reviewed',
|
|
131
|
+
'--personal-data-reviewed',
|
|
132
|
+
'--redactions-confirmed',
|
|
133
|
+
'--json',
|
|
134
|
+
], 'offline', 'analyst_selection', 'whoisleuth.cli.sharing-review', 'A clear lint result does not send, upload, publish, or authorise the artefact.'),
|
|
135
|
+
]),
|
|
136
|
+
}),
|
|
137
|
+
'planned-domain-change': Object.freeze({
|
|
138
|
+
label: 'Planned domain change',
|
|
139
|
+
subjectRequirement: 'domain',
|
|
140
|
+
objective: 'Review an analyst-authored desired state and prepare bounded change material without changing DNS, registry, mail, or hosted configuration.',
|
|
141
|
+
limitations: Object.freeze([
|
|
142
|
+
'Planning and packaging never apply, submit, schedule, or enforce a change.',
|
|
143
|
+
]),
|
|
144
|
+
steps: () => Object.freeze([
|
|
145
|
+
step('control', 'Review the desired-state manifest', 'domain-control', ['<review-input.json>', '--json'], 'offline', 'analyst_selection', DOMAIN_CONTROL_REVIEW_SCHEMA, 'Only supplied complete observations may produce drift.'),
|
|
146
|
+
step('assure', 'Review change and recovery assumptions', 'assurance', ['<assurance-input.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.domain-assurance', 'Record uncertainty, rollback dependencies, and unavailable evidence explicitly.'),
|
|
147
|
+
step('package', 'Build a reviewed change packet', 'change-packet', ['<change-packet-input.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.domain-change-packet', 'The packet is local reviewed material and performs no submission or enforcement.'),
|
|
148
|
+
]),
|
|
149
|
+
}),
|
|
150
|
+
'post-change-verification': Object.freeze({
|
|
151
|
+
label: 'Post-change verification',
|
|
152
|
+
subjectRequirement: 'domain',
|
|
153
|
+
objective: 'Perform one explicit later observation and compare it with analyst-selected retained evidence after an authorised change.',
|
|
154
|
+
limitations: Object.freeze([
|
|
155
|
+
'This is a one-time recheck, not monitoring setup or proof that every resolver or service has converged.',
|
|
156
|
+
]),
|
|
157
|
+
steps: () => Object.freeze([
|
|
158
|
+
step('recheck', 'Run one bounded retained-manifest review', 'monitor-once', ['<manifest.json>', '--limit', '1', '--json'], 'network', 'network_disclosure', CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, 'One later observation may remain partial, unavailable, stale, or conflicting.'),
|
|
159
|
+
step('compare', 'Compare selected before and after evidence', 'diff', ['<before.json>', '<after.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.cli.lookup-diff', 'Materiality is derived from compatible retained evidence and does not infer intent.'),
|
|
160
|
+
step('record', 'Record reviewed completion material', 'change-packet', ['<post-change-input.json>', '--json'], 'offline', 'analyst_selection', 'whoisleuth.domain-change-packet', 'Recording reviewed material does not submit it or start automatic monitoring.'),
|
|
161
|
+
]),
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
export const INVESTIGATION_PLAN_RECIPES = Object.freeze(Object.keys(INVESTIGATION_RECIPE_DEFINITIONS));
|
|
165
|
+
export const RUNNABLE_INVESTIGATION_PLAN_RECIPES = INVESTIGATION_PLAN_RECIPES;
|
|
166
|
+
function step(id, label, command, args, mode, approval, produces, completion) {
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
id,
|
|
169
|
+
label,
|
|
170
|
+
command,
|
|
171
|
+
arguments: Object.freeze([...args]),
|
|
172
|
+
mode,
|
|
173
|
+
approval,
|
|
174
|
+
produces,
|
|
175
|
+
completion,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { buildInvestigationPlan, isRunnableInvestigationRecipe, workflowReviewDeclarations, workflowStandardInputs, } from "./investigation-plan.mjs";
|
|
3
4
|
import { CliUsageError } from "./errors.mjs";
|
|
4
5
|
import { hasUnsafeCliText } from "./errors.mjs";
|
|
5
6
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
6
|
-
import { scanBoundedJson } from "../
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
export const MAX_INVESTIGATION_RUN_SELECTIONS = 16;
|
|
11
|
-
export const MAX_INVESTIGATION_RUN_SELECTION_LENGTH = 1_024;
|
|
7
|
+
import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
|
|
8
|
+
import { CLI_INVESTIGATION_RUN_SCHEMA, CLI_INVESTIGATION_PREVIEW_SCHEMA, CLI_INVESTIGATION_PREVIEW_VERSION, CLI_INVESTIGATION_RUN_VERSION, SUPPORTED_CLI_INVESTIGATION_RUN_VERSIONS, MAX_INVESTIGATION_RUN_BYTES, MAX_INVESTIGATION_RUN_SELECTIONS, MAX_INVESTIGATION_RUN_SELECTION_LENGTH, } from "../packages/contracts/investigation-run.mjs";
|
|
9
|
+
import { normalizeWorkflowBindings, validateWorkflowResult, workflowArtifactReference, workflowInputSchemas, } from "./investigation-artifacts.mjs";
|
|
10
|
+
export { CLI_INVESTIGATION_RUN_SCHEMA, CLI_INVESTIGATION_RUN_VERSION, SUPPORTED_CLI_INVESTIGATION_RUN_VERSIONS, MAX_INVESTIGATION_RUN_BYTES, MAX_INVESTIGATION_RUN_SELECTIONS, MAX_INVESTIGATION_RUN_SELECTION_LENGTH, };
|
|
12
11
|
const PLACEHOLDER_PATTERN = /^<[^>]+>$/u;
|
|
12
|
+
// These collectors can return useful incomplete observations. Offline validation
|
|
13
|
+
// and export failures are not observations and must remain retryable failures.
|
|
14
|
+
const PARTIAL_OBSERVATION_COMMANDS = new Set(['lookup', 'posture', 'discover-scan', 'ct-search', 'monitor-once']);
|
|
13
15
|
function boundedResult(value) {
|
|
14
16
|
if (Buffer.byteLength(value, 'utf8') > MAX_INVESTIGATION_RUN_BYTES) {
|
|
15
17
|
throw new CliUsageError(`Investigation step output is limited to ${MAX_INVESTIGATION_RUN_BYTES} bytes.`);
|
|
@@ -25,28 +27,56 @@ function boundedResult(value) {
|
|
|
25
27
|
return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '').slice(0, MAX_INVESTIGATION_RUN_BYTES);
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
function stepDisposition(step, exitCode, result, retained = false) {
|
|
31
|
+
const partial = exitCode === EXIT_CODES.PARTIAL_FAILURE && step.mode === 'network'
|
|
32
|
+
&& PARTIAL_OBSERVATION_COMMANDS.has(step.command);
|
|
33
|
+
if (exitCode === EXIT_CODES.SUCCESS || partial) {
|
|
34
|
+
try {
|
|
35
|
+
const artifact = validateWorkflowResult(step, result, retained);
|
|
36
|
+
return { disposition: partial ? 'partial' : 'complete', artifact };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
if (!partial)
|
|
40
|
+
throw new CliUsageError(`Investigation step ${step.id} returned an unexpected command contract or invalid result.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { disposition: 'failed', artifact: null };
|
|
32
44
|
}
|
|
33
45
|
function sameArguments(left, right) {
|
|
34
46
|
return Array.isArray(left)
|
|
35
47
|
&& left.length === right.length
|
|
36
48
|
&& left.every((value, index) => typeof value === 'string' && value === right[index]);
|
|
37
49
|
}
|
|
38
|
-
function selectedArguments(step, values) {
|
|
50
|
+
function selectedArguments(step, values, bindings, completed) {
|
|
39
51
|
let selectionIndex = 0;
|
|
52
|
+
let input = 0;
|
|
40
53
|
return Object.freeze(step.arguments.map((argument) => {
|
|
41
54
|
if (!PLACEHOLDER_PATTERN.test(argument))
|
|
42
55
|
return argument;
|
|
56
|
+
input += 1;
|
|
57
|
+
const binding = bindings.find((item) => item.stepId === step.id && item.input === input);
|
|
58
|
+
if (binding) {
|
|
59
|
+
const artifact = completed.find((item) => item.id === binding.sourceStepId)?.artifact;
|
|
60
|
+
return artifact ? workflowArtifactReference(artifact) : argument;
|
|
61
|
+
}
|
|
43
62
|
const selected = values[selectionIndex];
|
|
44
63
|
selectionIndex += 1;
|
|
45
64
|
return selected ?? argument;
|
|
46
65
|
}));
|
|
47
66
|
}
|
|
48
|
-
function selectedStep(step, values) {
|
|
49
|
-
return Object.freeze({ ...step, arguments: selectedArguments(step, values) });
|
|
67
|
+
function selectedStep(step, values, bindings, completed) {
|
|
68
|
+
return Object.freeze({ ...step, arguments: selectedArguments(step, values, bindings, completed) });
|
|
69
|
+
}
|
|
70
|
+
function resolveStepInputs(step, bindings, completed) {
|
|
71
|
+
const files = new Map();
|
|
72
|
+
const inputs = bindings.filter((binding) => binding.stepId === step.id).map((binding) => {
|
|
73
|
+
const source = completed.find((item) => item.id === binding.sourceStepId);
|
|
74
|
+
if (!source?.artifact)
|
|
75
|
+
throw new CliUsageError(`Workflow step ${step.id} has no validated source artefact.`);
|
|
76
|
+
files.set(workflowArtifactReference(source.artifact), JSON.stringify(source.result));
|
|
77
|
+
return Object.freeze({ input: binding.input, sourceStepId: source.id, artifactId: source.artifact.id });
|
|
78
|
+
});
|
|
79
|
+
return { files, inputs: Object.freeze(inputs) };
|
|
50
80
|
}
|
|
51
81
|
function placeholderCount(step) {
|
|
52
82
|
return step.arguments.filter((argument) => PLACEHOLDER_PATTERN.test(argument)).length;
|
|
@@ -107,11 +137,11 @@ function normalizeInvocationSelections(plan, selections) {
|
|
|
107
137
|
}
|
|
108
138
|
return normalizeRetainedSelections(plan, [...grouped].map(([stepId, values]) => ({ stepId, values })), 'workflow-run selections');
|
|
109
139
|
}
|
|
110
|
-
function mergeSelections(plan, retained, supplied, completed) {
|
|
140
|
+
function mergeSelections(plan, retained, supplied, completed, bindings) {
|
|
111
141
|
const merged = new Map(retained.map((item) => [item.stepId, item.values]));
|
|
112
142
|
for (const item of supplied) {
|
|
113
143
|
const completedStep = completed.find((step) => step.id === item.stepId);
|
|
114
|
-
if (completedStep && !sameArguments(completedStep.arguments, selectedArguments(plan.steps.find((step) => step.id === item.stepId), item.values))) {
|
|
144
|
+
if (completedStep && !sameArguments(completedStep.arguments, selectedArguments(plan.steps.find((step) => step.id === item.stepId), item.values, bindings, completed))) {
|
|
115
145
|
throw new CliUsageError(`workflow-run cannot change analyst selections for completed step ${item.stepId}.`);
|
|
116
146
|
}
|
|
117
147
|
merged.set(item.stepId, item.values);
|
|
@@ -121,9 +151,36 @@ function mergeSelections(plan, retained, supplied, completed) {
|
|
|
121
151
|
return values ? [Object.freeze({ stepId: step.id, values: Object.freeze([...values]) })] : [];
|
|
122
152
|
});
|
|
123
153
|
}
|
|
154
|
+
function mergeBindings(plan, retained, supplied, completed) {
|
|
155
|
+
const merged = [...retained];
|
|
156
|
+
for (const binding of supplied) {
|
|
157
|
+
const index = merged.findIndex((item) => item.stepId === binding.stepId && item.input === binding.input);
|
|
158
|
+
if (completed.some((step) => step.id === binding.stepId) && !isDeepStrictEqual(merged[index], binding)) {
|
|
159
|
+
throw new CliUsageError(`workflow-run cannot change artefact inputs for completed step ${binding.stepId}.`);
|
|
160
|
+
}
|
|
161
|
+
if (index === -1)
|
|
162
|
+
merged.push(binding);
|
|
163
|
+
else
|
|
164
|
+
merged[index] = binding;
|
|
165
|
+
}
|
|
166
|
+
return normalizeWorkflowBindings(plan, merged);
|
|
167
|
+
}
|
|
168
|
+
function validateSelectedInputs(plan, selections, bindings, completed) {
|
|
169
|
+
for (const selection of selections) {
|
|
170
|
+
const step = plan.steps.find((item) => item.id === selection.stepId);
|
|
171
|
+
if (selection.values.length + bindings.filter((item) => item.stepId === step.id).length > placeholderCount(step)) {
|
|
172
|
+
throw new CliUsageError(`Workflow step ${step.id} has more selections and artefact bindings than inputs.`);
|
|
173
|
+
}
|
|
174
|
+
// Ordinary filenames remain literal. A caller cannot make a selected file
|
|
175
|
+
// alias an explicitly bound in-memory document in the same invocation.
|
|
176
|
+
if (selection.values.some((value) => bindings.some((binding) => binding.stepId === step.id && completed.some((item) => item.id === binding.sourceStepId && item.artifact && value === workflowArtifactReference(item.artifact))))) {
|
|
177
|
+
throw new CliUsageError(`Workflow step ${step.id} has a filename that conflicts with an artefact reference.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
124
181
|
function parseResumeState(input, plan) {
|
|
125
182
|
if (!input)
|
|
126
|
-
return Object.freeze({ completed: Object.freeze([]), selections: Object.freeze([]) });
|
|
183
|
+
return Object.freeze({ completed: Object.freeze([]), selections: Object.freeze([]), bindings: Object.freeze([]), retryableStepId: null });
|
|
127
184
|
if (Buffer.byteLength(input, 'utf8') > MAX_INVESTIGATION_RUN_BYTES)
|
|
128
185
|
throw new CliUsageError('Investigation resume state exceeds the 24 MiB limit.');
|
|
129
186
|
const normalizedInput = input.replace(/^\uFEFF/u, '');
|
|
@@ -145,7 +202,7 @@ function parseResumeState(input, plan) {
|
|
|
145
202
|
}
|
|
146
203
|
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
147
204
|
if (root.schema !== CLI_INVESTIGATION_RUN_SCHEMA
|
|
148
|
-
|| (
|
|
205
|
+
|| !SUPPORTED_CLI_INVESTIGATION_RUN_VERSIONS.some((version) => root.version === version)
|
|
149
206
|
|| root.recipe !== plan.recipe.id || root.subject !== plan.subject || !Array.isArray(root.completedSteps)) {
|
|
150
207
|
throw new CliUsageError('Investigation resume state must match this versioned recipe and subject.');
|
|
151
208
|
}
|
|
@@ -155,14 +212,20 @@ function parseResumeState(input, plan) {
|
|
|
155
212
|
if (root.version === 1 && root.selections !== undefined) {
|
|
156
213
|
throw new CliUsageError('Investigation resume state version 1 cannot contain analyst selections.');
|
|
157
214
|
}
|
|
215
|
+
const bindings = root.version === CLI_INVESTIGATION_RUN_VERSION ? normalizeWorkflowBindings(plan, root.artifactBindings) : [];
|
|
216
|
+
if (root.version !== CLI_INVESTIGATION_RUN_VERSION && root.artifactBindings !== undefined) {
|
|
217
|
+
throw new CliUsageError('Legacy investigation checkpoints cannot declare artefact bindings.');
|
|
218
|
+
}
|
|
219
|
+
validateSelectedInputs(plan, selections, bindings, []);
|
|
158
220
|
const selectionsByStep = new Map(selections.map((item) => [item.stepId, item.values]));
|
|
159
221
|
if (root.completedSteps.length > plan.steps.length) {
|
|
160
222
|
throw new CliUsageError('Investigation resume state contains more steps than the installed fixed recipe.');
|
|
161
223
|
}
|
|
162
224
|
const completed = [];
|
|
225
|
+
let retryableStepId = null;
|
|
163
226
|
for (const [index, value] of root.completedSteps.entries()) {
|
|
164
227
|
const baseStep = plan.steps[index];
|
|
165
|
-
const planned = baseStep ? selectedStep(baseStep, selectionsByStep.get(baseStep.id) ?? []) : null;
|
|
228
|
+
const planned = baseStep ? selectedStep(baseStep, selectionsByStep.get(baseStep.id) ?? [], bindings, completed) : null;
|
|
166
229
|
if (!planned || !value || typeof value !== 'object' || Array.isArray(value)) {
|
|
167
230
|
throw new CliUsageError('Investigation resume state contains a malformed or out-of-order step.');
|
|
168
231
|
}
|
|
@@ -175,15 +238,23 @@ function parseResumeState(input, plan) {
|
|
|
175
238
|
throw new CliUsageError('Investigation resume state does not match the installed fixed recipe.');
|
|
176
239
|
}
|
|
177
240
|
const exitCode = Number(item.exitCode);
|
|
178
|
-
|
|
241
|
+
const { disposition, artifact } = stepDisposition(planned, exitCode, item.result, true);
|
|
242
|
+
const { inputs } = resolveStepInputs(planned, bindings, completed);
|
|
243
|
+
if (root.version === CLI_INVESTIGATION_RUN_VERSION) {
|
|
244
|
+
if (!isDeepStrictEqual(item.artifact, artifact) || !isDeepStrictEqual(item.inputs, inputs)) {
|
|
245
|
+
throw new CliUsageError('Investigation checkpoint artefact identity or input bindings do not match the retained results.');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (item.artifact !== undefined || item.inputs !== undefined) {
|
|
249
|
+
throw new CliUsageError('Legacy investigation steps cannot declare artefact identities.');
|
|
250
|
+
}
|
|
251
|
+
if (disposition === 'failed') {
|
|
179
252
|
if (index !== root.completedSteps.length - 1) {
|
|
180
253
|
throw new CliUsageError('A failed investigation step must be the final retained step.');
|
|
181
254
|
}
|
|
255
|
+
retryableStepId = planned.id;
|
|
182
256
|
break;
|
|
183
257
|
}
|
|
184
|
-
if (resultSchema(item.result) !== planned.produces) {
|
|
185
|
-
throw new CliUsageError('Investigation resume state contains output from an unexpected command contract.');
|
|
186
|
-
}
|
|
187
258
|
completed.push(Object.freeze({
|
|
188
259
|
id: planned.id,
|
|
189
260
|
command: planned.command,
|
|
@@ -191,27 +262,122 @@ function parseResumeState(input, plan) {
|
|
|
191
262
|
mode: planned.mode,
|
|
192
263
|
exitCode,
|
|
193
264
|
result: item.result,
|
|
265
|
+
artifact,
|
|
266
|
+
inputs,
|
|
194
267
|
}));
|
|
195
268
|
}
|
|
196
|
-
|
|
269
|
+
validateSelectedInputs(plan, selections, bindings, completed);
|
|
270
|
+
return Object.freeze({ completed: Object.freeze(completed), selections, bindings, retryableStepId });
|
|
197
271
|
}
|
|
198
|
-
|
|
272
|
+
function prepareInvestigationInvocation(recipe, subjectValue, options) {
|
|
199
273
|
if (!isRunnableInvestigationRecipe(recipe)) {
|
|
200
274
|
throw new CliUsageError('workflow-run supports only installed recipes whose exact steps satisfy the execution contract.');
|
|
201
275
|
}
|
|
202
276
|
const plan = buildInvestigationPlan(recipe, subjectValue, options.generatedAt);
|
|
203
277
|
const prior = parseResumeState(options.resumeInput, plan);
|
|
204
278
|
const suppliedSelections = normalizeInvocationSelections(plan, options.selections ?? []);
|
|
205
|
-
|
|
279
|
+
// A complete explicit input list replaces a not-yet-run step's standard or
|
|
280
|
+
// retained connections. Partial lists fill the remaining unbound slots.
|
|
281
|
+
const explicitSteps = new Set(suppliedSelections.filter(selection => selection.values.length === placeholderCount(plan.steps.find(step => step.id === selection.stepId))
|
|
282
|
+
&& !prior.completed.some(step => step.id === selection.stepId)).map(selection => selection.stepId));
|
|
283
|
+
const initialBindings = options.resumeInput === null ? workflowStandardInputs(recipe) : prior.bindings;
|
|
284
|
+
const bindings = mergeBindings(plan, initialBindings.filter(binding => !explicitSteps.has(binding.stepId)), normalizeWorkflowBindings(plan, options.artifactBindings ?? []), prior.completed);
|
|
285
|
+
const selections = mergeSelections(plan, prior.selections, suppliedSelections, prior.completed, bindings);
|
|
286
|
+
validateSelectedInputs(plan, selections, bindings, prior.completed);
|
|
287
|
+
return { plan, prior, bindings, selections };
|
|
288
|
+
}
|
|
289
|
+
/** Validate exactly what execution would resume, without an executor or authority. */
|
|
290
|
+
export function previewInvestigationRecipe(recipe, subjectValue, options) {
|
|
291
|
+
const { plan, prior, bindings, selections } = prepareInvestigationInvocation(recipe, subjectValue, options);
|
|
292
|
+
const selected = new Map(selections.map(item => [item.stepId, item.values]));
|
|
293
|
+
const steps = plan.steps.map(base => {
|
|
294
|
+
const retained = prior.completed.find(item => item.id === base.id);
|
|
295
|
+
const step = selectedStep(base, selected.get(base.id) ?? [], bindings, prior.completed);
|
|
296
|
+
let input = 0;
|
|
297
|
+
const inputs = base.arguments.flatMap((argument, index) => {
|
|
298
|
+
if (!PLACEHOLDER_PATTERN.test(argument))
|
|
299
|
+
return [];
|
|
300
|
+
input++;
|
|
301
|
+
const binding = bindings.find(item => item.stepId === base.id && item.input === input);
|
|
302
|
+
const source = binding ? prior.completed.find(item => item.id === binding.sourceStepId) : undefined;
|
|
303
|
+
return [{
|
|
304
|
+
input, placeholder: argument,
|
|
305
|
+
state: binding ? source?.artifact ? 'validated_output' : 'awaiting_output'
|
|
306
|
+
: PLACEHOLDER_PATTERN.test(step.arguments[index]) ? 'unresolved' : 'selected_file_or_value',
|
|
307
|
+
sourceStepId: binding?.sourceStepId ?? null,
|
|
308
|
+
artifactId: source?.artifact?.id ?? null,
|
|
309
|
+
value: binding ? null : PLACEHOLDER_PATTERN.test(step.arguments[index]) ? null : step.arguments[index],
|
|
310
|
+
}];
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
id: step.id, label: step.label, command: step.command, mode: step.mode,
|
|
314
|
+
state: retained ? retained.exitCode === EXIT_CODES.PARTIAL_FAILURE ? 'retained_partial' : 'retained_complete'
|
|
315
|
+
: prior.retryableStepId === step.id ? 'retryable_failure' : 'remaining',
|
|
316
|
+
artifact: retained?.artifact ?? null, inputs,
|
|
317
|
+
networkApprovalRequired: !retained && step.mode === 'network',
|
|
318
|
+
reviewDeclarationsRequired: retained ? [] : workflowReviewDeclarations(step),
|
|
319
|
+
arguments: step.arguments,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
schema: CLI_INVESTIGATION_PREVIEW_SCHEMA, version: CLI_INVESTIGATION_PREVIEW_VERSION,
|
|
324
|
+
generatedAt: options.generatedAt, recipe, subject: plan.subject, steps,
|
|
325
|
+
requestsExecuted: 0, checkpointWritten: false,
|
|
326
|
+
limitations: [
|
|
327
|
+
'This preview validates retained results and connections but does not open selected input files, execute steps, or grant approvals.',
|
|
328
|
+
'Retained partial observations are reused as partial, not silently recollected. Failed steps remain retryable.',
|
|
329
|
+
'File paths and selected values are included in this local preview. Review them before sharing.',
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export function formatInvestigationPreview(document) {
|
|
334
|
+
return [
|
|
335
|
+
`Workflow preview: ${document.recipe}`, `Subject: ${document.subject}`,
|
|
336
|
+
'Offline inspection only; no requests or checkpoint writes.',
|
|
337
|
+
...document.steps.flatMap(step => [
|
|
338
|
+
`${step.id}: ${step.state.replaceAll('_', ' ')} · ${step.label}`,
|
|
339
|
+
...(step.artifact ? [` Retained: ${step.artifact.schema} v${step.artifact.version} · ${step.artifact.id}`] : []),
|
|
340
|
+
...step.inputs.map(input => ` Input ${input.input}: ${input.state.replaceAll('_', ' ')}${input.sourceStepId ? ` from ${input.sourceStepId}` : ''}${input.value ? ` · ${input.value}` : ''}`),
|
|
341
|
+
...(step.networkApprovalRequired ? [' Requires fresh network approval.'] : []),
|
|
342
|
+
...(step.reviewDeclarationsRequired.length ? [` Requires human review: ${step.reviewDeclarationsRequired.join(', ')}.`] : []),
|
|
343
|
+
]),
|
|
344
|
+
...document.limitations, '',
|
|
345
|
+
].join('\n');
|
|
346
|
+
}
|
|
347
|
+
export async function runInvestigationRecipe(recipe, subjectValue, options) {
|
|
348
|
+
const prepared = prepareInvestigationInvocation(recipe, subjectValue, options);
|
|
349
|
+
const { plan, prior, bindings } = prepared;
|
|
350
|
+
let selections = prepared.selections;
|
|
351
|
+
const confirmedReviews = options.confirmedReviews ?? [];
|
|
352
|
+
if (!Array.isArray(confirmedReviews) || confirmedReviews.length > plan.steps.length
|
|
353
|
+
|| new Set(confirmedReviews).size !== confirmedReviews.length
|
|
354
|
+
|| confirmedReviews.some(id => !plan.steps.some(step => step.id === id && workflowReviewDeclarations(step).length))) {
|
|
355
|
+
throw new CliUsageError('--confirm-review must name each selected review-declaration step at most once.');
|
|
356
|
+
}
|
|
206
357
|
const selectionsByStep = new Map(selections.map((item) => [item.stepId, item.values]));
|
|
207
358
|
const completed = [...prior.completed];
|
|
208
|
-
let state = 'complete';
|
|
359
|
+
let state = completed.some((step) => step.exitCode === EXIT_CODES.PARTIAL_FAILURE) ? 'partial' : 'complete';
|
|
209
360
|
let currentStep = null;
|
|
210
361
|
for (const baseStep of plan.steps) {
|
|
211
362
|
options.signal?.throwIfAborted();
|
|
212
|
-
|
|
363
|
+
let step = selectedStep(baseStep, selectionsByStep.get(baseStep.id) ?? [], bindings, completed);
|
|
213
364
|
if (completed.some((item) => item.id === step.id))
|
|
214
365
|
continue;
|
|
366
|
+
const missingInputs = step.arguments.filter(argument => PLACEHOLDER_PATTERN.test(argument));
|
|
367
|
+
if (missingInputs.length && options.selectInputs) {
|
|
368
|
+
const supplied = await options.selectInputs({ stepId: step.id, label: step.label, inputs: missingInputs });
|
|
369
|
+
options.signal?.throwIfAborted();
|
|
370
|
+
if (!Array.isArray(supplied) || supplied.length > missingInputs.length)
|
|
371
|
+
throw new CliUsageError('Interactive selections exceed the remaining fixed-recipe inputs.');
|
|
372
|
+
if (supplied.length) {
|
|
373
|
+
const values = [...(selectionsByStep.get(step.id) ?? []), ...supplied];
|
|
374
|
+
const added = normalizeRetainedSelections(plan, [{ stepId: step.id, values }], 'Interactive selections');
|
|
375
|
+
selections = mergeSelections(plan, selections, added, completed, bindings);
|
|
376
|
+
validateSelectedInputs(plan, selections, bindings, completed);
|
|
377
|
+
selectionsByStep.set(step.id, values);
|
|
378
|
+
step = selectedStep(baseStep, values, bindings, completed);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
215
381
|
currentStep = step;
|
|
216
382
|
if (step.arguments.some((argument) => /^<[^>]+>$/u.test(argument))) {
|
|
217
383
|
state = 'awaiting_analyst_selection';
|
|
@@ -221,14 +387,19 @@ export async function runInvestigationRecipe(recipe, subjectValue, options) {
|
|
|
221
387
|
state = 'awaiting_network_approval';
|
|
222
388
|
break;
|
|
223
389
|
}
|
|
224
|
-
|
|
390
|
+
if (workflowReviewDeclarations(step).length && !confirmedReviews.includes(step.id)) {
|
|
391
|
+
state = 'awaiting_review_confirmation';
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
validateSelectedInputs(plan, selections, bindings, completed);
|
|
395
|
+
const { files, inputs } = resolveStepInputs(step, bindings, completed);
|
|
396
|
+
const result = await options.execute(step.command, step.arguments, files);
|
|
397
|
+
options.signal?.throwIfAborted();
|
|
225
398
|
if (result.exitCode === EXIT_CODES.CANCELLED) {
|
|
226
399
|
throw options.signal?.reason || new DOMException('Cancelled', 'AbortError');
|
|
227
400
|
}
|
|
228
401
|
const parsedResult = boundedResult(result.stdout);
|
|
229
|
-
|
|
230
|
-
throw new CliUsageError(`Investigation step ${step.id} returned an unexpected command contract.`);
|
|
231
|
-
}
|
|
402
|
+
const { disposition, artifact } = stepDisposition(step, result.exitCode, parsedResult);
|
|
232
403
|
completed.push(Object.freeze({
|
|
233
404
|
id: step.id,
|
|
234
405
|
command: step.command,
|
|
@@ -236,11 +407,17 @@ export async function runInvestigationRecipe(recipe, subjectValue, options) {
|
|
|
236
407
|
mode: step.mode,
|
|
237
408
|
exitCode: result.exitCode,
|
|
238
409
|
result: parsedResult,
|
|
410
|
+
artifact,
|
|
411
|
+
inputs,
|
|
239
412
|
}));
|
|
240
|
-
if (
|
|
413
|
+
if (disposition === 'failed') {
|
|
241
414
|
state = 'step_failed';
|
|
242
415
|
break;
|
|
243
416
|
}
|
|
417
|
+
if (disposition === 'partial') {
|
|
418
|
+
state = 'partial';
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
244
421
|
currentStep = null;
|
|
245
422
|
}
|
|
246
423
|
return Object.freeze({
|
|
@@ -251,23 +428,67 @@ export async function runInvestigationRecipe(recipe, subjectValue, options) {
|
|
|
251
428
|
subject: plan.subject,
|
|
252
429
|
state,
|
|
253
430
|
networkApprovedForThisRun: options.approveNetwork,
|
|
431
|
+
reviewsConfirmedForThisRun: Object.freeze([...confirmedReviews]),
|
|
254
432
|
selections: Object.freeze(selections),
|
|
433
|
+
artifactBindings: bindings,
|
|
255
434
|
completedSteps: Object.freeze(completed),
|
|
256
435
|
currentStep,
|
|
257
436
|
limitations: Object.freeze([
|
|
258
437
|
'Only commands and arguments from the installed fixed recipe can execute; no shell, script, arbitrary command, or enforcement action is accepted.',
|
|
259
438
|
'Network steps run only with --approve-network for the current invocation. Unresolved analyst selections pause; supplied values replace exact placeholders and are passed as arguments without shell interpretation.',
|
|
439
|
+
'Steps declaring human review require --confirm-review for that exact step in the current invocation. Confirm only after reviewing its selected material and each listed declaration; checkpoint metadata never grants permission to another step.',
|
|
260
440
|
'A resume file is a local checkpoint and can retain selected local paths or values. It is not proof that prior evidence remains current or that a human reviewed each stored result.',
|
|
441
|
+
'Standard and explicit artefact bindings reuse validated compatible earlier outputs without temporary extraction files. Full explicit input lists override uncompleted step connections. Resumes preserve their recorded connections; no new standard inputs are added to an older checkpoint. Content digests identify retained JSON, not source authenticity or currency.',
|
|
442
|
+
'An incomplete collection pauses for review. Resuming retains it without recollection; the run remains partial even after later steps finish. Failed validation or export steps are retried, not accepted as evidence.',
|
|
261
443
|
]),
|
|
262
444
|
});
|
|
263
445
|
}
|
|
446
|
+
export function investigationRunExitCode(document) {
|
|
447
|
+
if (document.state === 'step_failed') {
|
|
448
|
+
const code = document.completedSteps.at(-1)?.exitCode;
|
|
449
|
+
return typeof code === 'number' && code !== EXIT_CODES.SUCCESS && Object.values(EXIT_CODES).some((known) => code === known)
|
|
450
|
+
? code : EXIT_CODES.INTERNAL_ERROR;
|
|
451
|
+
}
|
|
452
|
+
return document.completedSteps.some((step) => step.exitCode === EXIT_CODES.PARTIAL_FAILURE)
|
|
453
|
+
? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
454
|
+
}
|
|
264
455
|
export function formatInvestigationRun(document) {
|
|
456
|
+
const stepLabel = document.state === 'step_failed' ? 'Failed' : document.state === 'partial' ? 'Review' : 'Next';
|
|
457
|
+
const plan = buildInvestigationPlan(document.recipe, document.subject, document.generatedAt);
|
|
458
|
+
const accepted = document.completedSteps.filter(step => step.artifact);
|
|
459
|
+
const current = document.currentStep;
|
|
460
|
+
const inputs = current?.arguments.flatMap((argument, position) => {
|
|
461
|
+
if (!PLACEHOLDER_PATTERN.test(argument))
|
|
462
|
+
return [];
|
|
463
|
+
// Slots refer to the fixed recipe, including inputs already bound or selected.
|
|
464
|
+
const base = plan.steps.find(step => step.id === current.id);
|
|
465
|
+
const input = base.arguments.slice(0, position + 1).filter(value => PLACEHOLDER_PATTERN.test(value)).length;
|
|
466
|
+
const compatible = workflowInputSchemas(current.command);
|
|
467
|
+
const reusable = accepted.filter(step => step.artifact && compatible.includes(step.artifact.schema));
|
|
468
|
+
return [
|
|
469
|
+
`Input ${current.id}:${input} ${argument} — --select ${current.id}=<path-or-value>`,
|
|
470
|
+
...reusable.map(source => ` Reuse deliberately: --use-artifact ${current.id}:${input}=${source.id}`),
|
|
471
|
+
];
|
|
472
|
+
}) ?? [];
|
|
265
473
|
return [
|
|
266
474
|
`Investigation run: ${document.recipe}`,
|
|
267
475
|
`Subject ${document.subject}`,
|
|
268
476
|
`State ${document.state.replaceAll('_', ' ')}`,
|
|
269
|
-
`
|
|
270
|
-
...(
|
|
477
|
+
`Progress ${accepted.length} of ${plan.steps.length} steps retained`,
|
|
478
|
+
...accepted.map(step => ` ${step.id}: ${step.exitCode === EXIT_CODES.PARTIAL_FAILURE ? 'partial observation' : 'completed'} · ${step.artifact.schema} v${step.artifact.version} · ${step.artifact.id}`),
|
|
479
|
+
...(document.currentStep ? [
|
|
480
|
+
`${stepLabel.padEnd(11)}${document.currentStep.label}`,
|
|
481
|
+
...(document.state === 'partial' || document.state === 'step_failed' ? [] : [`Approval ${document.currentStep.approval.replaceAll('_', ' ')}`]),
|
|
482
|
+
] : []),
|
|
483
|
+
...inputs,
|
|
484
|
+
...(document.state === 'awaiting_network_approval' ? ['Continue with --approve-network only after reviewing the next command’s network disclosure.'] : []),
|
|
485
|
+
...(document.state === 'awaiting_review_confirmation' && current ? [
|
|
486
|
+
`Declarations: ${workflowReviewDeclarations(current).join(', ')}`,
|
|
487
|
+
`After reviewing the selected material, continue with --confirm-review ${current.id}.`,
|
|
488
|
+
] : []),
|
|
489
|
+
...(current ? ['Keep the JSON checkpoint with --json --output <run.json>, then continue the same recipe and subject with --resume <run.json>.'] : [
|
|
490
|
+
document.state === 'partial' ? 'All steps have run; retained incomplete observations remain incomplete.' : 'All recipe steps have run. This is not an analyst verdict or proof that every finding is resolved.',
|
|
491
|
+
]),
|
|
271
492
|
'',
|
|
272
493
|
].join('\n');
|
|
273
494
|
}
|