# Tensor Grep Change Control

> Use when about to change, review, merge, or release ANY code in tensor-grep — adding a tg command or search flag, touching a backend/router/pipeline, editing CI/release/docs contracts, merging a PR, claiming a fix or speedup is done, or deciding whether a follow-up commit is truly "docs-only"/"comment-only". Encodes the non-negotiable gates (draft-PR-only autonomy, never-trust-a-self-report, no-speed-claim-without-numbers, experimental-until-proven, TDD-first, smallest-change, benchmark-hot-paths, the 4 registration sites, one-merge-per-tick / the push-race, dogfood-the-real-binary, contract-changes-need-validator-tests, a test proving nothing until seen fail on the pre-fix baseline, gating comments/docstrings with the same rigor as code, diff-review-is-not-measurement-review, and the `ast.dump()` behavior-neutral proof technique) and the historical incident behind each.

- Skill: `oimiragieo/tensor-grep-change-control` (Agent Skill)
- Install (CLI): `npx skillmds@latest add oimiragieo/tensor-grep-change-control`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/tensor-grep-change-control/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oimiragieo/tensor-grep-change-control

---


# tensor-grep change control

This is the **gate-and-discipline runbook** for changing `tensor-grep` (the `tg` CLI). It answers: *what must be true before a change is allowed to land, and why.* Every rule here was written in blood — each traces to a real incident that shipped a bug, blocked a release, or wasted CI cycles. Read it before you edit, merge, or claim "done."

`tensor-grep` is described in its own docs as a **benchmark-governed, contract-heavy codebase** (`CONTRIBUTING.md`, `AGENTS.md:15`). "Contract-heavy" means many behaviors are pinned by tests that fail if you drift; "benchmark-governed" means speed claims are gated by measured numbers, not review opinion. Do not optimize by guesswork.

## Who this is for

Two readers at once — write and act to the **lower bound** of each:

- A **Sonnet-class AI** in a cheap autonomous session: you need copy-pasteable commands and hard guardrails so you cannot silently skip a gate.
- A **mid-level human engineer** with zero repo context: you need the *why* and the domain theory so the rule makes sense and you apply it to new cases.

## When to use this skill vs a sibling

| Your task | Use |
|---|---|
| About to edit/merge/release; "is this allowed? what gate applies?" | **this skill** |
| Actually *using* `tg` to navigate a repo (search/defs/callers/orient) | `tensor-grep` (the usage skill) or `code-search-and-retrieval-reference` |
| A `tg` flag/env-var reference | `tensor-grep-config-and-flags` |
| A bug/test-failure to diagnose | `tensor-grep-debugging-playbook` (+ `superpowers:systematic-debugging`) |
| Deep detail on a past incident | `tensor-grep-failure-archaeology` |
| How the internals/contracts are wired | `tensor-grep-architecture-contract` |
| Build / toolchain / env setup | `tensor-grep-build-and-env` |
| Running a benchmark or proving a speed claim | `tensor-grep-benchmark-and-proof-toolkit` |
| Release mechanics / positioning depth | `tensor-grep-release-and-positioning` |
| Validation-suite / CI-gate detail | `tensor-grep-validation-and-qa` |

**No skill routes around change-control.** If a sibling seems to let you skip a gate here, the sibling is wrong — stop and reconcile.

---

## Part 1 — The seven UNWRITTEN non-negotiables

These are not in a config file; they are CEO-confirmed law. Breaking one is a process failure even if the code is clean.

### 1. Autonomy is draft-PR-only

**Rule:** Never auto-merge, never admin-merge, never auto-restart a service unattended. Every self-acting behavior ships **default-OFF** and graduates only via: council-verify → dry-run (preview what it would do on real data) → a **conscious flag-flip** by a human. The endpoint of any autonomous fan-out is a **draft PR** a human reviews and clicks merge on.

**Why / incident:** The dogfood follow-up workflow ends every fan-out at a draft PR precisely because a post-build adversarial audit once caught a **HIGH CUDA-fork hazard that 203 passing green tests missed** (`AGENTS.md:436`, `AGENTS.md:571`). Green tests are not a merge signal for autonomous work. A model that merges its own PR removes the one gate that catches what the tests can't.

**Applies to:** any agent orchestration, self-upgrade helper, watcher, or "just merge it" impulse.

### 2. Never trust a self-report

**Rule:** A subagent's or model's "tests pass" / "N green" / "I fixed it" is a **hypothesis** until **external state** confirms it: an exit code, a real-binary dogfood, or a `file:line` that actually resolves. Re-run any validation a subagent claims to have passed.

**Why / incidents:**
- Subagents can assert success without executing (`AGENTS.md:434`). Worktree fan-out branches have **no `.venv`**, so an agent's "tests pass" is literally un-runnable in its own tree — you must re-run pytest/ruff/mypy in the real venv before integrating (`AGENTS.md:2212,2241`).
- **Mock-based FFI tests passed GREEN while the real PyO3 bridge was DEAD** — it dropped every forwarded flag and silently fell back to the Python engine. Prove a bridge/FFI change with a **live runtime call into the built extension**, then confirm the flag actually reached `rg` (`AGENTS.md:901`).

