feat(v1.99 PR-19): update validation — truth enforcement (G3-U11/U12/U13)#476
Merged
feat(v1.99 PR-19): update validation — truth enforcement (G3-U11/U12/U13)#476
Conversation
Pinned sentence (repeated in PR body + contract file):
"PR-19 is truth-enforcement only: it may refine validation outcome
mapping, update metadata correctness, and history coherence, but may
not introduce any new state-mutation, recovery, or authority-taking
behavior."
Contract surfaces the three G3 sub-gates PR-19 closes:
G3-U11 exit-code truth:
- Extends the PR-18 stateForValidatorExit discipline to every state-
transitioning branch in runUpdateApply
- Flags an existing truth split on the preflight-fail branch:
transitions to StateFailedAbort (exits 3) but returns hard-coded
state.ExitDegraded (= 1). Same class the reviewer caught in PR-18.
- Locks every transition with a regression test that asserts
sf.State.ExitCode() == returned process exit
G3-U12 update history integrity:
- writeHistory (main.go:335) currently writes install_fail on any non-
committed state, but history fields like from/to are written
regardless. For intermediate states this is misleading.
- installType defaults to "rpm" and falls back to "deb"; source
installs get labeled "rpm" incorrectly.
- from/to should reflect operator-intended versions when a plan is
available, not just the installer binary's compile-time version.
G3-U13 source/package coherence:
- Source installs must record installType = "source" in history JSON
- P-7 (install_origin_coherent) failures must block success status
- New optional coherence field in history for origin_mismatch evidence
Non-goals pinned:
- No new mutation/recovery/authority logic
- No change to runUpdateApply call graph
- No new InstallState enum value
- No validator JSON reinterpretation
- No change to ApplyWhitelist (unless legitimately read-only)
Post-merge watch from PR-18 carried forward:
"keep the same discipline around validator severity and persisted
lifecycle state"
This commit lands the contract BEFORE any code change, same pattern as
PR-18 step 0. Implementation commits land on this branch in order:
step 2 (exit-code truth) → step 3 (history integrity) → step 4 (CI gates).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Same class of truth split PR-18 fixed for the validator-fail branch,
this time on the preflight-fail branch of runUpdateApply. The reviewer
flagged state↔exit discipline in the PR-18 post-merge watch item:
"keep the same discipline around validator severity and persisted
lifecycle state. Do not reintroduce any state↔exit split through a
new validation-focused path." PR-19 locks this in both directions.
Before:
sf.Transition(state.StateFailedAbort, ...) // ExitCode() = 3
return state.ExitDegraded // = 1
// persisted state disagreed with returned process exit
After:
st := stateForPreflightFailure(pre)
sf.Transition(st, ...)
return st.ExitCode()
// by construction, state.ExitCode() == returned rc
New helper stateForPreflightFailure(pre) in update_apply.go:
- Today: all critical preflight failures → StateFailedNoFirewall
(ExitCode = ExitFailed = 2). Honest: preflight exists to gate apply
on "nftban is functionally present and authoritative."
- Signature accepts full PreflightResult so a future PR can deepen
the mapping without changing callers.
- No mini policy engine inside: this stays a thin truth-enforcement
layer per the PR-19 contract sentence.
Test T2 extended:
- Asserts sf.State.ExitCode() == rc (state↔exit regression guard)
- Asserts sf.State == StateFailedNoFirewall specifically
Non-goals honoured: no new mutation path, no new recovery, no new
enum value, no change to runUpdateApply's call graph, no validator
JSON reinterpretation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G3-U12 (update history integrity):
cmd/nftban-installer/main.go
- Extract historyStatusForState(s) from writeHistory: pure helper
that maps InstallState → history status string. Every non-
StateCommitted state reports install_fail or verify_fail. No
success coercion for intermediate/non-terminal states.
- Extract historyInstallType(cfg): pure helper with priority order
source > deb > rpm > default. Both helpers unit-testable.
G3-U13 (source/package coherence):
cmd/nftban-installer/main.go
- historyInstallType now has a source case. Previously source
installs were silently mislabeled as "rpm" (default fallback),
creating a coherence violation between declared install origin
and recorded history. Fix: explicit cfg.source → "source".
New file cmd/nftban-installer/history_test.go:
- TestHistoryStatusForState_Committed_IsSuccess
- TestHistoryStatusForState_Degraded_IsVerifyFail
- TestHistoryStatusForState_AllFailureStates_AreInstallFail (6
failure states exhaustively)
- TestHistoryStatusForState_IntermediateStates_AreInstallFail
(5 non-terminal states — regression guard for G3-U12's
"timeout/signal mid-apply must not report success" rule)
- TestHistoryInstallType_{Source,DEB,RPM} — 3 base cases
- TestHistoryInstallType_Priority_SourceOverridesDEB
- TestHistoryInstallType_Priority_DEBOverridesRPM
- TestHistoryInstallType_NoFlag_DefaultsToRPM
CI gate additions in .github/workflows/ci-update-canonization.yml:
G3-U11 — structural check: no hard-coded exit literal returned
after a StateFailed transition (regression guard for the original
preflight-fail contradiction)
G3-U12 — structural check: StatusSuccess literal must not appear
outside the StateCommitted case arm (regression guard for future
success coercion)
G3-U13 — structural check: historyInstallType must contain a
cfg.source case (regression guard for mislabeling fix)
All three CI steps run in addition to the unit tests — belt and
suspenders so a careless future change can't slip past just the unit
tests OR just the grep.
Non-goals honoured:
- No mutation path change
- No recovery/rollback change
- No authority change
- No change to runUpdateApply call graph
- No new InstallState enum
- No validator JSON reinterpretation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wn helper
The previous grep filtered out lines containing 'StateCommitted' OR
'historyStatusForState', but the actual line in historyStatusForState:
case state.StateCommitted:
return history.StatusSuccess ← this line
only contains "history.StatusSuccess" — not either filtered token. So
the check flagged the correct implementation as a violation.
Fix: tighter grep that verifies the line IMMEDIATELY preceding every
"history.StatusSuccess" reference is "case state.StateCommitted:".
This catches any future refactor that moves the literal to a different
case arm or fabricates a new return site.
Behaviour:
- 0 matches total → FAIL (literal must exist somewhere)
- ≥1 matches not preceded by StateCommitted case → FAIL with
line number + offending previous line for diagnosis
- all matches gated → PASS
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Truth-enforcement layer on top of the PR-18 orchestrator. Closes the remaining G3 sub-gates on the validation/metadata surface.
Pinned sentence:
Scope (G3-U11 / U12 / U13)
G3-U11 — Exit-code truth
PR-18 fixed the validator-fail state↔exit split via
stateForValidatorExit(rc). During PR-19 survey I found the same class on the preflight-fail branch: transitions tostate.StateFailedAbort(maps to exit 3) but returns hard-codedstate.ExitDegraded(= 1). PR-19 fixes this and locks every state-transitioning branch with a regression test assertingsf.State.ExitCode() == <returned rc>.G3-U12 — Update history integrity
writeHistorymust emitinstall_fail(notsuccess) wheneversf.State != StateCommittedfrom/tometadata must reflect the operator-intended transition (the plan's versions), not just the installer binary's compile-time constantinstallTypemust have asourcecase — currently defaults torpm/debonly, silently mislabeling source installs as RPMG3-U13 — Source/package coherence
Builds on PR-17's
install_origin_coherentpreflight. Extends enforcement to the history/metadata surface:installType = "source"coherencefieldExplicit non-goals
runUpdateApply's call graphInstallStateenum valueImplementation plan (commit-by-commit)
validation_contract.md(landed as0dce2907)sourceinstallType + from/to from planci-update-canonization.ymlDepends on
22f8fb41) — providesstateForValidatorExitpattern +runUpdateApply+ contract audit harnessStatus: Draft — un-drafts after CI green.
🤖 Generated with Claude Code