@clear-capabilities/agentic-security-scanner 0.160.0 → 0.162.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/bin/agentic-security.js +11 -2
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2432.index.js +10 -1
  5. package/dist/4113.index.js +60 -9
  6. package/dist/5613.index.js +500 -0
  7. package/dist/6239.index.js +469 -0
  8. package/dist/7033.index.js +4 -1
  9. package/dist/7838.index.js +39 -3
  10. package/dist/8714.index.js +1 -1
  11. package/dist/8798.index.js +2 -2
  12. package/dist/9207.index.js +5 -0
  13. package/dist/9560.index.js +205 -62
  14. package/dist/{1905.index.js → 9999.index.js} +265 -8
  15. package/dist/agentic-security.mjs +12 -12
  16. package/dist/agentic-security.mjs.sha256 +1 -1
  17. package/package.json +14 -9
  18. package/src/dataflow/engine.js +20 -4
  19. package/src/discovery/CLAUDE.md +9 -0
  20. package/src/discovery/index.js +28 -0
  21. package/src/discovery/memory.js +7 -5
  22. package/src/engine.js +29 -8
  23. package/src/fix/apply-fix-service.js +33 -3
  24. package/src/language/CLAUDE.md +6 -1
  25. package/src/language/aibom.js +4 -1
  26. package/src/language/fix-lifecycle.js +9 -0
  27. package/src/language/haskell-ir.js +47 -7
  28. package/src/language/haskell-sca.js +87 -7
  29. package/src/language/haskell-supply.js +175 -6
  30. package/src/language/haskell-web.js +200 -18
  31. package/src/language/nix-fix.js +1 -1
  32. package/src/language/resolved-pass.js +4 -1
  33. package/src/language/witness.js +1 -1
  34. package/src/mcp/CLAUDE.md +43 -4
  35. package/src/mcp/tools.js +185 -43
  36. package/src/posture/CLAUDE.md +174 -0
  37. package/src/posture/assurance/baseline-inventory.js +218 -0
  38. package/src/posture/assurance/baseline.js +322 -0
  39. package/src/posture/assurance/bounded-io.js +145 -0
  40. package/src/posture/assurance/config.js +299 -0
  41. package/src/posture/assurance/contracts.js +315 -0
  42. package/src/posture/assurance/identity.js +62 -0
  43. package/src/posture/assurance/migrations.js +259 -0
  44. package/src/posture/assurance/schema-kit.js +111 -0
  45. package/src/posture/assurance/verification-record.js +258 -0
  46. package/src/posture/attestation.js +5 -0
  47. package/src/posture/autopilot.js +28 -1
  48. package/src/posture/blast-radius.js +15 -1
  49. package/src/posture/evidence-bundle.js +93 -7
  50. package/src/posture/execution-proof.js +11 -1
  51. package/src/posture/fix-verify-loop.js +20 -6
  52. package/src/posture/fix-verify.js +37 -2
  53. package/src/posture/oracles/adapters.js +360 -0
  54. package/src/posture/oracles/conformance.js +275 -0
  55. package/src/posture/oracles/oracle.js +433 -0
  56. package/src/posture/oracles/registry.js +76 -0
  57. package/src/posture/oracles/scenario-classes.js +223 -0
  58. package/src/posture/remediation-ledger.js +10 -1
  59. package/src/posture/replay/replay.js +249 -0
  60. package/src/posture/verification/advisory-state.js +71 -0
  61. package/src/posture/verification/emit.js +257 -0
  62. package/src/posture/verification/hypothesis-promotion.js +211 -0
  63. package/src/posture/verification/patch-negative.js +214 -0
  64. package/src/posture/verification/patch-promotion.js +105 -0
  65. package/src/posture/verification/projection.js +183 -0
  66. package/src/posture/verification/repair-records.js +124 -0
  67. package/src/posture/verifier.js +16 -0
  68. package/src/report/index.js +49 -0
  69. package/src/sandbox/CLAUDE.md +41 -0
  70. package/src/sandbox/backend-namespace.js +7 -0
  71. package/src/sandbox/backend-userspace.js +68 -29
  72. package/src/sandbox/control-probes.js +215 -0
  73. package/src/sandbox/supervise.js +201 -0
  74. package/src/sandbox/trust-boundary.js +113 -0
  75. package/src/sandbox/trust-domains.js +171 -0
  76. package/src/sast/pipeline.js +51 -4
@@ -1 +1 @@
1
- 6a0bcec455607538bd4485ce3b9cd5a266f51daf636b0ef6260cdc3bacf01776 agentic-security.mjs
1
+ e8b2c157e5948dbc9edd255a29b5b96f77378e8310baac39fc5f27a171cb2ec8 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.160.0",
3
+ "version": "0.162.0",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -67,28 +67,32 @@
67
67
  "test": "node ../scripts/run-unit-tests.mjs",
68
68
  "test:extras": "AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && node --experimental-test-module-mocks --test test/fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection-k8s-admission.test.js && node --experimental-test-module-mocks --test test/lineage-fault-injection.test.js && npm run test:python",
69
69
  "loop": "node ../scripts/loop-engineering/run.mjs",
70
- "test:loop": "node --test --test-concurrency=1 ../scripts/loop-engineering/test/loop-manifest.test.js ../scripts/loop-engineering/test/loop-watchdog.test.js ../scripts/loop-engineering/test/loop-background.test.js ../scripts/loop-engineering/test/loop-dashboard.test.js ../scripts/loop-engineering/test/loop-evidence.test.js ../scripts/loop-engineering/test/loop-recovery.test.js ../scripts/loop-engineering/test/loop-remote.test.js ../scripts/loop-engineering/test/loop-runbook.test.js",
70
+ "test:loop": "node --test --test-concurrency=1 ../scripts/loop-engineering/test/loop-manifest.test.js ../scripts/loop-engineering/test/loop-watchdog.test.js ../scripts/loop-engineering/test/loop-background.test.js ../scripts/loop-engineering/test/loop-dashboard.test.js ../scripts/loop-engineering/test/loop-evidence.test.js ../scripts/loop-engineering/test/loop-recovery.test.js ../scripts/loop-engineering/test/loop-remote.test.js ../scripts/loop-engineering/test/loop-runbook.test.js ../scripts/loop-engineering/test/loop-assurance-profile.test.js",
71
71
  "test:python": "node ../scripts/run-python-tests.mjs",
