Ticket graphs

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.

← all ticket-graphs

ACTIVE#443 congruency

Forge FAMILY TREE / evolution tool (v2, supersedes #442): cluster 264 repos -> 109 lineages, classify fork pattern incl. FORK-CAME-HOME, operator-supplied per-package home mapping, containment retirement gate

goal

Build the FORGE FAMILY TREE / EVOLUTION tool: cluster every forge repo into lineages, classify how each family forked, name the CANONICAL tip, and produce a stage-by-stage evolution tree plus a reconciliation plan for the few families that truly diverged. WHY (measured, not theoretical — a prototype was run on 2026-07-15 over config/forge/remotes/*.git): * 264 bare repos collapse to 109 DISTINCT LINEAGES (grouped by root-commit set). * Of those 109: 68 IDENTICAL (pure duplicates, same tip ingested from several paths) 36 LINEAR (one tip supersets the rest; the others are STALE copies) 3 DIVERGED (true forks: jazz.git, notes_db.git, z3_mcp.git) 2 SPLIT-STORE (objects not shared; need a fetch before they can be compared: big-jazz.git, code__code.git) So only ~5 families carry real reconciliation work. The other ~104 need NO merge — just pick the superset tip and drop the aliases. The 264 is largely an ingestion-path illusion (code__X, code__rebuild__X, gitlab__<group>__X, gitlab__<group>__jazz-forge__X, and bare X are the SAME project ingested repeatedly). * THE MOTIVATING CASE — "THE FORK THAT CAME HOME" (operator-defined, do not re-derive): jail.git (47 commits) is topologically a linear SUPERSET of agent_tools_suite.git (46) — shared root 379ac05319, ats tip 888d3447c6 is a direct ancestor of jail tip 38e95ad16d, and jail adds 6 files (crankws-mcp/server.py +138, start-crank.py, claude-gui.py, registry.crankws.json, crank_job_backlog.json, crank_job_congruency.json). BUT JAIL IS **NOT** CANONICAL AND **NOT** A DESTINATION. Per the operator: jail was an EXPEDIENT — a name wrapped around some packages, dropped in a folder, followed by cleaning up a large mess of tests in a hurry. jail is a GRAB-BAG whose packages belong to SEVERAL DIFFERENT home repos. The correct direction is the REVERSE of what the topology suggests: jail's contents MERGE BACK into the existing/main repos they belong to; the main repo is UPDATED; jail's code thereby becomes REDUNDANT (the updated codebase contains it) and jail is then OMITTED. Do NOT adopt jail as canonical. Do NOT re-scope the agent_tools_suite rebuild onto jail — agent_tools_suite IS a home repo and #412-436 are correctly scoped. DELIVERABLE: checkouts/current/congruency/tools/forge_family_tree.py — standalone, no deps beyond git + stdlib + sqlite3. It MUST: 1. DISCOVER + CLUSTER. Walk config/forge/remotes/*.git. Group by root-commit SET (`git rev-list --max-parents=0 --all`). Repos sharing a root are one lineage. 2. RESOLVE TIPS CORRECTLY — the prototype's key gotcha: MOST of these bare repos have an UNRESOLVABLE HEAD (`git rev-parse HEAD` returns the literal string "HEAD"; `--verify` fails and silently drops the repo, which produced a wrong "0 diverged" answer on the first run). Resolve tips via `git for-each-ref refs/heads`, preferring main/master, else the ref with most commits. 3 repos have NO refs at all — report them, never skip silently. 3. CLASSIFY each family: IDENTICAL — all members share one tip. LINEAR — the max-commit tip is an ancestor of every other tip (superset). Report the superset tip and the stale aliases. FORK-CAME-HOME — *** THE SAME GIT SHAPE AS LINEAR. *** A fork whose additions must be merged BACK into its parent/home, after which the fork is retired. Topology CANNOT distinguish this from LINEAR — they differ only in INTENT, which is not in the DAG. Therefore: NEVER auto-decide direction. The tool REPORTS topology, PROPOSES a direction, and MUST accept an explicit operator-supplied `home` mapping that overrides the proposal. "home" is per-PATH / per-PACKAGE, not per-repo: one grab-bag fork (jail) can have packages belonging to several different home repos. DIVERGED — some tip is NOT reachable from the candidate: compute `merge-base`, and report each side's unique commits and file-level diff. SPLIT-STORE — a member's tip object is absent from the candidate's store. Do NOT guess: fetch into a scratch object store (or use --alternates) and then re-classify. 4. EVOLUTION IN STAGES. For each family emit the tree: root -> shared spine -> fork points -> tips, annotated with commit dates (stage boundaries), which repo/alias sits at each node, and what each fork ADDED vs the spine. Additive-only divergence must be distinguished from conflicting divergence (both sides modified the same file) — that distinction IS the reconciliation cost. 5. RECONCILIATION PLAN per family: IDENTICAL -> keep canonical, drop N aliases (zero work). LINEAR -> fast-forward: canonical already contains everything; drop stale aliases. ADDITIVE -> union: the merge is mechanical (no conflicting hunks). CONFLICTING-> flag for synthesis: list the conflicting files; this is the ONLY case needing a human/competitive-synthesis decision (see #206). 5b. RETIREMENT GATE — CONTAINMENT PROOF (mechanical, not a judgement call). A fork is SAFE TO OMIT IF AND ONLY IF every file at the fork's tip resolves, by content identity, to a blob present in the home repo(s) after the merge-home update. Use the SAME module-sha the ticketer computes: sha256("raw:"+bytes)[:16], or sha256("ast:"+_ast_norm(src))[:16] for Python that parses. * ALL blobs resolve -> fork is a strict SUBSET of home = redundant BY CONSTRUCTION -> report "safe to omit (proven)". * ANY blob unresolved -> that file IS outstanding merge-home work. Emit the exact list. The fork MUST NOT be dropped while the list is non-empty. The unresolved list IS the worklist; it shrinks to empty exactly when the fork is safe to drop. AST-normalized identity means a file that came home with only whitespace/comment drift still counts as contained (same module), while a semantically changed file correctly FAILS and shows up as real work. "Prove the fork is a subset, then delete" — never "delete and hope". 6. OUTPUT: (a) machine-readable — a sqlite db or JSON: families, members, tips, classification, canonical tip, fork edges, reconciliation verdict; (b) human-readable ASCII family tree. 7. --selftest on THROWAWAY git repos built in a temp dir that synthesize each class (identical / linear / additive-diverged / conflicting-diverged / split-store / no-refs / unresolvable-HEAD), proving each is classified correctly. No network, no live db, no writes outside the temp dir. DO NOT USE omni.git AS THE SOURCE OF TRUTH — it is INCOMPLETE. Measured: omni.git is MISSING agent_tools_suite's tip 888d3447c6 (it holds the common line but not the fork's feature commits). config/forge/remotes/*.git is authoritative. Optionally REPORT omni coverage gaps as a side output. DOWNSTREAM VALUE (state it in the tool's own docstring, do not implement here): the canonical-tip map is what should drive forge2 rebuild scope — ticket modules ONLY from canonical tips, skip stale variants (a stale fork's older foo.py hashes to a DIFFERENT module-sha than the tip's foo.py, so today it mints a second ticket and we rebuild an obsolete module), and rebuild one lineage into ONE forge2 package. ACCEPTANCE: --selftest green; a real run over config/forge/remotes reproduces the prototype's headline numbers (264 repos -> 109 lineages; 68 IDENTICAL / 36 LINEAR / 3 DIVERGED / 2 SPLIT-STORE, with the 2 split-store families RESOLVED after fetch rather than left unknown); correctly classifies the jail.git / agent_tools_suite.git family as FORK-CAME-HOME (a topological superset whose direction is REVERSED: jail merges home and is then retired) and does NOT name jail canonical; honours an operator-supplied `home` mapping that overrides its proposal; implements the containment retirement gate and emits jail's unresolved-blob worklist; prints a readable family tree. RUN THIS ON CLAUDE (multi-file git reasoning; not a GLM task).

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

source (forge tree)

browse congruency source (forge-parked tree)

children

not yet recorded — lands with the ticket-class build