Skip to content

Phase 1: notification truth, damaged-record handling, OS lock, full mutation sweep - #1

Merged
QuietFireAI merged 10 commits into
mainfrom
phase1-2026-10-13
Oct 14, 2026
Merged

QuietFireAI merged 10 commits into
mainfrom
phase1-2026-10-13

Conversation

@QuietFireAI

Copy link
Copy Markdown
Owner

Phase 1 of the repair plan (2026-10-13), approved by the owner for merge.

  • 1.1 Notifications are recorded as delivered only when delivered (human.notify_failed otherwise). Each outcome names the entry hash of its escalation; the KPI and the after-action report pair them exactly.
  • 1.2 Damaged record: the first break is named. Writers refuse a file damaged, cut back, edited or replaced under them. A person repairs a cut-off last write in one step (tools/audit_repair.py).
  • Owner decision: an OS lock on <log>.lock (shared for reads on POSIX; reads need no write access).
  • 1.3 Pillars pinned. 1.5 The quickstart works with a relative path. 1.6 CI runs the identity tests for real (no skips) on Python 3.10/3.12/3.13 and Windows. 1.7 22 missing tests added.
  • 1.4 Mutation sweep: 931 mutants, 919 killed, 1 timeout, 11 survived (2 equivalent, 1 CPU-only, 8 on Windows-only lines). See findings/MUTATION_SWEEP_2026-10-13.md.
  • Reviews: three review rounds by a separate agent, plus a recheck before merge. Every finding was reproduced, fixed and tested.

CI run 38488817589: green on all 5 jobs (344 passed with the identity, including Windows).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR


Generated by Claude Code

claude added 10 commits October 13, 2026 17:03
… quickstart, add 22 missing tests

- pyproject.toml: the six pillars pinned to the same commits
  listing-assistants pins (they installed from branch tips).
- tests/test_day3.py, demo/run_p11_demo.py: IDENTITY_DIR made absolute
  before the P11 test changes directory. The README's command
  (IDENTITY_DIR=../listing-assistants) failed one test; it now passes
  (164 passed).
- tests/test_phase1_coverage.py (22 tests): the envelope's required
  fields and provenance.source, a misspelled audience label holding as
  counterparty, the escalation.confidence_underflow queue,
  Hub.arm_signer_registry(), and the WhatsApp notifier.

Not yet done: the re-run of the three mutants these tests are meant to
kill (stopped by the owner before it ran), Phase 1.1 (notification
truth), 1.2 (torn last line), 1.6 (CI), the full mutation sweep, and the
listing-assistants sync. This branch is not merged to main; listing-
assistants is unchanged. Push-log row goes in listing-assistants with
the sync commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
… audit line is named and repairable

1.1 Notification truth (owner decision D3)
- hub._notify records human.notified only when the notifier reports
  sent/delivered/shown. A failed status or a raised exception records
  human.notify_failed (status, http_status, channel, error - never the body).
  A notifier that reports nothing is recorded as transport "not reported".
- escalate() and queue_and_notify() both go through _notify.
- kpi escalation_transport counts confirmed / not_reported / notify_failed.
- after_action prints "NOTIFICATION FAILED ... no person was reached".
- tests/test_notification_truth.py: 16 tests. 7 targeted mutants, 7 killed.

1.2 Damaged or cut-off audit log (owner decision D4)
- AuditLog refuses to open a log with an unreadable line: AuditLogDamaged
  names the line, says nothing was written, and says whether it is a
  cut-off last write (repairable) or damage to investigate.
- verify_chain / check_file name the first bad line instead of raising.
- AuditLog.repair_torn_tail + tools/audit_repair.py (run by a person):
  moves the partial bytes to <log>.torn-<UTC>-<sha12>, cuts back to the
  last complete entry, records audit.tail_quarantined on the chain.
  Refuses, changing nothing, if anything before the last line is damaged
  or the chain breaks.
- A complete last entry missing only its newline opens; the next append
  writes the newline first. CRLF logs check and repair.
- tests/test_audit_log_damage.py: 18 tests. 8 targeted mutants, 8 killed
  (the truncation-length mutant is killed by the interrupted-repair test).

Suite: 198 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
….12/3.13 and Windows

