Skip to content

content-filter

Provenance

  • Source: .spec/spexcode/spec-cli/footprint/residence/content-filter/spec.md
  • Source SHA-256: 2d352f74d6fcd8586faf4591ee561e295164fee0e685917b449cde77570f045c

raw source

A mixed-content file is the one place exclude cannot reach: the filter chain lives on git's TRACKED pipeline (checkout smudges, stage/diff cleans), the ignore family on the untracked namespace, and a contract file that carries BOTH host prose and our block needs the tracked-pipeline tool — gitignoring a tracked file is a no-op, and the folded-in block would otherwise ride the file into every teammate's diff. The filter keeps the two contents on their own sides of the index: the repo stores the HOST's pristine prose (clean strips our sentinel block on stage/diff), the WORKING TREE carries prose + block (smudge re-injects on checkout), and history never sees the block.

expanded spec

The sentinel markers (<!-- spexcode:start/end -->) are the load-bearing anchor: clean strips exactly the marked block, smudge (re)injects it, and the invariant is clean(smudge(x)) == x — for text ending in one newline, git's own well-formed shape; a 0-or-2+-newline tail normalizes once on the first round-trip, then stays stable. Smudge strips defensively before injecting, so a block that somehow reached the index can never double-inject.

The driver is PER-CLONE, while its payload is PER-TREE. One stable git config filter.spexcode.smudge/clean and a managed .git/info/attributes binding cover the closed materialized text paths. At invocation the driver asks Git for the current toplevel, resolves that tree's existing runtime slot, and selects the exact %f payload there; no payload means identity. Thus two linked worktrees can carry different contracts and ignore blocks without extensions.worktreeConfig, without replacing the user's global excludes, and without a last-writer common contract-block.md.

Where it is planted — mixed content, present or imminent. The kind detection ([[residence]]) binds the filter for a contract file that is host-TRACKED, and PRE-ARMS it for an untracked contract file the user's own prose has entered: arming costs nothing while the file stays untracked (no pipeline events fire), and it makes the user's eventual git add — through any route, -p included; the block never even appears in a hunk, since add/diff compare clean(worktree) against the index — strip the block automatically. A wholly-ours contract file gets no filter: exclude is the weakest sufficient tool there.

Three field-verified edges the mechanism must hold:

  1. Graceful degradation. The configured filter command points at the STABLE shim path and tests it before exec, degrading to cat (identity) when the shim is missing — a bare missing filter command makes git spray fatals on EVERY operation touching the file.
  2. No self-propagation + the phantom-M. git re-smudges only on checkout, so a changed contract does not propagate by itself: the re-materialize writes the managed block straight into the working file (the block write IS the re-smudge) and refreshes the shim's block-content file. And a filtered path can never be verified by stat alone, so git status reports it modified FOREVER without content-checking (while git diff runs the filter and shows nothing) — settled by a content-GUARDED git add --renormalize: run only when the cleaned worktree already equals the index blob, a pure stat refresh that can never stage a user's real unstaged edit (a genuine edit keeps its honest M).
  3. Ordered unplant. Strip the block from the working files FIRST, then remove its tree payload; remove the common attributes/config/shim only after no registered tree payload needs them — the reverse order leaves the block exposed as an uncommitted modification the moment the clean filter disappears ([[harness-delivery]]'s erase order honors this).

The filter guards the ADD path; the index a commit is actually built from is re-checked once more at the last gate by [[commit-surgery]] (a blob staged before the filter existed — a -f, a pre-arming edit — is cleaned there in place). Between the two, no route into history carries the block.

JSON mixed content is NOT implemented — the designed successor, recorded here so nobody re-derives it: a host-tracked settings.json is answered by REDUCING THE DIMENSION, not by merging — claude's settings.local.json turns the mixed-content problem into a whole-file machine fact — with an identity stamp on any entry we'd ever have to co-own inside a shared JSON. No smudge/clean for JSON until a real host forces it.