Skip to content

feat(analyze-service): version the recon index + a deterministic autocommit - #361

Open
ZacxDev wants to merge 3 commits into
mainfrom
feat/analyze-service-index-autocommit
Open

feat(analyze-service): version the recon index + a deterministic autocommit#361
ZacxDev wants to merge 3 commits into
mainfrom
feat/analyze-service-index-autocommit

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The defect

~/.claude/analyze-service-index/ — the write-back store of /analyze-service — was 57 KB of curated, unversioned, unbacked-up nuance on a single disk with no history. Measured on the workbench 2026-08-06: 20 files, 56,862 bytes, mtimes running through that day. Not a git repo, not referenced by any .nix, and not synced between hosts (ship.sh rsyncs only ~/.claude/skills/).

It is not re-derivable by re-running recon — it records gotchas, incident tie-ins and pointers that were true at a moment in time. One bad agent Write destroyed it silently and permanently.

What landed

The store itself is now versioned (done out-of-band, not in this diff — it lives outside every repo):

Before 20 files, 56,862 bytes
After 20 files, 56,862 bytes — unchanged, verified by cmp per blob against the baseline commit, with a positive control proving cmp can see a difference
Baseline commit c18ca64 — pristine import, staged as 20 explicit pathspecs
Repo location the scope dir, not the store root — one repo per scope
Remote none, and that is a safety constraint (below)

This diff is the part that keeps it that way: scripts/analyze-service-index/commit.sh plus a systemd --user service+timer in nix/home.nix.

Timer vs. a line in the write-back protocol

I evaluated appending a commit step to analyze-service.md:83 and rejected it:

  • It is the mechanism already measured to fail in this repo — claude/skills/close-the-loop/STATE.md records opt-in prose steps not sticking, which is why that work pivoted to autonomous loops. PRINCIPLES.md: prefer the deterministic fix.
  • It only fires on the write-back path. Hand edits, moves and deletions — and every future writer — would be uncovered.
  • It puts the backup's reliability inside the same agent turn that might be the thing corrupting the store.

The timer costs one unit and covers every writer unconditionally. Daily is adequate rather than lazy: with no remote, a commit only buys recovery from a bad write, and a same-day clobber is still recoverable from the previous day's commit.

🔴 No remote, no push, no network

The scopes hold client-identifying infrastructure detail — public IPs and ports for client hosts, internal hostnames, a named client engineer. So:

  • the script has no push/fetch/clone call site, asserted structurally and behaviourally (a run with a remote configured leaves the far side untouched and creates no remote-tracking refs);
  • the scope README carries the sensitivity notice and points at 60e6d9d, which exists because this class of data already had to be scrubbed retroactively out of this public repo;
  • nothing was scrubbed or redacted — that is a separate decision.

serverMode: deliberately NOT gated

The units beside it (mail-actions-archive, initiatives-sync, ch-regrowth-check) are gated because they need the homelab kubeconfig or a server role. This needs only a local disk and git, so it follows claude-log-rotate — the other ~/.claude maintenance unit that runs everywhere. Gating it would leave the laptop's store unversioned forever while the workbench looked healthy, which is the exact silent gap being closed. Pinned by a test so it cannot be tidied into the mkIf serverMode block next to it.

Safety properties

  • Explicit staging only. Union of on-disk *.md and already-tracked paths, as literal pathspecs. Never -A/--all/.. That one call also covers deletions (git add <deleted-tracked-path> stages the removal — measured), so no -u anywhere either.
  • Clean-tree assertion after committing. Because staging is a filtered allowlist, an unexpected file would otherwise sit uncommitted forever while the unit reported success. It now fails loudly instead → failed unit → notify-failure@ toast.
  • rc checked separately from output everywhere. An empty git status --porcelain is also what a failed status call produces; an empty result cannot distinguish the two mechanisms.
  • Refuses a scope nested inside a foreign repogit rev-parse walks up, and without this it would commit client-sensitive content into whatever encloses the store.
  • Self-bootstrapping — initialises a scope with no repo and seeds an identity when none resolves, because the laptop's store has never been initialised and a systemd unit cannot answer git's "please tell me who you are".

Verification

