Skip to content

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

Merged
ZacxDev merged 2 commits into
mainfrom
feat/analyze-service-schema-repairs
Aug 7, 2026
Merged

fix(analyze-service): an alias could silently shadow a real index entry, and an unmarked client-sensitive file read as publishable#362
ZacxDev merged 2 commits into
mainfrom
feat/analyze-service-schema-repairs

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Four surgical repairs to the /analyze-service index schema, found by an experiment that hand-authored a process entry and an org entry in the current schema to see where it breaks. The larger "generalize into subsystems" proposal was rejected on that evidence — this PR adds no type:-driven sections, no dependency graph, no CLI. Prose/spec only, one file.

Deliberately does not touch scripts/analyze-service-index/, nix/home.nix or scripts/claude-hooks/#361 owns those, and claude/commands/analyze-service.md is not in its diff. Zero overlap.

1. Addressing — the shadowing bug

Two defects were conflated:

  • A slug can collide between different kinds of thing: devrc/repo-cos names both a code subsystem and the weekly ritual about it.
  • Worse, the old resolver matched filename and aliases: in one breath before declaring a miss — so an alias silently won and a later real entry became unreachable, with no error. Live example: bastion.md claims the alias ssh; a future real ssh.md would have been shadowed by it.

Fix:

  • Optional kind-qualified filename <slug>.<kind>.md (repo-cos.process.md), kind ∈ service | process | org | doc, used only when disambiguation is needed. A trailing dot-segment counts as a kind only if it is in that enum, so a dotted slug is unaffected.
  • Two-tier resolution, ambiguity is an ERROR. Tier 1 = filename (bare and qualified, together); tier 2 = aliases, consulted only when tier 1 returned zero hits. An alias can never outrank a filename. More than one hit in a tier → stop, call it ambiguous, list the candidates, let the human choose. Never pick.
  • _- during normalization, keeping the pre-fold spelling in aliases:. The old rule (any char outside [a-z0-9._-]-) preserved _, so image_ingestionimage-ingestion — while the index links _-spelled MEMORY.md slugs throughout.

Backward compatibility is a hard requirement and is stated as one. None of the 20 existing files needs renaming, and none resolves differently:

  • No existing filename contains _, so the fold changes nothing about what they match — image-ingestion.md, spine-controller.md, civitai-dp-prod.md resolve exactly as before.
  • No existing filename has a second dot, so nothing is retroactively parsed as kind-qualified.
  • With zero qualified files in a scope, tier 1 can only ever return 0 or 1 hit — bit-identical to today's behaviour.
  • Alias resolution still works and now works better: pghero.md carries aliases: [pg-hero, pg_hero]; both fold to pg-hero, both still resolve, and pg_hero now also matches when typed against a --spelled entry.

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, and public is a deliberate operator claim a recon run may never infer.

This is live, not hypothetical: bastion.md:10 carries a client bastion's public IP and SSH port, orchestration.md names a client engineer by handle, and several aliases: lists carry client hostnames. The spec now writes down that the store must never gain a git remote and that none of its content may be copied into devrc (PUBLIC) or any other public repo, PR body, issue, gist or commit message — pointing at 60e6d9d, which exists because this exact data class had to be scrubbed retroactively.

Marking only. Enforcement lands separately, on top of #361.

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 still carrying repo: read as scope.
  • namespace: becomes optional rather than removed — genuinely load-bearing for k8s infra entries, and meaningless for the non-infra test entries that were reduced to writing n/a. Omit it rather than 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. From the experiment — liveness of a weekly ritual was derived from an exclusions-file mtime vs. the timer's last fire; that method is the durable knowledge, and the answer it returned ("last followed 2026-08-01") is live status exactly like a pod count and stays forbidden. Written as an application of the existing rule, nested under it, not an exception to it.

Byte budget

12,460 → 15,358 (+2,898). Evictions taken in the same commit to offset:

  • the recon-start index-read paragraph was stated twice, near-verbatim — kept the operational copy in step 1;
  • "Always re-derived live" on Lives at / Config duplicated the Provenance-honesty line three lines below them;
  • the "Pointers, not copies" bloat bullet restated the ## Pointers schema it sits under.

Verification

  • test_no_public_ips.py + test_no_client_hostnames.py: 33 passed on this branch.
  • Negative control on that gate — injecting a routable IP into this file makes test_no_public_ips fail naming this exact path and line, so the green is a real read of the changed file, not a scan that misses it. Reverted; file restored byte-identical.
  • Full scripts/tests suite: 26 failed / 1388 passed on this branch and the identical 26 failures, by name, at the untouched base checkout. They are ad-hoc-nix-shell toolchain gaps (no logrotate on PATH, no opencode engine, run-tests preconditions harness), not regressions from this change.
  • No new files, so nothing can be silently omitted from the flake deploy.

Takes effect only after a home-manager switch — this path is a read-only nix-store symlink at ~/.claude/commands/, and it also ships to opencode.

🤖 Generated with Claude Code

ZacxDev and others added 2 commits August 6, 2026 22:00
…ry, 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>
…it 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>
@ZacxDev
ZacxDev merged commit d45b8dc into main Aug 7, 2026
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