Skip to content

P6a: Promotion decision (decide(), C-45 enforcement) - #24

Merged
Ayushxsingh100 merged 8 commits into
mainfrom
feature/p6a
Oct 29, 2026
Merged

Ayushxsingh100 merged 8 commits into
mainfrom
feature/p6a

Conversation

@Ayushxsingh100

Copy link
Copy Markdown
Owner

P6a REPORT

Process (C-28): do not merge until the external review is pasted into this PR.

1. Branch and commits

feature/p6a from main c3524ef, 8 commits. Each was run in a clean clone on Python 3.12 with dev.py test, and every one is OK:

commit step tests (3.12)
8bd30a1 0: P5 gate verdict recorded in PHASE_GATES, as supplied 3508
54f9c7d 1: C-157..C-161, C-37 and C-41 decided, rule R18 3511
ce1a0b8 2: promotion types and the two policies 3559
94ea4f9 3: carry_forward needs the impact report (C-45) 3571
d1d3418 4: decide() 3666
e9b1e0c 5: candidate_after 3683
689728a 6a: canonical transitions and all fixtures with a parent 3702
2a4cdf9 6b: properties and fault cases 3724

Every run shows OK (skipped=1). Final tree on Python 3.14: 3724 tests, OK (skipped=2). Python 3.13: NOT RUN — not installed.

2. Files

22 files changed, +5274 / −23.

  • New: core/domain/promotion.py, core/promotion/{decide,policy,lifecycle}.py; builders tests/promotion_builders.py and tests/promotion_pipeline.py; 7 test files test_promotion_{domain,policy,decide,lifecycle,fixtures,properties,faults}.py.
  • Edited: core/domain/invariant.py (signature of carry_forward); docstrings of core/domain/__init__.py and core/promotion/__init__.py; the contract test (R18 only, nothing loosened); the register; PHASE_GATES.md (step 0 only, plus the heading "P6a–P10 — NOT STARTED").
  • carry_forward callers changed by the new argument only: tests/unit/test_proof.py (8 calls plus a small impact() helper); tests/adversarial/test_state_machine_rules.py (1 call); tests/domain_builders.carried_proof, which feeds test_promotion_coverage.py.
  • Untouched: verification, impact, identity, dependency, fixtures, expected.json, the registry spec and requirements/.

3. Acceptance criteria A1–A11

All IMPLEMENTED.

  • A1: types, guards, hash and round trip in core/domain/promotion.py; the policy refuses oracle acceptance; two constructors. Tests: test_promotion_domain.py (31), test_promotion_policy.py (17).
  • A2: Appendix A order with the C-158 severity fold, independent of input order. Tests: TestSeverityFold, TestOrderIndependence.
  • A3: stale parent, structural and consistency failures, FAIL, UNKNOWN, UNSUPPORTED, VERIFIER_ERROR, missing, stale and insufficient evidence each give the C-158 decision, and none gives PROMOTE. Tests: test_promotion_decide.py and the properties.
  • A4: C-45 is structurally impossible; see section 7.
  • A5: oracle evidence never proves anything and never promotes automatically (TestOracleEvidence).
  • A6: emergency switch, retry budget, registry regression and completeness behave as C-158, C-161 and C-41 (the classes of those names).
  • A7: a non-PROMOTE keeps every blocking condition (domain guard and tests); a PROMOTE lists every result used.
  • A8: candidate_after uses only legal transitions and never touches the trusted state (17 tests).
  • A9: canonical transitions in test_promotion_fixtures.py; see section 6.
  • A10: purity, hash-seed determinism, R18, and untouched SEC/FUNC code.
  • A11: the tables below.

4. Commands

  • dev.py test 3.12: Ran 3724 tests, OK (skipped=1). Before: 3508 at c3524ef, so +216.
  • black: 239 files unchanged. ruff: all checks passed. mypy: no issues in 108 source files.
  • pytest: 3723 passed, 1 skipped, 79777 subtests passed.
  • Fresh clone, dev.py bootstrap: 3724 tests OK, health checks PASS, bootstrap complete, exit 0.
  • NOT RUN: Python 3.13 (not installed); pip-audit (no dependency changed).

5. Decision table (Doc 09 §20 and §41)

My hand derivation, the spec's rule and the code's answer agree in every row; tests are in TestAppendixAAndTheDecisionMatrix.

