Skip to content

P6b: the promotion commit (transaction, new state, idempotency, concurrency) - #25

Merged
Ayushxsingh100 merged 17 commits into
mainfrom
feature/p6b
Oct 29, 2026
Merged

Ayushxsingh100 merged 17 commits into
mainfrom
feature/p6b

Conversation

@Ayushxsingh100

Copy link
Copy Markdown
Owner

P6b REPORT

Written for: the external reviewer, and the PR description (the P6b prompt's report format).

No gate or mini-gate is claimed to have passed. Nothing is pushed; no PR is open.

1. Branch and commits

Branch feature/p6b from a7ca8af (main). 17 commits. Each was checked in its own clean clone on Python 3.12: black --check, ruff check, mypy, then scripts/dev.py test.

# commit step black / ruff / mypy dev.py test (3.12)
1 755b342 0 record the P6a verdict (PHASE_GATES) ok / ok / ok Ran 3724, OK (skipped=1)
2 89f9664 1 fix C-162 ok / ok / ok Ran 3739, OK
3 06ff3b7 2 register rows, C-38 decided, sole-caller test ok / ok / ok Ran 3741, OK
4 e2d07c5 3 domain: committed decision/evidence, ids, proofs, new-state arguments ok / ok / ok Ran 3784, OK
5 603413b 4 schema 6, decision repository, DATA-INT-003/004, PROMOTABLE→ESCALATED ok / ok / ok Ran 3826, OK
6 75c14c1 5 commit.py, binding.py, record_decision ok / ok / ok Ran 3863, OK
7 169e1a0 5 binding tests; integrity_of agrees with the evidence service ok / ok / ok Ran 3893, OK
8 e89b7fc 6 idempotency and concurrency ok / ok / ok Ran 3911, OK
9 227814b 6 fault injection ok / ok / ok Ran 3920, OK
10 fab89a8 6 canonical transitions through the database (real pipeline) ok / ok / ok Ran 3931, OK
11 5b13492 6 properties and the checker ok / ok / ok Ran 3936, OK
12 4d898fc 6 P6 on whole records; order of the writes ok / ok / ok Ran 3939, OK
13 e6b7632 6 proof-plan comparison at revalidation ok / ok / ok Ran 3940, OK
14 5e8ca84 6 register notes, C-169 Open ok / ok / ok Ran 3940, OK
15 0c2cbfd 6 package docstring ok / ok / ok Ran 3940, OK
16 f666c76 6 candidate PROMOTED before the decision is committed (prompt's order) ok / ok / ok Ran 3940, OK
17 f30eade 6 unique partial index tested with the triggers gone ok / ok / ok Ran 3941, OK

Final tree (f30eade): Python 3.12 Ran 3941, OK (skipped=1); Python 3.14 Ran 3941, OK (skipped=2; the extra skip is test_terraform_instances.py:221, the interpreter's Unicode version, not a P6b test). Python 3.13: NOT RUN, not installed (py -0 lists 3.12 and 3.14).

2. Files

Added: core/promotion/{ids,proofs,newstate,binding,commit}.py; tests/promotion_commit_builders.py, tests/promotion_pipeline_commit.py; tests/unit/test_promotion_{c162,committed,carry,newstate,exit,persistence,commit,binding,idempotency,concurrency,faults6,properties6,pipeline6}.py (215 tests).

Modified (code): core/domain/{verification,promotion,lifecycle,invariant,repositories}.py, core/persistence/{schema,sqlite}.py, core/promotion/{decide,__init__}.py, core/application/health.py (one table name). Docs: docs/DECISIONS_REGISTER.md, docs/PHASE_GATES.md (step 0 only).

Outside the prompt's expected-file list (each needed, none loosens anything): core/application/health.py (EXPECTED_TABLES gains promotion_decisions, or the health check fails on schema 6); core/promotion/__init__.py (docstring); tests/promotion_pipeline.py (new keyword parameters candidate_id, parent_trusted, sequence, patch_n, decision_id, defaults keep P6a behaviour; the 17 P6a fixture tests pass unchanged); tests/promotion_builders.py (record() evidence carries the engine's keys, so C-68's subject can be checked).

Existing tests whose literal, set or construction changed:

  • tests/unit/test_lifecycle.py: the pinned table gains (PROMOTABLE, ESCALATED); len(by_hand) 10→11 (C-38).
  • tests/unit/test_persistence_schema.py: version literals 5→6, "needs 6", the refused-versions loop ("6"→"5"), the table, trigger and statement-count sets, PROMOTABLE: {…, ESCALATED}.
  • tests/unit/test_persistence_repositories.py: "schema version 6"; the checkpoint list gains after_candidate_update (saving the candidate's status is now a seam).
  • tests/unit/test_repository_ports.py: DecisionRepository and decisions added to the pinned port lists; a logic edit, not a literal: allowed_save now also allows save on DecisionRepository (the port the prompt specifies has save(decision)). I flag it for the reviewer.
  • tests/contract/test_architecture_boundaries.py: additive only (R8_SOLE_CALLERS, two tests; nothing loosened).

3. Acceptance criteria

status evidence
B0 IMPLEMENTED test_promotion_c162.py (15): the three reproductions (TestTheReviewersReproductions), domain refusal, every matching result examined, property over seeded reports with decoys and order independence. Before/after in 5.
B1 IMPLEMENTED test_promotion_committed.py (14: committed, with_new_state, DATA-INT-004 in the domain); test_promotion_exit.py (5: the new edge and every neighbouring forbidden edge).
B2 IMPLEMENTED, one item NOT DONE by the user's decision test_promotion_persistence.py (38): round trip, immutability, one-way commit, DATA-INT-003/004 refused by raw SQL, golden hashes (schema-5 prefix and full schema 6), schema-5 refusal, duplicate committed decision per candidate (unique index, with triggers gone). The prompt's "a trusted state with version > 0 and no decision is refused" is NOT DONE: C-168 decision (b), made by the user on 29 Oct 2026 (about 25 existing tests call save_promoted directly). Enforced by construction in commit_promotion, by the order test, and by a check_database rule.
B3 IMPLEMENTED test_promotion_commit.py (39), test_promotion_pipeline6.py (11): parent, version, resources, refs, commit_decision_id, binding record on every ref, one STATE_PROMOTED, order of the writes.
B4 IMPLEMENTED TestRecordDecision; test_promotion_properties6.py (non-PROMOTE never changes trusted_states or lineage_heads).
B5 IMPLEMENTED test_promotion_idempotency.py (12), test_promotion_commit.py::TestIdempotency.
B6 IMPLEMENTED test_promotion_concurrency.py: 66 schedules (section 7).
B7 IMPLEMENTED test_promotion_faults6.py (9): section 8.
B8 IMPLEMENTED TestRevalidation, test_promotion_binding.py (32), test_promotion_carry.py, test_promotion_newstate.py (C-45 in carry_forward and at commit).
B9 IMPLEMENTED test_promotion_pipeline6.py: section 9.
B10 IMPLEMENTED R8 sole-caller test (core.domain.state, core.promotion.commit); R18 holds (core.promotion imports only the standard library and core.domain); files outside the list named in section 2.
B11 IMPLEMENTED this report.

4. Commands and real output

py -3.12 scripts/dev.py test     Ran 3941 tests in 701.802s   OK (skipped=1)
py -3.12 scripts/dev.py lint     259 files would be left unchanged. / All checks passed! / Success: no issues found in 113 source files / lint: all checks passed
black --check .                  259 files would be left unchanged.
ruff check .                     All checks passed!
mypy                             Success: no issues found in 113 source files
pytest                           3940 passed, 1 skipped, 80217 subtests passed in 537.68s (0:08:57)
py -3.14 scripts/dev.py test     Ran 3941 tests in 1732.720s   OK (skipped=2)
fresh clone f30eade, py -3.12 scripts/dev.py bootstrap
                                 Ran 3941 tests in 710.026s   OK (skipped=1); health: liveness PASS, readiness PASS, database PASS (16 expected tables present), artifact_store PASS, llm_provider SKIPPED; bootstrap complete

Test counts: 3724 at a7ca8af → 3941 (+217).

NOT RUN: Python 3.13 (not installed); pip-audit (no dependency or pin changed); Terraform checks of the README (no fixture or Terraform file touched, no Terraform CLI used). The per-commit table (section 1) was produced by percommit_p6b*.sh in the session scratchpad.

Process facts about the commands: the first three-way parallel per-commit run covered commits 1–15; commits 16 and 17 were run on their own afterwards; a verification run started before commit 16 was killed and restarted on the final tree.

5. C-162 (target id has at most one result, whatever its type)

Before the fix (tests/unit/test_promotion_c162.py against the unfixed code; progress line FF.F.FFFF.FFFFFF): TestTheReviewersReproductions.test_inv_sec_001, test_inv_func_001 and test_a_resource_baseline_item each failed with <DecisionType.PROMOTE> != <DecisionType.REJECT>; the property failed at seed 1 world 20 with PROMOTE; the domain tests failed with DomainValidationError not raised. After the fix all 15 pass.

Fix: VerificationReport rejects two results with one target_id (core/domain/verification.py); decide looks each required item up by (target_type, target_id) with the type its kind requires and examines every matching result (core/promotion/decide.py).

Mutation proof (re-run on the final code): part (1) removed → 2 tests fail; part (2) removed → test_a_result_of_the_wrong_type_alone_is_not_proof; part (3) removed → 8 tests fail (TestTheReviewersReproductions and others). Unmutated: green.

6. The commit, step by step (Doc 09 §26, Appendix B; C-163 to C-167)

commit_promotion(factory, outcome, candidate, impact, verification, policy, evidence_ids, context, *, now, …), in one unit of work (BEGIN IMMEDIATE, C-166):

  1. existing decision with this id: committed twin → CommitResult(duplicate=True), other content → ConflictingDuplicateError (C-163); a stored REJECT of this loser → its stale result; another committed decision for the candidate → ConflictingDuplicateError.
  2. read the candidate and the current state; candidate not PROMOTABLE → revalidation failure; parent is not the current state → stale (C-38 case 1).
  3. decide again on the database's candidate and state; the decision hash, the proof plan and the evidence hash must equal the stored ones (C-167).
  4. binding_problems (C-164) over every plan entry.
  5. build everything without writing: proofs, TrustedState.promote, committed decision, PromotionEvidence.with_new_state, binding record, mark_promoted.
  6. writes, each a fault seam: decisions.save (after_decision_insert) + PROMOTION_DECIDED if the decision was not stored; binding record append; save_promoted (after_trusted_state_insert, after_invariant_refs_insert, after_lineage_head_update; its compare-and-swap is the last line of defence); candidates.save_transition PROMOTED (after_candidate_update); decisions.commit (after_decision_commit); STATE_PROMOTED with the derived id; before_commit, COMMIT.

The order of the writes is pinned by TestOrderOfWrites. It was changed first and failed against the earlier order (decision committed, then candidate PROMOTED); commit f666c76 swaps them to the prompt's order.

Outcomes outside the commit: lost race → REJECT decision (STALE_PARENT, derived id), candidate REJECTED, stale=True; revalidation failure → PROMOTABLE→ESCALATED, PROMOTION_FAILED and ESCALATED events, the PROMOTE kept uncommitted, escalated=True; any other failure → rollback, candidate stays PROMOTABLE, best-effort PROMOTION_FAILED, PromotionCommitError.

7. Idempotency and concurrency

Idempotency (test_promotion_idempotency.py, 12 tests): three repeats → the same state, duplicate=True, every table unchanged; a later clock and other context ids change nothing; conflicting duplicates (other policy version, other attempt number, other content under a stored uncommitted id, a stored REJECT under a PROMOTE's id) raise and change nothing; a second PROMOTE for a promoted candidate is refused; all four decisions recorded twice change nothing; two threads committing one decision at each of the seven checkpoints → one state, one duplicate=True.

Concurrency (test_promotion_concurrency.py): 66 schedules, real threads, two connections to one file, Gate events at the checkpoint seam, no sleep. 42 systematic (first runner A/B × interleaving {overlap, sequential, first-fails-after-the-head-moved} × all seven checkpoints) + 24 drawn with random.Random(20261029). Mix: 21 overlap, 23 sequential, 22 first-fails; winners A 33, B 33; every checkpoint at least 7 times. Every schedule: exactly one new state (version = parent + 1), head = the winner's derived state, winner PROMOTED, loser REJECTED with a stored REJECT/STALE_PARENT decision under the derived id, 8 references, 2 decisions, one binding record, one STATE_PROMOTED, check_database empty (no orphan row). A first-fails schedule leaves the failed candidate PROMOTABLE and the retry loses. A unit of work that believes the head has not moved still loses at the compare-and-swap (TestCompareAndSwap). SQLite BEGIN IMMEDIATE serialises writers on one file: the MVP's single promotion worker (Doc 09 §39); nothing is claimed for several processes beyond what these tests show.

8. Fault injection (test_promotion_faults6.py)

Injected InjectedFault at each point; read from a new connection after each.

injected at previous state current rows of the attempt left committed decision / binding record / STATE_PROMOTED candidate retry
after_decision_insert v0 none 0 / 0 / 0 PROMOTABLE one new state
after_trusted_state_insert v0 none 0 / 0 / 0 PROMOTABLE one new state
after_invariant_refs_insert v0 none 0 / 0 / 0 PROMOTABLE one new state
after_lineage_head_update v0 none 0 / 0 / 0 PROMOTABLE one new state
after_candidate_update v0 none 0 / 0 / 0 PROMOTABLE one new state
after_decision_commit v0 none 0 / 0 / 0 PROMOTABLE one new state
before_commit v0 none 0 / 0 / 0 PROMOTABLE one new state
audit append (STATE_PROMOTED) v0 none 0 / 0 / 0 PROMOTABLE one new state
binding-record append v0 none 0 / 0 / 0 PROMOTABLE one new state

In every row the only new data is the best-effort PROMOTION_FAILED event (one audit event and its payload artifact, evidence_artifacts +1); the retry gives 2 trusted states. Run with the decision recorded before (all rows except after_decision_insert, which is not reached) the decision row stays uncommitted; run without, it rolls back. When even the failure event cannot be written (every checkpoint raises) the caller still gets PromotionCommitError and nothing is recorded. Revalidation failures (candidate ESCALATED, nothing committed, a second attempt changes nothing): tampered evidence payload; wrong state_hash on a source record; a source record produced for another candidate; a verification report and an impact report whose hash no longer recomputes.

9. Canonical transitions through the database (real P6a pipeline, test_promotion_pipeline6.py)

  • canonical_safe_repair_promote_v1 → PROMOTE → v1: INV-FUNC-001, INV-SEC-001, INV-SEC-002 VERIFIED, INV-SEC-003 CARRIED_FORWARD from v0, all PROTECTED (what the engine returned; nothing edited).
  • canonical_security_repair_breaks_function_reject_v1 → REJECT (blocking condition INVARIANT:INV-FUNC-001:1), candidate REJECTED, v0 still current, a REJECT cannot be committed; a second candidate from v0 (safe repair, own id) then promotes to v1 (Doc 06 §25–§26).
  • v1 → v2 with ec2_rds_5432_pass_v1 (its parent directory is the safe repair), pipeline run against the promoted v1: INV-SEC-003 CARRIED_FORWARD through v1's binding record, the other three VERIFIED.
  • Stateful-Full: four VERIFIED, nothing carried, each reference holds its source and the binding record.

Evidence context per reference (usable_as_proof in each context):

state invariant (origin) evidence bound to proof in
v1 FUNC-001, SEC-001, SEC-002 (VERIFIED) VERIFICATION verification.result candidate 1 candidate 1 only
v1 same three STATE promotion.committed (binding) v1 v1 only
v1 SEC-003 (CARRIED) v0's VERIFICATION seed v0 v0 only
v1 SEC-003 binding v1 v1 only
v2 FUNC-001, SEC-001, SEC-002 (VERIFIED) VERIFICATION / binding candidate 2 / v2 candidate 2 only / v2 only
v2 SEC-003 (CARRIED) v0 seed / v1's binding / v2's binding v0 / v1 / v2 v0 only / v1 only / v2 only

No evidence is valid in a context it was not produced for (C-52); validity for vN+1 comes only from the new STATE record bound to it.

10. Mutation checks and properties

22 mutations on a scratch copy outside the repository (mut_p6b_step6.py); unmutated 33 modules green. Final run, all KILLED, none survived:

mutation failing tests (modules) e.g.
stale check removed 4 (2) test_a_commit_whose_parent_is_no_longer_current_loses
CAS removed (both checks) 4 (3) test_a_commit_that_believes_the_head_has_not_moved_still_loses
write lock removed (BEGIN) 5 (4) test_at_least_fifty_schedules_leave_exactly_one_winner
decision committed before the state 54 (7) test_a_recorded_decision_is_committed_in_place
binding record skipped 54 (7) same family
binding check skipped, VERIFIED 20 (3) test_a_source_evidence_record_of_another_invariant
binding check skipped, CARRIED 13 (2) test_a_carried_reference_whose_parent_evidence_is_not_bound_to_the_parent
wrong state_hash accepted 2 (1) test_a_readable_record_with_another_state_hash_is_not_proof
STATE_PROMOTED before the commit point 1 (1) test_the_writes_are_made_in_the_order_of_the_commit
duplicate returns no stored state 7 (4) test_the_same_decision_again_returns_the_same_state_and_writes_nothing
derived ids random 36 (9) test_the_ids_are_distinct_and_stable
revalidation skipped 2 (2) test_a_verification_report_whose_hash_no_longer_recomputes
revalidation compares decision only 1 (1) test_a_carried_reference_whose_evidence_is_not_the_one_decided_on
carry-forward without impact (C-45) 3 (2) test_a_report_that_does_not_mention_the_invariant_is_still_refused
C-162 (1) / (2) / (3) 2 / 1 / 8 (1 each) section 5
DATA-INT-003 trigger dropped 2 (1) and, with the pinned tests left out, test_data_int_003_…
DATA-INT-004 trigger dropped 5 (1) and, pinned tests left out, test_only_a_promote_can_be_committed +3
unique partial index dropped 2 (1) and test_at_most_one_committed_decision_per_candidate
failed unit of work commits 10 (8) test_a_fault_leaves_the_previous_state_and_the_candidate_promotable
C-38 exit applied to a stale candidate 4 (2) test_a_commit_whose_parent_is_no_longer_current_loses
schema-5 prefix altered 2 (1) test_the_schema_5_statements_are_byte_identical

Two weaknesses were found by the first run and closed with tests (commits e6b7632, f30eade): comparing the proof plan at revalidation was not tested (the decision hash does not cover a carried reference's evidence ids); the dropped unique index was killed only by the test that lists index names. The mutation text of the interpreted items: "wrong state_hash on the binding record accepted" = evaluate_proof given the record's own hash; "duplicate returns a new state" = the duplicate branch falls through; "failed commit leaves the candidate PROMOTED" = a failed unit of work commits instead of rolling back.

Properties (test_promotion_properties6.py, 40 seeded chains; PASS/FAIL/UNKNOWN/ERROR draws; 1–3 states): non-PROMOTE never changes trusted_states or lineage_heads; references cover the parent's invariants with no lower version; every reference carries the binding record and it is proof for its state; at most one child per state; commit_decision_id unique; a replayed commit or record changes no row of any table (full dumps compared). Coverage asserted: PROMOTE, REJECT, RETRY, ESCALATE, a duplicate and a length-3 chain all occur. check_database fires for each of 14 broken rules (TestTheCheckerFires).

11. P6b evidence map (Doc 15 §13, the part this package reaches)

criterion tests
a valid candidate advances exactly once TestCommitPromotion, TestSafeRepair, test_promotion_idempotency.py, concurrency
stale parent never advances TestStaleParent, concurrency (66 schedules), TestCompareAndSwap
duplicate request idempotent test_promotion_idempotency.py
concurrent promotion only from the valid current parent test_promotion_concurrency.py
DB commit failure leaves the previous state current test_promotion_faults6.py, TestInfrastructureFailure
persisted promotion evidence the STATE binding record and the stored decisions (test_promotion_commit.py, test_promotion_pipeline6.py)

P6c, not here: the orchestrator, the baseline flow, the full fixed-patch loop, M3 and the P6 gate. No gate is claimed.

12. Register, spec gaps

Rows: C-162, C-163, C-164, C-165, C-166, C-167 Proposed (P6b); C-164 amended after the user's decisions (extra evidence ids; STATE-kind record); C-168 Decided (user, option (b)); C-169 Open (a second failure of one kind for one decision adds no second PROMOTION_FAILED event; for the authors); C-38 Decided (prompt); C-44 one sentence. No Accepted row touched.

Contradictions and gaps (each stopped and put to the user, or recorded):

  • The prompt says newstate.py calls TrustedState.promote and also that only commit may: resolved for the second; newstate.promotion_arguments returns the arguments.
  • The prompt's PROMOTION-kind record cannot satisfy trigger T2 (a candidate-bound record must carry the candidate's hash): STATE-kind, user decision.
  • The reverse trigger (version > 0 needs a decision): C-168 (b), user decision.
  • Signatures as built (C-167): commit_promotion takes a factory and a PromotionContext and builds the evaluations itself; record_decision takes the context and the clock; integrity_of(evidence_repository, event); build_proofs takes the binding record id.
  • Doc 06 §28 lists PROMOTION_FAILED without saying when it is appended; it is used for C-38 cases 2 and 3, with ESCALATED added for case 2 (Doc 11 §12).
  • scripts/dev.py:132 still prints "schema 5" during bootstrap; the health readiness line says "schema version 1" (configuration). Both are outside the file list and unchanged: suggestions.

13. What P6c needs from this package

record_decision(uow, outcome, candidate, context, *, now) then commit_promotion(factory, outcome, candidate, impact, verification, policy, evidence_ids, context, *, now, attempt_number=None, oracle_verdicts=()) returning CommitResult(decision, state, duplicate, stale, escalated); PromotionCommitError for infrastructure failure (retry is the caller's, outside the core, Doc 09 §23). The orchestrator must (a) store each verification result with to_evidence_submission(…, parent_state_id=…) and hand over verification_id → evidence_id; (b) keep the impact and verification reports and the policy in memory, or persist them, until the commit (reports are not persisted: R2 of the prompt, a recorded limit for P8); (c) pass the same attempt_number; (d) build the PromotionContext. Ids come from core/promotion/ids.py. tests/promotion_pipeline_commit.py is the helper that does (a) and (d) for the tests only.

14. Open questions and process lapses

For the reviewer: (1) the STATE-kind binding record promotion.committed as the proof carrier (the prompt's risk R1); (2) C-168 (b): "no promoted state without its decision" is enforced by construction and by check_database, not by a trigger; (3) C-169; (4) the factory API (C-167); (5) a revalidation failure also appends ESCALATED and keeps the PROMOTE uncommitted; (6) a record the adapter cannot rebuild counts as a binding problem and so escalates, rather than as infrastructure failure; (7) the loser's own PROMOTE decision is not stored by the commit (only the REJECT with the derived id).

Process lapses, plainly: (a) the binding, idempotency, concurrency, fault, pipeline and property tests were written after the code they test (the step-1 tests, the step-4 tests, the step-5 commit tests and the order test were written first and shown failing, with the red output kept in the scratchpad; for step 3 I kept no red run); for these the evidence that they can fail is the mutation table above, not a red run; (b) a shell lint gate that piped ruff into tail hid its exit status, so commit fab89a8 first contained two E501 docstring lines; I amended that unpushed commit (message unchanged) and gated the rest on the real exit codes; (c) a first mutation run left one survivor and one weak kill, closed by two later commits; (d) one background verification started on the pre-swap tree was killed and restarted.

🤖 Generated with Claude Code

Ayushxsingh100 and others added 17 commits October 29, 2026 16:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ts type (Doc 09 §18, §30; C-162)

The blocking condition of the P6a verdict: a STRUCTURAL-typed PASS with an INVARIANT target id hid
an INVARIANT FAIL, because decide keyed the report's results by target_id alone (PROMOTE).

(1) VerificationReport refuses two results with the same target_id under any two types.
(2) decide looks each required item up by the type its kind requires (INVARIANT, RESOURCE, or
STRUCTURAL for a gate) and examines every result that carries a required target id.
(3) A second result, or one of the wrong type, is an INCONSISTENT_REPORTS REJECT condition, the item
is not proven by any of them, and a FAIL among them stays blocking.

The 15 tests were written first: 13 failed on the unfixed code (the three reproductions gave
PROMOTE). Five scratch-copy mutants (each part of the fix removed) are killed. No existing test
built such a report on purpose: the full suite is unchanged (3739 tests OK, 15 new).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…st for promote and mark_promoted (Doc 09 §26; C-38, C-44, C-162 to C-168)

C-162 to C-167 (Proposed) as written in the work package; C-38 Decided (three cases of a failed
commit); one sentence added to C-44 for schema 6. C-168 (Open) records a conflict found while
reading the code: the schema-6 trigger "a promoted state needs its PROMOTE decision row" would
refuse about 25 direct save_promoted calls in existing persistence tests, while the work package
says no existing test may change except the schema literal. The user is asked before step 4.

The contract test gains a repository-wide check that core.promotion.commit (and core.domain.state)
are the only modules that call TrustedState.promote or mark_promoted; R8 itself is untouched.
The check was written first and failed (NameError) until the checker existed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ing, new-state arguments (Doc 09 §26-§27; DATA-INT-004; C-41, C-45, C-163, C-164, C-168)

PromotionDecision.committed(state_id) and PromotionEvidence.with_new_state(state_id): once, for a
PROMOTE only, hash recomputed, behind a private token so that nothing else can put a state id in
(DATA-INT-004 in the domain). core/promotion/ids.py: the C-163 derived ids under one fixed
namespace. core/promotion/proofs.py: evaluate_from_results and build_proofs turn the P6a proof plan
into InvariantProofs (VERIFIED through the evaluation lifecycle, CARRIED_FORWARD through
carry_forward, which still asks the impact report: C-45); every proof carries the binding record
id (C-164). core/promotion/newstate.py: the checked arguments of TrustedState.promote (C-41); the
call itself will be in commit.py only.

User decisions recorded (29 Oct 2026): C-168 option (b), no reverse trigger; C-164 option (1), an
additive keyword extra_evidence_ids on InvariantProof.carry_forward (core/domain/invariant.py).
The PROMOTABLE -> ESCALATED edge of C-38 is implemented with the schema-6 commit (step 4): the
table is pinned by tests of the database triggers, which change with the schema.

Tests first: the 43 new tests failed with ImportError / TypeError before the code. Full suite 3784
tests OK (45 new).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the PROMOTABLE -> ESCALATED edge (Doc 05 §15, §22, §32; C-38, C-163, C-165, C-168)

promotion_decisions with its triggers (DATA-INT-003: the parent is the candidate's parent; DATA-INT-004:
stored uncommitted, committed_state_id set once, from NULL, for a PROMOTE, to the state that names it
as its commit decision; every other column immutable; no delete or replace) and two unique partial
indexes (one committed decision per candidate, one state per commit decision). The 92 schema-5
statements are byte-identical (golden SHA-256 pinned, taken before the code changed); the candidate
status trigger is dropped and created again in the appended statements with PROMOTABLE -> ESCALATED,
which the domain lifecycle gains in the same commit (C-38). DecisionRepository port, SQLite adapter
(save is idempotent, a conflicting duplicate raises, commit is the only change, reads recompute the
hash), unit-of-work property, three new checkpoint names kept apart from the original four. The
health check lists the new table. No reverse trigger (C-168, decision (b)).

Existing tests changed (each only for the facts above): test_lifecycle (one legal pair),
test_persistence_schema (version literals, the new table, triggers and statement count, one legal
pair, the pair count 10 -> 11, the refused-version list), test_repository_ports (the new port and
property), test_persistence_repositories (version literal; the promotion sequence now includes the
candidate-update checkpoint). The register records C-164's STATE-kind binding record (user decision).

New tests written first: 42, all failed before the code. Full suite 3826 tests OK.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nding (Doc 09 §26, §27, Appendix B; Doc 11 §43; C-38, C-163 to C-167)

record_decision stores every decision and moves the candidate along its exit.
commit_promotion revalidates the decision with decide (C-167), checks that the
evidence binds to the new state (C-164, binding.py), builds the state, the STATE
binding record and the committed decision, then writes them in one transaction
in the order of Appendix B. A lost race stores a REJECT (STALE_PARENT), a
revalidation failure escalates the candidate, and any other failure rolls back
and raises PromotionCommitError (C-38). The tests (37) were written first and
failed with ModuleNotFoundError before core.promotion.commit existed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…service (Doc 11 §44, §45; C-52, C-68, C-164)

integrity_of now also compares the payload's hash with the record's, as
EvidenceService.verify_integrity does; a test shows both give VALID, TAMPERED
and MISSING_PAYLOAD for the same stored records. 30 tests for binding_problems
cover each condition of the VERIFIED and CARRIED_FORWARD checks, the chain from
a promoted parent and the unreadable record.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…9, Appendix B; C-163, C-166)

Sequential and concurrent duplicates, conflicting duplicates, recording twice
(test_promotion_idempotency, 12 tests), and 66 schedules of two promotions
racing from one parent with real threads, two connections and the checkpoint
seam (test_promotion_concurrency, 6 tests): every combination of first runner,
interleaving (overlap, sequential, first fails after the head moved) and
pause point of the seven, plus 24 seeded draws. A database checker
(check_database) reads the file with plain SQL after each schedule.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… Doc 09 §26, §41; C-38)

A failure injected at each of the seven checkpoints, at the STATE_PROMOTED
audit append and at the binding-record append leaves the previous state
current, none of the attempt's rows, the candidate PROMOTABLE and a retry that
makes exactly one state, with the decision recorded before and not. Evidence
that does not bind and reports whose hash no longer recomputes are revalidation
failures that escalate the candidate and change nothing on a second attempt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…real pipeline (Doc 06 §25, §26; Doc 09 §33, §34; C-52, C-164)

The safe repair promotes into v1 (INV-SEC-001 VERIFIED, INV-SEC-003
CARRIED_FORWARD, all PROTECTED, the binding record on every reference and
usable as proof for v1); the regression is REJECTED with v0 still current and a
second candidate from v0 then promotes; the first promotion's v1 is carried
forward into v2 through its binding record; Stateful-Full carries nothing.
tests/promotion_pipeline.py gains keyword parameters (candidate id, sequence,
patch number, parent state, decision id) whose defaults keep the P6a behaviour;
the 17 P6a fixture tests still pass unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s them (Doc 09 §26-§27, §38-§40; C-41, C-163, C-164)

40 seeded chains of decisions (PASS, FAIL, UNKNOWN and ERROR results; commits,
duplicate commits, second records) keep every rule: a non-PROMOTE never changes
the trusted states or the lineage head, a replay writes no row, references cover
the parent's invariants with no lower version, every reference carries a binding
record that is proof for its state, one child per state, one commit per decision.
check_database is shown to fire for each rule on a database that breaks it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the order of the commit writes (Doc 09 Appendix B; Doc 11 §44; C-164)

A record that the adapter reads (its columns agree with its content) but whose
state_hash is not the state's or the candidate's is refused by the binding
check, for a VERIFIED source and for a baseline's carried source. A spy on the
repositories pins the order of the writes: decision, its event, binding record,
state and head, decision committed, candidate PROMOTED, STATE_PROMOTED last.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t is not committed (Doc 09 Appendix B; C-167)

A mutation check showed that comparing the proof plan at revalidation, and not
only the decision's content hash, was not tested: the decision hash does not
cover the evidence ids of a carried reference. The new test decides against a
copy of v0 whose INV-SEC-002 reference names other evidence and shows the
commit escalates the candidate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…c 09 §38-§40; Doc 11 §12, §38; C-163, C-164, C-166, C-167)

Records the API as built against the prompt (factory, context, no evaluations),
the unreadable-evidence rule, the loser's records, and opens C-169: a repeated
failure of the same kind for one decision adds no second PROMOTION_FAILED event.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… (Doc 09 §26; C-163, C-164)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ommitted, as the prompt orders Appendix B (Doc 09 §26, Appendix B; C-163)

The order test was changed first and failed on the previous order (decision
committed, then candidate PROMOTED); the two writes are swapped in
commit_promotion and in its docstring. Both stay in the one transaction, so the
swap is visible only in the order of the checkpoints after_candidate_update and
after_decision_commit, which now follows the order of DECISION_CHECKPOINTS.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ion for one candidate, tested with the triggers gone (Doc 05 §22; C-163, C-165)

A mutation run showed the dropped index was killed only by the test that lists
the index names. The new test drops the triggers the way an attacker with file
access would and shows the index alone refuses the second commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Ayushxsingh100
Ayushxsingh100 merged commit b9b270a 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