72
72
  "test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
73
73
  "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
74
74
  "test:glob": "node --test test/glob-compat.test.js",
75
- "test:sast": "node --test test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/java-structural-cross-file.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js",
76
- "test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js",
77
- "test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js",
78
- "test:mcp": "node --test test/mcp.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js",
75
+ "test:sast": "node --test test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/java-structural-cross-file.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js test/pipeline-env-secret.test.js test/mark-used-vuln-functions.test.js",
76
+ "test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js",
77
+ "test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js",
78
+ "test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js",
79
79
  "test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
80
80
  "test:lineage": "node --test test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js",
81
81
  "test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
82
82
  "test:server": "node --test test/server/graph-loader.test.js test/server/security.test.js test/server/routes.test.js test/server/http-server.test.js test/server/cmd-explore.test.js test/server/cmd-dataflow-export.test.js test/server/static-assets.test.js test/server/dataflow-scenario-cli.test.js test/server/dataflow-impact-cli.test.js",
83
- "test:haskell": "node --test test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js",
84
- "test:nix": "node --test test/nix/nix-build-trust.test.js test/nix/nix-eval-isolation.test.js test/nix/nix-input-inventory.test.js test/nix/nix-parser-ir.test.js test/nix/nix-remediation.test.js test/nix/nix-resolved-closure.test.js test/nix/nix-sca-patches.test.js test/nix/nix-sca-severity.test.js test/nix/nix-haskell-closure-names.test.js test/nix/nix-script-taint.test.js test/nix/nix-secrets-store.test.js test/nix/nixos-hardening.test.js test/nix/nixos-module-resolution.test.js test/nix/nix-fix-multifile.test.js test/nix/nixos-evaluator-forms.test.js test/nix/nix-advisory-feed.test.js",
83
+ "test:haskell": "node --test test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js",
84
+ "test:nix": "node --test test/nix/nix-build-trust.test.js test/nix/nix-eval-isolation.test.js test/nix/nix-input-inventory.test.js test/nix/nix-parser-ir.test.js test/nix/nix-remediation.test.js test/nix/nix-resolved-closure.test.js test/nix/nix-sca-patches.test.js test/nix/nix-sca-severity.test.js test/nix/nix-haskell-closure-names.test.js test/nix/nix-script-taint.test.js test/nix/nix-secrets-store.test.js test/nix/nixos-hardening.test.js test/nix/nixos-module-resolution.test.js test/nix/nix-fix-multifile.test.js test/nix/nixos-evaluator-forms.test.js test/nix/nix-advisory-feed.test.js test/nix/nix-fix-prewrite.test.js",
85
85
  "test:language": "node --test test/language/capability-ledger.test.js test/language/language-accuracy-regression.test.js test/language/language-aibom.test.js test/language/language-assurance.test.js test/language/language-bom.test.js test/language/language-compliance.test.js test/language/language-contracts.test.js test/language/language-corpora-integrity.test.js test/language/language-discovery.test.js test/language/language-doc-coverage.test.js test/language/language-evidence-posture.test.js test/language/language-explorer.test.js test/language/language-gap-fixes.test.js test/language/language-gap-fixes-2.test.js test/language/language-governance.test.js test/language/language-integrations.test.js test/language/language-llm-agent.test.js test/language/language-local-ai.test.js test/language/language-polyglot-bridges.test.js test/language/language-privacy-lineage.test.js test/language/language-proof-remediation.test.js test/language/language-report-formats.test.js test/language/language-secrets.test.js test/language/language-trust-boundary.test.js",
86
86
  "test:language-stress": "node --test --test-concurrency=1 test/language/language-stress-offline.test.js",
87
87
  "test:language-tools": "node --test test/haskell/haskell-web-auth.test.js test/haskell/haskell-support-gate.test.js",
88
88
  "bench:language-support:check": "node ../bench/language-support/check.mjs",
89
89
  "bench:live-feed": "node ../bench/live-feed/run.mjs",
90
90
  "bench:language-support:perf": "node ../bench/language-support/perf.mjs",
91
- "test:lifecycle": "node --test test/ci-hang-protection.test.js test/pre-bash-guard.test.js test/lsp-protocol-smoke.test.js test/ide-surfaces.test.js test/detector-liveness.test.js test/bench-watchdog.test.js test/ci-parity.test.js test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/dist-chunks-tracked.test.js && node ../scripts/lint-command-descriptions.mjs test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js",
91
+ "test:verification": "node --test test/verification/verification-record.test.js test/verification/oracle-adapters.test.js test/verification/replay-manifest.test.js test/verification/patch-negative.test.js test/verification/non-taint-classes.test.js test/verification/interface-equivalence.test.js test/verification/advisory-gating.test.js test/verification/oracle-conformance.test.js",
92
+ "verification:conformance:check": "node ../scripts/verification-conformance-check.mjs",
93
+ "verification:conformance:static": "node ../scripts/verification-conformance-check.mjs --static",
94
+ "verification:replay-example": "node ../scripts/verification-replay-example.mjs",
95
+ "test:lifecycle": "node --test test/ci-hang-protection.test.js test/ci-test-shards.test.js test/pre-bash-guard.test.js test/lsp-protocol-smoke.test.js test/ide-surfaces.test.js test/detector-liveness.test.js test/bench-watchdog.test.js test/ci-parity.test.js test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/trust-boundary.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/dist-chunks-tracked.test.js && node ../scripts/lint-command-descriptions.mjs test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js",
92
96
  "test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
93
97
  "test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js",
94
98
  "eval:independent": "node ../bench/independent-eval/runner.mjs",
@@ -133,6 +137,7 @@
133
137
  "bench:self-scan:check": "node ../bench/self-scan/check.mjs",
134
138
  "bench:nist-800-171-labels:check": "node ../bench/nist-800-171-labels/check.mjs",
135
139
  "bench:mutation:check": "node ../bench/mutation/runner.mjs",
140
+ "baseline:capture": "node ../scripts/baseline-capture.mjs",
136
141
  "bench:protection-verdict:check": "node ../bench/protection-verdict/runner.mjs",
137
142
  "bench:fix-correctness": "node ../bench/fix-correctness/runner.mjs",
138
143
  "bench:iac-coverage": "node ../bench/iac-coverage/runner.mjs",
