verdict: classify every refusal with a typed class and owner - #97
Conversation
A refusal reason names the immediate cause but not the kind of boundary reached. unsupported-statement covers a data backfill, an imperative CREATE TABLE, a permanently refused CREATE INDEX IF NOT EXISTS, and a parse/route incoherence; unsupported-partitioned-parent spans three classes along its PartitionRefusalCause. Consumers rebuilt that split from reason strings. docs/refusal-classes.md decided the vocabulary; this change makes the engine emit it. pkg/verdict gains the Refusal proof type: NewRefusal(class, reason, owner) rejects a zero or unknown class, an unknown reason, and an owner outside no-online-safety-problem; the per-class constructors are total for valid inputs, so there is no panic path; Verdict.WithRefusal is the one place a verdict acquires outcome, reason, class, and owner together (RF-7). The classification registry has two halves keyed on the typed cause where one exists and on the refusal site where none does, never on the reason string. pkg/plan/refusal.go classifies plan-side keys (CreateShapeRefusal, PartitionRefusal, RouteRefusal); pkg/migrate/refusal_registry.go classifies statement kinds at the gate, both admission sentinel sets, and each imperative site. TestRefusalRegistryIsComplete derives its keys from the production closed sets and pins a sentinel subset so a broken deriver cannot pass on an empty set. statement.Kinds() is added and the kinds split so KindOther no longer absorbs provisioning, catalog-work, or data-change statements. Plan refusals fail closed. RefuseUnsupportedPartitionedParent now takes the causes and returns ErrInvariantViolation on a length mismatch or an unclassified cause; a plan statement refused without a class keeps its reason and is reported invariant-violation. JSON gains class and owner on verdicts, plan statements, and plan reports (additive, omitempty); the text renderer prints them on refusals; demo/tour.sh asserts the class beside reason and cause. Docs: RF-7 registered, refusal-classes.md rewritten from proposal to shipped behavior, limitations.md and README no longer claim every refusal is an online-safety gap, and the partitioned-parent NOT VALID FK matrix row corrected to agree with its environmental class. Exit codes and every existing reason string are unchanged.
JashLal
left a comment
There was a problem hiding this comment.
Reviewed the full diff. The design holds together well: verdict.Refusal as a proof type with unexported fields, WithRefusal/DesiredResult.refused as the only paths onto the verdict contract, and TestRefusalRegistryIsComplete deriving its keys from production closed sets (with sentinel pinning against a vacuous walk) rather than a shadow list. Fail-closed handling on unclassified causes and the RefuseUnsupportedPartitionedParent signature change are consistent across all three callers, docs guards were updated, and CI is green on PG 14–18. Fingerprints and reason strings are unchanged as claimed.
Non-blocking observations:
ParseOneclassifiesDROP TABLE(any non-indexDROP) asKindCatalogWork, so the gate refuses it withno-online-safety-problem/direct-operator. Defensible under the vocabulary (destructiveness ≠ lock safety), but the verdict now nudges an operator to run a destructive statement directly, and the detail text still only says "only ALTER TABLE and CREATE INDEX statements are supported". Worth a follow-up either to the detail wording or a destructive-aware kind.Verdict.String()andwriteVerdictTextprintclass:unconditionally for refused verdicts, so a verdict decoded from an older producer's JSON renders an empty class line. Cosmetic.- In
refuseStatements, the report-level stamp takes the current mutator's refusal even when the selected statement was already refused with a different classification, so the report class can disagree with the first statement's class. Same shape as the pre-existing reason behavior, just noting it now extends to class/owner.
…plan text renderers, and format_version 4
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/97, follow-up commit All five findings addressed in
Decisions to veto: bumped Source: #97, scratch review |
|
🤖 Adversarial correctness review, requested by Armand and performed by Armand's agent. Reviewed at head Verdict: correct and safe to land. The proof type, the two registry halves, and the fail-closed paths hold under attack: every refusal site now reaches the verdict through Findings1. The kind classifier splits siblings across classes, and the doc's own tables say otherwise. Driving 2. (nit) RF-7's Enforced line claims a type guarantee the type does not quite give. 3. (doc nit) A sentence written before the split survived it. docs/refusal-classes.md still says the imperative front door refuses through "a single catch-all in Action items
Verified (tried to break, couldn't)Built, vetted, and ran the full This review was generated by Claude Code (claude-fable-5-1). |
|
🤖 Two-lens product review (OSS adoption, SchemaBot integration), requested by Armand and performed by Armand's agent. Reviewed at head OSS adoption ease1. The class note assumes the reader already knows the vocabulary. 2. Finding 1 of the correctness review is worst for exactly this reader. SchemaBot integration1. The 2. 3. Fail-closed direction is right for the consumer. An unclassified refusal in the desired-state loop surfaces as an This review was generated by Claude Code (claude-fable-5-1). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving on Armand's behalf after the adversarial correctness review above. The findings there are yours to pick up as follow-ups — flagging them, not gating on them.
This stamp was left by Claude Code (claude-fable-5-1).
…ities-ci-gate * origin/main: capabilities: make the embedded YAML matrix the source of the support tables (#98) verdict: classify every refusal with a typed class and owner (#97) fix(dbconn): strip explicit pg_catalog from pooled search_path (#93) # Conflicts: # Makefile # docs/capabilities-contract.md # docs/capabilities.md # pkg/capabilities/capabilities.go # pkg/capabilities/capabilities.yaml # pkg/capabilities/capabilities_test.go
…ities-subcommand * origin/main: capabilities: make the embedded YAML matrix the source of the support tables (#98) verdict: classify every refusal with a typed class and owner (#97) fix(dbconn): strip explicit pg_catalog from pooled search_path (#93) # Conflicts: # Makefile # docs/capabilities-contract.md # docs/capabilities.md # pkg/capabilities/capabilities.go # pkg/capabilities/capabilities.yaml # pkg/capabilities/capabilities_test.go
Stamp every refusal verdict with a typed
class(and anownerwhere there is no online-safety problem to solve), classified by a registry keyed on the typed cause or refusal site, and carry both through the plan report asformat_version4.Why
A refusal reason names the immediate cause but not the kind of boundary reached:
unsupported-statementalone covers a data backfill (another tool's job), an imperativeCREATE TABLE(the declarative front door's job),CREATE INDEX IF NOT EXISTS(permanently refused, with a safer idiom), and a parse/route incoherence (pg-sprite's own defect).unsupported-partitioned-parentspans three classes along itsPartitionRefusalCause. Consumers were rebuilding that split from reason strings; docs/refusal-classes.md decided the vocabulary and this change makes the engine emit it.What
pkg/verdict:Refusalproof type with unexported fields.NewRefusal(class, reason, owner)rejects a zero/unknown class, unknown reason, or an owner outsideno-online-safety-problem; per-class constructors (CapabilityBoundary,NoOnlineSafetyProblem,ByDesign,Environmental,InvariantViolation) are total for valid inputs — no panic path.Verdict.WithRefusalis the one place a verdict getsoutcome: refused+ reason + class + owner together (// INV: RF-7);Verdict.Refusal()reconstructs the proof from a refused verdict so an aggregator (the desired-state loop) propagates class and owner through that same path instead of copying fields.pkg/plan/refusal.goclassifies plan-side keys (CreateShapeRefusal(cause),PartitionRefusal(cause),RouteRefusal());pkg/migrate/refusal_registry.goclassifies statement kinds at the gate, both admission sentinel sets, and each imperative refusal site. Nothing is derived from the reason string.TestRefusalRegistryIsCompletederives its keys from production closed sets (executor.CreateShapeCauses(),preflight.PartitionRefusalCauses(),statement.Kinds()× concurrent, the sentinel sets, a walk of the site refusals) and pins a sentinel subset so a broken deriver cannot pass on an empty set.pkg/statementgainsKinds(), whose closed-set test parses the iota block with go/ast so a kind appended to the source but left out ofKinds()fails; kinds are split soKindOtherno longer absorbs provisioning / catalog-work / data-change statements.RefuseUnsupportedPartitionedParent(report, causes) errorreplaces the bool form and returnsexecutor.ErrInvariantViolationon a length mismatch or unclassified cause; a plan statement refused without a class keeps its reason and is reportedinvariant-violation. The desired-state loop returnsErrInvariantViolationif a refused statement verdict carries no valid class.format_version4.classandownerjoinReport(present exactly when the aggregatereasonis) andStatement(present exactly whendispositionisrefuse; excluded from the fingerprint). docs/plan-report.md documents both fields and both closed vocabularies;TestDocListsEveryVocabularyValuewalksverdict.Classes()/Owners(), andpkg/verdict/docs_test.goguards the same values against docs/refusal-classes.md.class:/owner:; the plan text renderers (migrate --dry-run,diff) print arefusal class: …; owner: …note under each refused statement's error diagnostic. JSON gainsclass/owneron verdicts, plan statements, and plan reports (omitempty).demo/tour.shassertsformat_version4 and the class alongside reason and cause.NOT VALIDFK matrix row corrected to ✅ with a server-version precondition (environmental on 14–17), matching the cause table.Exit codes and every existing reason string are unchanged.
Before / after
One refused statement, seen through both front doors.
Refs: docs/refusal-classes.md, docs/invariants.md RF-7, docs/plan-report.md
🤖 Drafted with Amp (Claude Opus 4.6); reviewed and edited by the author.