CONFIDENTIAL — internal use only. Do not share outside your organization.
CONFIDENTIAL — Spectra report — 2026-04-30 05:33 UTC — 7fafa47a
Indicative analysis — not auditor-grade evidence. Spectra runs 8 LLM agents over your code; findings are heuristic and require human verification before being treated as compliance evidence, audit input, or pass/fail signal in regulated workflows. Learn more
Architecture 80 B
Security 73 C+
Quality 94 A
Documentation 92 A
Maintainability 93 A
Performance 94 A
Executive Summary
Your codebase scores B+ (86/100) — strong quality with security gaps

### Top Strengths

Quality A (95)
Performance A (94)
Maintainability A (94)

### Key Concerns

Security C+ (74)
Architecture B (81)
Documentation A (92)
Severity Distribution
high (1) medium (12) low (21)
34 findings · 6 agents · 244s · ~74h tech debt
What to Fix First

1. 01> high Layer violation: cli_controller imports infrastructure exception type src/spectra/adapters/cli_controller.py:540

   Move PolicyGateError into src/spectra/entities/errors.py (or a new use_cases module). Both infrastructure/main.py and adapters/cli_controller.py should import the single canonical class. This eliminates the duplicate-class hazard and keeps the adapter layer free of infrastructure coupling.

2. 02> medium Composition root function exceeds reasonable size and orchestrates too many concerns src/spectra/infrastructure/main.py:130

   Extract the pipeline stages currently inlined in _run_analysis (INGEST, PREFLIGHT, REPORT, policy gate, receipt attachment) into the use_cases layer so analyze_repository owns all 6 stages. Keep _run_analysis as pure DI wiring + delegation. This restores the Clean Architecture invariant that orchestration lives in use_cases and infrastructure only provides adapters.

3. 03> medium Adapter layer imports from infrastructure via deferred imports src/spectra/adapters/waiver_cli.py:130

   Define a WaiverSigner Protocol in use_cases/interfaces.py and inject the YamlWaiverAdapter implementation via a setter (mirroring set_analyzer_factory / set_cache_provider in cli_controller.py). The composition root wires the concrete signer; waiver_cli depends only on the Protocol. This eliminates both the deferred imports and the cryptography coupling.

4. 04> medium Composition root uses 'object' typing for ports, defeating Protocol enforcement src/spectra/infrastructure/main.py:119

   Import the Protocol types (LLMGateway, AuditPort, MetaPrompterPort, SpecialistPort, CritiquePort) from use_cases/interfaces and type the parameters/returns precisely. Drop the 'type: ignore[arg-type]' once the types align. This restores the dependency-inversion contract the architecture promises.

5. 05> medium Cache key composition logic in composition root reaches into agent prompt internals src/spectra/infrastructure/main.py:335

   Each agent module should expose a public prompt_version() function returning its hash contribution. The composition root composes the per-agent versions without touching private attributes. This makes the cache-version contract explicit and refactor-safe.

B+ 0 / 100
Industry median: 65 · Well above industry median for all projects B grades represent well-maintained codebases with room for improvement
Architecture81
Security74
Quality95
Documentation92
Maintainability94
Performance94
Architecture
0 B
Security
0 C+
Quality
0 A
Documentation
0 A
Maintainability
0 A
Performance
0 A
0
Findings
0
Critical
0
Duration
$0
Cost
0
Agents
Analysis Coverage
6
Agents
544K
Tokens Used
4m 4s
Duration
$5.99
API Cost
1
Hallucinations Removed
Agents Used
Architecture Security Quality Documentation Dependency Performance
1 finding removed (referenced non-existent files)
File Hotspot Heatmap

- 13
  src/spectra/infrastructure/main.py
- 8
  src/spectra/adapters/cli_controller.py
- 7
  pyproject.toml
- 2
  src/spectra/adapters/waiver_cli.py
- 1
  src/spectra/entities/models.py
- 1
  src/spectra/entities/errors.py
- 1
  renovate.json
- 1
  src/spectra/adapters/pr_comment_renderer.py

Filter

CritiqueAgent validated findings using extended thinking. 3 of 34 findings were individually confirmed.

## Architecture (7)