case decision reason
SEC PASS, FUNC PASS, parent current PROMOTE ALL_REQUIRED_PROVEN
SEC PASS, FUNC FAIL REJECT REQUIRED_CHECK_FAILED
SEC FAIL, FUNC PASS REJECT REQUIRED_CHECK_FAILED
required UNKNOWN RETRY REQUIRED_UNKNOWN
mandatory UNSUPPORTED REJECT REQUIRED_UNSUPPORTED
VERIFIER_ERROR ESCALATE (RETRY if the type is listed transient) VERIFIER_ERROR
result missing REJECT INSUFFICIENT_EVIDENCE
not sufficient (L0, other verifier) REJECT EVIDENCE_NOT_SUFFICIENT
stale result (older verifier version) REJECT EVIDENCE_STALE
stale parent, even with a FAIL REJECT STALE_PARENT
construction or structural failure REJECT STRUCTURAL_FAILURE
consistency failure REJECT INCONSISTENT_REPORTS
oracle FAIL against a proven check ESCALATE (REJECT by policy) EVIDENCE_CONFLICT

6. Canonical transitions and every fixture with a parent

All 17 fixtures with a parent give a decision among the reviewer's decision.allowed. The trusted state of each is built from the parent directory's own expected.json. Proof plan: V = verified, C = carried forward, − = no proof.

fixture decision / reason FUNC SEC1 SEC2 SEC3
canonical_safe_repair_promote PROMOTE V V V C
canonical_security_repair_breaks_function_reject REJECT REQUIRED_CHECK_FAILED (FUNC) − V V C
ambiguous_identity_uncertain PROMOTE C V V C
ec2_rds_5432_pass PROMOTE V V V C
ec2_rds_5432_break_fail REJECT REQUIRED_CHECK_FAILED (FUNC) − V V C
func_unknown_dependency_unknown RETRY REQUIRED_UNKNOWN − V V C
func_unsupported_relationship_unsupported REJECT REQUIRED_UNSUPPORTED (FUNC, baseline item) − C C C
process_stale_parent_reject (newer state passed in) REJECT STALE_PARENT − − − −
process_verifier_error_escalate (crashing verifier) ESCALATE VERIFIER_ERROR − V V C
rds_deleted_dependency_fail REJECT REQUIRED_CHECK_FAILED (FUNC) − C C V
rds_publicly_accessible_fail REJECT REQUIRED_CHECK_FAILED (SEC3) V C C −
rds_publicly_accessible_pass PROMOTE V C C V
sg_created_resource_fail REJECT REQUIRED_CHECK_FAILED (SEC1, baseline item) C − V C
sg_private_postgres_pass PROMOTE V V V C
sg_private_ssh_pass PROMOTE V V V C
sg_public_postgres_fail REJECT REQUIRED_CHECK_FAILED (SEC2) V V − C
sg_public_ssh_fail REJECT REQUIRED_CHECK_FAILED (SEC1, baseline item) V − V C
sg_replacement_resource_promote PROMOTE V V V C
  • Canonical safe repair: PROMOTE. INV-SEC-001 is VERIFIED because its parent reference is VIOLATED; FUNC and SEC-002 are VERIFIED because the engine's impact report affects them; SEC-003 is CARRIED_FORWARD. The test derives this from the impact report, not from a constant.
  • Regression: REJECT, with the functional check as the only blocking condition.
  • Stateful-Full: with every invariant re-verified, patch_safe promotes with nothing carried and the regression is rejected. With the DAIL scope and the Stateful-Full policy, the result is REJECT INSUFFICIENT_EVIDENCE.
  • After a REJECT, v0 is unchanged (hash and content) and the next candidate has the same parent, which decides to PROMOTE.
  • R1 fired 0 times: no violated invariant was left unverified in any fixture.

7. C-45 evidence

  • Negative tests (TestCarryForwardNeedsTheImpactReport, 11 tests): an affected invariant, an uncertain one, an unmentioned one, an unaffected one without eligible evidence, a report for another candidate, another parent or another version, and a report whose hash no longer recomputes all raise DomainValidationError naming C-45. The old two-argument call is a TypeError.
  • Red run: with the new signature absent, 31 tests failed (all TypeError). Five scratch-copy mutants of the new check are killed. The first mutation run left the "version not passed" mutant alive; I added a test with a reference at version 2 and it now dies.
  • In decide: the property check_c45 and TestCarryForwardRules (each precondition removed in turn).

8. Mutation, properties, faults

  • Mutation: 37 checks in a scratch copy outside the repository. 34 killed, 0 rule breakages uncaught, and 3 single-check mutants survive. Those three are deliberately redundant defence-in-depth checks: affected_for, the status check and the version check each duplicate carry_forward_ok or can_satisfy_proof. Removing them together is killed.
  • Properties: 5 seeds × 60 worlds. The generator reaches all four decisions and 13 reasons. Each checker also fails on a hand-built decision that breaks its rule. Order independence is checked by swapping results between items and by fold permutations.
  • Faults: verifier crash, hash mismatch, stale candidate, truncated and empty reports, missing STRUCTURAL. Each gives a known non-PROMOTE decision and leaves the trusted state unchanged.

