@clear-capabilities/agentic-security-scanner 0.160.0 → 0.161.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.
@@ -1 +1 @@
1
- 6a0bcec455607538bd4485ce3b9cd5a266f51daf636b0ef6260cdc3bacf01776 agentic-security.mjs
1
+ faee309bc53830c682a4e967147e6dbc6073b046d2220278b30e64f2e19b1675 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.161.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",
@@ -72,23 +72,23 @@
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",
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: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/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
92
  "test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
93
93
  "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
94
  "eval:independent": "node ../bench/independent-eval/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
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))||[]));
@@ -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);
@@ -175,7 +175,23 @@ function exportedLookup(proj, target, name, depth, seen) {
175
175
  return null;
176
176
  }
177
177
 
178
- function resolveGlobal(proj, mod, name, qual, depth = 0, seen = new Set()) {
178
+ // A top-level resolution (depth 0, nothing seen yet) is a pure function of the module table, which is complete before any function
179
+ // is lowered, so its answer is kept per module: the same name is resolved again at every use site, and each resolution walks the
180
+ // module's imports and the re-export chains of every module it imports (hub modules re-export hundreds of names). Resolutions
181
+ // made inside a re-export chain (depth > 0) depend on the path taken, so they are never kept. Callers treat the result as
182
+ // read-only.
183
+ const RESOLVE_MEMO = new WeakMap(); // module info -> (qualifier, name) -> resolution; a module's memo dies with the module
184
+ function resolveGlobal(proj, mod, name, qual, depth = 0, seen = null) {
185
+ if (depth !== 0 || seen) return resolveGlobalUncached(proj, mod, name, qual, depth, seen || new Set());
186
+ let memo = RESOLVE_MEMO.get(mod);
187
+ if (!memo) { memo = new Map(); RESOLVE_MEMO.set(mod, memo); }
188
+ const key = qual ? `${qual}\0${name}` : name;
189
+ let r = memo.get(key);
190
+ if (r === undefined) { r = resolveGlobalUncached(proj, mod, name, qual, 0, new Set()); memo.set(key, r); }
191
+ return r;
192
+ }
193
+
194
+ function resolveGlobalUncached(proj, mod, name, qual, depth, seen) {
179
195
  if ((!qual || qual === mod.name) && mod.defs.has(name)) return defResult(mod, name);
180
196
  if (depth > HS_IR_LIMITS.maxResolveDepth) return { kind: 'unknown', reason: 'resolution-depth-cap' };
181
197
  const hits = [];
@@ -649,8 +665,7 @@ class FnLowerer {
649
665
  // call to it, not a mere reference: it runs when sequenced.
650
666
  executeRef(v, ln) {
651
667
  if (!v || v.kind !== 'ident' || !v.hs || !v.hs.functionRef || v.hs.closure) return v;
652
- let def = null;
653
- for (const m of this.proj.modules.values()) for (const d of m.defs.values()) if (d.qid === v.hs.functionRef) def = d;
668
+ const def = this.proj.defByQid(v.hs.functionRef);
654
669
  if (!def || def.kind !== 'fun' || def.arity !== 0) return v;
655
670
  return { kind: 'call', callee: v.name, args: [], line: ln, hs: { status: 'resolved', target: v.hs.functionRef, effect: 'io', action: true } };
656
671
  }
@@ -1103,9 +1118,35 @@ class Project {
1103
1118
  this.byFile.get(fn.file).push(fn);
1104
1119
  }
1105
1120
 
1121
+ // The three lookups below used to walk every module on every call (a constructor, a record type, a def by qid), which is
1122
+ // quadratic in project size. They are built once, lazily, on first use: the module table is complete before any function
1123
+ // is lowered, and nothing adds a module, a constructor or a definition afterwards. First-wins / last-wins orders match the
1124
+ // original scans exactly.
1106
1125
  findCon(name) {
1107
- for (const m of this.modules.values()) if (m.cons.has(name)) return m.cons.get(name);
1108
- return null;
1126
+ if (!this._conIndex) {
1127
+ this._conIndex = new Map();
1128
+ for (const m of this.modules.values()) for (const [n, c] of m.cons) if (!this._conIndex.has(n)) this._conIndex.set(n, c);
1129
+ }
1130
+ return this._conIndex.get(name) || null;
1131
+ }
1132
+
1133
+ // Record types declared in the project: type name -> field names (a later declaration of the same type name wins).
1134
+ recordTypes() {
1135
+ if (!this._recTypes) {
1136
+ const recTypes = new Map();
1137
+ for (const m of this.modules.values()) for (const d of (m.parse && m.parse.data) || []) if (d.constructors && d.constructors.some((c) => c.fields && c.fields.length)) recTypes.set(d.name, d.constructors.flatMap((c) => c.fields || []));
1138
+ this._recTypes = recTypes;
1139
+ }
1140
+ return this._recTypes;
1141
+ }
1142
+
1143
+ // A definition by its qid (the last one in module order wins, as the linear scan it replaces did).
1144
+ defByQid(qid) {
1145
+ if (!this._defIndex) {
1146
+ this._defIndex = new Map();
1147
+ for (const m of this.modules.values()) for (const d of m.defs.values()) this._defIndex.set(d.qid, d);
1148
+ }
1149
+ return this._defIndex.get(qid) || null;
1109
1150
  }
1110
1151
 
1111
1152
  instancesOf(cls, method) { return [...(this.instanceIndex.get(`${cls}.${method}`) || [])]; }
@@ -1123,8 +1164,7 @@ function lowerTopFunction(proj, mod, fun, def) {
1123
1164
  fn.hs.textParams = ann.map((a) => a.name);
1124
1165
  // Parameters of an exported function whose type is a record DECLARED IN THE PROJECT: the lineage view treats each such
1125
1166
  // parameter as a source of that record's fields (a customer record handed to an entry point), field by field.
1126
- const recTypes = new Map();
1127
- for (const m of proj.modules.values()) for (const d of (m.parse && m.parse.data) || []) if (d.constructors && d.constructors.some((c) => c.fields && c.fields.length)) recTypes.set(d.name, d.constructors.flatMap((c) => c.fields || []));
1167
+ const recTypes = proj.recordTypes();
1128
1168
  fn.hs.recordParams = [];
1129
1169
  if (exported && sig && Array.isArray(sig.argTypes) && Array.isArray(fn.params)) {
1130
1170
  sig.argTypes.forEach((t, i) => { const m = /^\(?\s*([A-Z][A-Za-z0-9_']*)\s*\)?$/.exec(t); if (m && recTypes.has(m[1]) && fn.params[i]) fn.hs.recordParams.push({ name: fn.params[i], type: m[1], fields: [...new Set(recTypes.get(m[1]))] }); });
@@ -32,7 +32,24 @@ export const GHC_BOOT_PACKAGES = Object.freeze(new Set([
32
32
  'process', 'stm', 'text', 'time', 'transformers', 'unix', 'xhtml', 'terminfo', 'Cabal', 'Cabal-syntax',
33
33
  ]));
34
34
 
35
- export const hackagePurl = (name, version) => `pkg:hackage/${encodeURIComponent(name).replace(/%2D/g, '-')}${version ? `@${encodeURIComponent(version)}` : ''}`;
35
+ /**
36
+ * The `base` series each GHC release series ships (the series only: patch releases of one GHC can ship different `base` patch levels, so
37
+ * the answer is a range, never a single version). A release series absent from this table is not guessed.
38
+ */
39
+ export const GHC_BASE_SERIES = Object.freeze({
40
+ '8.8': '4.13', '8.10': '4.14', '9.0': '4.15', '9.2': '4.16', '9.4': '4.17', '9.6': '4.18', '9.8': '4.19', '9.10': '4.20', '9.12': '4.21', '9.14': '4.22',
41
+ });
42
+
43
+ /** `>=4.18 && <4.19` for a GHC version such as "9.6.4", or null when the series is not in the table. */
44
+ export function baseRangeForGhc(ghcVersion) {
45
+ const m = /^(\d+)\.(\d+)(?:\.|$)/.exec(String(ghcVersion || ''));
46
+ const series = m && GHC_BASE_SERIES[`${m[1]}.${m[2]}`];
47
+ if (!series) return null;
48
+ const [a, b] = series.split('.').map(Number);
49
+ return `>=${a}.${b} && <${a}.${b + 1}`;
50
+ }
51
+
52
+ export const hackagePurl =(name, version) => `pkg:hackage/${encodeURIComponent(name).replace(/%2D/g, '-')}${version ? `@${encodeURIComponent(version)}` : ''}`;
36
53
 
37
54
  /** The exact JSON body an OSV `/v1/query` takes for one component. */
38
55
  export function osvWireQuery(name, version) {
@@ -234,17 +251,46 @@ export function matchComponent(aff, comp) {
234
251
  }
235
252
  // `unbounded` = the manifest declared the dependency with NO version bound (`build-depends: aeson`), which in Cabal means "any version".
236
253
  // That is a declared range like any other (it overlaps an advisory, so possibly-affected), not an absent one.
237
- if (comp.declaredRange || comp.unbounded) {
238
- const declared = comp.declaredRange ? rangeToIntervals(comp.declaredRange) : [iv(null, false, null, false)];
254
+ const projectConstraints = Array.isArray(comp.projectConstraints) ? comp.projectConstraints : [];
255
+ if (comp.declaredRange || comp.unbounded || projectConstraints.length) {
256
+ const eff = effectiveDeclared(comp);
257
+ const declared = eff.intervals;
239
258
  if (declared === null) return { status: 'unknown', reason: 'the declared range could not be parsed' };
240
- if (!declared.length) return { status: 'unknown', reason: 'the declared range admits no version' };
259
+ if (!declared.length) return { status: 'unknown', reason: projectConstraints.length ? `the declared range and the project constraints (${projectConstraints.map((c) => c.text).join(', ')}) admit no common version` : 'the declared range admits no version' };
241
260
  const overlap = intervalsOverlap(declared, aff.intervals);
242
- if (!overlap) return { status: 'not-affected', reason: 'no version allowed by the declared range is affected' };
261
+ if (!overlap) return { status: 'not-affected', reason: `no version allowed by the declared range is affected${eff.note}` };
243
262
  const subset = declared.every((d) => aff.intervals.some((a) => !isEmpty(intersectIntervals(d, a)) && containsInterval(a, d)));
244
- return subset ? { status: 'affected', reason: 'every version allowed by the declared range is affected' } : { status: 'possibly-affected', reason: comp.declaredRange ? 'the declared range allows both affected and unaffected versions; no resolved version is known' : 'the dependency is declared with no version bound, so any version may be selected; no resolved version is known' };
263
+ return subset ? { status: 'affected', reason: `every version allowed by the declared range is affected${eff.note}` } : { status: 'possibly-affected', reason: `${comp.declaredRange ? 'the declared range allows both affected and unaffected versions; no resolved version is known' : 'the dependency is declared with no version bound, so any version may be selected; no resolved version is known'}${eff.note}` };
245
264
  }
246
265
  return { status: 'unknown', reason: 'neither a resolved version nor a declared range is known' };
247
266
  }
267
+
268
+ /**
269
+ * The versions a component may be built with: its declared range, widened by `allow-newer` / `allow-older` (which only ever ADD
270
+ * versions: an upper or lower bound dropped), then narrowed by project `constraints:` (hard solver constraints, so they intersect).
271
+ * Widening comes first because a relaxation applies to the package's own bounds and a constraint to whatever remains.
272
+ * @returns {{intervals: object[]|null, note: string}} null intervals = the declared range could not be parsed
273
+ */
274
+ export function effectiveDeclared(comp) {
275
+ let intervals = comp.declaredRange ? rangeToIntervals(comp.declaredRange) : [iv(null, false, null, false)];
276
+ if (intervals === null) return { intervals: null, note: '' };
277
+ const notes = [];
278
+ const rel = Array.isArray(comp.relaxation) && comp.relaxation.length ? new Set(comp.relaxation.map((r) => r.direction)) : null;
279
+ if (rel && comp.declaredRange) {
280
+ intervals = intervals.map((x) => iv(rel.has('older') ? ZERO : x.lo, rel.has('older') ? true : x.loInc, rel.has('newer') ? null : x.hi, rel.has('newer') ? false : x.hiInc));
281
+ notes.push(`widened by ${[...rel].map((d) => `allow-${d}`).join(' and ')}, so the declared bounds are not authoritative`);
282
+ }
283
+ const cons = Array.isArray(comp.projectConstraints) ? comp.projectConstraints : [];
284
+ for (const c of cons) {
285
+ const ci = rangeToIntervals(c.text);
286
+ if (ci === null) continue;
287
+ const out = [];
288
+ for (const a of intervals) for (const b of ci) out.push(intersectIntervals(a, b));
289
+ intervals = normalizeIntervals(out);
290
+ }
291
+ if (cons.length) notes.push(`narrowed by project constraints: ${cons.map((c) => c.text).join(', ')}`);
292
+ return { intervals, note: notes.length ? ` (${notes.join('; ')})` : '' };
293
+ }
248
294
  function containsInterval(outer, inner) {
249
295
  const loOk = outer.lo === null || (inner.lo !== null && (cmp(inner.lo, outer.lo) > 0 || (cmp(inner.lo, outer.lo) === 0 && (outer.loInc || !inner.loInc))));
250
296
  const hiOk = outer.hi === null || (inner.hi !== null && (cmp(inner.hi, outer.hi) < 0 || (cmp(inner.hi, outer.hi) === 0 && (outer.hiInc || !inner.hiInc))));
@@ -291,6 +337,10 @@ export function evaluateComponents(components, db, opts = {}) {
291
337
  ...(ghc ? { remediation: `${comp.name} is provided by the compiler: upgrade GHC (the fix is in ${comp.name} ${aff.fixedIn.join(', ') || '(no fixed version published)'}), not a Cabal dependency bound.` } : { remediation: aff.fixedIn.length ? `Upgrade ${comp.name} to ${aff.fixedIn.join(' or ')}.` : `No fixed version of ${comp.name} is published; remove or replace the dependency.` }),
292
338
  kev: kevHit, epss: epssVals.length ? Math.max(...epssVals) : 'unknown',
293
339
  file: comp.manifest || null, line: comp.line || null, versionSource: comp.versionSource || null,
340
+ carriedBy: (comp.carriers && comp.carriers.length ? comp.carriers : [{ file: comp.manifest || null, line: comp.line || null, target: comp.target || null, componentKind: comp.componentKind || null, declaredRange: comp.declaredRange || null }]),
341
+ ...(comp.projectConstraints && comp.projectConstraints.length ? { projectConstraints: comp.projectConstraints } : {}),
342
+ ...(comp.relaxation && comp.relaxation.length ? { relaxation: comp.relaxation } : {}),
343
+ ...(comp.compilerDerived ? { compilerDerived: comp.compilerDerived } : {}),
294
344
  dataSource: { feed: db.source, generatedAt: db.generatedAt, integrity: db.integrity, feedStatus: feed.status },
295
345
  language: 'haskell', capability: 'sca', analysisKind: 'application', evidenceKind: 'manifest',
296
346
  confidence: m.status === 'affected' ? 0.9 : 0.5,
@@ -299,7 +349,37 @@ export function evaluateComponents(components, db, opts = {}) {
299
349
  }
300
350
  if (!any && !statuses.some((s) => s.name === comp.name && s.status !== 'not-affected')) statuses.push({ ...base, status: 'not-affected' });
301
351
  }
302
- return { findings, statuses, feed };
352
+ return { findings: collapseToWorst(findings), statuses, feed };
353
+ }
354
+
355
+ const STATUS_RANK = { affected: 3, 'possibly-affected': 2, unknown: 1 };
356
+ /**
357
+ * One finding per (package, scope, advisory) carrying the WORST status among the declared uses of that package, with every component that
358
+ * carries that worst status listed in `carriedBy`. Distinct declared ranges used to be collapsed by name and scope with the first one
359
+ * winning, which hid a worse use behind a milder one (found by the live Hackage feed bench). The milder uses are not dropped from the
360
+ * record: `otherUses` lists them, and every use keeps its own row in `statuses`.
361
+ */
362
+ export function collapseToWorst(findings) {
363
+ const groups = new Map();
364
+ for (const f of findings) {
365
+ const k = `${f.name}\u0000${f.scope || ''}\u0000${f.osvId}\u0000${(f.ids && f.ids[0]) || ''}`; // two records for one advisory stay two findings
366
+ if (!groups.has(k)) groups.set(k, []);
367
+ groups.get(k).push(f);
368
+ }
369
+ const out = [];
370
+ for (const members of groups.values()) {
371
+ if (members.length === 1) { out.push(members[0]); continue; }
372
+ const rank = (f) => STATUS_RANK[f.matchStatus] || 0;
373
+ const worst = Math.max(...members.map(rank));
374
+ const top = members.filter((f) => rank(f) === worst);
375
+ const rest = members.filter((f) => rank(f) !== worst);
376
+ const carried = [];
377
+ for (const f of top) for (const c of f.carriedBy || []) if (!carried.some((x) => x.file === c.file && x.line === c.line && x.target === c.target && x.declaredRange === c.declaredRange)) carried.push(c);
378
+ const lead = { ...top[0], carriedBy: carried };
379
+ if (rest.length) lead.otherUses = rest.map((f) => ({ matchStatus: f.matchStatus, declaredRange: f.declaredRange, version: f.version, carriedBy: f.carriedBy }));
380
+ out.push(lead);
381
+ }
382
+ return out;
303
383
  }
304
384
 
305
385
  // ── reachability (imports) ───────────────────────────────────────────────────