@@ -329,14 +329,14 @@ function _resolveCalleeForSummary(calleeExpr, callContext) {
329
329
  // call's OWN arguments, silently losing the callee's return-taint. Mirrors
330
330
  // the same resolve -> get-or-compute -> merge sequence step()'s two existing
331
331
  // call sites use, via the Task 3/4 shared _resolveCalleeForSummary.
332
- function _nestedCallReturnTainted(calleeExpr, argExprs, state, callContext) {
332
+ function _nestedCallReturnTainted(calleeExpr, argExprs, state, callContext, argTaint) {
333
333
  if (!callContext || !callContext._summaryCache) return false;
334
334
  const target = _resolveCalleeForSummary(calleeExpr, callContext);
335
335
  if (!target) return false;
336
336
  const { qid, fn } = target;
337
337
  const paramNames = (fn && Array.isArray(fn.params)) ? fn.params : [];
338
338
  const entry = paramNames.length
339
- ? entryStateFromCall(paramNames, argExprs || [], state, (a) => exprTaint(a, state, callContext))
339
+ ? entryStateFromCall(paramNames, argExprs || [], state, argTaint || ((a) => exprTaint(a, state, callContext)))
340
340
  : new Set();
341
341
  let sum = callContext._summaryCache.get(qid, entry);
342
342
  if (!sum && fn && fn.cfg) {
@@ -470,7 +470,11 @@ function _calleeGetterFieldTainted(calleeExpr, state, callContext) {
470
470
  return !!(receiverPath && isCoveredBy(state, `${receiverPath}.${field}`));
471
471
  }
472
472
 
473
+ // Work counter for the scaling guard in test/dataflow-expr-taint-scaling.test.js (a count, not a clock).
474
+ export const _perf = { exprTaintCalls: 0 };
475
+
473
476
  function exprTaint(expr, state, callContext) {
477
+ _perf.exprTaintCalls += 1;
474
478
  if (expr && (expr.kind === 'member' || expr.kind === 'call' || expr.kind === 'ident') && exprIsSource(expr, callContext)) return true;
475
479
  if (!expr) return false;
476
480
  // P1.1 — field-sensitive access path: if the expression is a pure
@@ -518,8 +522,20 @@ function exprTaint(expr, state, callContext) {
518
522
  // direction) so the merge always runs when a call expression is
519
523
  // visited; _resolveCalleeForSummary + SummaryCache make repeat
520
524
  // resolution/computation for the same (qid, entry-state) cheap.
521
- const argsTainted = (expr.args || []).some((a, i) => !_isSprintfSafeArg(expr, i) && exprTaint(a, state, callContext));
522
- const nestedTainted = _nestedCallReturnTainted(expr.callee, expr.args, state, callContext);
525
+ // Each argument's taint is computed at most once per visit of this call. The argument loop below and the
526
+ // callee's entry state (built inside _nestedCallReturnTainted) both ask for it, and with the second ask
527
+ // re-walking the whole argument subtree the work doubled at every level of call nesting: a chain of N nested
528
+ // calls cost 2^N exprTaint visits (a deep-mode scan of a large Haskell project did not finish). The value asked
529
+ // for is the same pure function of (argument, state) both times, so reusing it changes no verdict.
530
+ const _argMemo = new Map();
531
+ const _argTaint = (a) => {
532
+ if (_argMemo.has(a)) return _argMemo.get(a);
533
+ const r = exprTaint(a, state, callContext);
534
+ _argMemo.set(a, r);
535
+ return r;
536
+ };
537
+ const argsTainted = (expr.args || []).some((a, i) => !_isSprintfSafeArg(expr, i) && _argTaint(a));
538
+ const nestedTainted = _nestedCallReturnTainted(expr.callee, expr.args, state, callContext, _argTaint);
523
539
  // Taint-recall PRD (80%): a call's RECEIVER can itself be tainted
524
540
  // independent of its arguments — `tainted.toString()`, `tainted.trim()`,
525
541
  // `it.getBytes()` — and neither argsTainted (there are no/unrelated
@@ -52,3 +52,12 @@ panel · `judge.js` shapes findings and dedupes against the prior scan ·
52
52
  `partition.js` `partitionNixFiles` groups `.nix` files by their import structure (a configuration and the modules it imports are
53
53
  one area), merged areas are capped, and hunter prompts for Haskell and Nix are redacted (split literals joined first) before they
54
54
  leave the process. `hunt` includes `.hs`, `.lhs` and `.nix`. With no endpoint every run is degraded and says so.
55
+
56
+ ## Advisory/gating separation (X-207)
57
+
58
+ Hunt output stays advisory in code, not just by convention. `memory.js` writes only through
59
+ `posture/verification/advisory-state.js` (`writeAdvisoryState`: one allowlisted file, atomic rename, never through a link), and
60
+ `report/index.js` excludes every hunt hypothesis from findings and exit codes. A hypothesis crosses into a finding only through
61
+ `promoteHypothesis` (`posture/verification/hypothesis-promotion.js`), reached from `runDiscovery` only with
62
+ `opts.promote = { policy, verify }`: a verifier receipt the promotion code verifies, an explicit policy, and an audit record. The
63
+ panel's votes, a confidence score and the confirmation tier are never inputs to that decision.
@@ -14,6 +14,8 @@ import { confirmAll } from './confirm.js';
14
14
  import { disprovePanel } from './disprove.js';
15
15
  import { judgeCandidates } from './judge.js';
16
16
  import { loadMemory, saveMemory, rememberRun, previouslyRefuted, nextWavePlan } from './memory.js';
17
+ import { emitVerification, headCommit } from '../posture/verification/emit.js';
18
+ import { promoteHypothesis } from '../posture/verification/hypothesis-promotion.js';
17
19
 
18
20
  // Bridge a candidate to the deterministic layer. A taint finding at or within
19
21
  // two lines of the candidate corroborates it; a modelled sink on the line
@@ -283,11 +285,37 @@ export async function runDiscovery(ctx = {}, opts = {}) {
283
285
  }));
284
286
  }
285
287
 
288
+ // X-201: one version-1 verification record per fresh hypothesis, ADDITIVE and advisory (hunt output never enters
289
+ // last-scan.json). A taint-corroborated candidate is `inconclusive` (a static probe is corroboration, not
290
+ // execution) and an uncorroborated one is `not-run`; a model refutation panel is recorded as inference only.
291
+ const hypothesisCommit = headCommit(opts.scanRoot);
292
+ const verificationRecords = [];
293
+ for (const f of fresh) {
294
+ const e = emitVerification('hunt', f, { finding: f, commit: hypothesisCommit });
295
+ if (e.ok) verificationRecords.push(e.record);
296
+ }
297
+
298
+ // X-207: opt-in promotion. A hypothesis crosses into a finding ONLY through `promoteHypothesis` (trusted verifier receipt,
299
+ // explicit policy, audit record). `fresh` is unchanged; promoted findings and every decision's audit record are reported
300
+ // separately, and nothing here writes `last-scan.json`. `opts.promote = { policy, verify(hypothesis) -> runOracle result }`.
301
+ let promotions = null;
302
+ if (opts.promote && typeof opts.promote.verify === 'function') {
303
+ promotions = [];
304
+ for (const f of fresh) {
305
+ let result = null;
306
+ try { result = await opts.promote.verify(f); } catch { result = null; }
307
+ const p = promoteHypothesis({ hypothesis: f, result, policy: opts.promote.policy });
308
+ promotions.push({ hypothesisId: p.audit.sourceHypothesis.id, decision: p.decision, audit: p.audit, finding: p.finding });
309
+ }
310
+ }
311
+
286
312
  return {
287
313
  schema: 'agentic-security/discovery@1',
288
314
  focusAreas: areas.map(a => ({ id: a.id, label: a.label, files: a.files.length, size: a.size })),
289
315
  runs,
290
316
  fresh,
317
+ verificationRecords,
318
+ ...(promotions ? { promotions } : {}),
291
319
  duplicates,
292
320
  suppressed,
293
321
  // `refutedCandidates` holds RAW candidates straight from `disprovePanel`,
@@ -40,6 +40,7 @@ import * as fs from 'node:fs';
40
40
  import * as path from 'node:path';
41
41
 
42
42
  import { stateWritesEnabled } from '../posture/state-dir.js';
43
+ import { writeAdvisoryState } from '../posture/verification/advisory-state.js';
43
44
  const MEMORY_SCHEMA = 'agentic-security/discovery-memory@1';
44
45
  export const MEMORY_FILE = path.join('.agentic-security', 'discovery-memory.json');
45
46
 
@@ -73,14 +74,15 @@ export function loadMemory(scanRoot) {
73
74
  }
74
75
  }
75
76
 
76
- /** Persist. Failure is non-fatal — the run still produced its report. */
77
+ /**
78
+ * Persist. Failure is non-fatal -- the run still produced its report. X-207: the write goes through the advisory-state writer
79
+ * (closed file-name allowlist, atomic rename, never through a link), so hunt output cannot reach `last-scan.json` or its
80
+ * signature even when a hostile tree planted a link at the memory file.
81
+ */
77
82
  export function saveMemory(scanRoot, memory) {
78
83
  try {
79
- const p = path.join(scanRoot, MEMORY_FILE);
80
84
  if (!stateWritesEnabled()) return;
81
- fs.mkdirSync(path.dirname(p), { recursive: true });
82
- fs.writeFileSync(p, JSON.stringify(memory, null, 2) + '\n');
83
- return true;
85
+ return writeAdvisoryState(scanRoot, path.basename(MEMORY_FILE), JSON.stringify(memory, null, 2) + '\n').ok;
84
86
  } catch {
85
87
  return false;
86
88
  }
package/src/engine.js CHANGED
@@ -6684,17 +6684,32 @@ function _semverSatisfies(ver,range){
6684
6684
  function markUsedVulnFunctions(supplyChain,fc){
6685
6685
  const used={};
6686
6686
  const perFile={};
6687
+ // The per-(package, function) expressions are compiled once, not once per file, and a file is split into lines only when
6688
+ // it contains the text a match needs: `\b(?:pkg|_)\.fn\b` can only match a line holding the substring `.fn`, and the Rust
6689
+ // bare-call expression one holding `fn`. Both prefilters are exact for a plain identifier; any other name skips the
6690
+ // prefilter and is judged line by line, as before. Match order (file, package, function, line) is unchanged.
6691
+ const _plainIdent=/^[A-Za-z_]\w*$/;
6692
+ const _hintRes=new Map();
6693
+ const _hintRe=(pkg,fn)=>{const k=pkg+'\0'+fn;let r=_hintRes.get(k);if(!r){r=new RegExp(`\\b(?:${pkg.replace(/\W/g,'\\$&')}|_)\\.${fn}\\b`,'g');_hintRes.set(k,r);}return r;};
6694
+ const _bareRes=new Map();
6695
+ const _bareRe=(fn)=>{let r=_bareRes.get(fn);if(!r){r=new RegExp(`\\b${fn.replace(/\W/g,'\\$&')}\\s*[(<]`,'g');_bareRes.set(fn,r);}return r;};
6696
+ const _hintEntries=Object.entries(VULN_FUNCTION_HINTS);
6697
+ for(const pkg of Object.keys(VULN_FUNCTION_HINTS))if(!perFile[pkg])perFile[pkg]=[];
6687
6698
  for(const[fp,content] of Object.entries(fc)){
6688
- const lines=content.split('\n');
6699
+ let lines=null;
6689
6700
  // Rust import-aware matching: build import map for .rs files
6690
6701
  let _rustImports=null;
6691
6702
  if(/\.rs$/i.test(fp)){try{_rustImports=extractRustImportMap(content);}catch(_){}}
6692
- for(const[pkg,fns] of Object.entries(VULN_FUNCTION_HINTS)){
6693
- if(!perFile[pkg])perFile[pkg]=[];
6703
+ for(const[pkg,fns] of _hintEntries){
6694
6704
  for(const fn of fns){
6695
- const re=new RegExp(`\\b(?:${pkg.replace(/\W/g,'\\$&')}|_)\\.${fn}\\b`,'g');
6705
+ const plain=_plainIdent.test(fn);
6706
+ const dotPossible=!plain||content.includes('.'+fn);
6707
+ const barePossible=_rustImports&&(!plain||content.includes(fn));
6708
+ if(!dotPossible&&!barePossible)continue;
6709
+ const re=_hintRe(pkg,fn);
6696
6710
  // Rust: also match bare function calls if import map traces them to this package
6697
- const rustBareRe=_rustImports?new RegExp(`\\b${fn.replace(/\W/g,'\\$&')}\\s*[(<]`,'g'):null;
6711
+ const rustBareRe=_rustImports?_bareRe(fn):null;
6712
+ if(lines===null)lines=content.split('\n');
6698
6713
  for(let li=0;li<lines.length;li++){
6699
6714
  let matched=re.test(lines[li]);
6700
6715
  re.lastIndex=0;
@@ -6733,9 +6748,12 @@ function markUsedVulnFunctions(supplyChain,fc){
6733
6748
  // Search codebase for these functions (if not already searched via VULN_FUNCTION_HINTS)
6734
6749
  if(osvFns.length&&!hardcoded.length){
6735
6750
  for(const[fp,content] of Object.entries(fc)){
6736
- const lines=content.split('\n');
6751
+ let lines=null;
6737
6752
  for(const fn of osvFns){
6738
6753
  const shortFn=fn.lastIndexOf('.')>0?fn.slice(fn.lastIndexOf('.')+1):fn;
6754
+ // the expression escapes every non-word character, so a match needs the literal text on its line
6755
+ if(!content.includes(shortFn))continue;
6756
+ if(lines===null)lines=content.split('\n');
6739
6757
  const re=new RegExp(`\\b${shortFn.replace(/\W/g,'\\$&')}\\b`,'g');
6740
6758
  for(let li=0;li<lines.length;li++){
6741
6759
  if(re.test(lines[li])){
@@ -9086,6 +9104,11 @@ function _deterministicFileTimings(timings) {
9086
9104
  setProgress({current:i,total:files.length,file:"Config file cross-ref...",phase:"Linking"});aLogic.push(...(runDetector(_detectorErrors,'<project>','scanConfigFiles',()=>scanConfigFiles(fc))||[]));
9087
9105
  setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
9088
9106
  const allFileContents={...fc, ...depFileContents};
9107
+ // The opt-in live advisory feeds are fetched HERE, as soon as the manifests are in hand, not after the expensive passes: a scan that is
9108
+ // killed or times out later still has its feed data on disk (the lookup used to run ~1447 s into a 1460 s scan, so an early kill looked like
9109
+ // "feed unavailable"). The inputs are the file contents only, so the result is the same as when this ran at the end.
9110
+ try{if(process.env.AGENTIC_SECURITY_HACKAGE_ADVISORIES_LIVE==='1'){const{prefetchHackageFeed}=await import('./language/haskell-supply.js');const{resolvedHackageComponents,nixHaskellPackageNames}=await import('./language/resolved-pass.js');let _rc=[],_nx=[];try{_rc=resolvedHackageComponents(allFileContents).components||[];}catch(_){}try{_nx=nixHaskellPackageNames(allFileContents);}catch(_){}await prefetchHackageFeed(allFileContents,{resolvedComponents:_rc,extraNames:_nx});}}catch(_){/* the live feed is optional: a failure leaves the scan on whatever snapshot exists */}
9111
+ try{if(process.env.AGENTIC_SECURITY_NIX_ADVISORIES_LIVE==='1'){const{prefetchNixAdvisoryFeed}=await import('./language/resolved-pass.js');await prefetchNixAdvisoryFeed(allFileContents);}}catch(_){/* the live feed is optional: a failure leaves the scan on whatever snapshot exists */}
9089
9112
  // PRD F11.4 — malicious install hooks, scanned where package.json actually
9090
9113
  // lives. `scanInstallScripts` was wired into the per-file SAST dispatch, but
9091
9114
  // that loop iterates `files`, which comes from `fileContents` — and
@@ -9231,8 +9254,6 @@ function _deterministicFileTimings(timings) {
9231
9254
  const annotatedComponents=components.map(c=>{const key=`${c.ecosystem}:${c.name}:${c.version}`;const vulns=vulnsByKey[key]||[];const riKey=c.ecosystem==='maven'&&c.group?`maven:${c.group}/${c.name}`:`${c.ecosystem}:${c.name}`;const ri=registryInfo.get(riKey)||{};const latestVersion=ri.latestVersion||'';const vd=(ri.versions||{})[c.version]||{};const isDeprecated=typeof vd.deprecated==='string'&&vd.deprecated.length>0;const deprecationMessage=isDeprecated?vd.deprecated:'';const isOutdated=!isDeprecated&&typeof vd.outdated==='string'&&vd.outdated.length>0;const outdatedMessage=isOutdated?vd.outdated:'';const license=ri.license||vd.license||'';return{...c,vulns,hasVulns:vulns.length>0,hasAttackPath:attackResult.flagged.has(key),attackPaths:attackResult.pathsByKey.get(key)||[],latestVersion,isDeprecated,deprecationMessage,isOutdated,outdatedMessage,license};});
9232
9255
  // X-010: Hackage and Nix components, dependency edges and target provenance. Kept beside (not inside) the ordinary
9233
9256
  // component list so the existing consumers are untouched; the BOM emitters and the SBOM diff read it explicitly.
9234
- try{if(process.env.AGENTIC_SECURITY_HACKAGE_ADVISORIES_LIVE==='1'){const{prefetchHackageFeed}=await import('./language/haskell-supply.js');const{resolvedHackageComponents,nixHaskellPackageNames}=await import('./language/resolved-pass.js');let _rc=[],_nx=[];try{_rc=resolvedHackageComponents(allFileContents).components||[];}catch(_){}try{_nx=nixHaskellPackageNames(allFileContents);}catch(_){}await prefetchHackageFeed(allFileContents,{resolvedComponents:_rc,extraNames:_nx});}}catch(_){/* the live feed is optional: a failure leaves the scan on whatever snapshot exists */}
9235
- try{if(process.env.AGENTIC_SECURITY_NIX_ADVISORIES_LIVE==='1'){const{prefetchNixAdvisoryFeed}=await import('./language/resolved-pass.js');await prefetchNixAdvisoryFeed(allFileContents);}}catch(_){/* the live feed is optional: a failure leaves the scan on whatever snapshot exists */}
9236
9257
  let _languageBom=null;try{if(Object.keys(allFileContents).some(f=>/\.(?:l?hs|nix)$|\.cabal$|(?:^|\/)(?:cabal\.project(?:\.freeze)?|package\.yaml|stack\.yaml(?:\.lock)?|flake\.lock)$/i.test(f))){const{languageBom}=await import('./language/bom.js');const{resolvedHaskellGraph,nixClosureOf}=await import('./language/resolved-pass.js');const _rg=resolvedHaskellGraph(allFileContents);const _nc=nixClosureOf(allFileContents);_languageBom=languageBom(allFileContents,{...(_rg?{resolved:_rg.graph}:{}),...(_nc&&!_nc.refused.length?{closure:_nc.closure}:{})});}}catch(_){_languageBom=null;}
9237
9258
  aF.push(...(runDetector(_detectorErrors,'<project>','scanDbTaintCrossFile',()=>scanDbTaintCrossFile(fc))||[]));
9238
9259
  aF.push(...(runDetector(_detectorErrors,'<project>','scanCsharpCrossFile',()=>scanCsharpCrossFile(fc))||[]));
@@ -42,6 +42,7 @@ import { verifyLastScan } from '../posture/integrity.js';
42
42
  import { stateDir } from '../posture/state-dir.js';
43
43
  import { applyFix as applyFixHistory, revertEntryById as revertFixEntry } from '../posture/fix-history.js';
44
44
  import { classifyFixMaterialRisk } from '../posture/material-change.js';
45
+ import { appendRepairRecord } from '../posture/verification/repair-records.js';
45
46
  import { loadApproverRegistry, verifyApprover, requiredRolesFor, checkSeparationOfDuties } from './approver-registry.js';
46
47
 
47
48
  const RESERVED_WRITE_PREFIXES = [
@@ -191,6 +192,12 @@ async function getVerifyFixCore() {
191
192
  * .agentic-security/authorized-approvers.json is configured, `approvedBy` is ALSO checked
192
193
  * against it (fix/approver-registry.js) — an approvedBy the registry doesn't recognize, or one
193
194
  * missing a role a touched category requires, is refused. A no-op with no registry configured.
195
+ * @param {object} [opts.patchNegative] - X-204: `{ original: {files, entry, oracleId, inputs}, functional: {inputs}, commit?, ... }`,
196
+ * the exploit scenario and functional cases the patch must satisfy. Honoured only when the `patch-negative-verification`
197
+ * feature (and `verification-oracles`) is enabled; otherwise ignored and this function behaves exactly as before. When it
198
+ * runs, a patch is written only if it is `verified-fix`, and the result carries `patchNegative` plus `repairRecords`: SEPARATE
199
+ * records for the proposal, its promotion or rejection, its application and any rollback.
200
+ * @param {object} [opts.assuranceConfig] - resolved assurance config (defaults to the process environment)
194
201
  * @param {boolean} [opts.dryRun]
195
202
  * @param {boolean} [opts.skipVerification] - escape hatch for callers that have ALREADY verified
196
203
  * (none currently use this — present so a future caller cannot be forced into double verification
@@ -202,7 +209,7 @@ async function getVerifyFixCore() {
202
209
  * configured, tests when a runner is detected — genuinely ran and passed, not silently skipped.
203
210
  * `verified:true, verifiedFull:false` is a real, honest state: applied, but on a degraded pass.
204
211
  */
205
- export async function applyVerifiedFix({ scanRoot, finding, files, fixMeta = null, dryRun = false, skipVerification = false } = {}) {
212
+ export async function applyVerifiedFix({ scanRoot, finding, files, fixMeta = null, dryRun = false, skipVerification = false, patchNegative = null, assuranceConfig } = {}) {
206
213
  if (!scanRoot) return { ok: false, applied: false, reason: 'scanRoot required' };
207
214
  if (!finding) return { ok: false, applied: false, reason: 'finding required' };
208
215
  if (!files || typeof files !== 'object' || !Object.keys(files).length) {
@@ -316,6 +323,9 @@ export async function applyVerifiedFix({ scanRoot, finding, files, fixMeta = nul
316
323
  }
317
324
 
318
325
  let verify = null;
326
+ let pn = null;
327
+ let repairRecords = [];
328
+ const pnView = (r) => ({ status: r.status, verifiedFix: r.verifiedFix, incompleteStep: r.incompleteStep, failureCode: r.failureCode, reason: r.reason, steps: r.steps, patchDigest: r.patchDigest, summary: r.summary, verificationRecord: r.verificationRecord });
319
329
  if (!skipVerification) {
320
330
  if (!finding.stableId) {
321
331
  return { ok: false, applied: false, reason: 'finding has no stableId — cannot verify a candidate fix against it' };
@@ -328,22 +338,31 @@ export async function applyVerifiedFix({ scanRoot, finding, files, fixMeta = nul
328
338
  originalFindingStableId: finding.stableId,
329
339
  files: Object.fromEntries(Object.entries(confined).map(([rel, v]) => [rel, v.content])),
330
340
  fixMeta,
341
+ ...(patchNegative ? { patchNegative, assuranceConfig } : {}),
331
342
  });
332
343
  } catch (e) {
333
344
  return { ok: false, applied: false, reason: `verification failed: ${e.message}` };
334
345
  }
335
346
  verify = verdict;
347
+ // X-204: present only when the caller asked and the feature is on. `pn` stays null on every legacy path.
348
+ const pnRun = verdict.patchNegative && verdict.patchNegative.status !== 'disabled' ? verdict.patchNegative : null;
349
+ if (pnRun) { pn = pnRun; repairRecords = pnRun.repairRecords || []; }
336
350
  if (!verdict.ok) {
351
+ // the patch-negative leg may have passed while another leg did not: the rejection is still its own record
352
+ if (pn && pn.verifiedFix) {
353
+ repairRecords = appendRepairRecord(repairRecords, { kind: 'rejected', hypothesisId: finding.stableId, revision: pn.promotion?.revision ?? null, diffDigest: pn.patchDigest, step: 'other-verification-legs', reason: `rejected by verifier: ${String(verdict.summary || 'did not verify').slice(0, 200)}` }).records;
354
+ }
337
355
  return {
338
356
  ok: false, applied: false,
339
357
  reason: `rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
340
358
  verify: { rescan: verdict.rescan, lint: verdict.lint ? { runner: verdict.lint.runner, ok: verdict.lint.ok } : null, honesty: verdict.honesty || null },
359
+ ...(pn ? { patchNegative: pnView(pn), repairRecords } : {}),
341
360
  };
342
361
  }
343
362
  }
344
363
 
345
364
  if (dryRun) {
346
- return { ok: true, applied: false, dryRun: true, verified: !skipVerification, files: Object.keys(confined), verify, materialClassification };
365
+ return { ok: true, applied: false, dryRun: true, verified: !skipVerification, files: Object.keys(confined), verify, materialClassification, ...(pn ? { patchNegative: pnView(pn), repairRecords } : {}) };
347
366
  }
348
367
 
349
368
  // FR-304: re-check every file against its pre-verification baseline hash,
@@ -381,11 +400,21 @@ export async function applyVerifiedFix({ scanRoot, finding, files, fixMeta = nul
381
400
  for (const w of written) {
382
401
  try { await revertFixEntry(scanRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
383
402
  }
403
+ // X-204: the rollback is its own record, after the promotion it undoes (a write that failed part-way)
404
+ if (pn) {
405
+ const rolled = appendRepairRecord(repairRecords, { kind: 'rolled-back', hypothesisId: finding.stableId, revision: pn.promotion?.revision ?? null, diffDigest: pn.patchDigest, reason: `the write failed and every file written in this batch was restored: ${String(e?.message || e).slice(0, 160)}` });
406
+ repairRecords = rolled.records;
407
+ }
384
408
  if (e && e.name === 'FixAttemptBudgetExceededError') {
385
- return { ok: false, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
409
+ return { ok: false, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key, ...(pn ? { patchNegative: pnView(pn), repairRecords } : {}) };
386
410
  }
411
+ if (pn && e && typeof e === 'object') e.repairRecords = repairRecords;
387
412
  throw e;
388
413
  }
414
+ // X-204: application is a separate record from the promotion that allowed it
415
+ if (pn) {
416
+ repairRecords = appendRepairRecord(repairRecords, { kind: 'applied', hypothesisId: finding.stableId, revision: pn.promotion?.revision ?? null, diffDigest: pn.patchDigest, reason: `written to ${written.length} file(s) after verified-fix promotion` }).records;
417
+ }
389
418
 
390
419
  // FR-305: `verified: true` only ever meant "verification was attempted",
391
420
  // not "every required leg genuinely ran and passed" — a caller that
@@ -400,5 +429,6 @@ export async function applyVerifiedFix({ scanRoot, finding, files, fixMeta = nul
400
429
  written,
401
430
  verify: verify ? { summary: verify.summary, verifiedFull: verify.verifiedFull, degradedLegs: verify.degradedLegs || [] } : null,
402
431
  materialClassification,
432
+ ...(pn ? { patchNegative: pnView(pn), repairRecords } : {}),
403
433
  };
404
434
  }
@@ -31,6 +31,9 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
31
31
  queried (`covered`); a package it did not cover is `feed-incomplete` and a stale one `feed-stale`, in BOTH matchers
32
32
  (`evaluateComponents` and `nix-sca.js`). Any new consumer of an `AdvisoryDb` must check `db.coverage(name)` before saying "no advisory".
33
33
  The snapshot lives in the operator configuration directory, never the project (`trusted-inputs.js`).
34
+ - **Auth guards must stop the handler.** In `haskell-web.js` a Scotty `status 40x` only sets the code, so both the named-guard path (`guardSummary`) and the inline path (`handlerFacts`) need a halting call (`finish`, `raise`, `raiseStatus`, `redirect`; `finish` is an IR identifier with no line of its own). WAI, Servant and Yesod inline guards cannot be ordered by line (the IR reports a tail-position call at the enclosing line), so after the IR finds a candidate (credential read plus a rejection that ends the handler), `inlineOrder` decides the ORDER from the handler's syntax tree: do-block statements in sequence, a scrutinee/condition before its branches, `when`/`unless`/`if`/`case` as forks (a branch that always rejects after a read guards its siblings and everything after the fork). Leaves are classified by lowering them alone and reusing `credentialReadsOf`/`isRejection`/`stopsHandler`/`isSensitiveCall`, so there is no second definition of those. Verdicts: `before` (authenticated), `after` (read, write, then reject: `late`, CWE-306 "runs after"), `unknown` (order not provable: `late` with `evidence[0].reason`, never authenticated). `throwError`/`permissionDenied`/`notAuthenticated` stop the handler anywhere; a WAI `respond`/`return` of a 401 stops it only in TAIL position (a mid-block `respond` is followed by the next statement). Remaining limits, all failing toward `late`/missing and never toward authenticated: a rejection and a sensitive call inside one opaque expression (`maybe`/`either` with both), a handler whose source structure is unavailable, and a sensitive call the walk cannot find; also unchanged, the guarding condition is not required to mention the credential. Pinned both ways in `test/haskell/haskell-web-guards.test.js`. A WAI `responseLBS` rejection counts only when handed back (`respond`/`return`).
35
+ - **Declared uses are never collapsed to the first one, and project bounds are disclosed.** `hackageComponents` returns `components` (one per package, scope and resolved version, for inventories) AND `uses` (every distinct declared use, identical ones merged with all declaring components in `carriers`); the evaluator runs over `uses` and `collapseToWorst` reports ONE finding per package, scope and advisory carrying the WORST status, `carriedBy` naming the components that carry it and `otherUses` the milder ones. cabal.project `constraints:` are intersected with a declared range and `allow-newer`/`allow-older` widen it (`effectiveDeclared`), only for a use with no resolved version and only from a project file whose `packages:` governs the manifest; a constraint under an `if` is not applied and is disclosed as a gap. Widening is the safe direction: never narrow on a relaxation you cannot match.
36
+ - **Compiler-provided packages are one finding per advisory.** GHC boot-library findings are grouped (`grouped: 'compiler'`, `members`, `memberCount`) and counted in `counts.compilerAdvisories`, not `counts.dependencyFindings`; every use keeps its `ghc-component:*` status row. Which advisory applies is decided against `projectCompiler` (`with-compiler`, Stack compiler, a fresh plan's compiler-id, or a `tested-with` naming exactly one version; conflicting or several versions is `unknown compiler`, never a guess). `base` takes the base series of a known GHC release (`GHC_BASE_SERIES`, series only, so the result is a range); a plan's boot unit gives the exact version. A series not in the table is not guessed.
34
37
  - **Auth guards must stop the handler.** In `haskell-web.js` a Scotty `status 40x` only sets the code, so both the named-guard path (`guardSummary`) and the inline path (`handlerFacts`) need a halting call (`finish`, `raise`, `raiseStatus`, `redirect`; `finish` is an IR identifier with no line of its own). WAI, Servant and Yesod inline guards order the credential READ against the first sensitive call, because a tail-position rejection reports the enclosing line (a disclosed gap, pinned in `test/haskell/haskell-web-guards.test.js`). A WAI `responseLBS` rejection counts only when handed back (`respond`/`return`).
35
38
  - **Severity is the advisory's own** (`cvss.js`): CVSS v3.x base score, else a named `database_specific.severity`, else `medium` with the basis stated. Never guess from an unparseable or v4 vector.
36
39
  - **Option fixes work on the effective configuration, across files.** `nix-fix.js` edits EVERY contributing definition of an option
@@ -41,7 +44,8 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
41
44
  first write, rolls back on a failed write) and records one history entry per file sharing `languageGroupId`; `undoFix` and
42
45
  `posture/fix-history.js` `undoLast`/`revertEntryById` restore a group as a unit. A winner outside the root or not a single
43
46
  literal is refused with an `override` suggestion (`overrideSuggestion`), never an automatic edit. Any new consumer of a fix
44
- plan (LSP, MCP, CLI) must honour `plan.edits`: `apply_fix` writes one file, so a multi-file fix is not offered there.
47
+ plan (LSP, MCP, CLI) must honour `plan.edits`. MCP `apply_fix` does so through `plan_digest` (the server recomputes the plan and
48
+ compares a digest; see `mcp/CLAUDE.md`), using the lifecycle's optional `preWrite` hook, which runs after every gate and before the first write.
45
49
  - **Boundaries are narrowed only by sound, closed rules.** `haskell-cpp.js` decides a CPP conditional only from the file, a project-stated
46
50
  compiler (`with-compiler` pin; `tested-with` only when every item is an exact version and all agree, recorded as an assumption) or the hull
47
51
  of cabal `build-depends` bounds; an unknown macro is undecided, never 0, and an undecided conditional keeps every branch. `haskell-boundaries.js`
@@ -57,6 +61,7 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
57
61
  will use; metadata comes only from the operator (`AGENTIC_SECURITY_NIX_META`). NVD is rate limited (5/30 s keyless): never raise
58
62
  the request caps or lower the spacing in `nix-advisory-feed.js` without re-reading the NVD limits.
59
63
  - **Pragma** (`pragma.js`): comment-aware, line-scoped, exact rule-id match, logged. A line-less finding cannot be suppressed.
64
+ - **Scale: no per-use scan of the project.** Pandoc and cabal-install (hundreds to thousands of modules) did not finish a scan because of four superlinear costs, now fixed; do not reintroduce the shapes. (1) `haskell-ir.js`: anything a lowering step needs "from every module" (a constructor by name, the record types, a definition by qid, a name's resolution) is an index on `Project` / a per-module memo (`findCon`, `recordTypes`, `defByQid`, `RESOLVE_MEMO`), built once, never a loop over `proj.modules` per use. Only a top-level resolution (depth 0) is memoised: a resolution inside a re-export chain depends on the path taken. (2) `aibom.js` looks a file's imports up in a map, not `Object.values(perFile).find`. (3) `sast/pipeline.js`: the workflow-wide `env:` secret rule is a one-pass matcher (`scanEnvSecrets`), because the single regular expression it replaced backtracked catastrophically on a comment-blanked workflow (blanking leaves runs of blank lines). A detector expression that runs on comment-blanked text must not let `\s*`, `\n` and a lazy line loop claim the same blank lines. (4) `dataflow/engine.js` `exprTaint`: a call's argument taint is computed once per visit (`_argTaint`); computing it twice re-walked the subtree and cost 2^N for N nested calls. Guards: `test/pipeline-env-secret.test.js`, `test/dataflow-expr-taint-scaling.test.js`, `test/haskell/haskell-ir-scaling.test.js`, `test/mark-used-vuln-functions.test.js` compare work counters between two sizes, never a clock.
60
65
 
61
66
  ## Module map
62
67
 
@@ -80,7 +80,10 @@ export function extractHaskellAI(files) {
80
80
  const declared = new Map(comps.filter((c) => HS_AI_PACKAGES[c.name]).map((c) => [c.name, c]));
81
81
  let ir = null;
82
82
  try { ir = buildHaskellIR(Object.fromEntries(sources)); } catch (e) { gaps.push({ kind: 'haskell-ir-failed', detail: e.message }); }
83
- const importsOf = (f) => (ir && ir.perFile ? (Object.values(ir.perFile).find((x) => x.file === f) || {}).imports || [] : []);
83
+ // file -> its imports, first entry wins; built once (a per-call scan of every file made this quadratic in project size)
84
+ const importsByFile = new Map();
85
+ if (ir && ir.perFile) for (const x of Object.values(ir.perFile)) if (!importsByFile.has(x.file)) importsByFile.set(x.file, x.imports || []);
86
+ const importsOf = (f) => importsByFile.get(f) || [];
84
87
 
85
88
  for (const [file, text] of sources) {
86
89
  const toks = tokenizeHaskell(text);
@@ -191,6 +191,15 @@ export async function runFixLifecycle(o) {
191
191
  const res = { status: 'verified', applied: false, plan, gates, label: plan.label, verified, preview: edits.map((e) => unifiedDiff(e.file, e.before, e.after)).join('\n'), files: edits.map((e) => e.file) };
192
192
  if (o.apply) {
193
193
  if (!o.root) return blocked('apply requires a root directory');
194
+ // Optional last check, after every gate has passed and before the first byte is written, so a caller that bound this plan
195
+ // to something outside the lifecycle (a digest, a confinement policy) refuses atomically with the write it guards. A hook
196
+ // that throws or answers ok:false blocks the apply; nothing has been written at this point.
197
+ if (o.preWrite) {
198
+ let pre;
199
+ try { pre = o.preWrite(plan, edits); } catch (err) { pre = { ok: false, detail: `pre-write check failed: ${err.message}` }; }
200
+ gates.preWrite = pre || { ok: false, detail: 'no pre-write result' };
201
+ if (gates.preWrite.ok !== true) return blocked(`pre-write: ${gates.preWrite.detail || 'refused'} (no file was written)`);
202
+ }
194
203
  const base = { scanRoot: o.root, findingId: finding.id || `${finding.file}:${finding.line}`, ruleId: finding.rule || null, vuln: finding.vuln || null, stableId: finding.stableId || null, fixLabel: plan.label || null, verification: verified, findingProvenance: finding.findingProvenance || null };
195
204
  if (edits.length === 1) {
196
205
  res.backup = writeWithBackup(o.root, plan.file, plan.before, plan.after);