9. P6a evidence map (the part of Doc 15 §13 this package reaches)

Evidence, not a verdict. The criteria are those of the work package; the text of Doc 15 §13 was not available.

  • Known-good promotes: the safe repair and every PROMOTE fixture.
  • Known-bad rejects: the regression and every REJECT fixture.
  • UNKNOWN, UNSUPPORTED, VERIFIER_ERROR and insufficient evidence never promote: TestAppendixAAndTheDecisionMatrix, TestEvidenceSufficiencyAndFreshness and the properties.
  • Stale parent never promotes: TestStaleParent, the properties, process_stale_parent.
  • Conflicting oracle evidence never promotes automatically: TestOracleEvidence.
  • C-45 impossible by construction: section 7.
  • P6b: atomic commit, idempotency, concurrency, exactly one new state.

10. Register rows, spec gaps, new Open rows

  • Rows: C-157 to C-161 added (Proposed); C-37 and C-41 set to "Decided 29 Oct 2026 (P6a prompt); implemented in P6a"; C-159 amended with the level rule. No Open row added.
  • Spec gaps and choices, recorded in the rows:
    1. Doc 06 §17 "verifier configuration changed" is not modelled; equal definition versions count as compatible.
    2. Baseline items accept the result's own verifier, because the engine picks it per resource type.
    3. Gate items are L0 evidence, so the L1 minimum applies to invariants and baseline items only.
    4. A PASS marked "not applicable" (C-148, open) is a PASS.
    5. Doc 05 §15 has no reason field; reason, attempt_number and content_hash were added.
    6. An oracle FAIL on a carried-forward invariant is treated as a conflict.
    7. For a non-PROMOTE, an incomplete decision is recorded but keeps its decision.

11. What P6b needs from this package

  • API: decide(...) -> DecisionOutcome (decision, proof_plan, evidence). A PROMOTABLE candidate may be decided again at commit time.
  • Proof plan: VERIFIED entries carry the verification result id; CARRIED entries carry the source state and the parent's evidence ids. P6b turns these into InvariantProofs: VERIFIED through an evaluation, CARRIED through carry_forward(ref, candidate, impact).
  • Decision hash: content_hash excludes the id and the clock.
  • committed_state_id and new_state_id are forced to null today. P6b needs a sanctioned way to set them once, such as a with_commit(state_id) method.
  • Policy and gates: dail_policy and stateful_full_policy take the reviewed automatic_enabled flag as a required argument. The gate verifier ids are copied from the engine and pinned by a test.

12. Open questions and disclosures

  • For the authors: the severity order REJECT above ESCALATE above RETRY (R4); exhaustion of the retry budget in the core policy (R3); an unrelated edit from a violated baseline invariant gives INSUFFICIENT_EVIDENCE (R1).
  • Question from the fixtures: in ambiguous_identity_uncertain the engine's impact report leaves FUNC unaffected, so it is carried forward although some identity is uncertain. That is P4's call; P6a only consumes it. Is that intended?
  • Process lapses:
    • The real pipeline found a defect in decide (every gate item was rejected as below L1) after the synthetic tests were green, because my test builder had used L1 for gates too. It is fixed and tested.
    • An exploration script printed decide's outputs for all 17 fixtures before the fixture test was written. The canonical expectations in its docstring come from CLAUDE.md and expected.json, and the cross-check uses the reviewer's allowed data, but the exploration came first.
    • Commit 689728a went in with one E501 that 6b fixed.
    • The message of 689728a says "15 of the other 15 decisions", which is wrong; the correct statement is that 17 of 17 are among allowed.
    • Red runs: step 2 failed with ImportError. Step 4 passed 88 of 95 at first run and the 7 failures were test mistakes. Step 5 passed 14 of 17 (one test compared two separate worlds).

🤖 Generated with Claude Code

Ayushxsingh100 and others added 8 commits October 29, 2026 13:17
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…C-41, boundary rule R18 (Doc 09 §17-§29; C-37, C-41, C-157 to C-161)

R18 was written test-first: its self-tests failed until the rule existed in
check_module (core.promotion imports only the standard library and core.domain.*).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 05 §15, Doc 11 §26; C-157, C-158, C-160, C-161)

