@complior/engine 0.9.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.
- package/.well-known/ai-compliance.json +16 -0
- package/COMPLIANCE.md +64 -0
- package/data/data-integrity.test.ts +75 -0
- package/data/eval/eval-mappings.json +33 -0
- package/data/llm/model-pricing.json +15 -0
- package/data/llm/model-routing.json +36 -0
- package/data/onboarding/risk-profile.json +17 -0
- package/data/regulations/eu-ai-act/README.md +245 -0
- package/data/regulations/eu-ai-act/applicability-tree.json +160 -0
- package/data/regulations/eu-ai-act/cross-mapping.json +175 -0
- package/data/regulations/eu-ai-act/localization.json +186 -0
- package/data/regulations/eu-ai-act/obligations.json +3981 -0
- package/data/regulations/eu-ai-act/regulation-meta.json +482 -0
- package/data/regulations/eu-ai-act/scoring.json +342 -0
- package/data/regulations/eu-ai-act/technical-requirements.json +2590 -0
- package/data/regulations/eu-ai-act/timeline.json +160 -0
- package/data/regulations/jurisdictions/at.json +15 -0
- package/data/regulations/jurisdictions/be.json +15 -0
- package/data/regulations/jurisdictions/bg.json +15 -0
- package/data/regulations/jurisdictions/cy.json +15 -0
- package/data/regulations/jurisdictions/cz.json +15 -0
- package/data/regulations/jurisdictions/de.json +15 -0
- package/data/regulations/jurisdictions/dk.json +15 -0
- package/data/regulations/jurisdictions/ee.json +15 -0
- package/data/regulations/jurisdictions/es.json +15 -0
- package/data/regulations/jurisdictions/fi.json +15 -0
- package/data/regulations/jurisdictions/fr.json +15 -0
- package/data/regulations/jurisdictions/gr.json +15 -0
- package/data/regulations/jurisdictions/hr.json +15 -0
- package/data/regulations/jurisdictions/hu.json +15 -0
- package/data/regulations/jurisdictions/ie.json +15 -0
- package/data/regulations/jurisdictions/is.json +15 -0
- package/data/regulations/jurisdictions/it.json +15 -0
- package/data/regulations/jurisdictions/li.json +15 -0
- package/data/regulations/jurisdictions/lt.json +15 -0
- package/data/regulations/jurisdictions/lu.json +15 -0
- package/data/regulations/jurisdictions/lv.json +15 -0
- package/data/regulations/jurisdictions/mt.json +15 -0
- package/data/regulations/jurisdictions/nl.json +15 -0
- package/data/regulations/jurisdictions/no.json +15 -0
- package/data/regulations/jurisdictions/pl.json +15 -0
- package/data/regulations/jurisdictions/pt.json +15 -0
- package/data/regulations/jurisdictions/ro.json +15 -0
- package/data/regulations/jurisdictions/se.json +15 -0
- package/data/regulations/jurisdictions/si.json +15 -0
- package/data/regulations/jurisdictions/sk.json +15 -0
- package/data/scanner/check-id-categories.json +81 -0
- package/data/scanner/confidence-params.json +16 -0
- package/data/scanner/limits.json +4 -0
- package/data/schemas/http-contract-sample.json +79 -0
- package/data/schemas/http-contract.json +144 -0
- package/data/semgrep-rules/bare-call.yaml +37 -0
- package/data/semgrep-rules/injection.yaml +73 -0
- package/data/semgrep-rules/missing-error-handling.yaml +58 -0
- package/data/semgrep-rules/unsafe-deser.yaml +65 -0
- package/data/templates/eu-ai-act/ai-literacy.md +184 -0
- package/data/templates/eu-ai-act/art5-screening.md +131 -0
- package/data/templates/eu-ai-act/data-governance.md +145 -0
- package/data/templates/eu-ai-act/declaration-of-conformity.md +161 -0
- package/data/templates/eu-ai-act/fria.md +127 -0
- package/data/templates/eu-ai-act/gpai-systemic-risk.md +150 -0
- package/data/templates/eu-ai-act/gpai-transparency.md +166 -0
- package/data/templates/eu-ai-act/incident-report.md +188 -0
- package/data/templates/eu-ai-act/instructions-for-use.md +202 -0
- package/data/templates/eu-ai-act/monitoring-policy.md +110 -0
- package/data/templates/eu-ai-act/qms.md +180 -0
- package/data/templates/eu-ai-act/risk-management-system.md +123 -0
- package/data/templates/eu-ai-act/technical-documentation.md +287 -0
- package/data/templates/eu-ai-act/worker-notification.md +143 -0
- package/data/templates/policies/biometrics-ai-policy.md +214 -0
- package/data/templates/policies/critical-infra-ai-policy.md +228 -0
- package/data/templates/policies/education-ai-policy.md +184 -0
- package/data/templates/policies/finance-ai-policy.md +191 -0
- package/data/templates/policies/healthcare-ai-policy.md +197 -0
- package/data/templates/policies/hr-ai-policy.md +178 -0
- package/data/templates/policies/legal-ai-policy.md +189 -0
- package/data/templates/policies/migration-ai-policy.md +239 -0
- package/engine.log +7 -0
- package/package.json +74 -0
- package/src/composition-root.ts +791 -0
- package/src/data/eval/conformity-tests.test.ts +122 -0
- package/src/data/eval/ct-1-transparency.ts +106 -0
- package/src/data/eval/ct-10-gpai.ts +25 -0
- package/src/data/eval/ct-11-industry.ts +42 -0
- package/src/data/eval/ct-2-oversight.ts +41 -0
- package/src/data/eval/ct-3-explanation.ts +14 -0
- package/src/data/eval/ct-4-bias.ts +83 -0
- package/src/data/eval/ct-5-accuracy.ts +41 -0
- package/src/data/eval/ct-6-robustness.ts +81 -0
- package/src/data/eval/ct-7-prohibited.ts +52 -0
- package/src/data/eval/ct-8-logging.ts +68 -0
- package/src/data/eval/ct-9-risk-awareness.ts +33 -0
- package/src/data/eval/deterministic-evaluator.ts +120 -0
- package/src/data/eval/index.ts +55 -0
- package/src/data/eval/judge-prompts.ts +146 -0
- package/src/data/eval/llm-judged-tests.ts +279 -0
- package/src/data/eval/llm-tests.test.ts +83 -0
- package/src/data/eval/remediation/ct-1-transparency.ts +91 -0
- package/src/data/eval/remediation/ct-10-gpai.ts +94 -0
- package/src/data/eval/remediation/ct-11-industry.ts +94 -0
- package/src/data/eval/remediation/ct-2-oversight.ts +71 -0
- package/src/data/eval/remediation/ct-3-explanation.ts +70 -0
- package/src/data/eval/remediation/ct-4-bias.ts +70 -0
- package/src/data/eval/remediation/ct-5-accuracy.ts +70 -0
- package/src/data/eval/remediation/ct-6-robustness.ts +70 -0
- package/src/data/eval/remediation/ct-7-prohibited.ts +94 -0
- package/src/data/eval/remediation/ct-8-logging.ts +94 -0
- package/src/data/eval/remediation/ct-9-risk-awareness.ts +94 -0
- package/src/data/eval/remediation/index.ts +89 -0
- package/src/data/eval/remediation/owasp-art5.ts +15 -0
- package/src/data/eval/remediation/owasp-llm01.ts +72 -0
- package/src/data/eval/remediation/owasp-llm02.ts +72 -0
- package/src/data/eval/remediation/owasp-llm03.ts +15 -0
- package/src/data/eval/remediation/owasp-llm04.ts +15 -0
- package/src/data/eval/remediation/owasp-llm05.ts +15 -0
- package/src/data/eval/remediation/owasp-llm06.ts +15 -0
- package/src/data/eval/remediation/owasp-llm07.ts +15 -0
- package/src/data/eval/remediation/owasp-llm08.ts +15 -0
- package/src/data/eval/remediation/owasp-llm09.ts +15 -0
- package/src/data/eval/remediation/owasp-llm10.ts +15 -0
- package/src/data/eval/remediation/remediation.test.ts +229 -0
- package/src/data/eval/remediation/test-mapping.ts +290 -0
- package/src/data/eval/security-rubrics.ts +381 -0
- package/src/data/finding-explanations.json +453 -0
- package/src/data/industry-patterns.ts +161 -0
- package/src/data/registry-cards.ts +368 -0
- package/src/data/regulation/index.ts +5 -0
- package/src/data/regulation/jurisdiction-data.test.ts +73 -0
- package/src/data/regulation/jurisdiction-data.ts +65 -0
- package/src/data/regulation/regulation-data.ts +19 -0
- package/src/data/regulation/regulation-loader.test.ts +107 -0
- package/src/data/regulation/regulation-loader.ts +56 -0
- package/src/data/scanner-constants.ts +46 -0
- package/src/data/schemas/schemas-core.ts +140 -0
- package/src/data/schemas/schemas-supplementary.ts +211 -0
- package/src/data/schemas/schemas.ts +28 -0
- package/src/data/security/attack-probes.test.ts +62 -0
- package/src/data/security/attack-probes.ts +496 -0
- package/src/data/security/eu-ai-act-security.ts +40 -0
- package/src/data/security/index.ts +19 -0
- package/src/data/security/mitre-atlas.test.ts +43 -0
- package/src/data/security/mitre-atlas.ts +93 -0
- package/src/data/security/nist-ai-rmf.ts +43 -0
- package/src/data/security/owasp-llm-top10.test.ts +60 -0
- package/src/data/security/owasp-llm-top10.ts +138 -0
- package/src/data/template-registry.ts +53 -0
- package/src/data/tool-versions.json +22 -0
- package/src/domain/audit/audit-package.test.ts +152 -0
- package/src/domain/audit/audit-package.ts +166 -0
- package/src/domain/audit/audit-trail.test.ts +121 -0
- package/src/domain/audit/audit-trail.ts +174 -0
- package/src/domain/audit/index.ts +8 -0
- package/src/domain/audit/permissions-matrix.test.ts +136 -0
- package/src/domain/audit/permissions-matrix.ts +121 -0
- package/src/domain/certification/adversarial/bias-tests.ts +95 -0
- package/src/domain/certification/adversarial/evaluators.ts +304 -0
- package/src/domain/certification/adversarial/index.ts +11 -0
- package/src/domain/certification/adversarial/prompt-injection.ts +103 -0
- package/src/domain/certification/adversarial/safety-boundary.ts +132 -0
- package/src/domain/certification/aiuc1-readiness.test.ts +236 -0
- package/src/domain/certification/aiuc1-readiness.ts +298 -0
- package/src/domain/certification/aiuc1-requirements.ts +235 -0
- package/src/domain/certification/index.ts +10 -0
- package/src/domain/certification/redteam-runner.test.ts +97 -0
- package/src/domain/certification/redteam-runner.ts +205 -0
- package/src/domain/certification/test-runner.test.ts +232 -0
- package/src/domain/certification/test-runner.ts +289 -0
- package/src/domain/cost/cost-estimator.test.ts +187 -0
- package/src/domain/cost/cost-estimator.ts +133 -0
- package/src/domain/disclaimer.test.ts +52 -0
- package/src/domain/disclaimer.ts +39 -0
- package/src/domain/documents/ai-enricher.test.ts +120 -0
- package/src/domain/documents/ai-enricher.ts +159 -0
- package/src/domain/documents/document-generator.test.ts +318 -0
- package/src/domain/documents/document-generator.ts +239 -0
- package/src/domain/documents/index.ts +9 -0
- package/src/domain/documents/passport-helpers.ts +25 -0
- package/src/domain/documents/policy-generator.test.ts +252 -0
- package/src/domain/documents/policy-generator.ts +94 -0
- package/src/domain/documents/worker-notification-generator.test.ts +162 -0
- package/src/domain/documents/worker-notification-generator.ts +141 -0
- package/src/domain/eval/adapters/adapter-port.ts +94 -0
- package/src/domain/eval/adapters/adapters.test.ts +303 -0
- package/src/domain/eval/adapters/anthropic-adapter.ts +57 -0
- package/src/domain/eval/adapters/auto-detect.ts +104 -0
- package/src/domain/eval/adapters/create-chat-adapter.ts +106 -0
- package/src/domain/eval/adapters/custom-adapter.ts +74 -0
- package/src/domain/eval/adapters/http-adapter.ts +66 -0
- package/src/domain/eval/adapters/index.ts +7 -0
- package/src/domain/eval/adapters/ollama-adapter.ts +48 -0
- package/src/domain/eval/adapters/openai-adapter.ts +58 -0
- package/src/domain/eval/adapters/with-timeout.ts +25 -0
- package/src/domain/eval/conformity-score.test.ts +161 -0
- package/src/domain/eval/conformity-score.ts +135 -0
- package/src/domain/eval/eval-constants.ts +55 -0
- package/src/domain/eval/eval-evidence.test.ts +85 -0
- package/src/domain/eval/eval-evidence.ts +103 -0
- package/src/domain/eval/eval-fix-generator.test.ts +421 -0
- package/src/domain/eval/eval-fix-generator.ts +205 -0
- package/src/domain/eval/eval-passport.test.ts +82 -0
- package/src/domain/eval/eval-passport.ts +89 -0
- package/src/domain/eval/eval-remediation-report.test.ts +682 -0
- package/src/domain/eval/eval-remediation-report.ts +170 -0
- package/src/domain/eval/eval-report.ts +108 -0
- package/src/domain/eval/eval-runner.test.ts +609 -0
- package/src/domain/eval/eval-runner.ts +593 -0
- package/src/domain/eval/eval-to-findings.test.ts +293 -0
- package/src/domain/eval/eval-to-findings.ts +83 -0
- package/src/domain/eval/index.ts +31 -0
- package/src/domain/eval/llm-judge.test.ts +139 -0
- package/src/domain/eval/llm-judge.ts +168 -0
- package/src/domain/eval/remediation-types.ts +90 -0
- package/src/domain/eval/security-integration.test.ts +196 -0
- package/src/domain/eval/security-integration.ts +136 -0
- package/src/domain/eval/types.test.ts +173 -0
- package/src/domain/eval/types.ts +244 -0
- package/src/domain/eval/verdict-utils.ts +45 -0
- package/src/domain/fixer/create-fixer.ts +101 -0
- package/src/domain/fixer/diff.ts +70 -0
- package/src/domain/fixer/fix-history.ts +23 -0
- package/src/domain/fixer/fixer.test.ts +306 -0
- package/src/domain/fixer/index.ts +9 -0
- package/src/domain/fixer/strategies/bandit-fix.ts +61 -0
- package/src/domain/fixer/strategies/bias-testing.ts +49 -0
- package/src/domain/fixer/strategies/ci-compliance.ts +57 -0
- package/src/domain/fixer/strategies/content-marking.ts +45 -0
- package/src/domain/fixer/strategies/cve-upgrade.ts +66 -0
- package/src/domain/fixer/strategies/data-governance.ts +65 -0
- package/src/domain/fixer/strategies/disclosure.ts +69 -0
- package/src/domain/fixer/strategies/doc-code-sync.ts +53 -0
- package/src/domain/fixer/strategies/documentation.ts +59 -0
- package/src/domain/fixer/strategies/error-handler.ts +63 -0
- package/src/domain/fixer/strategies/hitl-gate.ts +67 -0
- package/src/domain/fixer/strategies/index.ts +61 -0
- package/src/domain/fixer/strategies/kill-switch-test.ts +85 -0
- package/src/domain/fixer/strategies/kill-switch.ts +53 -0
- package/src/domain/fixer/strategies/license-fix.ts +57 -0
- package/src/domain/fixer/strategies/log-retention.ts +40 -0
- package/src/domain/fixer/strategies/logging.ts +59 -0
- package/src/domain/fixer/strategies/metadata.ts +45 -0
- package/src/domain/fixer/strategies/permission-guard.ts +84 -0
- package/src/domain/fixer/strategies/record-keeping.ts +69 -0
- package/src/domain/fixer/strategies/secret-rotation.ts +52 -0
- package/src/domain/fixer/strategies.test.ts +341 -0
- package/src/domain/fixer/template-engine.test.ts +64 -0
- package/src/domain/fixer/template-engine.ts +38 -0
- package/src/domain/fixer/types.ts +88 -0
- package/src/domain/frameworks/aiuc1-framework.test.ts +159 -0
- package/src/domain/frameworks/aiuc1-framework.ts +126 -0
- package/src/domain/frameworks/collect-foundation-metrics.test.ts +96 -0
- package/src/domain/frameworks/collect-foundation-metrics.ts +34 -0
- package/src/domain/frameworks/eu-ai-act-framework.test.ts +117 -0
- package/src/domain/frameworks/eu-ai-act-framework.ts +100 -0
- package/src/domain/frameworks/framework-registry.test.ts +91 -0
- package/src/domain/frameworks/framework-registry.ts +38 -0
- package/src/domain/frameworks/index.ts +8 -0
- package/src/domain/frameworks/mitre-atlas-framework.test.ts +53 -0
- package/src/domain/frameworks/mitre-atlas-framework.ts +53 -0
- package/src/domain/frameworks/owasp-llm-framework.test.ts +77 -0
- package/src/domain/frameworks/owasp-llm-framework.ts +54 -0
- package/src/domain/frameworks/score-plugin-framework.ts +117 -0
- package/src/domain/fria/fria-generator.test.ts +273 -0
- package/src/domain/fria/fria-generator.ts +366 -0
- package/src/domain/import/promptfoo-importer.test.ts +103 -0
- package/src/domain/import/promptfoo-importer.ts +151 -0
- package/src/domain/onboarding/guided-onboarding.test.ts +144 -0
- package/src/domain/onboarding/guided-onboarding.ts +135 -0
- package/src/domain/passport/builder/domain-mapper.ts +9 -0
- package/src/domain/passport/builder/manifest-builder.test.ts +546 -0
- package/src/domain/passport/builder/manifest-builder.ts +535 -0
- package/src/domain/passport/builder/manifest-diff.test.ts +105 -0
- package/src/domain/passport/builder/manifest-diff.ts +89 -0
- package/src/domain/passport/builder/manifest-files.ts +17 -0
- package/src/domain/passport/crypto-signer.test.ts +93 -0
- package/src/domain/passport/crypto-signer.ts +157 -0
- package/src/domain/passport/discovery/agent-discovery.test.ts +296 -0
- package/src/domain/passport/discovery/agent-discovery.ts +325 -0
- package/src/domain/passport/discovery/autonomy-analyzer.test.ts +141 -0
- package/src/domain/passport/discovery/autonomy-analyzer.ts +113 -0
- package/src/domain/passport/discovery/permission-scanner.test.ts +191 -0
- package/src/domain/passport/discovery/permission-scanner.ts +414 -0
- package/src/domain/passport/export/a2a-mapper.ts +75 -0
- package/src/domain/passport/export/aiuc1-mapper.ts +126 -0
- package/src/domain/passport/export/export.test.ts +207 -0
- package/src/domain/passport/export/index.ts +41 -0
- package/src/domain/passport/export/nist-mapper.ts +227 -0
- package/src/domain/passport/import/a2a-importer.test.ts +133 -0
- package/src/domain/passport/import/a2a-importer.ts +156 -0
- package/src/domain/passport/import/index.ts +2 -0
- package/src/domain/passport/index.ts +32 -0
- package/src/domain/passport/obligation-field-map.test.ts +113 -0
- package/src/domain/passport/obligation-field-map.ts +117 -0
- package/src/domain/passport/passport-validator.test.ts +156 -0
- package/src/domain/passport/passport-validator.ts +126 -0
- package/src/domain/passport/scan-to-compliance.test.ts +336 -0
- package/src/domain/passport/scan-to-compliance.ts +166 -0
- package/src/domain/passport/test-generator.test.ts +93 -0
- package/src/domain/passport/test-generator.ts +136 -0
- package/src/domain/proxy/index.ts +11 -0
- package/src/domain/proxy/json-rpc.test.ts +72 -0
- package/src/domain/proxy/json-rpc.ts +53 -0
- package/src/domain/proxy/policy-engine.test.ts +259 -0
- package/src/domain/proxy/policy-engine.ts +137 -0
- package/src/domain/proxy/proxy-bridge.ts +125 -0
- package/src/domain/proxy/proxy-interceptor.test.ts +184 -0
- package/src/domain/proxy/proxy-interceptor.ts +120 -0
- package/src/domain/proxy/proxy-types.ts +35 -0
- package/src/domain/registry/compute-agent-score.test.ts +279 -0
- package/src/domain/registry/compute-agent-score.ts +162 -0
- package/src/domain/reporter/audit-report.test.ts +87 -0
- package/src/domain/reporter/audit-report.ts +116 -0
- package/src/domain/reporter/badge-generator.test.ts +54 -0
- package/src/domain/reporter/badge-generator.ts +40 -0
- package/src/domain/reporter/compliance-md.ts +45 -0
- package/src/domain/reporter/index.ts +7 -0
- package/src/domain/reporter/pdf-renderer.ts +282 -0
- package/src/domain/reporter/share.test.ts +92 -0
- package/src/domain/reporter/share.ts +80 -0
- package/src/domain/scanner/ast/swc-analyzer.test.ts +49 -0
- package/src/domain/scanner/ast/swc-analyzer.ts +124 -0
- package/src/domain/scanner/attestations.ts +97 -0
- package/src/domain/scanner/checks/ai-disclosure.test.ts +90 -0
- package/src/domain/scanner/checks/ai-disclosure.ts +54 -0
- package/src/domain/scanner/checks/ai-literacy.ts +163 -0
- package/src/domain/scanner/checks/behavioral-constraints.test.ts +167 -0
- package/src/domain/scanner/checks/behavioral-constraints.ts +86 -0
- package/src/domain/scanner/checks/compliance-metadata.ts +63 -0
- package/src/domain/scanner/checks/content-marking.ts +74 -0
- package/src/domain/scanner/checks/dep-deep-scan.test.ts +318 -0
- package/src/domain/scanner/checks/dep-deep-scan.ts +137 -0
- package/src/domain/scanner/checks/documentation.test.ts +88 -0
- package/src/domain/scanner/checks/documentation.ts +79 -0
- package/src/domain/scanner/checks/git-history.test.ts +120 -0
- package/src/domain/scanner/checks/git-history.ts +163 -0
- package/src/domain/scanner/checks/gpai-systemic-risk.test.ts +84 -0
- package/src/domain/scanner/checks/gpai-systemic-risk.ts +98 -0
- package/src/domain/scanner/checks/gpai-transparency.ts +94 -0
- package/src/domain/scanner/checks/index.ts +28 -0
- package/src/domain/scanner/checks/industry/index.ts +40 -0
- package/src/domain/scanner/checks/industry/industry.test.ts +287 -0
- package/src/domain/scanner/checks/interaction-logging.test.ts +113 -0
- package/src/domain/scanner/checks/interaction-logging.ts +142 -0
- package/src/domain/scanner/checks/nhi-scanner.test.ts +158 -0
- package/src/domain/scanner/checks/nhi-scanner.ts +78 -0
- package/src/domain/scanner/checks/passport-completeness.test.ts +127 -0
- package/src/domain/scanner/checks/passport-completeness.ts +82 -0
- package/src/domain/scanner/checks/passport-presence.test.ts +56 -0
- package/src/domain/scanner/checks/passport-presence.ts +78 -0
- package/src/domain/scanner/checks/pattern-check-factory.ts +70 -0
- package/src/domain/scanner/checks/permission-scanner.test.ts +279 -0
- package/src/domain/scanner/checks/permission-scanner.ts +90 -0
- package/src/domain/scanner/checks/presence-check-factory.test.ts +124 -0
- package/src/domain/scanner/checks/presence-check-factory.ts +275 -0
- package/src/domain/scanner/compliance-diff.test.ts +165 -0
- package/src/domain/scanner/compliance-diff.ts +138 -0
- package/src/domain/scanner/confidence.test.ts +235 -0
- package/src/domain/scanner/confidence.ts +156 -0
- package/src/domain/scanner/constants.ts +13 -0
- package/src/domain/scanner/create-scanner.ts +573 -0
- package/src/domain/scanner/cross-layer.test.ts +372 -0
- package/src/domain/scanner/cross-layer.ts +232 -0
- package/src/domain/scanner/data/ai-packages.ts +82 -0
- package/src/domain/scanner/debt-calculator.test.ts +89 -0
- package/src/domain/scanner/debt-calculator.ts +111 -0
- package/src/domain/scanner/drift.test.ts +191 -0
- package/src/domain/scanner/drift.ts +73 -0
- package/src/domain/scanner/evidence-store.test.ts +207 -0
- package/src/domain/scanner/evidence-store.ts +195 -0
- package/src/domain/scanner/evidence.test.ts +104 -0
- package/src/domain/scanner/evidence.ts +71 -0
- package/src/domain/scanner/external/bandit-runner.test.ts +45 -0
- package/src/domain/scanner/external/bandit-runner.ts +90 -0
- package/src/domain/scanner/external/checks.ts +321 -0
- package/src/domain/scanner/external/dedup.test.ts +79 -0
- package/src/domain/scanner/external/dedup.ts +94 -0
- package/src/domain/scanner/external/detect-secrets-runner.test.ts +58 -0
- package/src/domain/scanner/external/detect-secrets-runner.ts +81 -0
- package/src/domain/scanner/external/external-scanner.test.ts +221 -0
- package/src/domain/scanner/external/external-scanner.ts +36 -0
- package/src/domain/scanner/external/finding-mapper.test.ts +95 -0
- package/src/domain/scanner/external/finding-mapper.ts +138 -0
- package/src/domain/scanner/external/index.ts +15 -0
- package/src/domain/scanner/external/mappings.ts +93 -0
- package/src/domain/scanner/external/modelscan-runner.test.ts +35 -0
- package/src/domain/scanner/external/modelscan-runner.ts +101 -0
- package/src/domain/scanner/external/path-utils.ts +8 -0
- package/src/domain/scanner/external/runner-port.ts +45 -0
- package/src/domain/scanner/external/semgrep-runner.test.ts +52 -0
- package/src/domain/scanner/external/semgrep-runner.ts +94 -0
- package/src/domain/scanner/external/types.ts +32 -0
- package/src/domain/scanner/finding-attribution.test.ts +444 -0
- package/src/domain/scanner/finding-attribution.ts +195 -0
- package/src/domain/scanner/finding-explainer.test.ts +157 -0
- package/src/domain/scanner/finding-explainer.ts +73 -0
- package/src/domain/scanner/fix-diff-builder.test.ts +272 -0
- package/src/domain/scanner/fix-diff-builder.ts +477 -0
- package/src/domain/scanner/import-graph.test.ts +162 -0
- package/src/domain/scanner/import-graph.ts +198 -0
- package/src/domain/scanner/languages/adapter.test.ts +105 -0
- package/src/domain/scanner/languages/adapter.ts +239 -0
- package/src/domain/scanner/layers/index.ts +24 -0
- package/src/domain/scanner/layers/layer1-files.ts +54 -0
- package/src/domain/scanner/layers/layer2-docs.test.ts +1207 -0
- package/src/domain/scanner/layers/layer2-docs.ts +297 -0
- package/src/domain/scanner/layers/layer2-parsing.ts +217 -0
- package/src/domain/scanner/layers/layer3-config.test.ts +187 -0
- package/src/domain/scanner/layers/layer3-config.ts +279 -0
- package/src/domain/scanner/layers/layer3-parsers.ts +73 -0
- package/src/domain/scanner/layers/layer4-patterns.test.ts +397 -0
- package/src/domain/scanner/layers/layer4-patterns.ts +216 -0
- package/src/domain/scanner/layers/layer5-docs.test.ts +99 -0
- package/src/domain/scanner/layers/layer5-docs.ts +250 -0
- package/src/domain/scanner/layers/layer5-llm.test.ts +146 -0
- package/src/domain/scanner/layers/layer5-llm.ts +262 -0
- package/src/domain/scanner/layers/layer5-targeted.test.ts +93 -0
- package/src/domain/scanner/layers/layer5-targeted.ts +233 -0
- package/src/domain/scanner/layers/lockfile-parsers.test.ts +320 -0
- package/src/domain/scanner/layers/lockfile-parsers.ts +184 -0
- package/src/domain/scanner/regulation-version.test.ts +54 -0
- package/src/domain/scanner/regulation-version.ts +23 -0
- package/src/domain/scanner/role-filter.test.ts +116 -0
- package/src/domain/scanner/role-filter.ts +51 -0
- package/src/domain/scanner/rules/banned-packages-data.ts +553 -0
- package/src/domain/scanner/rules/banned-packages-sdk.ts +65 -0
- package/src/domain/scanner/rules/banned-packages.test.ts +249 -0
- package/src/domain/scanner/rules/banned-packages.ts +55 -0
- package/src/domain/scanner/rules/comment-filter.test.ts +115 -0
- package/src/domain/scanner/rules/comment-filter.ts +297 -0
- package/src/domain/scanner/rules/index.ts +9 -0
- package/src/domain/scanner/rules/nhi-patterns.test.ts +128 -0
- package/src/domain/scanner/rules/nhi-patterns.ts +60 -0
- package/src/domain/scanner/rules/pattern-rules.ts +1152 -0
- package/src/domain/scanner/sbom.test.ts +136 -0
- package/src/domain/scanner/sbom.ts +103 -0
- package/src/domain/scanner/scan-cache.test.ts +136 -0
- package/src/domain/scanner/scan-cache.ts +115 -0
- package/src/domain/scanner/scanner.test.ts +125 -0
- package/src/domain/scanner/score-calculator.test.ts +363 -0
- package/src/domain/scanner/score-calculator.ts +189 -0
- package/src/domain/scanner/security-score.test.ts +107 -0
- package/src/domain/scanner/security-score.ts +116 -0
- package/src/domain/scanner/source-filter.ts +24 -0
- package/src/domain/scanner/validators.ts +223 -0
- package/src/domain/shared/compliance-constants.ts +48 -0
- package/src/domain/shared/disclosure-patterns.ts +16 -0
- package/src/domain/shared/index.ts +6 -0
- package/src/domain/shared/parse-dependencies.ts +21 -0
- package/src/domain/supply-chain/dependency-analyzer.ts +138 -0
- package/src/domain/supply-chain/index.ts +3 -0
- package/src/domain/supply-chain/supply-chain.test.ts +211 -0
- package/src/domain/supply-chain/types.ts +32 -0
- package/src/domain/whatif/config-fixer.ts +187 -0
- package/src/domain/whatif/index.ts +6 -0
- package/src/domain/whatif/scenario-engine.ts +121 -0
- package/src/domain/whatif/simulate-actions.test.ts +161 -0
- package/src/domain/whatif/simulate-actions.ts +114 -0
- package/src/domain/whatif/whatif.test.ts +135 -0
- package/src/e2e/gaps-e2e.test.ts +259 -0
- package/src/e2e/smoke.test.ts +101 -0
- package/src/hooks/hooks-export.test.ts +81 -0
- package/src/hooks/installer.ts +113 -0
- package/src/http/cors.test.ts +38 -0
- package/src/http/create-router.ts +259 -0
- package/src/http/routes/agent.route.ts +380 -0
- package/src/http/routes/audit.route.ts +66 -0
- package/src/http/routes/badge.route.ts +23 -0
- package/src/http/routes/cert.route.ts +66 -0
- package/src/http/routes/chat.route.ts +228 -0
- package/src/http/routes/cost.route.ts +33 -0
- package/src/http/routes/debt.route.ts +29 -0
- package/src/http/routes/disclaimer.route.ts +64 -0
- package/src/http/routes/eval.route.ts +161 -0
- package/src/http/routes/events.route.test.ts +108 -0
- package/src/http/routes/events.route.ts +71 -0
- package/src/http/routes/external-scan.route.ts +24 -0
- package/src/http/routes/file.route.ts +54 -0
- package/src/http/routes/fix.route.ts +219 -0
- package/src/http/routes/frameworks.route.test.ts +66 -0
- package/src/http/routes/frameworks.route.ts +36 -0
- package/src/http/routes/git.route.ts +27 -0
- package/src/http/routes/guided-onboarding.route.ts +65 -0
- package/src/http/routes/import.route.ts +64 -0
- package/src/http/routes/jurisdiction.route.ts +22 -0
- package/src/http/routes/obligations.route.test.ts +122 -0
- package/src/http/routes/obligations.route.ts +110 -0
- package/src/http/routes/onboarding.route.ts +53 -0
- package/src/http/routes/provider.route.ts +42 -0
- package/src/http/routes/proxy.route.ts +40 -0
- package/src/http/routes/redteam.route.ts +84 -0
- package/src/http/routes/report.route.ts +29 -0
- package/src/http/routes/scan.route.ts +104 -0
- package/src/http/routes/share.route.ts +44 -0
- package/src/http/routes/shell.route.ts +27 -0
- package/src/http/routes/status.route.ts +66 -0
- package/src/http/routes/supply-chain.route.ts +121 -0
- package/src/http/routes/sync.route.ts +328 -0
- package/src/http/routes/tools.route.ts +29 -0
- package/src/http/routes/whatif.route.ts +96 -0
- package/src/http/utils/validation.ts +31 -0
- package/src/index.ts +1 -0
- package/src/infra/bundle-fetcher.ts +77 -0
- package/src/infra/cache-storage.ts +34 -0
- package/src/infra/event-bus.ts +31 -0
- package/src/infra/file-collector.ts +61 -0
- package/src/infra/file-ops-adapter.ts +95 -0
- package/src/infra/file-watcher.test.ts +90 -0
- package/src/infra/file-watcher.ts +106 -0
- package/src/infra/git-adapter.ts +93 -0
- package/src/infra/git-history-adapter.ts +41 -0
- package/src/infra/headless-browser.ts +178 -0
- package/src/infra/llm-adapter.test.ts +83 -0
- package/src/infra/llm-adapter.ts +86 -0
- package/src/infra/logger.ts +27 -0
- package/src/infra/project-config.test.ts +74 -0
- package/src/infra/project-config.ts +35 -0
- package/src/infra/rate-limiter.test.ts +36 -0
- package/src/infra/rate-limiter.ts +34 -0
- package/src/infra/retry.ts +46 -0
- package/src/infra/saas-client.ts +123 -0
- package/src/infra/search-adapter.ts +113 -0
- package/src/infra/shell-adapter.ts +68 -0
- package/src/infra/tool-manager.test.ts +99 -0
- package/src/infra/tool-manager.ts +197 -0
- package/src/llm/agents/agent-modes.test.ts +44 -0
- package/src/llm/agents/modes.ts +68 -0
- package/src/llm/routing/cost-routing.test.ts +37 -0
- package/src/llm/routing/cost-tracker.ts +74 -0
- package/src/llm/routing/model-routing.test.ts +79 -0
- package/src/llm/routing/model-routing.ts +38 -0
- package/src/llm/routing/pricing.ts +19 -0
- package/src/llm/sse-protocol.ts +77 -0
- package/src/llm/tool-definitions.ts +83 -0
- package/src/llm/tool-executors.ts +80 -0
- package/src/llm/tools/types.ts +13 -0
- package/src/mcp/create-mcp-stack.ts +82 -0
- package/src/mcp/handlers.ts +245 -0
- package/src/mcp/index.ts +28 -0
- package/src/mcp/mcp-server.test.ts +80 -0
- package/src/mcp/server.ts +79 -0
- package/src/mcp/tools.ts +48 -0
- package/src/onboarding/auto-detect.ts +164 -0
- package/src/onboarding/onboarding.test.ts +89 -0
- package/src/onboarding/profile.ts +169 -0
- package/src/onboarding/questions.ts +112 -0
- package/src/onboarding/wizard.ts +66 -0
- package/src/output/github-issue.ts +32 -0
- package/src/output/json-output.ts +67 -0
- package/src/ports/browser.port.ts +23 -0
- package/src/ports/events.port.ts +28 -0
- package/src/ports/llm.port.ts +23 -0
- package/src/ports/logger.port.ts +6 -0
- package/src/ports/process.port.ts +6 -0
- package/src/ports/scanner.port.ts +15 -0
- package/src/server.ts +134 -0
- package/src/services/badge-service.ts +67 -0
- package/src/services/chat-service.test.ts +162 -0
- package/src/services/chat-service.ts +152 -0
- package/src/services/cost-service.ts +52 -0
- package/src/services/debt-service.ts +65 -0
- package/src/services/eval-integration.test.ts +132 -0
- package/src/services/eval-service.test.ts +373 -0
- package/src/services/eval-service.ts +463 -0
- package/src/services/external-scan-service.ts +60 -0
- package/src/services/file-service.ts +37 -0
- package/src/services/fix-service.test.ts +470 -0
- package/src/services/fix-service.ts +648 -0
- package/src/services/framework-service.test.ts +159 -0
- package/src/services/framework-service.ts +67 -0
- package/src/services/onboarding-service.ts +165 -0
- package/src/services/passport-audit.ts +244 -0
- package/src/services/passport-documents.ts +258 -0
- package/src/services/passport-service-utils.ts +72 -0
- package/src/services/passport-service.test.ts +251 -0
- package/src/services/passport-service.ts +339 -0
- package/src/services/proxy-service.ts +81 -0
- package/src/services/report-service.ts +72 -0
- package/src/services/scan-service.test.ts +470 -0
- package/src/services/scan-service.ts +335 -0
- package/src/services/share-service.ts +108 -0
- package/src/services/shared/backup.ts +23 -0
- package/src/services/status-service.ts +38 -0
- package/src/services/undo-service.test.ts +190 -0
- package/src/services/undo-service.ts +144 -0
- package/src/test-helpers/factories.ts +116 -0
- package/src/types/common.schemas.ts +147 -0
- package/src/types/common.types.ts +292 -0
- package/src/types/contract.test.ts +217 -0
- package/src/types/errors.ts +52 -0
- package/src/types/framework.types.ts +87 -0
- package/src/types/passport-schemas.ts +241 -0
- package/src/types/passport.types.ts +296 -0
- package/src/version.ts +1 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { resolve, dirname } from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { backupFile } from './shared/backup.js';
|
|
5
|
+
import type { Finding, ScanResult } from '../types/common.types.js';
|
|
6
|
+
import type { EventBusPort } from '../ports/events.port.js';
|
|
7
|
+
import type { Fixer } from '../domain/fixer/create-fixer.js';
|
|
8
|
+
import type { FixPlan, FixResult, FixValidation } from '../domain/fixer/types.js';
|
|
9
|
+
import type { ScanService } from './scan-service.js';
|
|
10
|
+
import type { UndoService } from './undo-service.js';
|
|
11
|
+
import type { EvidenceStore } from '../domain/scanner/evidence-store.js';
|
|
12
|
+
import { createEvidence } from '../domain/scanner/evidence.js';
|
|
13
|
+
import type { AgentPassport } from '../types/passport.types.js';
|
|
14
|
+
import { generateDocument, TEMPLATE_FILE_MAP, type DocType, type DocResult } from '../domain/documents/document-generator.js';
|
|
15
|
+
import { enrichDocumentWithAI, detectWeakSections } from '../domain/documents/ai-enricher.js';
|
|
16
|
+
import type { LlmPort } from '../ports/llm.port.js';
|
|
17
|
+
|
|
18
|
+
/** Resolve [PASSPORT:field] tokens from agent passport data. */
|
|
19
|
+
function resolvePassportPlaceholders(content: string, passport: AgentPassport): string {
|
|
20
|
+
const fields: Record<string, string | undefined> = {
|
|
21
|
+
'display_name': passport.display_name ?? passport.name,
|
|
22
|
+
'name': passport.name,
|
|
23
|
+
'description': passport.description,
|
|
24
|
+
'owner.team': passport.owner?.team,
|
|
25
|
+
'owner.contact': passport.owner?.contact,
|
|
26
|
+
'owner.responsible_person': passport.owner?.responsible_person,
|
|
27
|
+
'model.provider': passport.model?.provider,
|
|
28
|
+
'model.model_id': passport.model?.model_id,
|
|
29
|
+
'risk_class': passport.compliance?.eu_ai_act?.risk_class,
|
|
30
|
+
'autonomy_level': passport.autonomy_level,
|
|
31
|
+
'disclosure_text': passport.disclosure?.disclosure_text,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return content.replace(/\[PASSPORT:([^\]]+)\]/g, (match, key: string) => {
|
|
35
|
+
return fields[key] ?? match;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FixServiceDeps {
|
|
40
|
+
readonly fixer: Fixer;
|
|
41
|
+
readonly scanService: ScanService;
|
|
42
|
+
readonly events: EventBusPort;
|
|
43
|
+
readonly getProjectPath: () => string;
|
|
44
|
+
readonly getLastScanResult: () => ScanResult | null;
|
|
45
|
+
readonly loadTemplate: (templateFile: string) => Promise<string>;
|
|
46
|
+
readonly undoService?: UndoService;
|
|
47
|
+
readonly evidenceStore?: EvidenceStore;
|
|
48
|
+
readonly passportService?: { listPassports: (path?: string) => Promise<readonly AgentPassport[]> };
|
|
49
|
+
readonly llm?: LlmPort;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const createFixService = (deps: FixServiceDeps) => {
|
|
53
|
+
const { fixer, scanService, events, getProjectPath: _getProjectPath, getLastScanResult, loadTemplate, undoService } = deps;
|
|
54
|
+
|
|
55
|
+
const getProjectPath = () => _getProjectPath();
|
|
56
|
+
|
|
57
|
+
const createBackup = (filePath: string): Promise<string> =>
|
|
58
|
+
backupFile(filePath, getProjectPath());
|
|
59
|
+
|
|
60
|
+
const applyAction = async (action: FixPlan['actions'][number], projectPath: string, useAi = false): Promise<readonly string[] | undefined> => {
|
|
61
|
+
const fullPath = resolve(projectPath, action.path);
|
|
62
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
63
|
+
|
|
64
|
+
if (action.type === 'create') {
|
|
65
|
+
let content = action.content ?? '';
|
|
66
|
+
let manualFields: readonly string[] | undefined;
|
|
67
|
+
// Resolve template placeholder — pre-fill with passport data when available
|
|
68
|
+
const templateMatch = content.match(/^\[TEMPLATE:(.+)]$/);
|
|
69
|
+
if (templateMatch) {
|
|
70
|
+
const templateFile = templateMatch[1]!;
|
|
71
|
+
const template = await loadTemplate(templateFile);
|
|
72
|
+
|
|
73
|
+
// Reverse-lookup: templateFile → DocType
|
|
74
|
+
const docTypeEntry = (Object.entries(TEMPLATE_FILE_MAP) as [DocType, string][])
|
|
75
|
+
.find(([, file]) => file === templateFile);
|
|
76
|
+
|
|
77
|
+
if (docTypeEntry && deps.passportService) {
|
|
78
|
+
try {
|
|
79
|
+
const passports = await deps.passportService.listPassports(projectPath);
|
|
80
|
+
const passport = passports[0];
|
|
81
|
+
if (passport) {
|
|
82
|
+
// Check if file already exists on disk — enhance existing instead of overwriting
|
|
83
|
+
let existingContent: string | undefined;
|
|
84
|
+
try { existingContent = await readFile(fullPath, 'utf-8'); } catch { /* file doesn't exist */ }
|
|
85
|
+
|
|
86
|
+
if (existingContent && existingContent.trim().length > 0) {
|
|
87
|
+
if (useAi && deps.llm) {
|
|
88
|
+
// Existing doc + --ai: enhance with L2-informed LLM feedback (preserve user edits)
|
|
89
|
+
const weakSections = detectWeakSections(existingContent);
|
|
90
|
+
const baseResult: DocResult = {
|
|
91
|
+
markdown: existingContent,
|
|
92
|
+
docType: docTypeEntry[0],
|
|
93
|
+
prefilledFields: [],
|
|
94
|
+
manualFields: [...weakSections],
|
|
95
|
+
};
|
|
96
|
+
manualFields = weakSections;
|
|
97
|
+
|
|
98
|
+
if (weakSections.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
const selection = deps.llm.routeModel('document-generation');
|
|
101
|
+
const model = await deps.llm.getModel(selection.provider, selection.modelId);
|
|
102
|
+
const enriched = await enrichDocumentWithAI({ baseResult, manifest: passport, model });
|
|
103
|
+
// Second pass: re-check for remaining weak sections and enrich again
|
|
104
|
+
let finalMarkdown = enriched.markdown;
|
|
105
|
+
const remainingWeak = detectWeakSections(finalMarkdown);
|
|
106
|
+
if (remainingWeak.length > 0) {
|
|
107
|
+
try {
|
|
108
|
+
const secondPass = await enrichDocumentWithAI({
|
|
109
|
+
baseResult: { markdown: finalMarkdown, docType: docTypeEntry[0], prefilledFields: [], manualFields: [...remainingWeak] },
|
|
110
|
+
manifest: passport,
|
|
111
|
+
model,
|
|
112
|
+
});
|
|
113
|
+
finalMarkdown = secondPass.markdown;
|
|
114
|
+
} catch {
|
|
115
|
+
// Keep first-pass result on second-pass failure
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Remove scaffold marker after successful LLM enrichment — scanner upgrades to 'draft'
|
|
119
|
+
content = finalMarkdown.replace(/^<!-- COMPLIOR:SCAFFOLD -->\n/, '');
|
|
120
|
+
} catch (err) {
|
|
121
|
+
events.emit('log', { level: 'warn', message: `LLM enrichment failed: ${err instanceof Error ? err.message : err}` });
|
|
122
|
+
content = existingContent; // LLM failed — keep existing
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
content = existingContent; // All sections adequate — no changes needed
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Existing doc without --ai: keep existing content, never overwrite with scaffold
|
|
129
|
+
content = existingContent;
|
|
130
|
+
manualFields = detectWeakSections(existingContent);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// No existing file — generate fresh scaffold
|
|
134
|
+
const baseResult = generateDocument({ manifest: passport, template, docType: docTypeEntry[0] });
|
|
135
|
+
manualFields = baseResult.manualFields;
|
|
136
|
+
|
|
137
|
+
// LLM enrichment for fresh scaffold
|
|
138
|
+
if (useAi && deps.llm && baseResult.manualFields.length > 0) {
|
|
139
|
+
try {
|
|
140
|
+
const selection = deps.llm.routeModel('document-generation');
|
|
141
|
+
const model = await deps.llm.getModel(selection.provider, selection.modelId);
|
|
142
|
+
const enriched = await enrichDocumentWithAI({ baseResult, manifest: passport, model });
|
|
143
|
+
// Second pass: re-check for remaining weak sections and enrich again
|
|
144
|
+
let finalMarkdown = enriched.markdown;
|
|
145
|
+
const remainingWeak = detectWeakSections(finalMarkdown);
|
|
146
|
+
if (remainingWeak.length > 0) {
|
|
147
|
+
try {
|
|
148
|
+
const secondPass = await enrichDocumentWithAI({
|
|
149
|
+
baseResult: { markdown: finalMarkdown, docType: docTypeEntry[0], prefilledFields: [], manualFields: [...remainingWeak] },
|
|
150
|
+
manifest: passport,
|
|
151
|
+
model,
|
|
152
|
+
});
|
|
153
|
+
finalMarkdown = secondPass.markdown;
|
|
154
|
+
} catch {
|
|
155
|
+
// Keep first-pass result on second-pass failure
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// LLM enriched → no scaffold marker (scanner classifies as 'draft')
|
|
159
|
+
content = finalMarkdown;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
events.emit('log', { level: 'warn', message: `LLM enrichment failed: ${err instanceof Error ? err.message : err}` });
|
|
162
|
+
content = `<!-- COMPLIOR:SCAFFOLD -->\n${baseResult.markdown}`;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// No LLM → mark as scaffold
|
|
166
|
+
content = `<!-- COMPLIOR:SCAFFOLD -->\n${baseResult.markdown}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
content = `<!-- COMPLIOR:SCAFFOLD -->\n${template}`;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
content = `<!-- COMPLIOR:SCAFFOLD -->\n${template}`;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
content = `<!-- COMPLIOR:SCAFFOLD -->\n${template}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Resolve [PASSPORT:*] tokens from agent passport (for non-template content)
|
|
180
|
+
if (content.includes('[PASSPORT:') && deps.passportService) {
|
|
181
|
+
try {
|
|
182
|
+
const passports = await deps.passportService.listPassports(projectPath);
|
|
183
|
+
const passport = passports[0];
|
|
184
|
+
if (passport) {
|
|
185
|
+
content = resolvePassportPlaceholders(content, passport);
|
|
186
|
+
}
|
|
187
|
+
} catch { /* ignore — keep placeholders */ }
|
|
188
|
+
}
|
|
189
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
190
|
+
events.emit('file.changed', { path: fullPath, action: 'create' });
|
|
191
|
+
return manualFields;
|
|
192
|
+
} else if (action.type === 'splice') {
|
|
193
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
194
|
+
const lines = content.split('\n');
|
|
195
|
+
const startIdx = (action.startLine ?? 1) - 1;
|
|
196
|
+
const beforeLines = action.beforeLines ?? [];
|
|
197
|
+
const endIdx = startIdx + beforeLines.length;
|
|
198
|
+
|
|
199
|
+
// Stale diff protection — validate before-lines match (trimmed)
|
|
200
|
+
if (endIdx > lines.length) {
|
|
201
|
+
throw new Error(`Stale diff: line range exceeds file length — re-scan first`);
|
|
202
|
+
}
|
|
203
|
+
for (let i = 0; i < beforeLines.length; i++) {
|
|
204
|
+
if ((lines[startIdx + i] ?? '').trim() !== (beforeLines[i] ?? '').trim()) {
|
|
205
|
+
throw new Error(`Stale diff at line ${(action.startLine ?? 1) + i} — re-scan first`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Splice: replace before-lines with after-lines
|
|
210
|
+
lines.splice(startIdx, beforeLines.length, ...(action.afterLines ?? []));
|
|
211
|
+
|
|
212
|
+
// Import injection — after last existing import, or at top
|
|
213
|
+
if (action.importLine && !lines.some((l) => l.includes(action.importLine!))) {
|
|
214
|
+
let insertAt = 0;
|
|
215
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
216
|
+
if (lines[i]!.startsWith('import ')) { insertAt = i + 1; break; }
|
|
217
|
+
}
|
|
218
|
+
lines.splice(insertAt, 0, action.importLine);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Write back, preserving trailing newline
|
|
222
|
+
let output = lines.join('\n');
|
|
223
|
+
if (content.endsWith('\n') && !output.endsWith('\n')) output += '\n';
|
|
224
|
+
await writeFile(fullPath, output, 'utf-8');
|
|
225
|
+
events.emit('file.changed', { path: fullPath, action: 'edit' });
|
|
226
|
+
return undefined;
|
|
227
|
+
} else {
|
|
228
|
+
const current = await readFile(fullPath, 'utf-8');
|
|
229
|
+
const updated = current.replace(action.oldContent ?? '', action.newContent ?? '');
|
|
230
|
+
await writeFile(fullPath, updated, 'utf-8');
|
|
231
|
+
events.emit('file.changed', { path: fullPath, action: 'edit' });
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const preview = (finding: Finding): FixPlan | null => {
|
|
237
|
+
return fixer.previewFix(finding);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const previewAll = (): readonly FixPlan[] => {
|
|
241
|
+
const lastScan = getLastScanResult();
|
|
242
|
+
if (!lastScan) return [];
|
|
243
|
+
return fixer.generateFixes(lastScan.findings);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const applyFix = async (plan: FixPlan, useAi = false): Promise<FixResult> => {
|
|
247
|
+
const projectPath = getProjectPath();
|
|
248
|
+
// Normalize: run a basic scan first so scoreBefore matches scoreAfter tier
|
|
249
|
+
const baselineScan = await scanService.scan(projectPath);
|
|
250
|
+
const scoreBefore = baselineScan.score.totalScore;
|
|
251
|
+
const backedUp: string[] = [];
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
let manualFields: readonly string[] | undefined;
|
|
255
|
+
for (const action of plan.actions) {
|
|
256
|
+
const bk = await createBackup(action.path);
|
|
257
|
+
backedUp.push(bk);
|
|
258
|
+
const mf = await applyAction(action, projectPath, useAi);
|
|
259
|
+
if (mf) manualFields = mf;
|
|
260
|
+
}
|
|
261
|
+
const enrichedPlan = manualFields ? { ...plan, manualFields } : plan;
|
|
262
|
+
|
|
263
|
+
// Re-scan to get updated score
|
|
264
|
+
const newResult = await scanService.scan(projectPath);
|
|
265
|
+
const scoreAfter = newResult.score.totalScore;
|
|
266
|
+
|
|
267
|
+
events.emit('score.updated', { before: scoreBefore, after: scoreAfter });
|
|
268
|
+
|
|
269
|
+
// C.R20: Record fix event in evidence chain
|
|
270
|
+
if (deps.evidenceStore) {
|
|
271
|
+
const evidence = createEvidence(
|
|
272
|
+
plan.checkId,
|
|
273
|
+
'fix',
|
|
274
|
+
'fix',
|
|
275
|
+
{ file: plan.actions[0]?.path },
|
|
276
|
+
);
|
|
277
|
+
await deps.evidenceStore.append([evidence], randomUUID());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result: FixResult = {
|
|
281
|
+
plan: enrichedPlan,
|
|
282
|
+
applied: true,
|
|
283
|
+
scoreBefore,
|
|
284
|
+
scoreAfter,
|
|
285
|
+
backedUpFiles: backedUp,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (undoService) {
|
|
289
|
+
await undoService.recordFix(result, enrichedPlan);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return {
|
|
295
|
+
plan,
|
|
296
|
+
applied: false,
|
|
297
|
+
scoreBefore,
|
|
298
|
+
scoreAfter: scoreBefore,
|
|
299
|
+
backedUpFiles: backedUp,
|
|
300
|
+
error: err instanceof Error ? err.message : String(err),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const applyAndValidate = async (plan: FixPlan, useAi = false): Promise<FixResult & { validation: FixValidation }> => {
|
|
306
|
+
const lastScan = getLastScanResult();
|
|
307
|
+
const findingBefore = lastScan?.findings.find(
|
|
308
|
+
(f) => f.checkId === plan.checkId && (!plan.obligationId || f.obligationId === plan.obligationId),
|
|
309
|
+
);
|
|
310
|
+
const beforeType = findingBefore?.type ?? 'fail';
|
|
311
|
+
|
|
312
|
+
const result = await applyFix(plan, useAi);
|
|
313
|
+
|
|
314
|
+
const newScan = getLastScanResult();
|
|
315
|
+
const findingAfter = newScan?.findings.find(
|
|
316
|
+
(f) => f.checkId === plan.checkId && (!plan.obligationId || f.obligationId === plan.obligationId),
|
|
317
|
+
);
|
|
318
|
+
const afterType = findingAfter?.type ?? (result.applied ? 'pass' : beforeType);
|
|
319
|
+
const scoreDelta = result.scoreAfter - result.scoreBefore;
|
|
320
|
+
|
|
321
|
+
const validation: FixValidation = {
|
|
322
|
+
checkId: plan.checkId,
|
|
323
|
+
obligationId: plan.obligationId,
|
|
324
|
+
article: plan.article,
|
|
325
|
+
before: beforeType,
|
|
326
|
+
after: afterType,
|
|
327
|
+
scoreDelta,
|
|
328
|
+
totalScore: result.scoreAfter,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
events.emit('fix.validated', {
|
|
332
|
+
checkId: plan.checkId,
|
|
333
|
+
passed: afterType === 'pass',
|
|
334
|
+
scoreDelta,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return { ...result, validation };
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const applyAllAndValidate = async (useAi = false): Promise<{
|
|
341
|
+
results: readonly (FixResult & { validation: FixValidation })[];
|
|
342
|
+
totalDelta: number;
|
|
343
|
+
}> => {
|
|
344
|
+
const plans = previewAll();
|
|
345
|
+
const results: (FixResult & { validation: FixValidation })[] = [];
|
|
346
|
+
for (const plan of plans) {
|
|
347
|
+
const result = await applyAndValidate(plan, useAi);
|
|
348
|
+
results.push(result);
|
|
349
|
+
}
|
|
350
|
+
const totalDelta = results.reduce((sum, r) => sum + r.validation.scoreDelta, 0);
|
|
351
|
+
return { results, totalDelta };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
type FixProgressCallback = (event: {
|
|
355
|
+
type: 'applying' | 'applied' | 'failed';
|
|
356
|
+
checkId: string;
|
|
357
|
+
path: string;
|
|
358
|
+
action?: string;
|
|
359
|
+
scoreAfter?: number;
|
|
360
|
+
error?: string;
|
|
361
|
+
}) => Promise<void>;
|
|
362
|
+
|
|
363
|
+
const applyAll = async (useAi = false, overridePath?: string, onProgress?: FixProgressCallback): Promise<readonly FixResult[]> => {
|
|
364
|
+
const plans = fixer.generateFixes(
|
|
365
|
+
getLastScanResult()?.findings ?? [],
|
|
366
|
+
useAi ? { useAi: true } : undefined,
|
|
367
|
+
);
|
|
368
|
+
if (plans.length === 0) return [];
|
|
369
|
+
|
|
370
|
+
const projectPath = overridePath ?? getProjectPath();
|
|
371
|
+
// Normalize: ensure scoreBefore reflects basic scan (same tier as scoreAfter)
|
|
372
|
+
const baselineScan = await scanService.scan(projectPath);
|
|
373
|
+
const scoreBefore = baselineScan.score.totalScore;
|
|
374
|
+
const results: FixResult[] = [];
|
|
375
|
+
const appliedActionKeys = new Set<string>(); // Track path:startLine (splice) or path (create/edit)
|
|
376
|
+
|
|
377
|
+
// Separate splice-only plans (batch per file) from others (apply individually)
|
|
378
|
+
const splicePlans: FixPlan[] = [];
|
|
379
|
+
const otherPlans: FixPlan[] = [];
|
|
380
|
+
for (const plan of plans) {
|
|
381
|
+
if (plan.actions.length === 1 && plan.actions[0]!.type === 'splice') {
|
|
382
|
+
splicePlans.push(plan);
|
|
383
|
+
} else {
|
|
384
|
+
otherPlans.push(plan);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Phase 1a: Apply non-splice plans individually
|
|
389
|
+
for (const plan of otherPlans) {
|
|
390
|
+
const backedUp: string[] = [];
|
|
391
|
+
const actionPath = plan.actions[0]?.path ?? '?';
|
|
392
|
+
const actionType = plan.actions[0]?.type === 'create' ? 'CREATE' : 'MODIFY';
|
|
393
|
+
if (onProgress) {
|
|
394
|
+
await onProgress({ type: 'applying', checkId: plan.checkId, path: actionPath, action: actionType });
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
let manualFields: readonly string[] | undefined;
|
|
398
|
+
for (const action of plan.actions) {
|
|
399
|
+
const bk = await createBackup(action.path);
|
|
400
|
+
backedUp.push(bk);
|
|
401
|
+
const mf = await applyAction(action, projectPath, useAi);
|
|
402
|
+
if (mf) manualFields = mf;
|
|
403
|
+
}
|
|
404
|
+
const enrichedPlan = manualFields ? { ...plan, manualFields } : plan;
|
|
405
|
+
for (const action of plan.actions) {
|
|
406
|
+
appliedActionKeys.add(action.type === 'splice' ? `${action.path}:${action.startLine}` : action.path);
|
|
407
|
+
}
|
|
408
|
+
results.push({ plan: enrichedPlan, applied: true, scoreBefore, scoreAfter: 0, backedUpFiles: backedUp });
|
|
409
|
+
if (onProgress) {
|
|
410
|
+
await onProgress({ type: 'applied', checkId: plan.checkId, path: actionPath });
|
|
411
|
+
}
|
|
412
|
+
} catch (err) {
|
|
413
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
414
|
+
results.push({
|
|
415
|
+
plan,
|
|
416
|
+
applied: false,
|
|
417
|
+
scoreBefore,
|
|
418
|
+
scoreAfter: scoreBefore,
|
|
419
|
+
backedUpFiles: backedUp,
|
|
420
|
+
error: errorMsg,
|
|
421
|
+
});
|
|
422
|
+
if (onProgress) {
|
|
423
|
+
await onProgress({ type: 'failed', checkId: plan.checkId, path: actionPath, error: errorMsg });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Phase 1b: Batch same-file splices — single read-modify-write per file
|
|
429
|
+
const fileGroups = new Map<string, FixPlan[]>();
|
|
430
|
+
for (const plan of splicePlans) {
|
|
431
|
+
const path = plan.actions[0]!.path;
|
|
432
|
+
if (!fileGroups.has(path)) fileGroups.set(path, []);
|
|
433
|
+
fileGroups.get(path)!.push(plan);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (const [filePath, group] of fileGroups) {
|
|
437
|
+
// Sort bottom-up by startLine so splicing doesn't shift indices for remaining ops
|
|
438
|
+
group.sort((a, b) => (b.actions[0]!.startLine ?? 0) - (a.actions[0]!.startLine ?? 0));
|
|
439
|
+
|
|
440
|
+
const bk = await createBackup(filePath);
|
|
441
|
+
const fullPath = resolve(projectPath, filePath);
|
|
442
|
+
|
|
443
|
+
let content: string;
|
|
444
|
+
try {
|
|
445
|
+
content = await readFile(fullPath, 'utf-8');
|
|
446
|
+
} catch {
|
|
447
|
+
for (const plan of group) {
|
|
448
|
+
results.push({
|
|
449
|
+
plan, applied: false, scoreBefore, scoreAfter: scoreBefore,
|
|
450
|
+
backedUpFiles: [bk], error: `File not found: ${filePath}`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const lines = content.split('\n');
|
|
457
|
+
const importLines: string[] = [];
|
|
458
|
+
|
|
459
|
+
// Apply each splice to in-memory lines (bottom-up)
|
|
460
|
+
for (const plan of group) {
|
|
461
|
+
const action = plan.actions[0]!;
|
|
462
|
+
const startIdx = (action.startLine ?? 1) - 1;
|
|
463
|
+
const beforeLines = action.beforeLines ?? [];
|
|
464
|
+
|
|
465
|
+
// Validate before-lines against current in-memory state
|
|
466
|
+
let valid = true;
|
|
467
|
+
if (startIdx + beforeLines.length > lines.length) {
|
|
468
|
+
results.push({
|
|
469
|
+
plan, applied: false, scoreBefore, scoreAfter: scoreBefore,
|
|
470
|
+
backedUpFiles: [bk], error: `Stale diff: line range exceeds file length — re-scan first`,
|
|
471
|
+
});
|
|
472
|
+
valid = false;
|
|
473
|
+
} else {
|
|
474
|
+
for (let i = 0; i < beforeLines.length; i++) {
|
|
475
|
+
if ((lines[startIdx + i] ?? '').trim() !== (beforeLines[i] ?? '').trim()) {
|
|
476
|
+
results.push({
|
|
477
|
+
plan, applied: false, scoreBefore, scoreAfter: scoreBefore,
|
|
478
|
+
backedUpFiles: [bk], error: `Stale diff at line ${(action.startLine ?? 1) + i} — re-scan first`,
|
|
479
|
+
});
|
|
480
|
+
valid = false;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!valid) continue;
|
|
486
|
+
|
|
487
|
+
if (onProgress) {
|
|
488
|
+
await onProgress({ type: 'applying', checkId: plan.checkId, path: action.path, action: 'MODIFY' });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Splice in-memory
|
|
492
|
+
lines.splice(startIdx, beforeLines.length, ...(action.afterLines ?? []));
|
|
493
|
+
if (action.importLine) importLines.push(action.importLine);
|
|
494
|
+
appliedActionKeys.add(`${action.path}:${action.startLine}`);
|
|
495
|
+
results.push({ plan, applied: true, scoreBefore, scoreAfter: 0, backedUpFiles: [bk] });
|
|
496
|
+
|
|
497
|
+
if (onProgress) {
|
|
498
|
+
await onProgress({ type: 'applied', checkId: plan.checkId, path: action.path });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Inject unique imports after all splices (after last import/from line)
|
|
503
|
+
const uniqueImports = [...new Set(importLines)].filter(
|
|
504
|
+
(imp) => !lines.some((l) => l.includes(imp)),
|
|
505
|
+
);
|
|
506
|
+
if (uniqueImports.length > 0) {
|
|
507
|
+
let insertAt = 0;
|
|
508
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
509
|
+
if (lines[i]!.startsWith('import ') || lines[i]!.startsWith('from ')) {
|
|
510
|
+
insertAt = i + 1;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
lines.splice(insertAt, 0, ...uniqueImports);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Write file once
|
|
518
|
+
let output = lines.join('\n');
|
|
519
|
+
if (content.endsWith('\n') && !output.endsWith('\n')) output += '\n';
|
|
520
|
+
await writeFile(fullPath, output, 'utf-8');
|
|
521
|
+
events.emit('file.changed', { path: fullPath, action: 'edit' });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Phase 2: Iterative scan+fix (up to 2 extra passes for cascading findings)
|
|
525
|
+
// After fixing line N, the scanner may detect line N+2 which was previously shadowed.
|
|
526
|
+
let scoreAfter = 0;
|
|
527
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
528
|
+
const scanResult = await scanService.scan(projectPath);
|
|
529
|
+
scoreAfter = scanResult.score.totalScore;
|
|
530
|
+
|
|
531
|
+
if (pass >= 2) break; // Last pass is just the final scan
|
|
532
|
+
|
|
533
|
+
// Check for new fixable findings (splice + cascading create/edit)
|
|
534
|
+
const newPlans = previewAll().filter((p) => {
|
|
535
|
+
if (p.actions.length === 0) return false;
|
|
536
|
+
const a = p.actions[0]!;
|
|
537
|
+
const key = a.type === 'splice' ? `${a.path}:${a.startLine}` : a.path;
|
|
538
|
+
return !appliedActionKeys.has(key);
|
|
539
|
+
});
|
|
540
|
+
if (newPlans.length === 0) break;
|
|
541
|
+
|
|
542
|
+
// Apply new splice fixes using the same batched approach
|
|
543
|
+
const spliceCascadePlans = newPlans.filter(p => p.actions.length === 1 && p.actions[0]!.type === 'splice');
|
|
544
|
+
const newFileGroups = new Map<string, FixPlan[]>();
|
|
545
|
+
for (const plan of spliceCascadePlans) {
|
|
546
|
+
const path = plan.actions[0]!.path;
|
|
547
|
+
if (!newFileGroups.has(path)) newFileGroups.set(path, []);
|
|
548
|
+
newFileGroups.get(path)!.push(plan);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let anyApplied = false;
|
|
552
|
+
for (const [fp, group] of newFileGroups) {
|
|
553
|
+
group.sort((a, b) => (b.actions[0]!.startLine ?? 0) - (a.actions[0]!.startLine ?? 0));
|
|
554
|
+
const bk = await createBackup(fp);
|
|
555
|
+
const full = resolve(projectPath, fp);
|
|
556
|
+
let content: string;
|
|
557
|
+
try { content = await readFile(full, 'utf-8'); } catch { continue; }
|
|
558
|
+
const lines = content.split('\n');
|
|
559
|
+
const importLines: string[] = [];
|
|
560
|
+
|
|
561
|
+
for (const plan of group) {
|
|
562
|
+
const action = plan.actions[0]!;
|
|
563
|
+
const startIdx = (action.startLine ?? 1) - 1;
|
|
564
|
+
const beforeLines = action.beforeLines ?? [];
|
|
565
|
+
let valid = true;
|
|
566
|
+
if (startIdx + beforeLines.length > lines.length) { valid = false; }
|
|
567
|
+
else {
|
|
568
|
+
for (let i = 0; i < beforeLines.length; i++) {
|
|
569
|
+
if ((lines[startIdx + i] ?? '').trim() !== (beforeLines[i] ?? '').trim()) { valid = false; break; }
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (!valid) continue;
|
|
573
|
+
lines.splice(startIdx, beforeLines.length, ...(action.afterLines ?? []));
|
|
574
|
+
if (action.importLine) importLines.push(action.importLine);
|
|
575
|
+
appliedActionKeys.add(`${action.path}:${action.startLine}`);
|
|
576
|
+
results.push({ plan, applied: true, scoreBefore, scoreAfter: 0, backedUpFiles: [bk] });
|
|
577
|
+
anyApplied = true;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const uniqueImports = [...new Set(importLines)].filter((imp) => !lines.some((l) => l.includes(imp)));
|
|
581
|
+
if (uniqueImports.length > 0) {
|
|
582
|
+
let insertAt = 0;
|
|
583
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
584
|
+
if (lines[i]!.startsWith('import ') || lines[i]!.startsWith('from ')) { insertAt = i + 1; break; }
|
|
585
|
+
}
|
|
586
|
+
lines.splice(insertAt, 0, ...uniqueImports);
|
|
587
|
+
}
|
|
588
|
+
let output = lines.join('\n');
|
|
589
|
+
if (content.endsWith('\n') && !output.endsWith('\n')) output += '\n';
|
|
590
|
+
await writeFile(full, output, 'utf-8');
|
|
591
|
+
events.emit('file.changed', { path: full, action: 'edit' });
|
|
592
|
+
}
|
|
593
|
+
// Apply non-splice cascading actions (create/edit) using existing applyAction helper
|
|
594
|
+
const nonSplicePlans = newPlans.filter(p => p.actions.length > 0 && p.actions[0]!.type !== 'splice');
|
|
595
|
+
for (const plan of nonSplicePlans) {
|
|
596
|
+
try {
|
|
597
|
+
const bk = await createBackup(plan.actions[0]!.path);
|
|
598
|
+
await applyAction(plan.actions[0]!, projectPath);
|
|
599
|
+
appliedActionKeys.add(plan.actions[0]!.path);
|
|
600
|
+
results.push({ plan, applied: true, scoreBefore, scoreAfter: 0, backedUpFiles: [bk] });
|
|
601
|
+
anyApplied = true;
|
|
602
|
+
} catch { /* skip failed cascading fixes */ }
|
|
603
|
+
}
|
|
604
|
+
if (!anyApplied) break;
|
|
605
|
+
}
|
|
606
|
+
events.emit('score.updated', { before: scoreBefore, after: scoreAfter });
|
|
607
|
+
|
|
608
|
+
// Phase 3: Record undo history + evidence for applied fixes
|
|
609
|
+
for (const result of results) {
|
|
610
|
+
if (!result.applied) continue;
|
|
611
|
+
// Patch final score into result (reconstruct to preserve readonly contract)
|
|
612
|
+
Object.assign(result, { scoreAfter });
|
|
613
|
+
|
|
614
|
+
if (undoService) {
|
|
615
|
+
await undoService.recordFix(result, result.plan);
|
|
616
|
+
}
|
|
617
|
+
if (deps.evidenceStore) {
|
|
618
|
+
const evidence = createEvidence(
|
|
619
|
+
result.plan.checkId,
|
|
620
|
+
'fix',
|
|
621
|
+
'fix',
|
|
622
|
+
{ file: result.plan.actions[0]?.path },
|
|
623
|
+
);
|
|
624
|
+
await deps.evidenceStore.append([evidence], randomUUID());
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return results;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const getUnfixedFindings = (): readonly Finding[] => {
|
|
632
|
+
const lastScan = getLastScanResult();
|
|
633
|
+
if (!lastScan) return [];
|
|
634
|
+
const fixableIds = new Set(previewAll().map((p) => p.checkId));
|
|
635
|
+
return lastScan.findings.filter(
|
|
636
|
+
(f) => f.type === 'fail' && !fixableIds.has(f.checkId),
|
|
637
|
+
);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const getCurrentScore = (): number => {
|
|
641
|
+
const lastScan = getLastScanResult();
|
|
642
|
+
return lastScan?.score.totalScore ?? 0;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
return Object.freeze({ preview, previewAll, applyFix, applyAll, applyAndValidate, applyAllAndValidate, getUnfixedFindings, getCurrentScore });
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
export type FixService = ReturnType<typeof createFixService>;
|