Gates. nix build .#checks.x86_64-linux.pytests6545 collected / 6544 passed / 1 skipped / 0 failed (the 1 skip is the pre-existing pinned one; my 38 tests add zero skips, as EXPECTED_SKIPS is an exact set). nodetests green.

Mutation sweep — 15 mutations, and one genuinely survived. Disabling the post-commit clean assertion killed no test: every case that left a stray file also staged nothing, so the earlier "nothing staged" guard always fired and the later one never executed (RULES.md → unreachable guards). Added a case that reaches it — a real .md commit plus a surviving non-.md file — then re-swept with three differently-constructed mutants of that guard (never taken / warns-but-returns-0 / filename dropped from the message). All three killed. The other 14 were killed by their intended test.

Live, as a real systemd unit. Ran via systemd-run --user as a Type=oneshot with the PATH nix generated (not my login PATH), script sha256 confirmed identical to the nix store copy:

probe result
dirty store committed, repo at the scope dir, no remote
modification committed
.git/index.lock held rc=1, unit failedgit add failed (rc=128), and the change was not silently dropped
lock cleared recovered on the next run
the real live store (clean) clean no-op, HEAD unmoved, 56,862 bytes intact

Generated .service/.timer read out of the built home-manager generation; timer confirmed wired into timers.target.wants; systemd-analyze calendar confirms the schedule.

Not verified live, stated plainly: the timer firing on its own schedule (read from the generated unit, not waited out), and the real store's dirty-commit path — the live store was exercised only on the clean no-op path, deliberately, rather than fabricating content inside curated client data.

Notes

  • MIN_TESTS 5600 → 5638 (+38). No new HERMETIC_TARGETS entry neededscripts/tests is already a directory target, so the file is collected by the existing one.
  • ⚠ While measuring I found the floor has drifted again: real total is 6545, so 5638 sits ~900 below it — the same failure mode as the old 2850. Recorded in the comment; correcting it needs its own cross-tier measurement and is not in this PR.
  • Not merged, not shipped. ship.sh not run.

🤖 Generated with Claude Code