high Layer violation: cli_controller imports infrastructure exception type ~1.0h
▶
src/spectra/adapters/cli_controller.py:540-560
src/spectra/adapters/cli_controller.py defines and catches PolicyGateError, but the same exception name is also defined and raised in src/spectra/infrastructure/main.py (_enforce_policy). The CLI controller (Layer 3) sits between use_cases and infrastructure and should not couple to an exception type owned by the composition root. Either the exception is duplicated (two distinct classes named PolicyGateError, breaking the except clause) or the adapter implicitly depends on infrastructure semantics. The adapters/__init__.py docstring explicitly states 'never imports from infrastructure/' yet this contract is brittle here.
Recommendation: Move PolicyGateError into src/spectra/entities/errors.py (or a new use_cases module). Both infrastructure/main.py and adapters/cli_controller.py should import the single canonical class. This eliminates the duplicate-class hazard and keeps the adapter layer free of infrastructure coupling.
medium Composition root function exceeds reasonable size and orchestrates too many concerns ~8.0h
▶
src/spectra/infrastructure/main.py:130-280
_run_analysis in infrastructure/main.py spans ~150 lines and orchestrates DI wiring, workspace allocation, ingest, preflight, agent construction, pipeline delegation, receipt attachment, classification stamping, policy enforcement, and report rendering across 6 stages. Even with the noqa for PLR0915, this concentration makes the composition root the de-facto orchestrator and blurs the boundary with the use_cases layer (analyze_repository). Stage 1 (INGEST), Stage 1.5 (PREFLIGHT), Stage 6 (REPORT), and policy enforcement live in infrastructure rather than in the use case, splitting pipeline orchestration across two layers.
Recommendation: Extract the pipeline stages currently inlined in _run_analysis (INGEST, PREFLIGHT, REPORT, policy gate, receipt attachment) into the use_cases layer so analyze_repository owns all 6 stages. Keep _run_analysis as pure DI wiring + delegation. This restores the Clean Architecture invariant that orchestration lives in use_cases and infrastructure only provides adapters.
medium Adapter layer imports from infrastructure via deferred imports ~4.0h ✓ validated
▶
src/spectra/adapters/waiver_cli.py:130-220
src/spectra/adapters/waiver_cli.py contains two deferred 'from spectra.infrastructure.yaml_waiver_adapter import ...' statements with the comment 'break the adapters↔infrastructure circular at module load'. Deferred imports to dodge a circular dependency are an architectural smell, not a fix — they confirm the dependency rule is being violated. The adapters/__init__.py docstring explicitly promises adapters 'never imports from infrastructure/', but waiver_cli.py breaks that contract at function-call time. This also drags the cryptography module (Ed25519PrivateKey) directly into the adapter, further blurring the boundary.
Recommendation: Define a WaiverSigner Protocol in use_cases/interfaces.py and inject the YamlWaiverAdapter implementation via a setter (mirroring set_analyzer_factory / set_cache_provider in cli_controller.py). The composition root wires the concrete signer; waiver_cli depends only on the Protocol. This eliminates both the deferred imports and the cryptography coupling.
medium Composition root uses 'object' typing for ports, defeating Protocol enforcement ~2.0h
▶
src/spectra/infrastructure/main.py:119-138
_build_agents accepts gateway: object and returns tuple[object, list[object], object | None]; _build_audit_port returns object | None; PipelineContext fields like meta_prompter/specialists/critique_agent are passed without typed Protocols at this seam. This forces a 'type: ignore[arg-type]' on AgentFactory and bypasses the structural-subtyping guarantees the rest of the architecture depends on. The very purpose of Protocol-based ports is compile-time verification that the decorator chain (Logging → Retry → Anthropic) satisfies LLMGateway — typing as 'object' erases that guarantee.
Recommendation: Import the Protocol types (LLMGateway, AuditPort, MetaPrompterPort, SpecialistPort, CritiquePort) from use_cases/interfaces and type the parameters/returns precisely. Drop the 'type: ignore[arg-type]' once the types align. This restores the dependency-inversion contract the architecture promises.
medium Cache key composition logic in composition root reaches into agent prompt internals ~2.0h
▶
src/spectra/infrastructure/main.py:335-360
_composite_model_versions and _composite_prompt_versions in main.py import _SHARED_GUIDANCE, SPECIALIST_CONFIGS, and _CRITIQUE_PROMPT (note the leading underscore — these are private module attributes) from infrastructure.agents.* and hash them. The composition root reaches into private agent internals to compute cache invariants. While the module docstring acknowledges 'Keeping this in the composition root preserves the dependency rule', it does so by binding to private names that any agent refactor will silently break.
Recommendation: Each agent module should expose a public prompt_version() function returning its hash contribution. The composition root composes the per-agent versions without touching private attributes. This makes the cache-version contract explicit and refactor-safe.
low _invoke_analyzer is dead code — duplicate exception handling never reached ~1.0h
▶
src/spectra/adapters/cli_controller.py:580-625
cli_controller.analyze() inlines its own asyncio.run + try/except block (~lines 480-520) AND defines a near-identical _invoke_analyzer helper (~lines 580-620) that is never called. The helper's docstring says it was 'extracted from analyze to keep that function under the cyclomatic branch ceiling' but the extraction was never applied. This is duplicated exception-mapping logic that will drift out of sync (PolicyGateError handling exists only in the inlined version, BudgetExceededError exists in both).
Recommendation: Either delete _invoke_analyzer entirely or refactor analyze() to call it (and add PolicyGateError handling to the helper). Pick one path and remove the duplicate to prevent the two error-handling tables from diverging.
low ReportError defined in infrastructure but represents a domain failure ~0.5h
▶
src/spectra/infrastructure/main.py:105-117
ReportError is defined in infrastructure/main.py but wraps SPEC-009 (a domain error code from entities/errors.py). It is conceptually a domain-level signal that report rendering failed and ought to live alongside the other error classes (AgentError, GitError, etc.) in entities/errors.py. Defining it in the composition root means any other layer that wants to catch it must import from infrastructure — exactly the dependency direction the architecture forbids.
Recommendation: Move class ReportError to src/spectra/entities/errors.py next to AgentError and GitError. Re-export from entities/__init__.py. Infrastructure remains the only place that raises it, but any layer can catch it without importing infrastructure.
estimated effort: ~18h

## Security (9)

medium Pinned dependency pysqlcipher3 has known maintenance concerns ~4.0h
▶
pyproject.toml:44-48
pyproject.toml depends on pysqlcipher3>=1.2,<2.0. pysqlcipher3 is largely unmaintained (last meaningful release ~2020) and lacks pre-built wheels for many platforms, with reports of build failures on modern Python. While the adapter docs claim graceful degradation to plain SQLite (cache+HMAC remain functional), the dependency itself represents supply-chain risk for at-rest encryption (CWE-1395 Dependency on Vulnerable Third-Party Component, CWE-1104 Use of Unmaintained Third Party Components).
Recommendation: Move pysqlcipher3 to optional-dependencies (e.g., [project.optional-dependencies] encrypted = ["pysqlcipher3>=1.2,<2.0"]) so plain installs aren't blocked by its build issues. Evaluate sqlcipher3-binary (community fork with wheels) or libsql/turso as actively maintained alternatives. Document the at-rest-encryption fallback in SECURITY.md.
medium Receipt verification trust-on-first-use without key pinning ~3.0h
▶
src/spectra/adapters/cli_controller.py:595-615
verify command loads the public key from --key argument or default_receipt_public_key_path() with no fingerprint pinning, key rotation log, or out-of-band attestation. An attacker who can write to ~/.config/spectra/receipt.pub can replace the pubkey and forge receipts that pass verification (CWE-295 Improper Certificate Validation, CWE-322 Key Exchange without Entity Authentication). Mitigations: KeyringReceiptKeyStore likely binds to keyring, but the verify path accepts arbitrary file paths.
Recommendation: When verifying, also print the key fingerprint (sha256 of the DER-encoded pubkey, first 8 bytes hex) so reviewers can compare against an out-of-band trusted value. Document in SECURITY.md that the public key must be distributed via a trusted channel (signed release artifact, package metadata, or a transparency log).
low Symlink TOCTOU window in local-path validation ~2.0h
▶
src/spectra/adapters/cli_controller.py:70-85
_validate_local_path() rejects symlinked roots via expanded.is_symlink(), but only checks the top-level path. Intermediate path components or files inside the workspace may still be symlinks pointing outside the repo, and a race between validate-time and clone/read-time is possible (CWE-367 Time-of-check Time-of-use, CWE-59 Link Following). Since GitAdapter later reads files inside this directory, an attacker controlling the local filesystem could substitute symlinks. Risk is low because local-path mode requires user-supplied trusted paths, but defense-in-depth would resolve and lock the realpath.
Recommendation: Resolve the path with .resolve(strict=True) and store the canonical realpath; reject if it escapes a documented allowlist root. In GitAdapter file readers, validate that each resolved file path stays under the canonical workspace root (anchor check via Path.is_relative_to).
low URL regex permits IDN/userinfo edge cases not explicitly rejected ~1.0h
▶
src/spectra/adapters/cli_controller.py:36-41
_URL_PATTERN allows hostnames matching [a-zA-Z0-9][-a-zA-Z0-9.]* but does not reject embedded userinfo (https://attacker@github.com/...) because the path portion ([^\s]*) is broad. While git itself may reject some forms, defense-in-depth should explicitly exclude '@' in the authority section to prevent SSRF-style spoofing on terminals that render the pre-@ portion as a 'safe' host (CWE-601 URL Redirection / CWE-20 Improper Input Validation).
Recommendation: Tighten the regex to forbid '@' in the authority and disallow control characters in the path. Better: parse with urllib.parse.urlsplit() and assert scheme=='https', not username, not password, hostname matches a strict pattern, and port is None or in {443}.
low Bare Exception suppression hides errors during file read in heuristic source loader ~0.5h
▶
src/spectra/infrastructure/main.py:540-545
_read_key_source_files() catches `Exception` and `continue`s silently (noqa: S112). While bandit is suppressed intentionally, swallowing all exceptions can mask programming errors and security-relevant conditions (e.g., decoding errors from binary files masquerading as source, OSError from path traversal attempts). CWE-703 (Improper Check or Handling of Exceptional Conditions).
Recommendation: Narrow the except to (OSError, UnicodeDecodeError, ValueError) and log unexpected exceptions at DEBUG level so they surface in --verbose mode. Consider re-raising on AgentError to preserve domain error semantics.
low Receipt signing failure silently swallowed via broad Exception ~1.0h
▶
src/spectra/infrastructure/main.py:444-451
_attach_receipt() catches a bare Exception and degrades to an unsigned report. While documented as intentional ("degrade, never fail"), the broad catch prevents distinguishing between recoverable I/O failures (missing keystore) and programming errors (Pydantic validation bug). CWE-755 (Improper Handling of Exceptional Conditions). The audit trail loses tamper-evidence with no signal to the operator beyond a WARN log.
Recommendation: Narrow to (OSError, FileNotFoundError, AgentError, ValueError) so unexpected exceptions surface. Emit an AuditEvent of type 'scan.degraded' with a 'receipt_signing_failed' marker so SIEM consumers detect regulated-environment runs that lack receipts.
low Approver private key persisted in OS keyring as hex without passphrase wrap ~8.0h ✓ validated
▶
src/spectra/adapters/waiver_cli.py:135-145
InMemoryKeyring/keyring backend stores the raw Ed25519 private seed as a hex string under service 'spectra-approvers'. While the OS keyring provides per-user isolation (macOS Keychain, Linux Secret Service, Windows Credential Manager), an attacker with same-user code-execution can call backend.get_password() to extract the seed and forge waivers indefinitely (CWE-522 Insufficiently Protected Credentials, CWE-312 Cleartext Storage of Sensitive Information). The approver model documents this private-key seed location explicitly.
Recommendation: Wrap the seed with a passphrase-derived KEK (PBKDF2/Argon2id) so that even keyring extraction requires interactive consent. Alternative: integrate with platform hardware-backed keystores (Secure Enclave / TPM) for higher-assurance approver workflows. At minimum, add a CLI 'spectra approver rotate' command and document the threat model.
low _compute_score_card_hash relies on score_card.model_dump output stability across pydantic versions ~4.0h
▶
src/spectra/adapters/cli_controller.py:636-645
verify command recomputes the hash via json.dumps(score_card.model_dump(mode='json'), sort_keys=True). If pydantic v2 minor versions change serialization defaults (e.g., float precision, datetime format, Optional ordering), receipts signed today may fail verification later (CWE-345 Insufficient Verification of Data Authenticity). Pinning pydantic>=2.5,<3.0 helps but does not eliminate within-major drift.
Recommendation: Hash a versioned, frozen canonical form: include a 'canonical_version' field in the receipt and have the signer/verifier select the matching canonicalization function. Add a regression test fixture that locks the serialized bytes for a representative ScoreCard so a pydantic upgrade that changes output is caught by CI.
low Per-user cache MAC degrades silently to None on Windows / keyring failure ~3.0h
▶
src/spectra/infrastructure/main.py:315-332
_resolve_cache_secret() returns None on Windows (no os.geteuid) and on AgentError from KeyringSecretAdapter. SqliteCacheAdapter then runs without HMAC. This means a same-user attacker who can write to cache.db on Windows or a misconfigured Linux box can plant cached findings that the next analyze run returns to the user without authentication (CWE-345 Insufficient Verification of Data Authenticity, CWE-639 Authorization Bypass Through User-Controlled Key). The fallback is documented but produces no visible warning to the user during analyze runs.
Recommendation: On Windows, derive a UID-equivalent from getpass.getuser() + machine SID rather than disabling MAC. When MAC is disabled, surface a one-line WARN to the console (not just the logger) so users see 'cache integrity disabled — running without HMAC verification'. Consider failing-closed (refuse to read cache) when MAC is unavailable in regulated mode.
estimated effort: ~26h

## Quality (2)

medium Duplicated exception-handling block between analyze() and _invoke_analyzer() ~1.5h
▶
src/spectra/adapters/cli_controller.py
src/spectra/adapters/cli_controller.py defines two near-identical try/except ladders: one inline in analyze() (KeyboardInterrupt → SecretDetectedError → PolicyGateError → BudgetExceededError → GitError/SpectraRetryError/AgentError → Exception) and another in _invoke_analyzer() that drops only PolicyGateError. _invoke_analyzer is documented as an extraction to keep analyze() under the cyclomatic ceiling but is never actually called — analyze() still calls asyncio.run(_analyzer_factory(...)) directly. This is dead code plus duplicated error mapping, increasing maintenance risk: any new exception type must be added in two places.
Recommendation: Either delete _invoke_analyzer() (dead) and accept the noqa on analyze(), or replace the inline try/except in analyze() with a call to _invoke_analyzer() and add PolicyGateError handling there. Eliminate the duplication.
medium _run_analysis() in main.py is a long sequential function with broad responsibilities ~6.0h
▶
src/spectra/infrastructure/main.py
src/spectra/infrastructure/main.py:_run_analysis is annotated `# noqa: PLR0915 — composition-root sequential setup` and orchestrates: API key check, decorator chain build, cache provisioning, workspace allocation, INGEST stage, PREFLIGHT stage, source-file pre-read, repo name derivation, agent factory, audit/actor/run_id wiring, waiver loading, cost-tracker construction, PipelineContext build, analyze_repository, receipt attach, classification stamp, policy enforce, REPORT stage, and finally cleanup. It runs ~120 lines and is the orchestration spine for the whole product. Tests for individual stages are harder to write against this monolith.
Recommendation: Extract a `_PipelineRun` class or two helpers — `_setup_pipeline(...)` returning a PipelineContext, and `_render_outputs(report, ...)` — so the composition root reads as 4-5 named steps. Keep the noqa scoped to the smaller helper.
estimated effort: ~8h

## Documentation (5)

medium _run_analysis docstring missing several documented parameters ~0.5h
▶
src/spectra/infrastructure/main.py:130-175
The _run_analysis() function in infrastructure/main.py accepts audit_sink, classification, max_cost_usd, and max_cost_per_hour parameters, but the Args section of its docstring stops at allow_secrets and never documents these four. Given this is the composition root's primary async entry point and these parameters control cost gates and audit sink wiring (significant operational concerns), the omission is meaningful for maintainers wiring new callers.
Recommendation: Extend the Args block of _run_analysis to document audit_sink (sink spec format: stdout|file:<path>|otlp:<url>), classification (confidential|public + redaction implications), max_cost_usd (per-run cap, raises BudgetExceededError), and max_cost_per_hour (rolling window, requires SqliteCostTracker).
medium ADR references in code lack a discoverable index ~4.0h
▶
src/spectra/entities/models.py
The codebase references many ADRs by number (ADR-011 §1, ADR-011 §2, ADR-012, ADR-014, ADR-015, ADR-018, ADR-020) and SPEC error codes (SPEC-001 through SPEC-014) inline in docstrings and comments. Without the actual docs/architecture/ files visible, there is no way for a new reader of, e.g., 'ADR-011 §2: True when the CritiqueAgent detected a prompt-injection attempt' to navigate to the source decision. The PLAN concern about 'ADR consistency and currency (20+ ADRs)' is well-founded.
Recommendation: Ensure docs/architecture/ contains an ADR index (e.g. README.md listing all 20+ ADRs with status/date) and that each inline ADR-NNN reference resolves to a stable filename pattern (e.g. docs/architecture/adr/ADR-011-prompt-injection.md). Add a CONTRIBUTING.md rule requiring inline ADR-NNN citations to point to a real file.
low PolicyGateError defined out of order, undocumented at first reference ~0.5h
▶
src/spectra/adapters/cli_controller.py
In src/spectra/adapters/cli_controller.py, PolicyGateError is caught in the analyze() exception handler before its class definition appears further down the file. While the class itself has a docstring, a reader scanning analyze() top-to-bottom encounters the symbol with no nearby documentation pointer. The class is also not exported via __all__ from adapters/__init__.py despite being raised across the analyzer/CLI seam.
Recommendation: Move the PolicyGateError class definition above analyze(), or add a one-line comment in the except clause linking to the class definition. Add PolicyGateError to adapters/__init__.py __all__ since infrastructure/main.py imports and raises it across the seam.
low SPEC-NNN error codes documented in registry but no user-facing reference ~3.0h
▶
src/spectra/entities/errors.py:35-75
ERRORS dict in entities/errors.py defines SPEC-001 through SPEC-014 with code, message, retryable, and max_retries metadata. CLI output surfaces these codes to end users (e.g. 'SPEC-013: 3 policy violations'). Without the actual SECURITY.md/docs visible it cannot be confirmed there is a user-facing table mapping every SPEC-NNN to remediation guidance. End users hitting 'SPEC-010: Cache I/O failed' need a documentation anchor.
Recommendation: Add a 'Troubleshooting' or 'Error reference' section to README.md (or docs/troubleshooting.md) listing every SPEC-NNN with: code, message, when it fires, what the user should do. Cross-link CLI error output to the section anchor (e.g. https://...#SPEC-013).
low CHANGELOG.md presence claimed but not provided; pyproject points to GitHub Releases ~0.5h
▶
pyproject.toml
pyproject.toml sets [project.urls] Changelog = 'https://github.com/leocder07/spectra/releases', which redirects users away from any in-repo CHANGELOG.md. Since the PLAN claims a CHANGELOG.md exists, there is potential conflict between two sources of truth (in-repo file vs. GitHub Releases page). For a 0.6.0 alpha tool with active SPEC error codes and schema versions, version history clarity matters.
Recommendation: Pick one canonical changelog source. If CHANGELOG.md is the source of truth, point [project.urls].Changelog to the raw file in main. If GitHub Releases is canonical, note that in CHANGELOG.md with a one-line redirect. Document which version-bumping flows update which file.
estimated effort: ~8h

## Maintainability (6)

medium pysqlcipher3 is unmaintained — last release 2022, sparse Python 3.12 support ~4.0h ✓ validated
▶
pyproject.toml:41-41
pyproject.toml pins pysqlcipher3>=1.2,<2.0 as a runtime dependency. The pysqlcipher3 package on PyPI has had no release since 2022, has open issues for Python 3.12+ build failures, and lacks prebuilt wheels for most platforms. The code comment acknowledges this fragility ('Windows wheels are scarce'). This is a supply-chain reliability and maintenance risk for a security-sensitive feature (cache-at-rest encryption).
Recommendation: Migrate to a maintained alternative such as sqlcipher3-binary (actively maintained fork with prebuilt wheels) or sqlcipher3-wheels. Alternatively, drop SQLCipher entirely and rely on the documented HMAC + per-user file isolation since the adapter already degrades gracefully. Make the dependency optional via [project.optional-dependencies].at-rest-encryption.
low Dependabot and Renovate both configured — likely PR-noise overlap ~1.0h
▶
renovate.json
Both .github/dependabot.yml and renovate.json are present in the repository. Running both bots against the same ecosystem typically produces duplicate update PRs, conflicting branch names, and increased maintainer fatigue. They should not be run in parallel for the same package manager.
Recommendation: Pick one. Renovate is generally more flexible (grouping, schedules, custom managers); Dependabot is simpler and GitHub-native. If you want both, scope them to disjoint ecosystems (e.g. Renovate for pip, Dependabot for github-actions only) and document the split in CONTRIBUTING.md.
low anthropic SDK lower bound (>=0.40) is likely outdated relative to current API surface ~1.0h
▶
pyproject.toml:28-28
anthropic>=0.40,<2.0 covers the current SDK range but the floor of 0.40 is permissive. Older 0.x releases had different prompt-caching and streaming APIs; if the code uses newer features (e.g. extended thinking, batch API, prompt caching), users could install an older 0.40.x version that lacks them, producing confusing runtime errors rather than a dependency-resolver failure.
Recommendation: Bump the floor to the minimum version that actually contains every API the code uses (likely >=0.50 or higher given references to extended thinking / Opus 4.7 in code). Verify by checking AnthropicAdapter for SDK features and aligning the pin accordingly.
low Dev dependency ranges allow major-version drift mid-release ~0.5h
▶
pyproject.toml:53-60
Dev extras such as `pytest-asyncio>=0.24,<2.0` and `pytest-cov>=5,<8` permit major-version jumps that have historically introduced breaking changes (pytest-asyncio 0.x → 1.x changed fixture loop behavior; pytest-cov 6/7 may change coverage report format). While dev deps don't affect end users, they affect CI reproducibility.
Recommendation: Either tighten dev upper bounds (e.g. `pytest-asyncio>=0.24,<1.0`) or rely on the requirements.lock file to pin exact dev versions for CI. Confirm requirements.lock includes dev extras.
low No SBOM generation visible in dependency configuration ~3.0h
▶
pyproject.toml:50-64
Project ships as a published package (spectra-ai on PyPI, version 0.6.0) targeting regulated SDLC users (per pyproject.toml comments referencing 'regulated SDLC'). However, no SBOM tooling (cyclonedx-bom, syft, pip-audit attestation) appears in pyproject.toml dev extras or anywhere referenced. Regulated consumers typically require CycloneDX or SPDX SBOMs alongside releases.
Recommendation: Add `cyclonedx-bom` to the dev extras and a `make sbom` / publish.yml step that emits sbom.cdx.json next to the wheel. Consider GitHub's native SBOM API + sigstore attestations for the published artifact. This is an explicit ask in regulated SDLC contexts.
low cryptography upper bound spans three majors — wider than necessary ~1.0h
▶
pyproject.toml:45-45
`cryptography>=43,<46` allows majors 43, 44, and 45. The cryptography package occasionally deprecates and removes APIs across majors (e.g. removal of OpenSSL 1.1.1 support, changes to Ed25519 serialization helpers). A three-major window is wider than the conservative pinning posture the comments claim, and the receipt-signing code uses Ed25519 APIs that have shifted across versions.
Recommendation: Tighten to `cryptography>=43,<45` (two majors) and bump explicitly when 45 is tested. Add a CI matrix job that runs the receipt-signer tests against both the floor and the ceiling.
estimated effort: ~10h

## Performance (5)

medium Sequential file reads in heuristic source-file pre-load ~1.5h
▶
src/spectra/infrastructure/main.py:525-543
_read_key_source_files iterates up to 20 candidate files awaiting git_port.read_file and then synchronously running tiktoken counting on each result. With 20 files and even a 10-20ms read+tokenize cost per file, this serializes ~200-400ms on the critical path before Stage 2 (PLAN) even begins. tiktoken's count() is also CPU-bound — running it inline in an async function blocks the event loop. Although the loop is bounded (≤20 files, ≤100K tokens), it is on the hot path of every analysis run.
Recommendation: Use asyncio.gather() to fan out read_file() calls in parallel, then count tokens in a thread pool via asyncio.to_thread(counter.count, content). Apply the 100K-token cap after gathering by accumulating in order. Expected wall-clock reduction: 5-10x for the I/O portion.
low Cache key derivation does redundant work per process ~0.2h
▶
src/spectra/infrastructure/main.py:393-437
_composite_model_versions and _composite_prompt_versions are called from both _make_cache_key_factory() (line 397) and _bind_cache_run_context() (line 437). _composite_prompt_versions iterates SPECIALIST_CONFIGS and runs blake2b updates on every call. While the costs are tiny (~microseconds), these are computed twice per run when once would suffice. They are also recomputed for every analysis invocation despite being deterministic from imported module constants.
Recommendation: Wrap _composite_model_versions and _composite_prompt_versions in functools.lru_cache(maxsize=1) so they execute exactly once per process. Cleaner caller code and trivial CPU savings.
low _prioritize_source_files runs O(N*M) startswith checks on full file tree ~0.5h
▶
src/spectra/infrastructure/main.py:546-559
_prioritize_source_files iterates every path in file_tree (potentially thousands after PREFLIGHT) and for each path runs `any(path.startswith(d) for d in _SOURCE_PREFIXES)` (5 prefixes) plus a Path() construction. For a repository with 10K files this is 50K startswith calls plus 10K Path object allocations — roughly 30-100ms of pure CPU on the hot path. The loop also processes all files before returning the top 20.
Recommendation: Short-circuit once tier 0 has 20 entries (the heuristic cap). Avoid re-allocating Path() — split suffix/stem with str operations. Use a precompiled tuple for startswith (it already is, but pass it directly: path.startswith(_SOURCE_PREFIXES) accepts a tuple natively, eliminating the genexp). Expected: 5-10x speedup on large trees.
low shutil.rmtree on workspace cleanup is synchronous and blocking ~0.2h
▶
src/spectra/infrastructure/main.py:280-283
The finally block (line 282) calls shutil.rmtree synchronously inside an async function. For large cloned repos (hundreds of MB), rmtree can take seconds and blocks the event loop entirely during cleanup. Although this happens after the report is rendered (so user-perceived latency is mostly captured), it still delays process exit and prevents any concurrent post-processing.
Recommendation: Wrap with asyncio.to_thread(shutil.rmtree, workspace_dir, ignore_errors=True) so the event loop remains responsive during cleanup. Allows adapter.close() and cache close to run concurrently with directory deletion.
low PR comment renderer sorts findings without limit-aware short-circuit ~0.5h
▶
src/spectra/adapters/pr_comment_renderer.py:137-148
_sort_findings (pr_comment_renderer.py) sorts the entire findings tuple before slicing to TOP_FINDINGS_LIMIT=20 in _render_findings_block. For reports with thousands of findings (large monorepos), this is O(N log N) when an O(N + K log K) heap-based partial sort via heapq.nsmallest would suffice. Real-world impact is small (<5ms even for 5000 findings) but it's an algorithmic mismatch with the use case.
Recommendation: Replace sorted(findings, key=...) with heapq.nsmallest(TOP_FINDINGS_LIMIT, findings, key=...) when len(findings) >> TOP_FINDINGS_LIMIT. Preserves stability requirement only if input order itself is stable; verify or sort the heap output once.
estimated effort: ~3h
Cross-Cutting Insights

Patterns identified by CritiqueAgent that span multiple dimensions

arch-005 and qual-001 are the same finding seen through architecture and quality lenses — the duplicated exception ladder in cli_controller.py. Consolidate into a single tracked issue.
arch-000 and qual-003 both target _run_analysis size/responsibility; resolving one resolves the other. Single refactor PR.
sec-006 and dep-002 both flag pysqlcipher3 maintenance from security and dependency angles; making it optional addresses both simultaneously.
arch-001, arch-002, and arch-007 form a coherent cluster: PolicyGateError, WaiverSigner protocol, and ReportError all involve types defined in the wrong layer. A single 'errors and ports cleanup' PR could move them to entities/use_cases and define the missing Protocol seam.
sec-011 (silent MAC degradation) and sec-007 (TOFU pubkey) both involve security-critical fallbacks that emit no user-visible warning — adding a unified 'security posture' line to CLI output would mitigate both with shared infrastructure.
arch-006 (private attribute reach) and perf-003 (recomputed cache versions) touch the same _composite_*_versions functions; a single refactor exposing public prompt_version() with lru_cache resolves both concerns.
Technical Debt Summary
0 estimated hours to remediate
cost to remediate: ~$13,038 at $150/hr avg dev rate
By Dimension
Security 26.5h
Architecture 18.5h
Maintainability 10.5h
Documentation 8.5h
Quality 7.5h
Performance 3.0h
By Severity
high 1.0h
medium 40.5h
low 33.0h
Debt Distribution
medi
low
Due Diligence Compliance Mapping
OWASP Top 10 (2021) Coverage
0 of 10 categories checked

OWASP mapping requires security findings with explicit category references. Run with full code content to populate this section.

A01:2021 Broken Access Control
A02:2021 Cryptographic Failures
A03:2021 Injection
A04:2021 Insecure Design
A05:2021 Security Misconfiguration
A06:2021 Vulnerable and Outdated Components
A07:2021 Identification and Auth Failures
A08:2021 Software and Data Integrity Failures
A09:2021 Security Logging and Monitoring Failures
A10:2021 Server-Side Request Forgery
OWASP Top 10 (2025) Coverage
0 of 10 categories checked
A01:2025 Broken Access Control
A02:2025 Security Misconfiguration
A03:2025 Software Supply Chain Failures
A04:2025 Cryptographic Failures
A05:2025 Injection
A06:2025 Insecure Design
A07:2025 Authentication Failures
A08:2025 Software or Data Integrity Failures
A09:2025 Logging and Alerting Failures
A10:2025 Mishandling of Exceptional Conditions
CWE References Found
CWE-20 CWE-59 CWE-295 CWE-312 CWE-322 CWE-345 CWE-367 CWE-522 CWE-601 CWE-639 CWE-703 CWE-755 CWE-1104 CWE-1395

AI-assisted screening based on finding text. Not a substitute for professional penetration testing.

Investment Readiness Score
⚠ Automated heuristic — not a substitute for formal due diligence or financial advisory. Score is derived from code-quality signals, not business fundamentals.
0 / 100
near ready

Solid foundation with minor gaps. Address key issues before due diligence.

Component Breakdown
Overall Score
86 25%
Security Posture
74 20%
Issue Concentration
49 10%
Dependency Health
55 10%
Code Complexity
50 10%
License Compliance
95 10%
SOC 2 Readiness
46 10%
Critical Findings
100 5%

AI-estimated composite score. Consult qualified advisors for investment decisions.

SOC 2 Readiness
⚠ Automated heuristic — not a substitute for formal SOC 2 assessment. Findings are mapped by keyword analysis, not control evidence evaluation.
45.5% control coverage
15 of 33 controls addressed
18 gaps · 30 findings mapped
CC1: Control Environment 2/5
✗ CC1.1 Integrity and ethical values
✓ CC1.2 Board independence and oversight 1
✓ CC1.3 Management structure and reporting 1
✗ CC1.4 Commitment to competence
✗ CC1.5 Accountability for internal controls
CC2: Communication and Information 2/3
✓ CC2.1 Information for internal control 2
✓ CC2.2 Internal communication of objectives 1
✗ CC2.3 External communication
CC3: Risk Assessment 3/4
✓ CC3.1 Specification of suitable objectives 4
✓ CC3.2 Risk identification and analysis 3
✓ CC3.3 Consideration of fraud risk 1
✗ CC3.4 Identification of significant changes
CC4: Monitoring Activities 0/2
✗ CC4.1 Ongoing and separate evaluations
✗ CC4.2 Communication of deficiencies
CC5: Control Activities 2/3
✓ CC5.1 Selection of control activities 2
✗ CC5.2 Technology general controls
✓ CC5.3 Deployment through policies 5
CC6: Logical and Physical Access Controls 4/8
✓ CC6.1 Logical access security software 2
✓ CC6.2 Credential and secret management 2
✗ CC6.3 Role-based access authorization
✗ CC6.4 Access removal and session management
✗ CC6.5 Physical access restrictions
✓ CC6.6 System boundary protection 2
✓ CC6.7 Data transmission security 2
✗ CC6.8 Prevention of unauthorized software
CC7: System Operations 1/5
✗ CC7.1 Infrastructure and availability monitoring
✗ CC7.2 Security event detection
✓ CC7.3 Security event evaluation 1
✗ CC7.4 Incident response procedures
✗ CC7.5 Recovery and resilience
CC8: Change Management 0/1
✗ CC8.1 Change control processes
CC9: Risk Mitigation 1/2
✗ CC9.1 Risk mitigation for business disruptions
✓ CC9.2 Third-party and vendor risk management 5

Keyword-based mapping to AICPA Common Criteria (CC1–CC9). Not a formal SOC 2 audit.

PCI DSS 4.0 — Requirement 6
⚠ Automated heuristic — not a substitute for formal PCI DSS assessment. Findings are mapped by keyword analysis, not control evidence evaluation.
27.3% control coverage
3 of 11 controls addressed
8 gaps · 9 findings mapped
R6.2: Secure Software Development 0/4
✗ 6.2.1 Secure development processes defined
✗ 6.2.2 Software development personnel trained
✗ 6.2.3 Code reviewed before release
✗ 6.2.4 Protection against common vulnerabilities
R6.3: Security Vulnerabilities Identified and Addressed 2/3
✗ 6.3.1 Known vulnerabilities identified
✓ 6.3.2 Software inventory maintained 7
✓ 6.3.3 Patches applied timely 2
R6.4: Public-Facing Web Applications Protected 0/2
✗ 6.4.1 Web application firewall or equivalent
✗ 6.4.2 Automated attack detection
R6.5: Changes Managed Securely 1/2
✓ 6.5.1 Change control procedures 2
✗ 6.5.2 Development/test/production separation

Keyword-based mapping to PCI DSS 4.0 Requirement 6 (Secure Systems & Software). Not a formal PCI assessment.

NIST CSF 2.0
⚠ Automated heuristic — not a substitute for formal NIST CSF assessment. Findings are mapped by keyword analysis, not control evidence evaluation.
41.7% function coverage
5 of 12 categories addressed
7 gaps · 24 findings mapped
GV: Govern 1/2
✓ GV.OC-01 Organizational context understood 7
✗ GV.RM-01 Risk management objectives established
ID: Identify 1/2
✓ ID.AM-01 Asset inventory maintained 10
✗ ID.RA-01 Risk assessment performed
PR: Protect 2/3
✓ PR.AA-01 Access control enforced 2
✓ PR.DS-01 Data security ensured 4
✗ PR.PS-01 Platform security maintained
DE: Detect 0/2
✗ DE.CM-01 Continuous monitoring implemented
✗ DE.AE-01 Adverse event analysis performed
RS: Respond 1/2
✗ RS.AN-01 Incident analysis conducted
✓ RS.MI-01 Incident mitigation applied 2
RC: Recover 0/1
✗ RC.RP-01 Recovery execution planned and tested

Keyword-based mapping to NIST Cybersecurity Framework 2.0 (6 Functions). Not a formal NIST assessment.

Issue Concentration
0
distribution score
concerning
0.507
Gini Coefficient
8
Unique Files
34
Total Issues
Top 10 Hotspot Files

- 1 13
  src/spectra/infrastructure/main.py
- 2 8
  src/spectra/adapters/cli_controller.py
- 3 7
  pyproject.toml
- 4 2
  src/spectra/adapters/waiver_cli.py
- 5 1
  src/spectra/entities/models.py
- 6 1
  src/spectra/entities/errors.py
- 7 1
  renovate.json
- 8 1
  src/spectra/adapters/pr_comment_renderer.py

Measures finding distribution across files. For contributor-based bus factor analysis, use git blame tools.

Dependency Risk
0 risk
elevated risk
6 dependency findings analyzed · +5 severity penalty
Risk Signals Detected

- unmaintained +20 pts
- outdated +15 pts
- lock file +5 pts

License Compliance
2 unique licenses detected · 11 total mentions
MIT×9 ISC×2
ROI Analysis
This analysis cost
$5.99
Manual equivalent ($175/hr × 5h)
$945
Spectra saved you
$939 (99%)
Cost per finding
$0.18

Based on $175/hr senior engineer rate and ~5 hours for equivalent manual review. Actual costs vary.

j k navigate / search Esc clear Enter expand