Validation report — S1-01 (SUT contract types)¶
Date: 2026-05-25
Validator: phase-story-validator (inline four-lens analysis — Coverage, Test-Quality, Consistency, Design-Patterns — applied directly after Stage 1's Context Brief; the story is small enough and the lenses converge sharply enough that spawning four parallel critic agents would have burned tokens without changing the verdict, per the skill's token-economy guidance).
Verdict: HARDENED
Story path: docs/phases/06-sherpa-vuln-loop/stories/S1-01-sut-contract-types.md
Why HARDENED (not STRONG, not RESCUE)¶
The story's architectural intent is correct: it names the four ADR-0001 symbols and the redaction theme, and Step 1 of High-level-impl.md is the contracts-first foundation the rest of Phase 6 depends on. But every AC was a vague qualitative statement and the TDD plan reversed CLAUDE.md's load-bearing rule. Specifically:
- AC-1 was un-verifiable.
"X, Y, Z exist"— exist with what shape? what fields? what types? An executor could ship four emptyBaseModelsubclasses and pass. - AC-2 was vague. "Result serialization exposes only sanitized evidence" names no enforcement mechanism. The executor's Validator pass can't binary-pass-fail "sanitized" — needs Pydantic field validators + property-based rejection tests against the canonical regex set.
- AC-3 was hollow. "Contract snapshot tests pass" — what symbols? what shape? Where's the golden? Phase-3 S6-06 validation already showed how badly contract-snapshot stories drift when these aren't pinned (six block-tier corrections).
- TDD plan ordering contradicted CLAUDE.md. "Refactor: extract shared identifiers into newtypes" inverted the rule. CLAUDE.md states Newtype identifiers under
codegenie.types.identifiers— never rawstrfor domain IDs as a load-bearing commitment. Newtypes are Red+Green, not Refactor. - Cross-phase contract was unencoded. Phase 9 S4-05 already commits the
SutDigestto byte-identical output acrossLocalVulnRemediationSutandTemporalVulnRemediationSut. Without the pure-helper / no-side-effects discipline pinned here, the executor of S5-01 (the concrete adapter) would write adigest()that touches the clock and silently break the Phase-9 conformance test 800 commits later. - Phase-6.5 import fence was missing. Phase-arch-design §"Failure modes" mandates "SUT result leaks prompt/raw path → contract serialization test → CI failure" and §"Contract boundary" says Phase 6.5 may not depend on graph internals. There was no AC turning that into an actual test.
- No mutation-resistance. "Forbidden-field tests" is too generic — a mutant model with
extra="allow"passes. A mutant model withterminal_state: str(instead ofLiteral[…]) passes. A mutantdigest()returninguuid.uuid4()passes "serialization tests." The new ACs encode the specific shape so mutants die. - Contract snapshot meta-test missing. The S6-06 validation report singled this out as the scariest failure mode: an additive-vs-breaking classifier with
==swapped for!=silently leaks breaking changes. Phase 6's snapshot must inherit the meta-test discipline. - Newtype registry drift wasn't required. Phase-3 ADR-0010 + the existing
_NEWTYPE_REGISTRYdrift test exist precisely so a new newtype lands with its registry entry in the same commit. The story added three newtypes without naming the registry. - Public-surface allowlist sentinel missing. CLAUDE.md "Extension by addition — no silent edits" needs an actual test; otherwise an executor enthusiastically adds a fifth public name and the next phase inherits it.
All in-place fixable, none requires re-running phase-story-writer. The story's structure (one paragraph goal, three-section TDD plan, contract-snapshot ambition) survives — the four ACs grew to twelve, the TDD plan was reordered, and Files-to-touch + Notes-for-implementer were added. Verdict: HARDENED.
Context Brief (Stage 1)¶
Story snapshot¶
- Goal (post-edit): ship
src/codegenie/workflows/{__init__.py,vuln_sut.py}plus three kernel-tier newtypes + smart constructors +_NEWTYPE_REGISTRYentries; ship a Phase-6.5-facing public-import boundary with an allowlist sentinel, sanitization-by-construction validators, a digest-substrate pure helper with byte-stability + sensitivity property tests, an AST no-side-effects fence on any futuredigest()implementation, and a contract snapshot + meta-test pair undertests/integration/. - Status pre-validation:
Ready— never executed; never validated. - Status post-validation:
HARDENED.
What ADR-0001 commits us to¶
The four names are the harness-facing public surface. Concrete graph builder stays private. Contract changes require ADR amendment. Phase 6.5 imports the contract only. Future task classes (Phase 7 migration; Phase 8+ planner integration) may add sibling SUTs but never modify these four shapes.
What final-design.md §"State model" pins¶
Closed sum-type ledger: NeedsPlan, PlanReady, PatchApplied, GateFailedRetryable, AwaitingHumanReview, Completed, FailedUnrecoverable. Three of those seven are terminal. The Result's terminal_state field is the closed Literal {completed, awaiting_human_review, failed_unrecoverable} — the four non-terminal states MUST NOT be reachable through the public Result.
What phase-arch-design.md §"Failure modes" pins¶
| Failure | Detection | Required behavior |
|---|---|---|
| SUT result leaks prompt/raw path | contract serialization test | CI failure |
| node attempts direct peer call | AST test | CI failure |
This story owns the first row. The second belongs to S3-01 (subgraph topology) but the AC-6 placeholder fence asserts the substrate is in place.
What Phase 9 S4-05 forward-depends on¶
S4-05's G5 conformance: LocalVulnRemediationSut.digest() and TemporalVulnRemediationSut.digest() produce byte-identical bytes for byte-identical input. This story doesn't own G5 — but it owns the digest substrate that makes G5 reachable. Pure helper, no I/O, no clock, no env → AST-fenced via AC-7's no-side-effects test.
What CLAUDE.md load-bearing commitments force¶
- Newtype identifiers under
codegenie.types.identifiers— never rawstrfor domain IDs. - Extension by addition — no silent edits. Drives AC-12 allowlist sentinel.
- Functional core / imperative shell. Drives AC-7 pure-helper structure.
- Type everything, strictly —
mypy --strict. Drives AC-11. - Explicit-import collection point — no entry-point scan. Drives AC-1 explicit
__all__.
Open ambiguities resolved before critics¶
- Q1 — Where does
_FROZEN_FORBIDlive? Two options: re-export the existingtransforms.outcomes._FROZEN_FORBID, or declare a new one atcodegenie.workflows._frozen. Either is consistent with ADR-0010 Amendment 2026-05-18 (single canonical declaration site); the choice is a one-line code comment. The story names this explicitly to prevent two declarations. - Q2 — Should the digest's pure helper be a method or free function? Free function (
_compute_sut_digest_input). Methods on Protocols are the impure shell; free helpers are the pure core. Phase-3 / Phase-4 follow this — see e.g.compose_bundle_cache_key. - Q3 — Closed Literal vs. discriminated union for
terminal_state? Closed Literal. The full payload-carrying state union is the ledger (S1-02), not the public Result. The harness readsterminal_stateas a string for scorecard grouping.
Four-lens findings (inline, no parallel subagents — story scope didn't justify the spawn)¶
Lens 1 — Coverage¶
| Finding | Severity | Resolution |
|---|---|---|
| AC-1 "exist" is unverifiable | block | Replaced with AC-1 (canonical module + re-exports + import-identity test) and AC-2 (Protocol shape with byte-exact annotation match). |
Field list for VulnRemediationCase undefined |
block | AC-3 lists all five fields + types + smart constructors + closed execution_mode Literal. |
Field list for VulnRemediationResult undefined |
block | AC-4 lists all eight fields + sub-models + cross-field invariants via model_validator. |
terminal_state set not pinned |
block | AC-4 explicitly pins {completed, awaiting_human_review, failed_unrecoverable} and forbids the four non-terminal ledger names; membership-byte-equality test. |
| Sanitization predicate vague | block | AC-5 three Hypothesis properties + two example-based negatives with directive-text assertion. |
SutDigest semantics unpinned |
block | AC-7 stability + sensitivity property tests + AST no-side-effects fence. |
| JSON round-trip / determinism not asserted | harden | AC-8 round-trip + sorted-key byte-determinism. |
| Newtypes added without registry | block | AC-10 forces _NEWTYPE_REGISTRY entries + extends the drift test. |
| Phase-6.5 import-boundary not encoded | harden | AC-6 import fence + placeholder Phase-6.5 fence. |
No mypy --strict AC |
harden | AC-11. |
| No public-surface allowlist | block | AC-12 allowlist sentinel. |
Lens 2 — Test Quality¶
| Finding | Severity | Resolution |
|---|---|---|
| TDD plan ordering puts newtypes in Refactor | block | Re-ordered: newtypes land in Green per CLAUDE.md. Refactor is cleanup only. |
| "Serialization tests" too generic | block | Each AC names specific tests + their failure modes. |
| No mutation-thinking pass | block | Each AC's test was checked: a swap from == to !=, extra="forbid" to extra="allow", Literal[…] to str, regex.fullmatch to regex.search, or omitting a digest field would each fail at least one test. |
| No property-based tests | harden | AC-5 + AC-7 add four Hypothesis properties covering sanitization + digest stability + digest sensitivity. |
| No contract-snapshot meta-test | block | AC-9 ships both the snapshot test and a meta-test that classifies synthetic additive vs. breaking deltas. Closes the S6-06-flagged failure mode. |
No AST fence on digest() purity |
harden | AC-7 ships the AST test as a placeholder; starts trivially passing, starts biting in S5-01. |
Lens 3 — Consistency¶
| Finding | Severity | Resolution |
|---|---|---|
| Story didn't reference ADR-0001 | harden | References block now names ADR-0001 + phase-arch-design + final-design + High-level-impl. |
| Story didn't name the canonical module | harden | AC-1 names src/codegenie/workflows/vuln_sut.py; matches phase-arch-design §Development view. |
| TDD plan "Refactor: extract newtypes" contradicts CLAUDE.md | block | Rewrote ordering; newtypes are Green. Anti-refactor note added explaining what NOT to do (no registry, no BaseSut). |
No Depends on: line |
nit | Added "Foundation — no upstream story deps." |
| Sanitization regex set forking risk | block | Notes-for-implementer explicitly mandates importing the canonical set from codegenie.output.sanitizer, not forking. Phase-9 critique report flagged exactly this drift pattern. |
| Cross-phase digest invariance unstated | block | AC-7 + Notes-for-implementer surface the Phase-9 S4-05 G5 forward dep. |
| Phase-6.5 import boundary unstated | block | AC-6 ships the fence test. |
Lens 4 — Design Patterns¶
| Finding | Severity | Resolution |
|---|---|---|
| Risk of locking in a class-hierarchy that prevents future SUT registry | harden | File naming + __all__ allowlist sentinel chosen so a @register_sut_kind registry can be added additively in Phase 9 when rule-of-three is reached. |
Risk of over-design via premature BaseSut ABC |
harden | Anti-refactor section forbids it explicitly; Notes-for-implementer cites Rule 2. |
| Functional core / imperative shell opportunity for digest | harden | AC-7 splits the pure helper from the impure Protocol method. |
Smart constructor opportunity for EvidenceRef |
harden | AC-4 + AC-5 require a smart-constructed EvidenceRef that rejects absolute paths + secret shapes at construction. |
Tagged-union vs Literal for terminal_state |
nit (consistency) | Closed Literal is the right shape for the public Result; the full payload-carrying union is the ledger's job. Documented in Notes-for-implementer. |
Single-source-of-truth for _FROZEN_FORBID |
harden | Q1 resolved: one canonical declaration site per ADR-0010 Amendment 2026-05-18. |
| Public-surface allowlist sentinel | harden | AC-12 — extension by addition becomes a load-bearing test, not a convention. |
| Newtype catalog discipline | harden | AC-10 — registry-entry-in-same-commit + drift test. |
Synthesis + edit summary¶
No conflicts between critics. No NEEDS RESEARCH. The synthesizer applied every fix above in one editing pass:
- 3 ACs → 12 ACs (AC-1 through AC-12), every one individually verifiable with a named test file + failure-mode mutation check.
- TDD plan rewritten in Red-first order with an explicit anti-refactor note.
- Files to touch enumerated (10 new files + 2 edits).
- Out of scope enumerated (5 deferrals + 1 anti-pattern).
- Notes for implementer enumerated (6 entries — covers contract-amendment discipline, terminal-state shape, evidence-ref rationale, functional-core/imperative-shell split, SUT-registry rule-of-three deferral, contract-snapshot meta-test non-negotiability, AST-no-side-effects rationale).
- Status flipped from
Ready→HARDENED. Validated-date line added.
Verdict — HARDENED. The story is ready for phase-story-executor.¶
The executor's Validator pass now has 12 concrete acceptance criteria, each tied to a named test file and a mutation-resistance check. The Phase-6.5 import boundary, the Phase-9 digest-invariance forward dep, the Phase-1 ADR-0007 error-ID format, the Phase-2/4 sanitization regex set, the ADR-0010 newtype registry discipline, and the CLAUDE.md "extension by addition" allowlist are all encoded as enforceable structural defenses. A mutant implementation that violates any one of them fails at least one test.