ZacxDev added a commit that referenced this pull request Aug 7, 2026
…ry, and an unmarked client-sensitive file read as publishable (#362)

* fix(analyze-service): an alias could silently shadow a real index entry, and an unmarked client-sensitive file read as publishable

Four surgical repairs to the `/analyze-service` index schema, from an experiment
that hand-authored a `process` entry and an `org` entry in the current schema to
find where it breaks. The larger "generalize into subsystems" proposal was
REJECTED on that evidence — no `type:`-driven sections, no dependency graph, no
CLI. Spec/prose only; no code, no behaviour outside the command.

1. Addressing — the shadowing bug. Two defects were conflated. A slug can name
   two different KINDS of thing (`devrc/repo-cos` is both a code subsystem and
   the weekly ritual about it), and the old resolver matched filename *and*
   `aliases:` in one breath — so an alias silently won and a later real entry
   became unreachable, with no error. Now: an optional `<slug>.<kind>.md`
   filename used ONLY when disambiguation is needed; a two-tier resolver where
   an alias can never outrank a filename match; and >1 hit in a tier is a hard
   ambiguity ERROR that lists candidates instead of picking one. Bare
   `<slug>.md` resolves exactly as before — backward compatibility is stated as
   a hard requirement, and none of the 20 existing files is renamed or resolves
   differently.

   Also folds `_` -> `-` during normalization (the old rule kept `_`, so
   `image_ingestion` != `image-ingestion` while the index links `_`-spelled
   MEMORY.md slugs), keeping the pre-fold spelling in `aliases:`.

2. Sensitivity marking. New `sensitivity:` front-matter field,
   `client-confidential | personal | public`, with a FAIL-SAFE default: absent
   or unrecognized means client-confidential, never public. Not hypothetical —
   live entries carry a client bastion's public IP and SSH port, client
   hostnames, and a named client engineer. The no-remote / never-copy-into-a-
   public-repo rule is written down alongside it, pointing at 60e6d9d, which
   exists because this exact data class had to be scrubbed retroactively.
   Marking only; enforcement lands separately.

3. Front-matter repair. `repo:` -> `scope:`, and a scope may be a non-repo word
   (a ritual is owned by no repo; a client spans several). `<repo-slug>`
   derivation stays the default so nothing breaks; older files carrying `repo:`
   still read as `scope`. `namespace:` becomes OPTIONAL rather than removed —
   genuinely load-bearing for k8s infra, meaningless for the non-infra entries
   that were writing `n/a`.

4. Process-liveness convention. "Is this still being followed?" is THE question
   for a process subsystem, but the anti-bloat rule forbids persisting live
   status — correctly, for infra, where a kubectl is two seconds away. For a
   ritual there is no live probe. Convention: persist the DERIVATION METHOD and
   the stale-tell, never the current reading. Written as an application of the
   existing rule, not an exception to it.

Byte budget: 12,460 -> 15,358 (+2,898). Offsetting evictions in the same commit
— the recon-start index-read paragraph was stated twice nearly verbatim (kept
the operational copy in step 1), the "Always re-derived live" tags on Lives
at/Config duplicated the Provenance-honesty line below them, and the
"Pointers, not copies" bullet restated the schema it sits under.

Verification: the public-IP and client-hostname gates pass on this file, and
were negative-controlled — injecting a routable IP into it makes
test_no_public_ips fail with this exact path, so the green is a real read of
this file rather than a scan that misses it. The full pytest suite fails
identically (same 26, same names) at this branch and at the untouched base
checkout, so nothing here is a regression; those 26 are ad-hoc-nix-shell
toolchain gaps, not code.

Scope note: deliberately does NOT touch scripts/analyze-service-index/,
nix/home.nix or scripts/claude-hooks/ — PR #361 owns those and this file is
not in its diff, so the two do not overlap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(analyze-service): the store is a git repo now — "there is no commit step" invited destructive git into an irreplaceable, unbacked-up store

`claude/commands/analyze-service.md` is the protocol an agent reads *before* writing
to `~/.claude/analyze-service-index/`. That store is now one git repo per scope
(verified: `<scope>/` is a repo on `trunk`, no remote; the store root is not a repo).
Three statements in the file predated that. Two were reworded earlier in this PR; the
dangerous one survived:

  - "The write is local and final; there is no commit/worktree step (the file is
     outside every repo)."
  + "The write is local and final — nothing leaves the machine: no remote, no push.
     But it is not outside git; committing the scope repo is the store's own concern
     (an out-of-band autocommit), never this command's, so write the file and run no
     git command (Store safety)."

An agent told "outside every repo" has no reason to check for one before reaching for
the stash stack, a hard reset, a clean, or a checkout-- inside a store that is curated,
irreplaceable, unbacked-up and client-sensitive. `refs/stash` is repo-global and
concurrent sessions share it. Per RULES.md, a comment is a claim too — a false safety
comment is what leads a maintainer to delete the guard it describes.

Also fixed, same staleness class, found by sweeping the rest of the file:
- The earlier rewording ("outside every repo you work in" / "never lands inside a
  cluster repo or devrc") was true but still only said where the store ISN'T, so it
  left the same inference intact. The Location bullet now states positively that each
  scope is its own remote-less git repo.
- "Lazy — nothing pre-created. ~/.claude/analyze-service-index/ may not exist" — the
  store root exists today; scoped to "a scope dir or service file may not exist yet".

Adds a Store safety block mirroring the scope README's wording (the four destructive
git operations, no remote, no push, no copying into a public repo) so the two agree
rather than drift.

Wording is deliberately true both before and after the autocommit timer lands — that
timer is NOT deployed yet (`analyze-service-index-commit.timer` does not exist on this
host), so the file asserts a design, not a deployed state.

Byte discipline: this file loads on every run and had grown +2,898 (+23%) in this PR.
Net this round: 15,358 -> 15,349 (-9). Paid for by evicting the "NOT notable" list
(a verbatim restatement of "Bloat discipline" — one rule, one place), folding the
sensitivity bullet's handling rules into the new Store safety block, and compressing
prose without dropping a rule.

Nix-managed: takes effect only after `home-manager switch`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev and others added 3 commits August 6, 2026 23:49
…commit

`~/.claude/analyze-service-index/` is the write-back store of /analyze-service.
Measured 2026-08-06 on the workbench: 20 files, 56,862 bytes, mtimes running
through that day — with NO git history, NO backup, and NO host sync (ship.sh
rsyncs only ~/.claude/skills/). Nothing in any .nix referenced it. The content is
curated and hand-confirmed: gotchas, incident tie-ins and pointers that were true
at a moment in time, so it is NOT re-derivable by re-running recon. One bad agent
Write destroyed it silently and permanently.

The store itself is now a git repo (done out-of-band; local history only, no
remote — see below). This commit is the part that keeps it that way.

WHY A TIMER, NOT A LINE IN THE WRITE-BACK PROTOCOL
--------------------------------------------------
The store is written by an agent's Write tool mid-recon, so no git operation
happens naturally. The obvious alternative — appending "then commit" to
analyze-service.md's write-back protocol — is the exact mechanism already
MEASURED to fail here: claude/skills/close-the-loop/STATE.md records opt-in prose
steps not sticking, which is why that work pivoted to autonomous loops. A backup
that depends on an agent remembering is not a backup. PRINCIPLES.md: prefer the
deterministic fix. The prose step also only fires on the write-back path, and
would silently cover none of the hand edits, moves or deletions.

ONE REPO PER SCOPE
------------------
The store root is deliberately NOT a repo; each `<scope>/` under it is its own
independent repository, so a future scope can carry its own policy. A store-root
repo would silently absorb every scope added later. The committer therefore walks
scope directories and commits each independently — one failing scope does not
cost the others their backup, but it does fail the run.

🔴 NO REMOTE, NO PUSH, NO NETWORK
---------------------------------
The scopes hold client-identifying infrastructure detail (public IPs and ports
for client hosts, internal hostnames, a named client engineer). The script never
adds a remote, never pushes and never fetches, and a test asserts no such call
site exists AND that a run with a remote configured still touches nothing on the
far side. cf. 60e6d9d, which exists because this class of data already had to be
scrubbed retroactively out of this public repo. Nothing was scrubbed or redacted
here — that is a separate decision.

DESIGN NOTES
------------
* Staging is explicit: the union of on-disk `*.md` and already-tracked paths,
  passed as literal pathspecs. Never `git add -A`/`--all`/`.` (RULES.md). That
  one call also covers deletions — `git add <deleted-tracked-path>` stages the
  removal (measured), so no `-u` and no `-A` anywhere.
* Because staging is a filtered allowlist, the tree is asserted CLEAN after
  committing and the run FAILS if it is not. An unexpected file becomes a failed
  unit (and a notify-failure@ toast) rather than being blind-staged or silently
  left behind.
* Exit codes are checked separately from output everywhere: an empty
  `git status --porcelain` is also what a FAILED status call produces, and an
  empty result cannot distinguish the two mechanisms (RULES.md).
* Self-bootstrapping — it initialises a scope that has no repo and seeds a git
  identity when none resolves, because the laptop's store has never been
  initialised and a systemd unit cannot answer "please tell me who you are".
* NOT gated on serverMode, deliberately, and pinned by a test. The gated units
  beside it need the homelab kubeconfig or a server role; this needs only a local
  disk and git. It follows claude-log-rotate, the other ~/.claude maintenance unit
  that runs everywhere. Gating it would leave the laptop's store unversioned
  forever while the workbench looked healthy — the exact gap being closed.

VERIFIED
--------
* 38 new tests, zero skips. `nix build .#checks.x86_64-linux.pytests`: 6545
  collected / 6544 passed / 1 skipped (the pre-existing pinned skip) / 0 failed.
  nodetests green too.
* Mutation swept, 15 mutations. 14 killed on the first pass; ONE SURVIVED — the
  post-commit clean assertion, because every case that left a stray file also
  staged nothing, so the earlier guard always won and the later one never
  executed (RULES.md → unreachable guards). Added a case that reaches it, then
  re-swept with three differently-constructed mutants of that guard; all three
  now killed.
* Ran as a REAL systemd --user oneshot with the nix-generated PATH: committed a
  dirty store, committed a modification, FAILED loudly with .git/index.lock held
  (negative control), recovered on the next run, and was a clean no-op against
  the live store. The generated unit + timer were read out of the built
  home-manager generation; `systemd-analyze calendar` confirms the schedule.

MIN_TESTS 5600 -> 5638 (+38). No new HERMETIC_TARGETS entry needed — scripts/tests
is already a directory target. The comment also records that the floor has drifted
~900 below the real total again; correcting that is a separate change.

