feat(transactionlog): B.1c — system-transaction-log-writer (15/15 ACs, 100%)#420
Merged
Merged
Conversation
This was referenced May 29, 2026
b9c185a to
125c1b4
Compare
…, 100%) Closes Slice B.1 trunk. Compliance write-on-change persistence for the Kensa-executor pipeline, complete with all 15 acceptance criteria. Spec Promoted system-transaction-log-writer from draft → approved. 15 ACs identical to PR #415's draft. Migration 0011_host_compliance_schedule.sql Copy of B.1a's migration. Identical content; goose treats duplicate identical migrations as no-ops when both B.1a (#418) and this PR merge. Migration 0012_transaction_log.sql - host_rule_state: ONE row per (host, rule). Current state, UPSERTed every Apply. Status CHECK constraint enforces the closed enum (pass/fail/skipped/error). - transactions: append-only state-change log. UNIQUE(scan_id, rule_id) enforces idempotency at the schema level (spec C-04). - Both tables FK to hosts(id) ON DELETE RESTRICT — historical findings outlive their host references (spec C-06). - Indexes: by (host_id, status) for current-fleet queries; by (host_id, rule_id, occurred_at DESC) for point-in-time temporal queries; by scan_id for idempotency check. Audit events Added two new codes to events.yaml: finding.persisted one per transactions row (spec AC-09) writer.apply.failed per Apply-rollback (spec AC-15) Codegen produces audit.FindingPersisted and audit.WriterApplyFailed constants; events.gen.go grew from 96 → 98 events total. internal/transactionlog package types.go ApplyBatch, Result, Status / ChangeKind / FailureReason enums, sentinel errors, MaxEvidenceBytes (256 KiB cap). writer.go Writer.Apply: single-tx-per-call. Steps: 1. Validate every result (status, evidence size + shape). Spec AC-08 / AC-14 reject BEFORE any INSERT — atomic. 2. Idempotency: if any transactions row exists for the scan_id, no-op (spec AC-05). 3. BEGIN tx. 4. Per result: read prior host_rule_state, decide change_kind (first_seen / state_changed / severity_changed / none), INSERT transactions only on change, UPSERT host_rule_state with COALESCE-style last_changed_at preservation. 5. COMMIT. 6. emit finding.persisted per state-change AFTER commit (audit reflects what persisted, not what attempted). On any error: tx.Rollback + emit writer.apply.failed with classified reason (FK / deadlock / oversize / sqlc). source_test.go (AC-12, AC-13) AC-12: walks every .go and .sql file under app/ asserting no scan_baselines / ScanBaseline references — the Python-era baselines table is explicitly dropped. AC-13: AST-parses every internal/transactionlog .go file asserting no database/sql import and no .Exec/.Query/.QueryRow whose SQL arg uses fmt.Sprintf or string concatenation. writer_test.go (AC-01 through AC-11, AC-14, AC-15) 16 sub-tests covering the writer behavior end-to-end against real Postgres: AC-01 pg_stat_database.xact_commit delta < 10 after 50-rule Apply AC-02 N first_seen rows on first scan AC-03 identical rescan = 0 new transactions, check_count++ AC-04 one flip pass→fail = exactly 1 state_changed row AC-05 same scan_id replay = no-op AC-06 FK violation rolls back the whole batch (zero rows persist) AC-07 DELETE hosts with extant transactions fails (ON DELETE RESTRICT) AC-08 non-JSON-object evidence rejected (table-driven over 4 cases) AC-09 finding.persisted emission count = transactions row count AC-10 1000-rule Apply ≤ 2 seconds wall-clock AC-11 50 concurrent Applys against distinct hosts complete AC-14 oversize evidence rejected BEFORE INSERT; writer.apply.failed audit emitted with reason=evidence_oversize AC-15 FK violation emits writer.apply.failed with reason=fk_violation and detail.rule_count_attempted populated Local validation go build ./internal/transactionlog/: clean go vet ./internal/transactionlog/: clean go test -race ./internal/transactionlog/ (unit + integration with real Postgres + migrations 0001-0012): 16 sub-tests pass specter coverage: system-transaction-log-writer 15/15 = 100% Architectural choices worth flagging - Atomicity: validation phase runs BEFORE BEGIN, so oversize-evidence rejection is genuinely zero-INSERT (no rollback needed). - Pre-commit pending audit emissions: scan.completed / finding.persisted fire only AFTER tx.Commit succeeds, so the audit log truly reflects persisted state. - Evidence schema check: minimal "must be JSON object" gate today; full KensaEvidence-schema validation slots into validateResult when the OpenAPI components.schemas.KensaEvidence shape lands. Slice B.1 trunk status B.1a scheduler PR #418 — 15/15 ACs, ready for review B.1b kensa-executor PR #419 — 16/16 ACs, ready for review B.1c transaction-log-writer this PR — 15/15 ACs Total Slice B.1: 46 ACs covered across 3 specs. Ready to move on to B.2 (liveness loop + drift detector) once these merge.
125c1b4 to
646f268
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slice B.1c —
system-transaction-log-writerimplementation. Complete. All 15 spec ACs covered. Closes the Slice B.1 trunk (B.1a scheduler + B.1b kensa-executor + B.1c transaction log writer).What landed
app/specs/system/transaction-log-writer.spec.yaml0011_host_compliance_schedule.sql0012_transaction_log.sqlhost_rule_state+transactionstablesfinding.persisted,writer.apply.failedinternal/transactionlog/— types, doc, writer (~440 LOC)ACs satisfied (all 15)
pg_statcommit delta ≤ 10 after 50-rule Applystate_changedrowfinding.persistedcount = transactions row countscan_baselinesreferences anywhere in app/writer.apply.failedArchitectural choices locked
finding.persistedfires only AFTERtx.Commitsucceeds. Audit log reflects persisted state, not attempted.KensaEvidenceschema validation slots intovalidateResultonce the OpenAPIcomponents.schemas.KensaEvidenceshape lands.scan_baselines: Python-era baseline table explicitly dropped. The priortransactionsrow IS the baseline. AC-12 source-inspection enforces project-wide.Local validation
go build ./internal/transactionlog/— cleango test -race ./internal/transactionlog/— 16 sub-tests pass against real Postgres + migrations 0001–0012specter coverage— system-transaction-log-writer 15/15 = 100%Slice B.1 trunk — DONE
Total Slice B.1: 46 ACs across 3 specs, all at 100%.
Migration ordering note
Migration
0011_host_compliance_schedule.sqlappears in PR #418, PR #419, and this PR. All three are byte-identical. When any of the three PRs merges first, the others' 0011 is a no-op for goose (already-applied). Migration0012_transaction_log.sqlis unique to this PR.Relationship to other PRs
host_factswriter reuses the write-on-change pattern proven here.Next steps
With Slice B.1 trunk complete:
Each will follow the same per-component PR pattern.