PromotionDecision and PromotionEvidence (guards, hash recomputed by the constructor, round trip),
the closed DecisionReason and ProofSource vocabularies, OracleVerdict, ProofPlanEntry,
DecisionOutcome and PromotionPolicy (non-success table, retry budget 1 to 10, the oracle cannot be
accepted). dail_policy and stateful_full_policy differ in carry-forward only (C-22).

The 48 tests were written first and failed with ImportError until the types existed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 06 §14, Doc 09 §28; C-45, C-141, C-159)

carry_forward(parent_ref, candidate, impact) now refuses unless the impact report of exactly
this candidate and parent says the invariant is unaffected with eligible evidence
(ImpactReport.carry_forward_ok): an affected, uncertain or unmentioned invariant, a report for
another candidate, parent or version, and a report whose hash no longer recomputes all raise.

The callers got the new argument only: tests/unit/test_proof.py (8 calls),
tests/adversarial/test_state_machine_rules.py (1) and tests/domain_builders.carried_proof
(which feeds tests/unit/test_promotion_coverage.py). The 33 changed or new tests were shown
failing first (31 TypeErrors), and 5 scratch-copy mutants of the new check are killed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ess table, proof sources, oracle conflicts, retry budget, emergency switch, completeness (Doc 09 §17-§29, Appendix A; C-41, C-45, C-157 to C-161)

decide(candidate, trusted, impact, verification, policy, ...) is a pure function that examines every
required item (gates, invariants, baseline items), proves each by a fresh sufficient PASS or by
carrying its reference forward (never for an affected, non-PROTECTED, version-changed or
policy-disabled invariant), folds the blocking conditions by severity (REJECT above ESCALATE above
RETRY), converts an exhausted RETRY to ESCALATE, treats an oracle FAIL against a proven check as a
conflict, and downgrades an incomplete or switched-off PROMOTE to ESCALATE. A stale parent wins.

95 tests, written from the spec first: they failed with ImportError until decide existed; at the
first run 88 passed and the 7 failures were mistakes in the tests (a duplicate result id, the
wrong definition picked by index, a replace() the domain forbids, an expected list order, an
expected proof plan, a text scan, a stale-parent that was not stale), not in the code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ycle without touching Trusted State (Doc 06 §5, §20, §22 SM-001 to SM-004, SM-010; C-29, C-157)

PROMOTE -> PROMOTABLE (never PROMOTED), REJECT -> REJECTED (a PROMOTABLE candidate only through the
stale-parent exit), RETRY -> RETRY_REQUIRED, ESCALATE -> ESCALATED, with the existing transition
functions only. The decision must be about this candidate and its parent; a PROMOTE against a
state that is no longer the parent raises StaleParentError (SM-010). The trusted state is read and
never changed. 17 tests, written first; at the first run 14 of 17 passed and the 3 failures were
one test comparing resources of two separate worlds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t through the real pipeline (Doc 12 §5-§7, Doc 06 §25-§26; C-22, C-45, C-159)

tests/promotion_pipeline.py runs normalisation, identity, graphs, impact (with the prior proofs of
the trusted state), the scope policy, the verification engine and decide. patch_safe promotes
(INV-SEC-001 VERIFIED because its reference is VIOLATED; the others VERIFIED or CARRIED_FORWARD as
the engine's own impact report says), patch_regression is rejected with the functional check as
its blocking condition, Stateful-Full carries nothing, and 15 of the other 15 decisions
are among the reviewer's decision.allowed.

The real pipeline found a defect in decide: every gate item was EVIDENCE_NOT_SUFFICIENT, because the
engine gives STRUCTURAL and CONSISTENCY level L0 and decide applied the L1 minimum to them; the test
builder had used L1 for them too. The level rule now applies to invariants and baseline items only
(C-159 updated), with tests for both.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…§17, §19; C-45, C-158 to C-161)

Five seeds, 60 generated worlds each: a required FAIL always rejects, a required non-success never
promotes, a PROMOTE has a full proof plan and only supporting results that exist, a stale parent
is rejected, nothing affected, uncertain, not PROTECTED or policy-disabled is carried forward
(C-45), the emergency switch, the retry budget and an oracle FAIL each stop a PROMOTE, and nothing
is mutated. The generator reaches all four decisions and 13 reasons. Every checker is also handed
a decision that breaks its rule and must fail. The fault cases (verifier crash, hash mismatch,
stale candidate, truncated report, missing STRUCTURAL) each give a known non-PROMOTE decision and
leave the trusted state unchanged. Also fixes a line over 100 columns in the fixtures test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Ayushxsingh100
Ayushxsingh100 merged commit a7ca8af into main Oct 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant