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 agentimprovement 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 4070sm_89/ RTX 5070sm_120--docs/gpu_crossover.md), but gated OFF the public release by the CI Actions varTENSOR_GREP_RELEASE_NATIVE_ASSET_PROFILE(defaultnative-frontdoor, CPU-only; GPU asset publishing needs the non-defaultnative-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 vsrg/tg_cpu, GPU auto-recommendation staysfalse, and the reviewer-gatedpublic-gpu-proof.ymlspeed-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/-cflag list). Any GPU-requested fallback must surfacegpu_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 = truefrom a completed request (AGENTS.md:375). - classify is deterministic-local by default; provider mode requires
TENSOR_GREP_CLASSIFY_PROVIDER=cybertand 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, andgit stash/git add -Ain a shared tree can destroy another agent's work — grep AGENTS.md for "Never edit a worktree a live agent owns" and "git stashIs 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.
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 mainalone does not restore CI. It firespull_requestactionedited, which is not in the default trigger set. Close/reopen (actionreopened, 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 withgit 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_NAMESexists in BOTHcli/main.py(22 entries — adds.claude,.git,AppData) andcli/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:
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":
- Verify every candidate PR is genuinely green (see §11 — count is not enough).
- 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.
- Then stop pushing entirely — including docs PRs, which consume the same runners.
- Wait for
gh run list --branch main --workflow=ci.yml --limit 1to readcompleted, not merely to exist. A created run is not a protected run. - 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.
# 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.py6→4,scan_guardrails.py5→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:
- Start with a failing test when behavior changes (TDD-first). See
superpowers:test-driven-development. - Make the smallest defensible change.
- 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.
- Benchmark every hot-path change.
- Reject regressions even if the code is otherwise clean.
- Do not change workflow, release, or docs contracts without updating the validator-backed tests.
- 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_FLAGSis a set,@router.posta decorator).--ranklived in a set, socallerswould never have found it.- So grep /
tg scanthe 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 honestresolution_gapsentry 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.
--pcre2through 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 distinctrouting_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
IndexErrora broadexceptthen 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)