**Concrete gate:** For generated/detached code (install scripts, self-upgrade helpers), adversarial-review by **executing** it — `compile()` + `exec()` the generated string and assert behavior (e.g. the checksum gate fires *before* `os.replace`), not substrings (`AGENTS.md:434`).

### 3. No speed / improvement claim without measured numbers

**Rule:** Never claim a speedup, regression, or "improvement" without a measured line **vs the accepted baseline** (not memory). Reject a candidate that is slower — or only "faster" in a microprofile while slower end-to-end — **even if the code is clean**. If a candidate is correct but slower, **revert it and record the attempt** (in `docs/PAPER.md`) so no future agent retries the losing idea.

**Why / incidents & theory:** `rg` (ripgrep) is the **raw cold-text parity baseline**; `ast-grep` is the **structural-search baseline** (`AGENTS.md:344-345`). tg's moat is the agent-native intelligence layer, *not* faster grep — so an unmeasured "it's faster" claim is both unverified and off-strategy. Hard-won architectural truths already in the repo: more caching is **not** always faster; onefile Nuitka binaries are **not** the Windows speed path for plain passthrough; GPU is currently **slower** than CPU (`AGENTS.md:796-826`). Benchmark artifacts must carry `tg_launcher_mode` + `tg_launcher_command_kind` and **refuse stale in-tree binaries by default** — a timing taken through a `.cmd` shim or a stale `rust_core/target/*/tg.exe` is not a claim (`AGENTS.md:364`). Run the *right* benchmark for the area (see `tensor-grep-benchmark-and-proof-toolkit`).

### 4. Experimental-until-proven

**Rule:** GPU, LSP, semantic-search, and provider-backed classify (`cybert`) paths stay **default-OFF and labeled experimental** until correctness **and** speed **and** UX are all proven. Never market an unproven wedge.

