Also §0 pre-flight: SCAFFOLD-TRANCHE-005 found approved/open (parent-
lifecycle neglect — child closed, parent forgotten); closed with real
existing evidence only, provenance recorded, process-defect entity added.
- construction_pattern.py: open-world ConstructionPattern (signals,
obligations with when-conditions, provenance, revision + history,
maturity, ext); evaluate_applicability — deterministic signal groups,
verdicts APPLIES/PARTIAL/DOES_NOT_APPLY/UNKNOWN, name similarity
establishes nothing, project exclusions override, model proposals stay
PENDING; derive_pattern_obligations — obligations FILTERED by project
evidence (REQUIRED/APPLICABLE/NOT_APPLICABLE/UNKNOWN + EXCEPTED with
provenance retained, §18); combine_pattern_obligations — dedupe with
also_from, conflicts surfaced never silently resolved (§26-27);
authority boundary: pattern = guidance, project evidence wins (§3)
- construction_patterns_builtin.py: 4 evidence-grounded patterns, all
appropriately immature (§15):
* capability-module-adaptation (UNIT-PROVEN; build-but-don't-break;
provenance T1/T3/T4 contracts)
* release-deployment-change (UNIT-PROVEN; 9 obligations incl.
fail-closed qualification, exact identity, staging!=promotion,
promotion truthfulness, monotonic ordering, negative+forward proofs,
deployed-identity verification; provenance deploy-order-race
incident + DEPLOY-MONOTONIC-001 + red-promotion incidents)
* ui-tui-structural-change (UNIT-PROVEN; layout inheritance, USER-LOCKED
preservation, UNKNOWN-never-PASS; provenance issue-105 + T3 real proof)
* store-schema-evolution (CANDIDATE; backward readability, roundtrip,
authority preservation, migration-only-if-needed; provenance T2/T4/T5)
- make_decision integration: APPLIES+PARTIAL patterns consulted (§7 —
per-obligation when-conditions do the precision work); provenance
PATTERN distinct from STRUCTURAL derivations (§8); pattern refs +
revisions carried into generated contracts (§17/§19)
- construction_guards.py + Scaffold.save_verified/verify_on_disk (§21-22):
Guard A durable-mutation readback — verify_on_disk is the pure no-write
readback that catches the forgotten-save lie (T5 defect class A);
Guard B source-edit postcondition — zero-match is a truthful no-op
(EditNoOp), expected-count + postcondition enforced (T5 defect class B)
- REAL PROOFS: §23 read-only replay of DEPLOY-MONOTONIC-001 Need —
release pattern APPLIES on evidence, 9 obligations derived WITHOUT the
Need listing them (negative/forward/monotonic/deployed-identity all
pattern-derived, provenance carried); resolution COMPOSE matches the
original repair; §24 live change save_verified/verify_on_disk — the
store pattern contributed store-roundtrip, an obligation the raw Need
did not state; §25 held-out — webhook-config-only deploy change does
NOT demand promotion rework but keeps release regression verification
Tests: 30 new (§28-§32 plans + held-out + live); suite 1556 green;
henxels rc=0; pyright clean.
124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
"""construction_guards.py — deterministic guards for observed process
|
|
defects (SCAFFOLD-TRANCHE-006 §21-22).
|
|
|
|
Two real defect classes from recent sessions:
|
|
|
|
A. DURABLE-STORE MUTATION WITHOUT PERSISTENCE (Tranche 5: closure script
|
|
mutated the contract in memory, never called save(); the next read saw
|
|
stale state). Guard: a durable-boundary operation verifies by
|
|
RELOAD/READBACK, not by successful in-memory mutation.
|
|
|
|
B. BLIND SOURCE-EDIT NO-OP (multiple sessions: python str.replace() on
|
|
ruff-reformatted files silently matched nothing; the script reported
|
|
success while changing nothing). Guard: an automated edit verifies its
|
|
POSTCONDITION — expected text present / old text absent — and a
|
|
zero-match replacement is an explicit, truthful no-op, never success.
|
|
|
|
Not a generic editor framework. Two small, deterministic postcondition
|
|
checks at the boundaries where the defects actually occurred.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
__all__ = ["verify_persisted", "assert_persisted", "apply_source_edit", "EditNoOp", "EditPostconditionFailed"]
|
|
|
|
|
|
class EditNoOp(Exception):
|
|
"""The edit matched zero targets — an explicit, truthful no-op."""
|
|
|
|
|
|
class EditPostconditionFailed(Exception):
|
|
"""The edit claimed success but the expected postcondition is absent."""
|
|
|
|
|
|
# ── Guard A: durable mutation verified by readback ──────────────────────────
|
|
|
|
|
|
def verify_persisted(path: Path | str, selector, expect) -> bool:
|
|
"""Reload the durable store from disk and check the expectation against
|
|
the RELOADED state. `selector(reloaded_doc) -> value`; pass == value
|
|
equals `expect` (or expect(value) is truthy when callable).
|
|
|
|
The reloaded document is parsed as JSON when the file is JSON-shaped;
|
|
otherwise the raw text is passed to the selector (plain-text stores).
|
|
|
|
This is the Tranche 5 defect guard: an in-memory mutation that was
|
|
never save()d fails here, because the disk state is what gets read.
|
|
"""
|
|
raw = Path(path).read_text(encoding="utf-8")
|
|
try:
|
|
doc = json.loads(raw)
|
|
except ValueError:
|
|
doc = raw
|
|
value = selector(doc)
|
|
if callable(expect):
|
|
return bool(expect(value))
|
|
return value == expect
|
|
|
|
|
|
def assert_persisted(path: Path | str, selector, expect, *, what: str = "durable state") -> None:
|
|
"""verify_persisted that raises on mismatch — for use right after a
|
|
save() at durable boundaries."""
|
|
if not verify_persisted(path, selector, expect):
|
|
raise AssertionError(
|
|
f"persistence verification FAILED for {what}: reloaded state "
|
|
f"does not match expectation — the mutation may not have been "
|
|
f"saved (Tranche 5 defect class A)"
|
|
)
|
|
|
|
|
|
# ── Guard B: source edit with postcondition ─────────────────────────────────
|
|
|
|
|
|
def apply_source_edit(
|
|
text: str,
|
|
old: str,
|
|
new: str,
|
|
*,
|
|
expected_count: int | None = None,
|
|
postcondition: str | None = None,
|
|
allow_noop: bool = False,
|
|
) -> str:
|
|
"""Apply a source edit WITH postcondition verification (Guard B).
|
|
|
|
- zero matches: raises EditNoOp (truthful no-op) unless allow_noop=True
|
|
- expected_count given: match count must equal it
|
|
- postcondition given: the RESULT must contain it, else
|
|
EditPostconditionFailed
|
|
Returns the new text. Never claims success for a change that did not
|
|
happen."""
|
|
count = text.count(old)
|
|
if count == 0:
|
|
if allow_noop:
|
|
return text # truthful, explicit no-op — caller records it
|
|
raise EditNoOp(
|
|
f"edit matched 0 targets — nothing changed (Tranche 5 defect "
|
|
f"class B: str.replace silently no-ops on reformatted files)"
|
|
)
|
|
if expected_count is not None and count != expected_count:
|
|
raise EditPostconditionFailed(f"edit matched {count} targets, expected {expected_count}")
|
|
result = text.replace(old, new)
|
|
if postcondition and postcondition not in result:
|
|
raise EditPostconditionFailed(
|
|
"postcondition text absent from result — edit did not achieve its expected effect"
|
|
)
|
|
return result
|
|
|
|
|
|
def apply_source_edit_file(
|
|
path: Path | str, old: str, new: str, *, expected_count: int | None = None, postcondition: str | None = None
|
|
) -> int:
|
|
"""File-level convenience: read, apply_source_edit, write back, then
|
|
VERIFY BY READBACK (both guards composed). Returns match count."""
|
|
p = Path(path)
|
|
text = p.read_text(encoding="utf-8")
|
|
result = apply_source_edit(text, old, new, expected_count=expected_count, postcondition=postcondition)
|
|
p.write_text(result, encoding="utf-8")
|
|
# Guard A composed: the written file must contain the postcondition
|
|
if postcondition:
|
|
assert_persisted(p, lambda t: t, lambda t: postcondition in t, what=f"edit of {p.name}")
|
|
return text.count(old)
|