NOT verified live: the timer firing on its own schedule (read from the generated
unit, not waited out), and the real store's dirty-COMMIT path — the live store was
exercised only on the clean no-op path, deliberately, rather than fabricating
content inside curated client data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…list

An adversarial audit built its mutants differently from the author's sweep and
11 of 27 survived the 38 tests. Reproduced independently here with a third,
differently-constructed sweep (whole-block substitution, obfuscation, and
exfiltration mutants across all three surfaces): 14 of 26 valid mutants survived
the pre-fix tree.

🔴 RETRACTED — this commit's original subject and BLOCKING entry claimed the
timer "had no Install.WantedBy … the timer NEVER FIRED — the whole feature was a
no-op". That is FALSE and the message has been rewritten to remove it. VERIFIED:
`git show fc1eede:nix/home.nix` already contains
`Install = { WantedBy = [ "timers.target" ]; };`, and the entire nix/home.nix
delta between fc1eede and this commit is 14 lines, ALL of them comments
(11 insertions / 3 deletions, one comment block about the daily cadence). The
true and much weaker claim is the one below.

SIGNIFICANT
  * The Install block had NO TEST. Deleting it passed all 38 tests, so the
    timer's enablement was an unguarded invariant — a surviving mutant, not a
    live defect. Now pinned by
    test_the_timer_is_actually_installed_into_timers_target, and verified at the
    artifact level: `xdg.configFile` contains
    systemd/user/timers.target.wants/analyze-service-index-commit.timer, and
    that entry disappears when the block is deleted.
  * The no-exfiltration guard enumerated five forbidden git subcommands. It is
    now an ASSERTED LEDGER — the set of git subcommands the script invokes must
    EQUAL a named set, failing when it grows *or* shrinks, plus a
    variable-supplied subcommand is a violation on its own. `git archive`,
    `git bundle` and `_s=pu"sh"; git $_s origin …` had all walked past the
    blocklist. Backed by a behavioural test that snapshots a sandboxed HOME and
    asserts a run writes nothing outside $STORE.
    ⚠ A plain `cp -r` of a scope is NOT caught by this and never can be — it
    invokes no git at all. The follow-up commit says so and adds the control
    that does stop it.
  * test_a_configured_remote_is_never_pushed_to was a vacuous zero: it pointed
    origin at a nonexistent path, so neither assertion could ever move. Measured:
    it killed ZERO mutants, including a literal `git push origin trunk`. The far
    side is now a real bare repo, with a positive control proving a push there
    does register.
  * `2>&1` folded stderr into the dirty-state predicate. Reproduced: an
    unreadable subdirectory makes `git status` exit 0 while warning, so a CLEAN
    tree read as dirty, failed the unit naming the wrong cause, inflated
    n_changed, and embedded the warning in the commit message. stdout and stderr
    are now captured separately; the warning is surfaced, never treated as data.
  * The post-commit guard misdiagnosed the one race the design guarantees. A
    benign .md write landing between `git add` and `git commit` produced rc=1 and
    "Something is not covered by the *.md allowlist" about a file that IS
    covered. The leftover set is now partitioned: covered-or-tracked paths mean
    the tree was re-dirtied (retried once, then a warning), and rc=1 is reserved
    for genuinely uncovered paths.
  * MIN_TESTS was 5638 against a real total of 6545 — 907 of slack, more than the
    entire 783-test initiatives suite could vanish through. Raised to the
    measured total. (Re-measured again in the follow-up commit, on the MERGED
    tree — this number was taken on a branch two commits behind main.)
  * The serverMode pin grepped for the token `mkIf`, which
    `= lib.optionalAttrs serverMode {` sails past while producing exactly the
    gating the test forbids. Now asserted positively: `= {` with nothing between.

