Every ticket as the root of its provenance graph — goal, patch, approval, runs and (as the ticket-class build lands) mandate, manifestations, guarantees, journey, artifacts and children. Rendered through the <<<TicketGraphList>>> tag.
TreeView: build TESTED ref-resolution tooling + record ref_resolve telemetry to `events` — 180/264 forge repos unbrowsable because 183 registry rows store the literal string 'HEAD' (dangling symref; rev-parse exits 0). Do NOT repair the data.
goal
Build TESTED tooling for forge ref-resolution in TreeView, and RECORD TELEMETRY so the underlying
registry corruption stops being invisible. Do NOT repair data. Do NOT hand-edit the db.
THE SYMPTOM (operator-reported, reproduced): ?page=tree&repo=code__rebuild__jazz_wallet renders
"Could not read the tree at ." — an unhelpful message that names neither the cause nor the repo state.
THE CAUSE (measured, not inferred):
* TreeView.php:49 does `$commit = (string) $repos[$repo]['head_commit'];` and hands that value
straight to `git ls-tree` (line 94) / `git show` (line 133). It trusts the registry blindly.
* 183 of the 263 rows in `repos` store head_commit = the LITERAL 4-CHAR STRING 'HEAD' — not a sha,
not null, not empty. Verified: SELECT DISTINCT head_commit ... -> 'HEAD' x183.
* WHY the string got stored: in those bare remotes HEAD is a DANGLING SYMREF — it points at
refs/heads/master, which does NOT exist (the real branch is `main`). On such a repo,
`git rev-parse HEAD` prints the literal text "HEAD" AND EXITS 0. It looks like success. Whatever
wrote the registry believed it.
* So `git ls-tree HEAD` -> "fatal: Not a valid object name HEAD" -> the useless page message.
BLAST RADIUS (measured over all 264 bare remotes in config/forge/remotes):
81 repos — `ls-tree HEAD` works
180 repos — fails, BUT the tree is fully readable via refs/heads/* -> RECOVERABLE (68% of forge)
3 repos — genuinely carry no refs at all -> must fail, but must fail CLEANLY
DO NOT "FIX" THE DATA. The 183 rows stay exactly as they are. Every tool that matters ALREADY treats
head_commit as untrustworthy input and routes around it — ingest_repo_docs.py documents the identical
183/263 finding and carries the fallback chain, and forge_refactor_ticketer.py resolves tips itself
("TIP BEATS OMNI"). TreeView is the ONLY consumer that trusts the value. Repairing 183 rows by hand
would be surgery on the governance db to fix data that three tools deliberately ignore. Not wanted.
DELIVERABLE: checkouts/current/congruency/invocators/tags/dev/TreeView.php
1. RESOLUTION AS TOOLING, NOT AN INLINE PATCH. Extract ref resolution into ONE small, separately
testable method. Resolve in this order — COPY the chain that already exists, do not reinvent it
(ingest_repo_docs.py:110, FALLBACK_REFS):
a. repos.head_commit — ONLY if it resolves: `rev-parse --verify --quiet <val>^{commit}`.
This single guard is what rejects the literal 'HEAD'. Never trust the column's contents.
b. HEAD
c. refs/heads/main, refs/heads/master, refs/heads/source, refs/heads/trunk
d. the first ref under refs/heads/
Every candidate MUST be validated with `rev-parse --verify --quiet <ref>^{commit}` and the
resolved 40-hex sha used thereafter — never a symbolic name. Keep TreeView's existing security
posture verbatim: escapeshellarg() every git argument, read-only subcommands only (ls-tree/show/
rev-parse/for-each-ref), htmlspecialchars() all output before it re-enters execute_all_tags.
2. FAIL PREDICTABLY. The 3 no-ref repos MUST produce a clear, typed, named outcome — e.g. "repo X
has no refs under refs/heads (bare remote is empty)" — not "Could not read the tree at .". Any
failure must name the repo AND which resolution tiers were attempted. A predictable failure is
the point of building this as tooling.
3. RECORD TELEMETRY — this is a REQUIREMENT, not a nice-to-have. Emit one row per resolution to the
EXISTING `events` table (id, ts, component, event, status, ms, meta) — do NOT invent a new sink,
do NOT use front_telemetry (that is the front-end page beacon, wrong shape).
component = 'TreeView'
event = 'ref_resolve'
status = 'ok' | 'fallback' | 'no_refs'
ms = resolution wall time
meta = json: {repo, tier: 'head_commit'|'HEAD'|'refs/heads/main'|..., resolved_sha,
head_commit_rejected: true|false}
The POINT: today the 183-row corruption is INVISIBLE because ingest_repo_docs.py falls back
silently and nobody learns. With this telemetry the fallback tier distribution is queryable, so
the corruption becomes observable. Telemetry must never throw — a telemetry failure must not
break the page (TreeView already degrades gracefully; preserve that).
4. TEST THE TOOLING. Two layers:
* synthetic: throwaway bare repos in a temp dir covering every case — dangling symref to a
missing master with only `main` present (the live 180-repo shape), HEAD resolving normally,
main-only, master-only, NO refs at all, and head_commit holding the literal 'HEAD'. Assert
the resolved sha (or the typed no-refs outcome) for each. No network, no live db.
* live: run the resolver across all 264 real remotes under config/forge/remotes and assert the
measured distribution — 261 resolve to a 40-hex sha, 3 report no_refs. Read-only.
ACCEPTANCE: ?page=tree renders the real tree for the previously-broken repos (verify with
code__rebuild__jazz_wallet, whose tree is README.md/deferred/install.json/install.py/src/tests via
refs/heads/main); 261/264 resolve, the 3 no-ref repos fail with a named reason; `events` gains one
ref_resolve row per resolution with the tier recorded; the registry is UNCHANGED (assert: SELECT
COUNT(*) FROM repos WHERE head_commit='HEAD' is STILL 183 afterwards — this ticket must not write to
the db); TreeView's escapeshellarg/htmlspecialchars posture is unchanged. RUN ON CLAUDE.
patch
none
approval
unapproved
runs
no runs recorded
mandate (clauses)
not yet recorded — lands with the ticket-class build
manifestations
not yet recorded — lands with the ticket-class build
guarantees
not yet recorded — lands with the ticket-class build
journey (blunders & successes)
not yet recorded — lands with the ticket-class build
artifacts (forge)
not yet recorded — lands with the ticket-class build