Until now every core CI run skipped the 5 identity-pinned tests in
tests/test_day3.py (no listing identity next to the clone), so green said
nothing about them. New job with-identity clones listing-assistants at main,
sets IDENTITY_DIR, and fails on any skip. Matrix: ubuntu 3.10 (the declared
floor), 3.12, 3.13; windows 3.12 with PYTHONUTF8=0. The original job stays
as the clone-alone path and now prints its skips (-rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
…dows CI)

Core CI run 38437120185, windows-latest 3.12: gap_incidents 2, expected 1.
The test slept 0.01 s then 0.06 s against a 0.01 s cadence (incident above
0.02 s); the Windows 15.6 ms timer tick plus a slow fsync pushed the first
gap over too. A test fault, not a Watchdog fault. heartbeat() is still
checked on a real hub; the gap logic now gets fixed timestamps, including
a gap of exactly 2x cadence (not an incident) and fewer than 2 beats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
- README quickstart: 198 tests with the listing identity (was 137).
- PILLAR_TESTING_MANUAL: 193 passed + 5 skipped alone, 198 with the identity
  (was 132 / 137); says CI runs both. splitvantage's own suite at the pinned
  commit is 14 tests (was stated 12) - each pinned pillar's suite was run
  2026-10-13: 9, 6, 11, 27, 20, 14.
- SKILL.md: 193 + 5 / 198 (was "89 doctrine tests").
README-BUILD.md's counts are a dated build log and stay as written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
…ed, 4 equivalent with evidence

Method (tools/mutation_sweep.py, now in the repo): one AST-span mutant at a
time (comparison, and/or, not, constants, negated tests, return None,
raise -> pass, call -> pass), the full suite with the listing identity
against each, in a copy of the repo, baseline checked first.

First sweep (fb7a04f): 807 mutants, 593 killed, 214 survived. They
clustered where the core had no direct tests: the after-action report,
hub integrity-violation and dead-letter records, the unconfirmed-audience
marker, wildcard and sender/receiver route limits, anchors, the hash
format, disk flushes, module audit records.

90 new tests (tests/test_sweep_*_2026_10_13.py). Code changes:
- loader.py: the routes check was an assert (stripped under python -O,
  an empty track would load); now an explicit check.
- core.py: a dead variable in repair_torn_tail removed.

Final sweep with the packaged tool on this commit: 808 mutants, 804
killed, 4 survived, 0 timeouts. The 4 are equivalent, each with run
evidence in findings/MUTATION_SWEEP_2026-10-13.md (chunk size of a
streamed hash; a route clause subsumed by the line's first test; a flush
on a handle that only truncates; Exception args set before __init__).
One unexplained single failure in run 2 is recorded there; it did not
recur in 25 reruns.

EVIDENCE.md: rows for decline, notification truth, a cut-off record,
pinned pillars, the identity CI job, the sweep.
Counts: 283 passed + 5 skipped alone, 288 with the listing identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
…repair, each escalation paired with its own notification

A separate agent reviewed Phase 1 cold. Its findings, reproduced, fixed:
- A writer that opened an intact log kept appending after the file was
  damaged under it (the running desk did). AuditLog now remembers the
  file's size after its own write; if it differs at the next append it
  re-verifies: damaged, cut back, replaced or deleted -> AuditLogDamaged,
  nothing written; another writer's valid entries -> continue from them
  (no fork).
- The repair cut the log and then appended its record: a crash between
  left a cut log with no record. It now writes the repaired log to a new
  file and swaps it in with os.replace (folder synced on POSIX).
- KPI and after-action matched notifications by queue and time, so a later
  delivery on the same queue covered an earlier failure. kpi.pair_escalations
  pairs each escalation with its own outcome; the report no longer gives a
  transport time to a notifier that reported nothing.
- An odd notifier answer (list status, object values) raised out of the hub
  after escalation.raised was written. Now recorded as notify_failed.