MINOR
  * The "no candidate paths" guard had zero coverage (reachable, now covered).
  * Scope enumeration was neither newline- nor symlink-safe and failed SILENTLY
    (exit 0, "nothing to do") — the exact outcome this work exists to prevent.
    A newline in a scope name split it into two phantom scopes; a symlinked scope
    was not enumerated at all. Both left content unversioned while reporting
    success. Now NUL-delimited, and find's rc is checked in the *.md probe.
    ⚠ The `-L` this added to the CANDIDATE walk is a live regression; the
    follow-up commit reverts it to `-H` and explains why.
  * `--print-plan` was honoured only as $1, so `commit.sh <STORE> --print-plan`
    silently COMMITTED (measured: it initialised a repo and wrote a commit). Real
    argument loop; unknown options and extra operands are now errors.
  * `desc` was a loop global with no `*)` default, so an unexpected state reused
    the previous scope's description. Reset per iteration, default added.
  * Newly covered: the README.md exemption, `git init -b`, the commit.gpgsign
    pin (including a behavioural case with signing forced on globally), and
    TimeoutStartSec.
  * The daily-cadence rationale was stated wider than it holds — content created
    AND destroyed between two 03:30 runs is unrecoverable, which is the
    motivating scenario. Scoped to what it actually buys.

Also fixed a self-inflicted two-tier defect caught by the authoritative gate:
_race_hook wrote its own `#!/usr/bin/env bash`, which works on the dev host and
does not exist in the nix sandbox. It now uses testlib.mockbin.write_exec, the
repo's one-place rule for this (test_runtime_shebangs.py enforces it).

Tests 38 -> 59 in this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g it — the guard was bypassable without editing the script

Third fix round. Rounds 1 and 2 each tried to make exfiltration *visible* to a
static reading of commit.sh, and each was evaded a new way; round 2 also shipped
a live regression. So this round changes the control rather than adding a fourth
layer of patterns.

🔴 THE HOLE THAT NEEDED NO EDIT TO commit.sh AT ALL
Reproduced end to end (git 2.55.0), before fixing: a global
`[core] hooksPath = <dir>` holding a post-commit hook copied a scope's
client-identifying content clean out of the store, while the script printed
"committed <sha>", "ok — 1 scope(s) processed" and exited 0. `init.templateDir`
is the same attack one level earlier — it bakes the hook into the repository this
script itself bootstraps. No static ledger could ever have seen either: the
payload was in ~/.gitconfig.
  Fixed WITHOUT enumerating dangerous keys (that is the mistake the ledger
  already made twice): GIT_CONFIG_NOSYSTEM=1 plus GIT_CONFIG_GLOBAL=/dev/null
  removes the entire ambient surface, every key. Repo-local `.git/hooks` — which
  an errant agent Write can plant, squarely in this unit's threat model — is
  closed by pinning core.hooksPath at an empty directory via GIT_CONFIG_COUNT
  (measured to override even a repo-local setting).

🔴 CONTAINMENT IS NOW THE PRIMARY CONTROL
The unit gains ProtectSystem=strict, ProtectHome=tmpfs, BindPaths (the store,
the ONE writable path), BindReadOnlyPaths (the script), PrivateTmp,
PrivateNetwork and NoNewPrivileges. VERIFIED LIVE on the workbench AND the
laptop, running the committer under the EXACT directives and PATH home-manager
renders, against a sandbox store, results read from the journal:
  * the committer works normally (two scopes, one with a newline in its name)
    and its commits persist; a second run is a clean no-op;
  * `cp -r <scope> <dir>` → "Read-only file system", rc=1, nothing lands
    (uncontained positive control: rc=0, 27 files copied);
  * bash's BUILTIN `/dev/tcp` egress → "Network is unreachable", rc=1, against
    rc=0 uncontained. No PATH restriction can reach that one — it needs no binary.
⚠ Stated plainly in the code because it reads like a control and is not: the
unit PATH is NOT containment. `cp` and `tee` are in pkgs.coreutils, which this
script genuinely needs, so no honest PATH removes them. What makes `cp` useless
is having nowhere to write.

REQUIRED FIXES
  * `find -L` in list_candidates was a LIVE REGRESSION from round 2. It descends
    symlinked subdirectories and emits paths beyond them; git refuses those
    ("pathspec 'linkdir/inner.md' is beyond a symbolic link"), so `git add` exited
    128. MEASURED: byte-identical failure on the next run — it never self-heals —
    with `ls-files` EMPTY, i.e. the scope's real index files left unversioned for
    as long as the symlink existed. `-L` is now on list_scopes ONLY (where
    -maxdepth 1 makes it safe); the candidate walk and the *.md probe use `-H`,
    which is all the symlinked-SCOPE case ever needed. Both are covered, and the
    probe/walk seam is covered too — mutating the probe alone moved nothing.
  * find's rc in list_candidates was discarded by a pipe into `sort -zu`.
    Enumeration now goes to files (which also stops `$(…)` eating the NUL
    framing). An incomplete enumeration is RECORDED, not aborted on: everything
    readable is still committed — refusing outright would let a bad `sub/` block
    `alpha.md` from ever being backed up — and the scope then fails loudly.
  * `_CMD_CHARS` gains `{` and `\`.
  * The wrapper class is closed at the root, not by listing wrappers. An
    unplaceable `git` used to `continue`; MEASURED, that single line made `env`,
    `timeout`, `nohup`, `command`, `eval "…"`, `xargs`, `sudo` and `{` all reach
    `git push` with the ledger reporting nothing. It is now a violation unless
    pinned in an exact prose ledger — which is 3 lines, because commit.sh's
    fourteen "git <verb> failed" strings were consolidated into one helper.
  * The remote test only exercised the dirty path; a clean-path variant is added.
  * Nothing asserted ExecStart is the only Exec* directive — a one-line
    `ExecStartPost = rsync …` exfiltrated the whole store daily with no test
    moving. The Service block's directive key set is now pinned EXACTLY.
  * A `*.md` pattern in a scope .gitignore made the unit permanently red. Now
    pre-filtered through `git check-ignore` and reported as its own named error,
    index untouched. (The "half-staged index" also reported did NOT reproduce:
    `git add` validates every pathspec before staging any of it. Not claimed.)
  * Two false claims corrected. commit.sh's "an allowlist has no such blind spot"
    was false — `cp -r` invokes no git and is invisible to it, permanently; the
    comment now says so and points at the control that does stop it. The previous
    commit's "the timer NEVER FIRED" claim is retracted in its own message.
  * MIN_TESTS re-measured on the MERGED tree: 6595 (was 6566, taken on a branch
    two commits behind main — #363 added 13 tests, this branch adds 16). Floor is
    live: raised to 99999 it fails with its own message, "only 6595 tests were
    collected".

ALSO
  * The exit-0 race branch is pinned — rc=0 AND the specific warning AND a new
    greppable ASI-RACE-UNSETTLED marker, so a chronically-racing scope is not
    invisible behind a zero exit.
  * test_an_identity_is_seeded_when_git_cannot_resolve_one was green in the
    sandbox and red on a host with a system gitconfig. Fixed in the PRODUCT, not
    the fixture: the script ignores system and global config, so the test now
    passes no config environment at all.
  * The unit PATH pin covers gnugrep/gnused, both of which the script uses.
  * `_race_hook` could no longer work (hooks are now disabled by design) and
    would have gone green measuring nothing. Replaced by a PATH `git` shim, which
    is also the more honest stand-in — the race is a concurrent WRITER, not a hook.
  * test_a_run_writes_nothing_outside_the_store now sandboxes TMPDIR: a sweep
    mutant writing to ${TMPDIR}/leak was NOT caught, because the snapshot only
    ever covered its own sandbox. Its remaining limit (a hardcoded absolute path
    is not enumerable) is now stated rather than implied.

A systemd .path unit was evaluated and deliberately NOT implemented — see the
report: scope directories are runtime data, there is no glob form of
PathModified/PathChanged, and a watch on the store root does not see writes one
level down.

Mutation sweep, third construction (single-atom edits + additive insertions,
across BOTH surfaces — previous rounds used whole-block substitution and
predicate inversion): 37 mutants, 35 killed each by ITS OWN test, verified by
failing-test name. Both harness controls hold: a no-op mutant survives (it can
report survival) and a hardcoded-path exfil mutant survives BY DESIGN, which is
the residual the containment closes and the static tier cannot.
Tests 59 -> 75 in this file.
nix build .#checks.x86_64-linux.pytests: collected=6595 passed=6594 skipped=1
failed=0, floor 6595, RESULT: PASS. nodetests: tests=1024 pass=1024, RESULT: PASS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev force-pushed the feat/analyze-service-index-autocommit branch from 51cc025 to a5e1f98 Compare August 7, 2026 05:45
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