@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,444 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildFileOwnership, attributeFindings, expandPerAgentFindings, PER_AGENT_DOC_CHECK_IDS, type AgentInfo } from './finding-attribution.js';
|
|
3
|
+
import { buildImportGraph } from './import-graph.js';
|
|
4
|
+
import type { FileInfo } from '../../ports/scanner.port.js';
|
|
5
|
+
import type { Finding } from '../../types/common.types.js';
|
|
6
|
+
|
|
7
|
+
const makeFile = (path: string, content: string): FileInfo => ({
|
|
8
|
+
path: `/project/${path}`,
|
|
9
|
+
relativePath: path,
|
|
10
|
+
content,
|
|
11
|
+
extension: '.' + (path.split('.').pop() ?? 'ts'),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const makeFinding = (overrides: Partial<Finding> = {}): Finding => ({
|
|
15
|
+
checkId: 'test-check',
|
|
16
|
+
type: 'fail',
|
|
17
|
+
message: 'test finding',
|
|
18
|
+
severity: 'medium',
|
|
19
|
+
...overrides,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('buildFileOwnership', () => {
|
|
23
|
+
it('assigns files reachable by one agent only', () => {
|
|
24
|
+
const files = [
|
|
25
|
+
makeFile('agent-a/index.ts', `import { helper } from './helper.js';`),
|
|
26
|
+
makeFile('agent-a/helper.ts', `export const helper = 1;`),
|
|
27
|
+
makeFile('agent-b/main.ts', `export const b = 2;`),
|
|
28
|
+
];
|
|
29
|
+
const graph = buildImportGraph(files);
|
|
30
|
+
const agents: AgentInfo[] = [
|
|
31
|
+
{ name: 'agent-a', sourceFiles: ['agent-a/index.ts'] },
|
|
32
|
+
{ name: 'agent-b', sourceFiles: ['agent-b/main.ts'] },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const ownership = buildFileOwnership(agents, graph);
|
|
36
|
+
|
|
37
|
+
expect(ownership.fileToOwner.get('agent-a/index.ts')).toBe('agent-a');
|
|
38
|
+
expect(ownership.fileToOwner.get('agent-a/helper.ts')).toBe('agent-a');
|
|
39
|
+
expect(ownership.fileToOwner.get('agent-b/main.ts')).toBe('agent-b');
|
|
40
|
+
expect(ownership.sharedFiles.size).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('marks files reachable by 2+ agents as shared', () => {
|
|
44
|
+
const files = [
|
|
45
|
+
makeFile('agent-a/index.ts', `import { util } from '../lib/util.js';`),
|
|
46
|
+
makeFile('agent-b/main.ts', `import { util } from '../lib/util.js';`),
|
|
47
|
+
makeFile('lib/util.ts', `export const util = 1;`),
|
|
48
|
+
];
|
|
49
|
+
const graph = buildImportGraph(files);
|
|
50
|
+
const agents: AgentInfo[] = [
|
|
51
|
+
{ name: 'agent-a', sourceFiles: ['agent-a/index.ts'] },
|
|
52
|
+
{ name: 'agent-b', sourceFiles: ['agent-b/main.ts'] },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const ownership = buildFileOwnership(agents, graph);
|
|
56
|
+
|
|
57
|
+
expect(ownership.sharedFiles.has('lib/util.ts')).toBe(true);
|
|
58
|
+
expect(ownership.fileToOwner.has('lib/util.ts')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('attributeFindings', () => {
|
|
63
|
+
// Test 1: Single agent — all findings attributed
|
|
64
|
+
it('attributes all findings to sole agent', () => {
|
|
65
|
+
const files = [makeFile('src/app.ts', `export const x = 1;`)];
|
|
66
|
+
const graph = buildImportGraph(files);
|
|
67
|
+
const agents: AgentInfo[] = [{ name: 'my-agent', sourceFiles: ['src/app.ts'] }];
|
|
68
|
+
const findings = [
|
|
69
|
+
makeFinding({ file: 'src/app.ts' }),
|
|
70
|
+
makeFinding({ file: 'other.ts' }),
|
|
71
|
+
makeFinding(), // no file
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const result = attributeFindings(findings, agents, graph);
|
|
75
|
+
|
|
76
|
+
expect(result).toHaveLength(3);
|
|
77
|
+
expect(result.every(f => f.agentId === 'my-agent')).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Test 2: Two agents, separate dirs — correct attribution
|
|
81
|
+
it('attributes findings to correct agents by import graph', () => {
|
|
82
|
+
const files = [
|
|
83
|
+
makeFile('agent-a/index.ts', `import { h } from './helper.js';`),
|
|
84
|
+
makeFile('agent-a/helper.ts', `export const h = 1;`),
|
|
85
|
+
makeFile('agent-b/main.ts', `import { u } from './utils.js';`),
|
|
86
|
+
makeFile('agent-b/utils.ts', `export const u = 2;`),
|
|
87
|
+
];
|
|
88
|
+
const graph = buildImportGraph(files);
|
|
89
|
+
const agents: AgentInfo[] = [
|
|
90
|
+
{ name: 'agent-a', sourceFiles: ['agent-a/index.ts'] },
|
|
91
|
+
{ name: 'agent-b', sourceFiles: ['agent-b/main.ts'] },
|
|
92
|
+
];
|
|
93
|
+
const findings = [
|
|
94
|
+
makeFinding({ checkId: 'c1', file: 'agent-a/helper.ts' }),
|
|
95
|
+
makeFinding({ checkId: 'c2', file: 'agent-b/utils.ts' }),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const result = attributeFindings(findings, agents, graph);
|
|
99
|
+
|
|
100
|
+
expect(result[0].agentId).toBe('agent-a');
|
|
101
|
+
expect(result[1].agentId).toBe('agent-b');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Test 3: Shared utility imported by both agents → project-level
|
|
105
|
+
it('leaves shared utility findings as project-level', () => {
|
|
106
|
+
const files = [
|
|
107
|
+
makeFile('agent-a/index.ts', `import { util } from '../lib/shared.js';`),
|
|
108
|
+
makeFile('agent-b/main.ts', `import { util } from '../lib/shared.js';`),
|
|
109
|
+
makeFile('lib/shared.ts', `export const util = 1;`),
|
|
110
|
+
];
|
|
111
|
+
const graph = buildImportGraph(files);
|
|
112
|
+
const agents: AgentInfo[] = [
|
|
113
|
+
{ name: 'agent-a', sourceFiles: ['agent-a/index.ts'] },
|
|
114
|
+
{ name: 'agent-b', sourceFiles: ['agent-b/main.ts'] },
|
|
115
|
+
];
|
|
116
|
+
const findings = [makeFinding({ file: 'lib/shared.ts' })];
|
|
117
|
+
|
|
118
|
+
const result = attributeFindings(findings, agents, graph);
|
|
119
|
+
|
|
120
|
+
expect(result[0].agentId).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Test 4: Transitive imports (3 levels deep)
|
|
124
|
+
it('follows transitive imports through 3 levels', () => {
|
|
125
|
+
const files = [
|
|
126
|
+
makeFile('src/entry.ts', `import { mid } from './mid.js';`),
|
|
127
|
+
makeFile('src/mid.ts', `import { deep } from './deep.js';`),
|
|
128
|
+
makeFile('src/deep.ts', `export const deep = 1;`),
|
|
129
|
+
];
|
|
130
|
+
const graph = buildImportGraph(files);
|
|
131
|
+
const agents: AgentInfo[] = [
|
|
132
|
+
{ name: 'agent-x', sourceFiles: ['src/entry.ts'] },
|
|
133
|
+
{ name: 'agent-y', sourceFiles: [] },
|
|
134
|
+
];
|
|
135
|
+
const findings = [
|
|
136
|
+
makeFinding({ checkId: 'c1', file: 'src/mid.ts' }),
|
|
137
|
+
makeFinding({ checkId: 'c2', file: 'src/deep.ts' }),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const result = attributeFindings(findings, agents, graph);
|
|
141
|
+
|
|
142
|
+
expect(result[0].agentId).toBe('agent-x');
|
|
143
|
+
expect(result[1].agentId).toBe('agent-x');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Test 5: Finding without file (L1) in multi-agent → project-level
|
|
147
|
+
it('leaves L1 findings without file as project-level in multi-agent', () => {
|
|
148
|
+
const files = [
|
|
149
|
+
makeFile('a/index.ts', `export const a = 1;`),
|
|
150
|
+
makeFile('b/main.ts', `export const b = 2;`),
|
|
151
|
+
];
|
|
152
|
+
const graph = buildImportGraph(files);
|
|
153
|
+
const agents: AgentInfo[] = [
|
|
154
|
+
{ name: 'agent-a', sourceFiles: ['a/index.ts'] },
|
|
155
|
+
{ name: 'agent-b', sourceFiles: ['b/main.ts'] },
|
|
156
|
+
];
|
|
157
|
+
const findings = [makeFinding({ file: undefined })];
|
|
158
|
+
|
|
159
|
+
const result = attributeFindings(findings, agents, graph);
|
|
160
|
+
|
|
161
|
+
expect(result[0].agentId).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Test 6: Directory prefix fallback (doc file under agent dir)
|
|
165
|
+
it('uses directory prefix fallback for non-code files', () => {
|
|
166
|
+
const files = [
|
|
167
|
+
makeFile('agents/bot-a/src/index.ts', `export const a = 1;`),
|
|
168
|
+
makeFile('agents/bot-a/config.ts', `export const c = 1;`),
|
|
169
|
+
makeFile('agents/bot-b/src/main.ts', `export const b = 2;`),
|
|
170
|
+
makeFile('agents/bot-b/config.ts', `export const c = 2;`),
|
|
171
|
+
];
|
|
172
|
+
const graph = buildImportGraph(files);
|
|
173
|
+
const agents: AgentInfo[] = [
|
|
174
|
+
{ name: 'bot-a', sourceFiles: ['agents/bot-a/src/index.ts', 'agents/bot-a/config.ts'] },
|
|
175
|
+
{ name: 'bot-b', sourceFiles: ['agents/bot-b/src/main.ts', 'agents/bot-b/config.ts'] },
|
|
176
|
+
];
|
|
177
|
+
// README under bot-a's directory, not in import graph
|
|
178
|
+
const findings = [makeFinding({ file: 'agents/bot-a/docs/README.md' })];
|
|
179
|
+
|
|
180
|
+
const result = attributeFindings(findings, agents, graph);
|
|
181
|
+
|
|
182
|
+
expect(result[0].agentId).toBe('bot-a');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Test 7: Root-level config (package.json) → project-level
|
|
186
|
+
it('leaves root-level configs as project-level', () => {
|
|
187
|
+
const files = [
|
|
188
|
+
makeFile('src/a.ts', `export const a = 1;`),
|
|
189
|
+
makeFile('src/b.ts', `export const b = 2;`),
|
|
190
|
+
];
|
|
191
|
+
const graph = buildImportGraph(files);
|
|
192
|
+
const agents: AgentInfo[] = [
|
|
193
|
+
{ name: 'agent-a', sourceFiles: ['src/a.ts'] },
|
|
194
|
+
{ name: 'agent-b', sourceFiles: ['src/b.ts'] },
|
|
195
|
+
];
|
|
196
|
+
const findings = [makeFinding({ file: 'package.json' })];
|
|
197
|
+
|
|
198
|
+
const result = attributeFindings(findings, agents, graph);
|
|
199
|
+
|
|
200
|
+
expect(result[0].agentId).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Test 8: Empty agents array → no attribution
|
|
204
|
+
it('returns findings unchanged when no agents', () => {
|
|
205
|
+
const graph = buildImportGraph([]);
|
|
206
|
+
const findings = [makeFinding({ file: 'src/app.ts' })];
|
|
207
|
+
|
|
208
|
+
const result = attributeFindings(findings, [], graph);
|
|
209
|
+
|
|
210
|
+
expect(result[0].agentId).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Test 9: Circular imports — handled, no infinite loop
|
|
214
|
+
it('handles circular imports without infinite loop', () => {
|
|
215
|
+
const files = [
|
|
216
|
+
makeFile('src/a.ts', `import { b } from './b.js';`),
|
|
217
|
+
makeFile('src/b.ts', `import { a } from './a.js';`),
|
|
218
|
+
];
|
|
219
|
+
const graph = buildImportGraph(files);
|
|
220
|
+
const agents: AgentInfo[] = [
|
|
221
|
+
{ name: 'circle-agent', sourceFiles: ['src/a.ts'] },
|
|
222
|
+
{ name: 'other', sourceFiles: [] },
|
|
223
|
+
];
|
|
224
|
+
const findings = [
|
|
225
|
+
makeFinding({ file: 'src/a.ts' }),
|
|
226
|
+
makeFinding({ file: 'src/b.ts' }),
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const result = attributeFindings(findings, agents, graph);
|
|
230
|
+
|
|
231
|
+
expect(result[0].agentId).toBe('circle-agent');
|
|
232
|
+
expect(result[1].agentId).toBe('circle-agent');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Test 10: Graph ownership beats directory prefix
|
|
236
|
+
it('prefers graph ownership over directory prefix', () => {
|
|
237
|
+
const files = [
|
|
238
|
+
makeFile('agent-a/index.ts', `import { helper } from '../lib/helper.js';`),
|
|
239
|
+
makeFile('lib/helper.ts', `export const helper = 1;`),
|
|
240
|
+
makeFile('agent-b/main.ts', `export const b = 2;`),
|
|
241
|
+
];
|
|
242
|
+
const graph = buildImportGraph(files);
|
|
243
|
+
const agents: AgentInfo[] = [
|
|
244
|
+
{ name: 'agent-a', sourceFiles: ['agent-a/index.ts'] },
|
|
245
|
+
{ name: 'agent-b', sourceFiles: ['agent-b/main.ts'] },
|
|
246
|
+
];
|
|
247
|
+
// lib/helper.ts is only reachable from agent-a via import graph
|
|
248
|
+
// It's NOT under agent-b's directory prefix
|
|
249
|
+
const findings = [makeFinding({ file: 'lib/helper.ts' })];
|
|
250
|
+
|
|
251
|
+
const result = attributeFindings(findings, agents, graph);
|
|
252
|
+
|
|
253
|
+
// Graph ownership: agent-a imports lib/helper → owned by agent-a
|
|
254
|
+
expect(result[0].agentId).toBe('agent-a');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('E2E: full multi-agent attribution pipeline', () => {
|
|
259
|
+
it('attributes findings correctly across complex multi-agent project', () => {
|
|
260
|
+
// Simulate a real multi-agent project:
|
|
261
|
+
// - agent-chat: imports openai, has chat service + helper
|
|
262
|
+
// - agent-embed: imports @ai-sdk/openai, has embed pipeline + processor
|
|
263
|
+
// - shared lib/logger used by both
|
|
264
|
+
// - config files under each agent dir
|
|
265
|
+
// - root-level package.json
|
|
266
|
+
const files = [
|
|
267
|
+
// Agent Chat subtree
|
|
268
|
+
makeFile('agents/chat/src/index.ts',
|
|
269
|
+
`import { OpenAI } from 'openai';\nimport { format } from './format.js';\nimport { log } from '../../lib/logger.js';`),
|
|
270
|
+
makeFile('agents/chat/src/format.ts',
|
|
271
|
+
`export const format = (msg: string) => msg.trim();`),
|
|
272
|
+
// Agent Embed subtree
|
|
273
|
+
makeFile('agents/embed/src/main.ts',
|
|
274
|
+
`import { embed } from '@ai-sdk/openai';\nimport { process } from './processor.js';\nimport { log } from '../../lib/logger.js';`),
|
|
275
|
+
makeFile('agents/embed/src/processor.ts',
|
|
276
|
+
`export const process = (data: string[]) => data;`),
|
|
277
|
+
// Shared lib — imported by both
|
|
278
|
+
makeFile('lib/logger.ts',
|
|
279
|
+
`export const log = (msg: string) => console.log(msg);`),
|
|
280
|
+
// Root config
|
|
281
|
+
makeFile('package.json', `{"name":"multi-agent-project"}`),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const graph = buildImportGraph(files);
|
|
285
|
+
const agents: AgentInfo[] = [
|
|
286
|
+
{ name: 'chat-agent', sourceFiles: ['agents/chat/src/index.ts'] },
|
|
287
|
+
{ name: 'embed-agent', sourceFiles: ['agents/embed/src/main.ts'] },
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const findings = [
|
|
291
|
+
// Finding on chat's direct file
|
|
292
|
+
makeFinding({ checkId: 'sdk-no-disclosure', file: 'agents/chat/src/index.ts', severity: 'high' }),
|
|
293
|
+
// Finding on chat's transitively-imported helper
|
|
294
|
+
makeFinding({ checkId: 'missing-error-handling', file: 'agents/chat/src/format.ts', severity: 'medium' }),
|
|
295
|
+
// Finding on embed's direct file
|
|
296
|
+
makeFinding({ checkId: 'sdk-no-disclosure', file: 'agents/embed/src/main.ts', severity: 'high' }),
|
|
297
|
+
// Finding on embed's transitively-imported processor
|
|
298
|
+
makeFinding({ checkId: 'missing-validation', file: 'agents/embed/src/processor.ts', severity: 'medium' }),
|
|
299
|
+
// Finding on shared logger — project-level
|
|
300
|
+
makeFinding({ checkId: 'logging-no-retention', file: 'lib/logger.ts', severity: 'low' }),
|
|
301
|
+
// Finding on root package.json — project-level
|
|
302
|
+
makeFinding({ checkId: 'banned-package', file: 'package.json', severity: 'critical' }),
|
|
303
|
+
// L1 finding without file — project-level
|
|
304
|
+
makeFinding({ checkId: 'missing-fria', file: undefined, severity: 'high' }),
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
const result = attributeFindings(findings, agents, graph);
|
|
308
|
+
|
|
309
|
+
// Chat agent owns its files
|
|
310
|
+
expect(result[0].agentId).toBe('chat-agent'); // index.ts
|
|
311
|
+
expect(result[1].agentId).toBe('chat-agent'); // format.ts (transitive)
|
|
312
|
+
|
|
313
|
+
// Embed agent owns its files
|
|
314
|
+
expect(result[2].agentId).toBe('embed-agent'); // main.ts
|
|
315
|
+
expect(result[3].agentId).toBe('embed-agent'); // processor.ts (transitive)
|
|
316
|
+
|
|
317
|
+
// Shared logger → project-level (no agentId)
|
|
318
|
+
expect(result[4].agentId).toBeUndefined();
|
|
319
|
+
|
|
320
|
+
// Root config → project-level (no agentId)
|
|
321
|
+
expect(result[5].agentId).toBeUndefined();
|
|
322
|
+
|
|
323
|
+
// L1 finding without file → project-level
|
|
324
|
+
expect(result[6].agentId).toBeUndefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('attributeFindings is pure — does not mutate input', () => {
|
|
328
|
+
const files = [
|
|
329
|
+
makeFile('src/app.ts', `import { OpenAI } from 'openai';`),
|
|
330
|
+
];
|
|
331
|
+
const graph = buildImportGraph(files);
|
|
332
|
+
const agents: AgentInfo[] = [{ name: 'test-agent', sourceFiles: ['src/app.ts'] }];
|
|
333
|
+
const original = makeFinding({ file: 'src/app.ts' });
|
|
334
|
+
const findings = [original];
|
|
335
|
+
|
|
336
|
+
const result = attributeFindings(findings, agents, graph);
|
|
337
|
+
|
|
338
|
+
// Original finding not mutated
|
|
339
|
+
expect(original.agentId).toBeUndefined();
|
|
340
|
+
// Result has new agentId
|
|
341
|
+
expect(result[0].agentId).toBe('test-agent');
|
|
342
|
+
// Different object reference
|
|
343
|
+
expect(result[0]).not.toBe(original);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('buildFileOwnership returns frozen immutable result', () => {
|
|
347
|
+
const files = [
|
|
348
|
+
makeFile('src/a.ts', `export const a = 1;`),
|
|
349
|
+
];
|
|
350
|
+
const graph = buildImportGraph(files);
|
|
351
|
+
const agents: AgentInfo[] = [{ name: 'agent', sourceFiles: ['src/a.ts'] }];
|
|
352
|
+
|
|
353
|
+
const ownership = buildFileOwnership(agents, graph);
|
|
354
|
+
|
|
355
|
+
expect(Object.isFrozen(ownership)).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('expandPerAgentFindings', () => {
|
|
360
|
+
const agents: AgentInfo[] = [
|
|
361
|
+
{ name: 'agent-x', sourceFiles: ['src/x.ts'] },
|
|
362
|
+
{ name: 'agent-y', sourceFiles: ['src/y.ts'] },
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
it('single agent → no expansion', () => {
|
|
366
|
+
const singleAgent: AgentInfo[] = [{ name: 'sole', sourceFiles: ['src/app.ts'] }];
|
|
367
|
+
const findings = [
|
|
368
|
+
makeFinding({ checkId: 'fria', type: 'fail' }),
|
|
369
|
+
makeFinding({ checkId: 'qms', type: 'fail' }),
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const result = expandPerAgentFindings(findings, singleAgent);
|
|
373
|
+
|
|
374
|
+
expect(result).toBe(findings); // same reference, no-op
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('multi-agent, FRIA fail → expanded to N per-agent fails', () => {
|
|
378
|
+
const findings = [
|
|
379
|
+
makeFinding({ checkId: 'fria', type: 'fail', message: 'No FRIA found' }),
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
const result = expandPerAgentFindings(findings, agents);
|
|
383
|
+
|
|
384
|
+
expect(result).toHaveLength(2);
|
|
385
|
+
expect(result[0].agentId).toBe('agent-x');
|
|
386
|
+
expect(result[0].checkId).toBe('fria');
|
|
387
|
+
expect(result[1].agentId).toBe('agent-y');
|
|
388
|
+
expect(result[1].checkId).toBe('fria');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('multi-agent, FRIA pass → expanded to per-agent passes', () => {
|
|
392
|
+
const findings = [
|
|
393
|
+
makeFinding({ checkId: 'fria', type: 'pass', message: 'FRIA found' }),
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const result = expandPerAgentFindings(findings, agents);
|
|
397
|
+
|
|
398
|
+
expect(result).toHaveLength(2);
|
|
399
|
+
expect(result[0]).toMatchObject({ checkId: 'fria', type: 'pass', agentId: 'agent-x' });
|
|
400
|
+
expect(result[1]).toMatchObject({ checkId: 'fria', type: 'pass', agentId: 'agent-y' });
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('multi-agent, mixed per-agent + project-only checks', () => {
|
|
404
|
+
const findings = [
|
|
405
|
+
makeFinding({ checkId: 'fria', type: 'fail' }),
|
|
406
|
+
makeFinding({ checkId: 'risk-management', type: 'fail' }),
|
|
407
|
+
makeFinding({ checkId: 'qms', type: 'fail' }), // project-level, not expanded
|
|
408
|
+
makeFinding({ checkId: 'technical-documentation', type: 'pass' }), // pass, expanded per-agent
|
|
409
|
+
makeFinding({ checkId: 'ai-literacy', type: 'fail' }), // project-level
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
const result = expandPerAgentFindings(findings, agents);
|
|
413
|
+
|
|
414
|
+
// fria fail → 2, risk-management fail → 2, qms → 1, tech-doc pass → 2, ai-literacy → 1
|
|
415
|
+
expect(result).toHaveLength(8);
|
|
416
|
+
|
|
417
|
+
// First 2: fria expanded
|
|
418
|
+
expect(result[0]).toMatchObject({ checkId: 'fria', agentId: 'agent-x' });
|
|
419
|
+
expect(result[1]).toMatchObject({ checkId: 'fria', agentId: 'agent-y' });
|
|
420
|
+
|
|
421
|
+
// Next 2: risk-management expanded
|
|
422
|
+
expect(result[2]).toMatchObject({ checkId: 'risk-management', agentId: 'agent-x' });
|
|
423
|
+
expect(result[3]).toMatchObject({ checkId: 'risk-management', agentId: 'agent-y' });
|
|
424
|
+
|
|
425
|
+
// qms stays project-level
|
|
426
|
+
expect(result[4]).toMatchObject({ checkId: 'qms' });
|
|
427
|
+
expect(result[4].agentId).toBeUndefined();
|
|
428
|
+
|
|
429
|
+
// tech-doc pass expanded per-agent (each agent gets credit)
|
|
430
|
+
expect(result[5]).toMatchObject({ checkId: 'technical-documentation', type: 'pass', agentId: 'agent-x' });
|
|
431
|
+
expect(result[6]).toMatchObject({ checkId: 'technical-documentation', type: 'pass', agentId: 'agent-y' });
|
|
432
|
+
|
|
433
|
+
// ai-literacy stays project-level
|
|
434
|
+
expect(result[7]).toMatchObject({ checkId: 'ai-literacy' });
|
|
435
|
+
expect(result[7].agentId).toBeUndefined();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('PER_AGENT_DOC_CHECK_IDS contains all 7 per-agent checks', () => {
|
|
439
|
+
expect(PER_AGENT_DOC_CHECK_IDS.size).toBe(7);
|
|
440
|
+
expect(PER_AGENT_DOC_CHECK_IDS.has('fria')).toBe(true);
|
|
441
|
+
expect(PER_AGENT_DOC_CHECK_IDS.has('data-governance')).toBe(true);
|
|
442
|
+
expect(PER_AGENT_DOC_CHECK_IDS.has('qms')).toBe(false);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Finding } from '../../types/common.types.js';
|
|
2
|
+
import type { ImportGraph } from './import-graph.js';
|
|
3
|
+
|
|
4
|
+
export interface AgentInfo {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly sourceFiles: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FileOwnershipMap {
|
|
10
|
+
readonly fileToOwner: ReadonlyMap<string, string>; // file → sole agent
|
|
11
|
+
readonly sharedFiles: ReadonlySet<string>; // files reachable by 2+ agents
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* BFS forward from each agent's sourceFiles through the import graph.
|
|
16
|
+
* Files reachable by exactly 1 agent → owned by that agent.
|
|
17
|
+
* Files reachable by 2+ agents → shared (project-level).
|
|
18
|
+
*/
|
|
19
|
+
export const buildFileOwnership = (
|
|
20
|
+
agents: readonly AgentInfo[],
|
|
21
|
+
importGraph: ImportGraph,
|
|
22
|
+
): FileOwnershipMap => {
|
|
23
|
+
// reachable[file] = set of agent names that can reach it
|
|
24
|
+
const reachableBy = new Map<string, Set<string>>();
|
|
25
|
+
|
|
26
|
+
for (const agent of agents) {
|
|
27
|
+
const visited = new Set<string>();
|
|
28
|
+
const queue = [...agent.sourceFiles];
|
|
29
|
+
|
|
30
|
+
// Seed: mark all sourceFiles as visited
|
|
31
|
+
for (const sf of agent.sourceFiles) {
|
|
32
|
+
visited.add(sf);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
while (queue.length > 0) {
|
|
36
|
+
const current = queue.shift()!;
|
|
37
|
+
|
|
38
|
+
// Record reachability
|
|
39
|
+
if (!reachableBy.has(current)) reachableBy.set(current, new Set());
|
|
40
|
+
reachableBy.get(current)!.add(agent.name);
|
|
41
|
+
|
|
42
|
+
// BFS forward through imports
|
|
43
|
+
const node = importGraph.nodes.get(current);
|
|
44
|
+
if (!node) continue;
|
|
45
|
+
for (const imp of node.imports) {
|
|
46
|
+
if (!visited.has(imp)) {
|
|
47
|
+
visited.add(imp);
|
|
48
|
+
queue.push(imp);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fileToOwner = new Map<string, string>();
|
|
55
|
+
const sharedFiles = new Set<string>();
|
|
56
|
+
|
|
57
|
+
for (const [file, owners] of reachableBy) {
|
|
58
|
+
if (owners.size === 1) {
|
|
59
|
+
fileToOwner.set(file, owners.values().next().value!);
|
|
60
|
+
} else {
|
|
61
|
+
sharedFiles.add(file);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Object.freeze({ fileToOwner, sharedFiles });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compute common directory prefix of an agent's sourceFiles.
|
|
70
|
+
* Returns '' if no common prefix.
|
|
71
|
+
*/
|
|
72
|
+
const computeAgentRoot = (sourceFiles: readonly string[]): string => {
|
|
73
|
+
const dirs = sourceFiles
|
|
74
|
+
.map(f => {
|
|
75
|
+
const idx = f.lastIndexOf('/');
|
|
76
|
+
return idx >= 0 ? f.substring(0, idx + 1) : '';
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
|
|
80
|
+
if (dirs.length === 0) return '';
|
|
81
|
+
|
|
82
|
+
let common = dirs[0];
|
|
83
|
+
for (const d of dirs) {
|
|
84
|
+
while (common && !d.startsWith(common)) {
|
|
85
|
+
const trimmed = common.substring(0, common.length - 1);
|
|
86
|
+
const slash = trimmed.lastIndexOf('/');
|
|
87
|
+
common = slash >= 0 ? trimmed.substring(0, slash + 1) : '';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return common;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build sorted (longest first) agent root directories for prefix fallback.
|
|
95
|
+
*/
|
|
96
|
+
const buildAgentRoots = (
|
|
97
|
+
agents: readonly AgentInfo[],
|
|
98
|
+
): readonly { readonly name: string; readonly root: string }[] => {
|
|
99
|
+
const roots: { name: string; root: string }[] = [];
|
|
100
|
+
for (const agent of agents) {
|
|
101
|
+
const root = computeAgentRoot(agent.sourceFiles);
|
|
102
|
+
if (root) roots.push({ name: agent.name, root });
|
|
103
|
+
}
|
|
104
|
+
// Longest root first → most specific match wins
|
|
105
|
+
roots.sort((a, b) => b.root.length - a.root.length);
|
|
106
|
+
return roots;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Check IDs that require per-agent documents in multi-agent projects (EU AI Act per-AI-system). */
|
|
110
|
+
export const PER_AGENT_DOC_CHECK_IDS: ReadonlySet<string> = new Set([
|
|
111
|
+
'fria',
|
|
112
|
+
'risk-management',
|
|
113
|
+
'technical-documentation',
|
|
114
|
+
'declaration-of-conformity',
|
|
115
|
+
'art5-screening',
|
|
116
|
+
'instructions-for-use',
|
|
117
|
+
'data-governance',
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* In multi-agent projects, expand project-level document-presence findings
|
|
122
|
+
* into per-agent findings. Each agent needs its own compliance documents,
|
|
123
|
+
* and each agent should receive credit for existing project-level docs.
|
|
124
|
+
*
|
|
125
|
+
* - agents.length ≤ 1 → no-op (single agent already gets all findings)
|
|
126
|
+
* - PASS/FAIL/SKIP on per-agent checkId → N findings, one per agent with agentId
|
|
127
|
+
*/
|
|
128
|
+
export const expandPerAgentFindings = (
|
|
129
|
+
findings: readonly Finding[],
|
|
130
|
+
agents: readonly AgentInfo[],
|
|
131
|
+
): readonly Finding[] => {
|
|
132
|
+
if (agents.length <= 1) return findings;
|
|
133
|
+
|
|
134
|
+
const result: Finding[] = [];
|
|
135
|
+
|
|
136
|
+
for (const f of findings) {
|
|
137
|
+
if (!PER_AGENT_DOC_CHECK_IDS.has(f.checkId)) {
|
|
138
|
+
result.push(f);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Expand doc findings to all agents — both passes and fails
|
|
143
|
+
for (const agent of agents) {
|
|
144
|
+
result.push({ ...f, agentId: agent.name });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Attribute findings to agents using import-graph ownership + directory prefix fallback.
|
|
153
|
+
*
|
|
154
|
+
* Algorithm:
|
|
155
|
+
* - 0 agents → return as-is
|
|
156
|
+
* - 1 agent → all findings → that agent (fast path)
|
|
157
|
+
* - N agents → graph-based ownership, then directory prefix fallback for non-code files
|
|
158
|
+
*/
|
|
159
|
+
export const attributeFindings = (
|
|
160
|
+
findings: readonly Finding[],
|
|
161
|
+
agents: readonly AgentInfo[],
|
|
162
|
+
importGraph: ImportGraph,
|
|
163
|
+
): readonly Finding[] => {
|
|
164
|
+
if (agents.length === 0) return findings;
|
|
165
|
+
|
|
166
|
+
// Single agent → all findings belong to it
|
|
167
|
+
if (agents.length === 1) {
|
|
168
|
+
const sole = agents[0].name;
|
|
169
|
+
return findings.map(f => ({ ...f, agentId: sole }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Multi-agent: graph-based ownership + directory prefix fallback
|
|
173
|
+
const ownership = buildFileOwnership(agents, importGraph);
|
|
174
|
+
const agentRoots = buildAgentRoots(agents);
|
|
175
|
+
|
|
176
|
+
return findings.map(f => {
|
|
177
|
+
// 1. No file (L1) → project-level
|
|
178
|
+
if (!f.file) return f;
|
|
179
|
+
|
|
180
|
+
// 2. Graph-based: sole owner
|
|
181
|
+
const owner = ownership.fileToOwner.get(f.file);
|
|
182
|
+
if (owner) return { ...f, agentId: owner };
|
|
183
|
+
|
|
184
|
+
// 3. Shared file (reachable by 2+ agents) → project-level
|
|
185
|
+
if (ownership.sharedFiles.has(f.file)) return f;
|
|
186
|
+
|
|
187
|
+
// 4. Directory prefix fallback (for docs, configs not in import graph)
|
|
188
|
+
for (const { name, root } of agentRoots) {
|
|
189
|
+
if (f.file.startsWith(root)) return { ...f, agentId: name };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 5. Root-level / unattributable → project-level
|
|
193
|
+
return f;
|
|
194
|
+
});
|
|
195
|
+
};
|