6.2 KiB
Research: Findings Workflow Enforcement and Audit Backstop
Decision 1: Use a service-owned workflow gateway as the single mutation path
Decision: Keep FindingWorkflowService as the implementation nucleus and evolve it into the single approved gateway for all meaningful findings lifecycle mutations, including automated reopen and auto-resolve paths.
Rationale: The current repo already concentrates the intended human-driven lifecycle logic in FindingWorkflowService, including capability checks, transaction handling, and audit writes. The architectural gap is not the absence of a service but the existence of bypass paths that do not use it. Promoting the existing service into the sole mutation gateway preserves established behavior while reducing new abstraction churn.
Alternatives considered:
- Add model observers to infer audit events and transition validity from any
save(). Rejected because observers are implicit, brittle for bulk or query updates, and make transition intent harder to reason about. - Move lifecycle rules into the
Findingmodel. Rejected because the model already exposes unsafe public mutators and would still need separate authorization and audit coordination. - Accept service-path discipline without stronger guardrails. Rejected because the repo already contains concrete bypass paths in automation and model methods.
Decision 2: Add explicit regression guards instead of relying on implicit ORM magic
Decision: Enforce the single-gateway rule with focused regression tests that fail when covered code paths mutate findings lifecycle truth directly, rather than attempting to guarantee correctness purely through hidden ORM hooks.
Rationale: The codebase already uses test-driven guardrails for other cross-cutting invariants. A grep or semantic guard for direct lifecycle writes, combined with focused feature tests, is explicit and maintainable. It also matches the repo constitution preference for actionable CI failures over convention-only guidance.
Alternatives considered:
- Rely exclusively on code review discipline. Rejected because current bypasses already made it through.
- Introduce a custom repository layer just for findings. Rejected because it adds structural overhead without solving automation paths by itself.
- Use database triggers. Rejected because they would hide application intent, complicate testing, and not encode authorization semantics.
Decision 3: Route automated lifecycle changes through the same gateway with system-origin semantics
Decision: Baseline auto-close, permission posture cleanup, Entra admin roles cleanup, recurrence reopen, and similar automation paths should call the same workflow gateway while explicitly recording that the actor is system-originated.
Rationale: The strongest remaining workflow-truth risk is that automation still mutates findings directly through forceFill() or duplicate reopen logic. Bringing automation under the same gateway preserves one lifecycle model and one audit contract. The audit layer already supports non-human actor semantics.
Alternatives considered:
- Leave automation paths unaudited because they are “internal”. Rejected because system-driven lifecycle changes are still meaningful governance history.
- Give automation a second lightweight service. Rejected because that would reintroduce split lifecycle truth.
- Audit automation separately but leave status mutation inline. Rejected because audit without transition enforcement still permits invalid state drift.
Decision 4: Keep legacy acknowledged readable but stop treating it as an active write target
Decision: Preserve compatibility for existing acknowledged rows and capability aliases, but normalize future lifecycle writes to the canonical Spec 111 status set instead of continuing to write new acknowledged states.
Rationale: The repo still contains STATUS_ACKNOWLEDGED and alias capability handling, but Spec 111 already established triaged as the canonical workflow state. Continuing to write acknowledged would prolong dual-state ambiguity and complicate enforcement, reopening, and audit expectations.
Alternatives considered:
- Remove
acknowledgedimmediately from the database contract. Rejected because existing data and compatibility surfaces still rely on it. - Leave
acknowledgedas a full first-class active state indefinitely. Rejected because it weakens the goal of one clean lifecycle model.
Decision 5: Normalize findings audit taxonomy through the canonical audit naming layer
Decision: Keep the established finding.* action vocabulary but register and document it as part of the canonical audit taxonomy instead of leaving it as an ungoverned free-form string set.
Rationale: The existing audit history and tests already use action IDs such as finding.triaged and finding.resolved. Preserving that vocabulary minimizes migration churn, but the plan should stop treating it as ad hoc. Bringing those actions under the canonical audit naming layer aligns findings with the broader audit foundation and improves summary consistency.
Alternatives considered:
- Replace all findings action IDs with a new naming scheme. Rejected because the audit value is in stronger governance, not vocabulary churn.
- Leave findings action IDs as raw service strings forever. Rejected because it leaves the same long-term drift risk seen in other audit surfaces.
Decision 6: Prefer additive code-path hardening over schema changes in the first slice
Decision: The first implementation slice should avoid new tables or nonessential schema changes and focus on consolidating mutation paths, normalizing audit action registration, and strengthening tests.
Rationale: The core problem is behavioral inconsistency, not missing storage primitives. Existing findings and audit_logs tables already contain the fields needed for lifecycle truth and durable history. Additive schema work would slow delivery without addressing the primary bypass risk.
Alternatives considered:
- Add a dedicated findings transition history table. Rejected because Spec 134 already established the audit log as the canonical history layer.
- Add versioning to the
findingstable. Rejected because it duplicates audit history rather than fixing mutation discipline.