- Hub.notify(queue, record): notify without queueing, outcome recorded
  (listing's decision alerts called the notifier around the hub).
- tools/kpi_gate.py checks the chain first (it crashed on a damaged log
  and passed an edited one).
- CI with-identity uses the listing branch of the same name when it exists.
- A test of mine passed for the wrong reason (its "cut back" case emptied
  the file); fixed, and the same read-after-truncate bug in a new test.

Not fixed (owner decision pending): two processes appending in the very
same instant - only an OS file lock stops that.

Sweep on this commit (tools/mutation_sweep.py): 859 mutants, 855 killed,
1 timeout, 3 equivalent (findings/MUTATION_SWEEP_2026-10-13.md).
309 passed + 5 skipped alone; 314 with the listing identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
…sk saves nothing unrecorded, outcomes name their escalation

Owner decision (2026-10-13): an operating-system lock on <log>.lock
(flock on POSIX, msvcrt.locking on Windows). Every AuditLog read and
write takes it, re-entrant within a process; a writer waits up to
LOCK_WAIT_S (30 s) and then raises AuditLogBusy, writing nothing. The
repair takes it too. Shown by three processes appending 450 entries at
once (fails without the lock).

Second review (same agent re-checking its findings), fixed:
- the change check compared size only; now size and modification time
- AuditLog.ensure_intact(): callers check before writing anything else
  (listing's swarm saved a snapshot the log could not record)
- each notification outcome carries raised_entry = the escalation's
  entry_hash; pairing is exact across processes (older logs by order)
- Windows CI red: a test assumed LF; now built byte for byte
- the repair keeps the log's permissions and owner where allowed, and
  a failed swap removes its side file; tools/audit_repair.py reports a
  busy lock or failed swap instead of a traceback
- AuditLog.reader(path): read-only view (replaces __new__ hacks)
- the repair's docstring said a holder would refuse; it follows the chain

Sweep on this commit: 921 mutants, 908 killed, 1 timeout, 12 survived:
3 equivalent, 1 that only changes CPU use (the lock's retry sleep), 8 on
Windows-only lines a Linux sweep cannot run (findings document).
325 passed + 5 skipped alone; 330 with the listing identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
…le per process, holding()

- Reads take the lock shared on POSIX (readers no longer block each other)
  and need no write access: if <log>.lock cannot be opened for writing a
  read opens it read-only, and if it cannot be opened at all (absent in a
  read-only folder) the read goes ahead unlocked. Before this, checking a
  log on read-only media crashed (third review, L1).
- One lock state per file per process (_PROCESS_HELD), shared by every
  AuditLog instance on that file: two instances in one process never wait
  on each other (L3); threads take turns. A write inside a held read
  upgrades the lock to exclusive.
- AuditLog.holding(): hold the lock across several steps (listing's state
  save: check, write the store, record it). The repair goes through the
  same per-process state.
- Tests: read-only folder (lock file present / absent), a read through a
  read-only lock file still waits for a writer, two instances and two
  threads in one process, readers sharing across processes, the upgrade
  seen from another process, no leaked lock file handles, a repair from
  inside a held lock.

Sweep: run 11 on this code, 942 mutants: 924 killed, 1 timeout, 17
survived; the 5 new ones killed by tests added after (run 12). Result:
929 killed, 1 timeout, 12 survived - 3 equivalent, 1 CPU-only, 8 on
Windows-only lines. 334 passed + 5 skipped alone; 339 with the identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
… escalations, count entries, drop dead route clauses

- Opening a log edited at line 2 and cut off at its end said "cut off, run
  the repair"; the repair then refused because of line 2. AuditLog now
  reports the FIRST break in the file (AuditLog._first_problem), so the
  repair is suggested only when it will accept the file.
- after_action: the run filter added every escalation and notification on
  the log regardless of context, so a report listed other runs'
  escalations as its own (the P11 demo's warm-lead report claimed the
  hot lead's escalation). Now only the run's own; pairing still uses the
  whole log.
- verify_chain()["entries"] counts entries, not line numbers (blank lines).
- Routes.tuple_legal: two clauses that could never change the answer
  removed (proved over every receiver set; one equivalent mutant gone).
- Tests for each, shown to fail on the previous code.

Sweep: run 13 on this code, 931 mutants: 917 killed, 1 timeout, 13
survived; the 2 new ones killed by tests added after (run 14). Result:
919 killed, 1 timeout, 11 survived - 2 equivalent, 1 CPU-only, 8 on
Windows-only lines. 339 passed + 5 skipped alone; 344 with the identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRunDQmfmnZdtJiP78e8WR
@QuietFireAI
QuietFireAI merged commit 6042ad6 into main Oct 14, 2026
5 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.

2 participants