**Why / incidents:**
- **GPU** Phase-0 SHIPPED (v1.75.1-v1.75.4, PRs #594-#597 -- #593/v1.75.0 was an UNRELATED
  `tg orient`/`tg agent` improvement that landed in the same version range by publish order, not part
  of the GPU wave; AGENTS.md's "GPU Phase-0 hardening wave" addendum records the same range): NVIDIA native assets are built and locally correctness-proven (RTX 4070 `sm_89` / RTX 5070 `sm_120` -- `docs/gpu_crossover.md`), but gated OFF the public release by the CI Actions var `TENSOR_GREP_RELEASE_NATIVE_ASSET_PROFILE` (default `native-frontdoor`, CPU-only; GPU asset publishing needs the non-default `native-frontdoor-gpu`) -- Phase 1 is now a reversible flag-flip, not a multi-week rebuild. That flip publishes assets only: no speed crossover is proven vs `rg`/`tg_cpu`, GPU auto-recommendation stays `false`, and the reviewer-gated `public-gpu-proof.yml` speed-crossover gate remains unmet (`grep -n "Public managed GPU promotion" docs/CONTRACTS.md` — was cited at `:80-82`, now `:123`; the old anchor pointed at the `--column`/`-c` flag list). Any GPU-requested fallback must surface `gpu_evidence_status = unsupported`, `gpu_proof = false`, `native_gpu_unavailable` (`AGENTS.md:843`). The only *candidate* CUDA wedge is many fixed strings over a large corpus — never single-pattern cold grep.
- **LSP** availability is install evidence only, not proof of working navigation; a row counts as LSP proof only with `lsp_provider_response = true` from a completed request (`AGENTS.md:375`).
- **classify** is deterministic-local by default; provider mode requires `TENSOR_GREP_CLASSIFY_PROVIDER=cybert` and provider failure must fall back **before** loading a tokenizer/model (`AGENTS.md:366`).

### 5. Mandatory adversarial security gate before merge

**Rule:** Every PR touching a security-sensitive surface — `apply_policy`, `mcp_server`, native-argv
construction (`cpu_backend`/`rg_passthrough`), `index_lock`, auth, money, a schema/data migration, or
**native asset / installer / doctor-probe construction** — gets a dedicated **adversarial** review
before merge, in addition to (never instead of) green tests:
"try to actually BREAK this, cite `file:line` for every claim, default to FIX-FIRST when uncertain."
This is a distinct pass from ordinary code review — a reviewer optimizing for "does this look right"
misses what a reviewer optimizing for "how would I exploit this" catches.

**Why / incident (2026-07-08 ultracode session):** this exact gate caught a **real symlink-follow RCE
bypass** on a security PR — `.resolve()` followed the symlink *before* the path-containment check ran,
so a crafted symlink escaped the intended root — and separately a **lock-release TOCTOU** on an
index-lock hardening PR. Both PRs had fully green test suites; neither bug was a test-coverage gap, it
was a missing adversarial pass. Ordinary review (Codex) proved unreliable/WSL-flaky for this role in
practice — run the security-adversarial pass on **Opus or Sonnet-5, never Fable** (Fable 5 ships a
semantic+cumulative cyber-safety classifier that auto-falls-back to Opus mid-turn on vuln-hunting
content, which just adds friction rather than blocking anything — see the global memory
`feedback-fable5-cyber-classifier-audit-on-opus`). **Precedent for the native-asset/installer/
doctor-probe addition:** the v1.75.2/v1.75.3 GPU Phase-0 installer-downgrade PR (#596, P0-5 -- loud
nvidia-to-cpu installer downgrade) was held in draft with an explicit "Opus gate pending before merge"
per its council-reviewed plan before shipping; construction of installer/asset-selection logic and
`doctor` probe payloads is exactly the class of code where a silent wrong-flavor install or a
misleading probe status is a security-relevant integrity failure, not just a UX nit.

**Verdict is binary, not a rubric score:** `SHIP` or `FIX-FIRST(file:line + repro + fix)`. A rubber-stamp
"looks fine" is not a passing verdict — the reviewer must state what they tried to break and why it held.

**Applies to:** any PR in the security-sensitive surface list above; extend the list as new
security-relevant subsystems appear (this is a floor, not an exhaustive enumeration).

### 6. Pin-first ranking gate (C-pin)

**Rule:** Before touching ANY scorer/graph/ranking code (a symbol scorer, a centrality/PageRank pass,
a blast-radius/import-graph traversal, a BM25/RRF weighting), write a test that **pins the CURRENT
ranked output GREEN on base** first. After the change, the ONLY acceptable diff against that pin is
the one the change intended — any OTHER legitimate-entry reorder is a STOP-finding, not a nit to wave
through.

**Why / incident (#709, v1.93.2):** the blast-radius reverse scoring prefilter was changed to exclude
`dynamic_unresolved` literals (a correctness fix, A10/A15). `test_blast_radius_legitimate_dependent_ranking_pin`
locked the pre-change ranked output first, so the fix's actual diff — removing exactly the decoy edges,
with zero reordering of legitimate dependents — was provable, not asserted. Ranking code is the class of
change where "the fix looks right" and "the fix didn't silently reorder something else" are different
claims; only a pin catches the second one.

**Applies to:** any PR touching `repo_map.py`'s scorers, the reverse-import/blast-radius graph, PageRank/
centrality, or any BM25/RRF/dense-fusion weighting.

### 7. "Not mine" / "CI doesn't flag it" is not a disposition — but ownership decides WHERE the fix lands

**Rule:** Authorship, CI visibility, tracked-vs-untracked status, and whether a finding sits inside the
current task's stated scope are all irrelevant to whether a real defect gets FIXED — but ownership decides
WHERE the fix may land. If you find a defect while doing something else, never reason your way past it;
the disposition is one of:

- **Your own / isolated tree (a worktree you own, a fresh branch off `origin/main`):** fix it in the same
  turn, in place.
- **Another agent's in-flight WIP, a file marked do-not-touch, or any foreign dirty state:** do NOT edit
  in place. PRESERVE the foreign dirty/untracked state exactly as found, RECORD the finding
  (`file:symbol` + repro) in the durable place for it (the owning PR/issue, the tracker, the handoff
  doc), and fix it in an owned/isolated tree or after EXPLICIT ownership transfer from the owner/human.
  A concurrent writer's tree is shared state: an in-place "fix" can collide with a rewrite in flight,
  and `git stash` / `git add -A` in a shared tree can destroy another agent's work — grep AGENTS.md for
  "Never edit a worktree a live agent owns" and "`git stash` Is UNSAFE Once Parallel Worktrees Exist"
  (the 2026-08-02 receipt); never stamp those as line numbers.

The only legitimate stop is a hard blocker (needs a build/fire, is irreversible, or is human/CEO-gated),
and that gets a tracked follow-up with a concrete acceptance test, never a sentence of justification.

**Why / incidents:** A lint/audit finding was named out loud and then waved past **twice** with exactly
this reasoning -- "not my file," "CI doesn't flag it" -- and both times the underlying defect was real. A
constraint on one verb (e.g. "do not **commit** this file") is not permission on another (silently
generalizing it into "do not **fix** this file" and leaving a live bug in the tree). The 2026-08-12
retention audit then found the ORIGINAL wording of this rule ("fix it in the same turn -- in place, even
... in another agent's in-flight WIP") contradicting AGENTS.md's never-edit-a-live-agent's-worktree law
and the 2026-08-02 parallel-worktree receipts — the wave-past failure and the ownership failure are two
distinct defects, and the rule must close BOTH, not trade one for the other.

**Applies to:** any lint/grep/audit finding you surface incidentally while doing something else, regardless
of who owns the file, whether it is tracked by git, or whether CI currently exercises it.

---

### 8. Before merging ANY PR, assert its base is `main` — a "skipping" rollup is an ABSENT gate

`.github/workflows/ci.yml` filters `pull_request: branches: ["main"]`, and that filter matches the
**base** ref. A stacked PR (base = another feature branch) therefore **never triggers `ci.yml` at
all** — and `gh pr checks` prints that absence as `skipping` while `mergeStateStatus` reports
`MERGEABLE`. Both read as benign.

Measured 2026-08-21: PRs #1068 and #1070 had **exactly one** check run each across their entire
life (`Dependabot Automation`, conclusion `skipped`). Control proving it is the base ref and not
the branch name or a runner outage: #1065, same `test/` prefix but base `main`, showed
`SUCCESS=39`. **Both stacked PRs went RED the moment real CI ran** — and they carried
error-handling hardening, sitting one click from merge with no test, lint, security, or
cross-platform evidence whatsoever.

```bash
gh pr list --state open --json number,baseRefName    # every row must say "main"
```

Two mechanics worth knowing before you try to fix one:

- `gh pr edit --base main` alone does **not** restore CI. It fires `pull_request` action `edited`,
  which is not in the default trigger set. **Close/reopen** (action `reopened`, which is) does.
- After the parent squash-merges, the child conflicts, because it still carries the parent's
  individual commits against a squashed `main`. Rebase with
  `git rebase --onto origin/main <parent-tip>` to drop exactly the absorbed commits.

Related: **A123** in `AGENTS.md`.

### 9. The file-size ratchet forbids GROWTH — pay for an addition, never raise the pin

`scripts/file_size_budget.py` fails any allowlisted file that grows: *"An allowlisted file may
shrink, never grow."* A 20-line security fix took `cli/main.py` 13,523 → 13,543 and CI rejected it.
Raising the pin is explicitly forbidden ("never raise it to make a new unreviewed handler pass").

**Pay for the addition instead**: move an equivalent amount OUT of the file, ideally something
cohesive with where it's going. The 2026-08-21 fix moved a scan-guardrail helper from `main.py`
into `scan_guardrails.py` (main.py → 13,512, budget 0 regressions, grandfathered 27 → 26).

Two constraints on what you may move:

- **A symbol tests monkeypatch by attribute cannot move.** Relocating it breaks the patch target
  with **no import error**, so the test keeps passing while patching nothing. Check with
  `grep -rn "<symbol>" tests/` before moving anything.
- **Do not merge same-named things without comparing them.** `_BROAD_GENERATED_SCAN_DIR_NAMES`
  exists in BOTH `cli/main.py` (22 entries — adds `.claude`, `.git`, `AppData`) and
  `cli/scan_guardrails.py` (19). Collapsing them would have silently changed behaviour. Pass the
  set in as a parameter instead. (A132)

**And know that the limit is currently UNREACHABLE for the three giants.** Run the repo's own
instrument before proposing any split:

```bash
uv run python scripts/measure_split_floor.py
```

It reports `SPLIT CANNOT REACH THE LIMIT` for `repo_map.py` (6,715 lines locked), `main.py`
(7,416) and `mcp_server.py` (2,506) — all against a 1,500 limit, all locked to their facades by
monkeypatch targets. The binding constraint is the **test strategy**, not code organisation, so the
honest options are to reduce monkeypatch coupling (a programme, not a refactor) or to state the
exception rather than carry an allowlist entry implying a completion that cannot come. The tool
states its own direction of error: it is a LOWER bound and function-only, so a patched module-level
CONSTANT is invisible to it — the real floor is never lower. (A130)

### 10. A QUEUED run is not protected — batch the merges, then STOP pushing

`ci.yml` sets `cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}`, which reads as "never
cancel a main run". **That reading is wrong in the case that matters.** The flag governs runs
already IN PROGRESS. A run still **QUEUED** in the same concurrency group is superseded by the next
push regardless.

This repo is runner-scarce — main runs sit queued for tens of minutes — so **every merge cancelled
the previous release run before it started.** Measured 2026-08-21 on
`gh run list --branch main --workflow=ci.yml`: `6909018` cancelled, `2d02a22` cancelled, `0eebab5`
cancelled. Three consecutive main runs, all killed while queued.

**This was a second, independent cause of "tagged but not published"** alongside PYPI-SIZE-CAP, and
it was initially misattributed entirely to the cap. Clearing the cap alone would not have fixed
publishing.

**The protocol, superseding "one merge per tick":**

1. Verify every candidate PR is genuinely green (see §11 — count is not enough).
2. **Merge them all in one burst.** The release is cumulative from the last tag, so merging more
   before the run starts LOSES NOTHING and gains a single publish covering everything.
3. **Then stop pushing entirely** — including docs PRs, which consume the same runners.
4. Wait for `gh run list --branch main --workflow=ci.yml --limit 1` to read **`completed`**, not
   merely to exist. A created run is not a protected run.
5. Verify the release **per-artifact** (A124): a tag or version appearing proves nothing; the
   expected filename set does.

**The same effect bites PR branches, where `cancel-in-progress` IS true.** Re-pushing to
"re-trigger CI" starves it: measured on one branch, `08a7fe20` cancelled, `16fc31d1` queued 30+
minutes and never started, head SHA with no run at all. Each rebase-push / fix-push /
empty-commit-push killed the queued predecessor. **The remedy is the opposite of the instinct: stop
pushing.** Before concluding CI is "broken", check queue depth
(`gh run list --limit N --json status`) — a sibling branch's run sitting queued identifies runner
scarcity rather than a dispatch fault. Laws **A133**, **A134**.

### 11. Assert checks by NAME — a count cannot tell a matrix run from CodeQL

A CI-watching script used `if total > 5 and pending == 0 -> GREEN`. Seven CodeQL + Dependabot
entries satisfy that, so it reported **two PRs on which `ci.yml` had never run** as TERMINAL GREEN,
and both became merge candidates on that basis.

```bash
# WRONG -- 7 CodeQL entries pass this
[ "$total" -gt 5 ] && [ "$pending" -eq 0 ] && echo GREEN

# RIGHT -- a real matrix run contains test-* checks
testcount=$(gh pr view "$pr" --json statusCheckRollup \
  -q '[.statusCheckRollup[]|.name]|map(select(startswith("test-")))|length')
[ "$testcount" -lt 4 ] && echo "NO-CI: absent gate, not a pass"
```

This is A123's "absent gate renders as a pass" with the faulty instrument being your own. Law
**A135**.

### 12. One change can trip several independent ratchets — say which case you are in

A single new `except Exception` had to satisfy BOTH the disposition ledger
(`docs/audits/2026-08-20-handler-dispositions.json`, which records WHAT it is) and
`TOTAL_BROAD_HANDLERS_CEILING` (which bounds HOW MANY exist). A single moved function tripped the
file-size ratchet AND the silent-loss census. They are deliberately separate gates; satisfy each on
its own terms.

The distinction that decides the response:

- **RELOCATION** — re-pin, but PROVE it: the total must be unchanged and the moved sites
  byte-identical. Measured example: `main.py` 6→4, `scan_guardrails.py` 5→7, **total 41→41**, with
  both new sites read and confirmed identical to the ones that left.
- **GROWTH** — harden or disposition it. **Never re-pin.** A ratchet exists because "every added
  site is a new way for an incomplete result to report success".

Write which case you are in **beside the number**, so nobody later cites your relocation as
precedent for absorbing real growth. Law **A137**.

## Part 2 — The written Operating Rules

From `AGENTS.md` "Operating Rules" (`:856`) and `CONTRIBUTING.md`:

1. **Start with a failing test when behavior changes** (TDD-first). See `superpowers:test-driven-development`.
2. **Make the smallest defensible change.**
3. **Run local gates before pushing**, scoped to this desktop unless the user approves heavy validation. Prefer targeted tests locally; use PR/main CI for the full matrices.
4. **Benchmark every hot-path change.**
5. **Reject regressions even if the code is otherwise clean.**
6. **Do not change workflow, release, or docs contracts without updating the validator-backed tests.**
7. Do not `wsl --shutdown` / restart WSL/Docker / reboot the host for "memory cleanup" without explicit user approval — other agents share WSL.

Rule 6 is easy to underrate: if you touch `.github/workflows/ci.yml`, `.github/workflows/release.yml`, `scripts/validate_release_assets.py`, docs contracts, or package-manager assets, the change is **incomplete** until the matching validator test is updated. Read `docs/CI_PIPELINE.md` first — it is the canonical pipeline contract (`AGENTS.md:789`).

---

## Part 3 — Registration completeness (the silent-misroute bug class)

**Jargon:** *registration* = an entry that must be added in multiple independent places for a feature to work; miss one and it fails **quietly** (no error, wrong route). This is a universal bug class, not a tg quirk — it also broke a downstream user's billing route.

### Adding a top-level `tg COMMAND` — 4 sites (miss one → silent misroute)

| # | Site | File | Verified anchor |
|---|---|---|---|
| 1 | `KNOWN_COMMANDS` (Python known-command registry) | `src/tensor_grep/cli/commands.py` | `commands.py:9` |
| 2 | `Commands::X` enum variant + dispatch arm (native front door) | `rust_core/src/main.rs` | `grep -n "enum Commands" rust_core/src/main.rs` (was `:889`, now `:910`) |
| 3 | `PUBLIC_TOP_LEVEL_COMMANDS` (parity contract test) | `tests/e2e/test_routing_parity.py` | `grep -n "PUBLIC_TOP_LEVEL_COMMANDS = " tests/e2e/test_routing_parity.py` (was `:18`, now `:46`); asserted by `test_top_level_help_visible_commands_match_public_contract` (was `:563-564`, now def `:583`, asserts `:592-593`) |
| 4 | `@app.command` function (Typer entry point) | `src/tensor_grep/cli/main.py` | `grep -c "@app.command" src/tensor_grep/cli/main.py` (re-run before citing a count — it drifts every release; do not trust a stamped number) |

### Adding a search flag (`tg search --myflag`) — 2 front doors (miss one → `rg: unrecognized flag` crash for installed users)

| # | Front door | File | Verified anchor |
|---|---|---|---|
| 1 | `SEARCH_PYTHON_PASSTHROUGH_FLAGS` (native allowlist) | `rust_core/src/main.rs` | grep `SEARCH_PYTHON_PASSTHROUGH_FLAGS` (was `:183`, now `:204`) |
| 2 | `bootstrap._TG_ONLY_SEARCH_FLAGS` (Python bootstrap allowlist) | `src/tensor_grep/cli/bootstrap.py` | `grep -n "_TG_ONLY_SEARCH_FLAGS" src/tensor_grep/cli/bootstrap.py` — def `:50`, checked at `:404` (was cited as checked at `:355`) |

**Why / incident:** The `tg search --rank` flag missed one of the two front doors. CliRunner tests were green — because CliRunner bypasses the bootstrap front door (Part 5) — so the crash shipped and only surfaced for users of the published binary (`AGENTS.md:405-410`). The **CI registration-completeness gate is BLOCKING since v1.17.1 (#282)** and its extractor is comment-aware (`#`-commented entries are not counted as registered) (`AGENTS.md:414`).

**Audit procedure before claiming a registration change is done:**
- `tg callers <registration-function>` lists every *callable* registration in ~1s — **but the call graph cannot see set/list/decorator/dispatch-table registrations** (e.g. `_TG_ONLY_SEARCH_FLAGS` is a *set*, `@router.post` a decorator). `--rank` lived in a set, so `callers` would never have found it.
- So **grep / `tg scan`** the set/decorator/table sites too. Confirm your new entry appears in **all** sites (`AGENTS.md:412`).

### Registering a new symbol-graph language — 5 seams (miss one → a silent half-integration)

**Jargon:** the *symbol-graph tier* is the deep per-language layer behind `tg defs`/`tg source`/
`tg imports`/`tg callers`/`tg agent` — distinct from plain text search (any language, via `rg`
passthrough). As of this pass **10** languages are registered: python, javascript, typescript, rust, go,
java, php, csharp, **c, cpp** (`lang_registry.LANGUAGE_REGISTRY`, pinned by
`test_language_registry_has_exactly_the_stage2_languages` in `tests/unit/test_lang_registry.py` --
grep the test NAME, not a line number). C/C++ ARE registered, via `lang_c.py`/`lang_cpp.py`; an
earlier revision of this section said they were not, which mattered because this is the skill that
gates every new-language change.

The registry entry point is `lang_registry.register_language(lang_registry.LanguageSpec(...))`
(`src/tensor_grep/cli/lang_registry.py:118`), called once per language inside
`src/tensor_grep/cli/repo_map.py`. **Do not cite a stamped count here** -- run
`grep -c "lang_registry.register_language(" src/tensor_grep/cli/repo_map.py` (10 as of 2026-07-27).
This line previously carried a hardcoded 8 and went stale the moment C/C++ landed, which is exactly
the failure the `@app.command` row in Part 10 already fixed by replacing a number with a command. A language's extraction
callables can live either inline in `repo_map.py` (python/rust/java) or in a dedicated `lang_<x>.py`
module mirroring `lang_go.py` (go/php/csharp — a separate module avoids an import cycle back into
`repo_map.py`); both are contract-consistent.

Registering the `LanguageSpec` is necessary but not sufficient — 5 more call sites either dispatch on
the registry or hardcode a language list directly, and missing one is a **silent half-integration** (the
language works for some commands and quietly does nothing for others):

| # | Seam | Feeds | File | Verified anchor |
|---|---|---|---|---|
| 1 | `_imports_and_symbols_for_path` | `tg imports` (import list + symbols) | `repo_map.py` | grep `def _imports_and_symbols_for_path` (was `:6244`, now `:6627`; branches `:6650-6679`) |
| 2 | `_imports_with_lines_for_path` | `tg imports`' line-numbered spans | `repo_map.py` | `grep -n "^def _imports_with_lines_for_path" src/tensor_grep/cli/repo_map.py` (was `:6440`, now `:6832`) — dispatches ALL 10 as of the top-10 campaign's final waves (python/js/ts/rust/java inline; go/php/csharp/c/cpp via their `lang_*` module extractors, `repo_map.py:7089-7116`; the old "go/php/csharp fall through to `[]`" note predates the top-10 wave) |
| 3 | `build_symbol_source_from_map` | `tg source` | `repo_map.py` | grep `def build_symbol_source_from_map` (was `:15815`, now `:16326` -- 511 lines adrift) |
| 4 | `_target_language_for_path` | **MOST-FORGOTTEN.** Feeds the `tg agent` capsule's query-language-vs-target-language confidence gate (`agent_capsule.py`) | `repo_map.py` | grep `def _target_language_for_path` (was `:7383`, now `:7867`) -- the function's own comments say "MOST-FORGOTTEN seam" at each of the 4 newest branches, grep that phrase rather than trusting sub-line numbers; skip it and the capsule can silently report "no target language" for a real target instead of downgrading confidence honestly |
| 5 | `_SUPPORTED_FILE_DEPENDENCY_LANGUAGES` | `tg imports <file>`'s file-dependency-resolution "supported" gate | `repo_map.py` | grep `_SUPPORTED_FILE_DEPENDENCY_LANGUAGES` (no line number: the file has been split, grep the symbol) — all 10 as of the top-10 campaign's final waves: `frozenset({python, javascript, typescript, rust, java, go, php, csharp, c, cpp})` (the `frozenset` beside that symbol in `repo_map.py`); go/php/csharp/c/cpp joined at the raw-imports tier — their deeper `import_update_target`/true `import-string -> target-file` resolution is still `None` (tracked follow-ups in `docs/BACKLOG.md`), so those files honestly report unresolved import edges instead of a fabricated resolved list |

**Fail closed for a missing grammar.** Every language added since the registry existed
(go/java/php/csharp) sets `provenance_when_missing="grammar-missing"` in its `register_language(...)`
call (grep `language_id="go"`; was `repo_map.py:6090`, now ~`:6368`) — never `"regex-heuristic"` — so a file whose tree-sitter grammar
package isn't installed surfaces as an honest `resolution_gaps` entry via
`_language_coverage_gaps_for_universe` (`grep -n "^def _language_coverage_gaps_for_universe" src/tensor_grep/cli/repo_map.py` — was `:8461`, now `:8478`; the fail-closed branch — `grep -n "fail_closed = True" src/tensor_grep/cli/repo_map.py`, now `:8521` — was previously cited as `:8019`, which today lands inside an unrelated AST-symbol-matching helper, `_thin_cli_dispatcher_call_targets`, not this function at all) instead
of a silent empty result. This is Part 4's Backend Fail-Closed Contract, applied inside the language
registry (see Part 4's own worked example below).

**Audit procedure:** grep all 5 seams plus the registration call
(`grep -n "lang_registry.register_language\|_imports_and_symbols_for_path\|_imports_with_lines_for_path\|_target_language_for_path\|_SUPPORTED_FILE_DEPENDENCY_LANGUAGES" src/tensor_grep/cli/repo_map.py`),
then widen `tests/unit/test_lang_registry.py:84-94` (`test_language_registry_has_exactly_the_stage2_languages`) to include the new language — this is a **pin test for registry membership** (same
principle as Part 1 Rule 6's ranking pin, applied to a set instead of a ranked list): it fails loud the
moment a rebase silently drops a language (see Part 7's sequential-drain corollary below).

### A census's population is itself a defect surface -- curate it, don't derive it

Every table above (4 command sites, 2 flag front doors, 5 language seams) is a **census**: a claim that
"these are all the places this thing lives." The census itself is where the recurring bugs live, not just
the code it protects.

**Never add a census member by reasoning it is covered -- CALL it.** The native-argv `--`-sentinel
completeness census (the round-4 argv item tracked in this skill's provenance table below,
`rust_core/src/rg_passthrough.rs`) had its population wrong **four times in one session** -- 5, then 8,
then 10, then 13 members -- and every single miss was the same judgment: "builder A transitively covers
builder B." Each was disproved only by deleting B's own guard and watching the suite stay green anyway.
One of the missed members (`_build_command`) carried the worst possible cost of the four: a path of
`-U`/`--update-all` reaching ast-grep's `run` is its **auto-fix switch**, so an unguarded miss there turns
a read-only scan into a file rewrite. **Rule:** treat a census list as **curated, not complete** -- prove
membership by deleting the candidate's guard and confirming red, and re-derive the whole list by sweep
every release; never claim it final (compare Part 3's own "do not cite a stamped count" rule for the
language-registration count above -- same discipline, applied to a security-relevant guard instead of a
head-count).

**Enumerate EMITTERS/ARTIFACTS, not the mechanism they happen to share.** A census keyed on a common
implementation mechanism (e.g. "every builder that uses decorator/pattern X") can report "N of N covered"
and still be wrong by one, because a sibling can produce the identical artifact by an entirely different
mechanism -- sharing no type, no decorator, nothing a mechanism-keyed grep would match -- and land
uncovered by the guard the census was supposed to feed. The same trap recurs one level down inside a
single site: **a function is not the unit, the artifact is** -- two independently-built argv sequences
constructed dozens of lines apart inside one function let a whole-function substring match report the
first one's sentinel as "covering" the second one's bare, unguarded positional. **Rule:** key a census on
the *shape of the output* (every place this exact artifact gets constructed), never on a shared
implementation mechanism -- a sibling that reaches the same artifact by a different path is exactly the
member a mechanism-keyed search cannot see.

**Generated code is a second interpreter and must join the population.** Discover every production
spawn/exec root, parse every statically resolvable payload as its own source unit, and fail closed on
dynamic/unparseable payloads. Resolve local imports, aliases, rebinding, and shadowing. Sanction exact
`source:callsite:operation:destination-provenance` fingerprints, not whole functions. Prove the census
with ordinary and generated-source mutation controls. Receipt: #859's codemap-only ratchet missed three
live writers plus generated helper sinks.

### A shared builder's flag belongs to its consumers, not its neighbors (#876, fixed #880)

**Rule:** before adding a flag/param to a SHARED builder, enumerate every consumer of what it builds and
ask which of them CONSUME the thing the flag changes -- not just where the flag fits among its neighbors.

**Why / incident:** `-q` was added to `RipgrepBackend._build_cmd` -- the shared argv builder, where ~30
other flags already live, so it looked like the natural home. `_build_cmd` has FOUR consumers; only ONE
streams rg's stdout, the other THREE parse it, and `-q` makes rg print nothing. Measured on the real
binary:

    rg --count-matches needle f.txt -> "2"     with -q -> ""
    rg -l             needle f.txt -> "f.txt"  with -q -> ""
    rg --json         needle f.txt -> 5 lines  with -q -> 1

So `tg search -q --count` on a MATCHING file reported `total_matches=0`, exit 1 -- a false no-match plus
an exit-contract violation, shipped in #876. A flag that alters OUTPUT belongs to the consumers that
stream, not the ones that parse.

**Applies to:** any shared builder (argv, query, request) with more than one consumer -- grep every call
site and classify each "streams the result" or "parses the result" before adding a flag that changes what
gets printed.

---

## Part 4 — Backend fail-closed contract (the silent-wrong-answer bug class)

**Jargon:** a *ComputeBackend* is a search engine implementation (CPU regex, Rust, GPU, ast-grep, …) behind a common interface (`src/tensor_grep/backends/base.py`).

**Rule (`backends/base.py:7`, `AGENTS.md:2090`):** Every backend **MUST raise `BackendExecutionError` on a real failure** — never return a clean empty / `0-match` result, and never silently swap to an engine that cannot preserve the requested semantics. The search loop catches `BackendExecutionError` to fall back **visibly**; a swallowed failure reaches a coding agent as a trustworthy "no matches" — the one failure a context tool cannot afford.

- **Fail closed** for any flag the fallback cannot preserve — e.g. `--pcre2` through a non-PCRE2 engine must **raise, not swap**.
- If a degraded fallback is *legitimate* (e.g. heuristic classify when the model is down), make it **visible**: set `fallback_reason` (and a distinct `routing_reason`) on the result so JSON/CLI consumers can tell degraded from real. **Never label heuristic output as model output.**
- Validate an untrusted response shape (e.g. a model's class count vs a fixed label list) before indexing, so a mismatch degrades instead of raising an `IndexError` a broad `except` then swallows.

**Why / incidents (this contract is violated repeatedly):** the Rust/PCRE2 bridge ran `--pcre2` through the Python-regex engine (wrong results); the ast-grep OOM mask read a killed subprocess as a clean 0-match; a tree-sitter invalid-query silently returned 0 matches; CyBERT labeled keyword-heuristic hits as real model output. The recurring smell is a **bare `except Exception:` that returns empty or falls to a different engine** (`AGENTS.md:442`). The same rule extends to any router/pipeline that could silently override explicit user intent — e.g. an explicit `--gpu` request quietly routed to CPU must raise `ConfigurationError` or emit a diagnostic (`AGENTS.md:448`; fix shipped in `src/tensor_grep/core/pipeline.py`). A `SafeBackendMixin` + fault-injection conformance CI gate is the planned structural fix so this stops recurring file-by-file.

**Concrete example outside `backends/` (the same contract, a different subsystem):** the multi-language
symbol registry (Part 3) applies this identically. `LanguageSpec.provenance_when_missing` must be
`"grammar-missing"` (never `"regex-heuristic"`) for any language with no text-heuristic fallback —
go/java/php/csharp all set it this way in their `register_language(...)` call (grep `language_id="go"`; was `repo_map.py:6090`, now ~`:6368`)
— so `_language_coverage_gaps_for_universe` (`grep -n "^def _language_coverage_gaps_for_universe" src/tensor_grep/cli/repo_map.py` — was `:8461`, now `:8478`) can tell "grammar not installed, fail
closed" apart from "language has a regex fallback, degrade quietly" at its branch — `grep -n "fail_closed = True" src/tensor_grep/cli/repo_map.py`, now `:8521` (was cited as `:8019`, which today lands inside an unrelated AST-symbol-matching helper, not this function). Get
this backwards (label a no-fallback language `"regex-heuristic"`) and a grammar-missing file would read
as a clean, silent "zero symbols found" instead of an honest gap — precisely the failure class this Part
exists to prevent, just reached through a registry field instead of a bare `except`.

**Domain note (ripgrep):** tg's default regex path matches invalid UTF-8; **PCRE2 requires valid UTF-8 and transcodes** — which is *why* swapping `--pcre2` to a non-PCRE2 engine changes results, not just performance. `rg` exit code `1` with empty output is a legitimate "no match" (`AGENTS.md:367`); exit code `2` with matches already parsed is treated as **partial** (kept + `result_incomplete=True`, the "surface degraded, don't discard" posture this Part argues for, not a swallow); any other case (`2`+ with nothing parsed, or `>2`) is a **real ripgrep failure** — `ripgrep_backend.py` raises `BackendExecutionError` (not a bare `RuntimeError`) on `returncode > 1 and not partial` at three call sites — `grep -n "if result.returncode > 1 and not partial" src/tensor_grep/backends/ripgrep_backend.py` (was `:126`, `:297`, `:413`, now `:127`, `:308`, `:426`) — and this must not be swallowed as non-fatal.

---

## Part 5 — Dogfood the REAL binary, not CliRunner

**The entry point is `tensor_grep.cli.bootstrap:main_entry`.** It intercepts plain-text sear

…(truncated)
