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.
goal
Make the artifact registry VIEWABLE AND INTERACTIVE in congruency, as a tag — not a path, not a
sqlite3 invocation. The registry is worthless if reading it requires a shell.
DELIVERABLE: checkouts/current/congruency/invocators/tags/dev/ArtifactRegistry.php
Invoked as <<<ArtifactRegistry>>> on ?page=artifacts.
MATCH THE EXISTING TAG CONTRACT — 31 dev tags already do this; copy their shape, invent nothing:
* `class ArtifactRegistry implements Tag_Interface`, `__construct($arguments)`,
`private static function esc($s) { return htmlspecialchars((string) $s, ENT_QUOTES); }`
* Read ONLY. Facets come from $_GET and are BOUND (:param), NEVER interpolated.
* ESCAPE-FIRST: EVERY value out of the db (name, kind, version, sha, path) is esc()'d BEFORE it
flows back through execute_all_tags — a stored <<<Tag>>> or <script> in an artifact name must
stay inert. This is the posture ForgeView/OmniExplorer/TreeView already hold; do not weaken it.
* Degrade gracefully, never crash the page.
ONE DIFFERENCE FROM ITS SIBLINGS: ForgeView/OmniExplorer/ShadowForge open a SECOND PDO to a separate
db (forge.db / omni_index.db). `ticket_artifacts` lives in the PRIMARY congruency db, so use the
existing handle. Still never write: SELECT only, no INSERT/UPDATE/DELETE anywhere in this file.
THE DATA: ticket_artifacts (id, ts, ticket_id, name, version, forge_sha, kind). TODAY IT HAS 0 ROWS —
#455 (VM registry) and #457 (the mint hook) are what populate it. This tag MUST render correctly
against an EMPTY table today and light up automatically as rows arrive.
KIND IS THE TAG; ITS ITEMS ARE INTERACTIVE. Three views, each a link away from the others:
1. ?page=artifacts — the index. GROUP BY kind: one row per kind with its count
(e.g. forge-vm 12, forge-source 5). EVERY KIND IS A LINK to its filtered view. Plus the N most
recent artifacts overall (ts DESC), each linking to its detail.
2. ?page=artifacts&kind=<k> — the items of that tag. One row per artifact: name, version, short
forge_sha, ts, minting ticket. EVERY ROW LINKS to the detail view; the ticket id links to that
ticket. Bound :kind. Unknown kind -> "no artifacts of kind X" + a link back, never an error.
3. ?page=artifacts&sha=<forge_sha> — ONE artifact, the interactive leaf:
* its identity: name, kind, version, full forge_sha (shown in full — it is the lookup key)
* EVERY LOCATION the same forge_sha was registered at (the same bytes in two places is ONE
artifact with several locations — that is the entire point of content addressing)
* the minting ticket(s), linked
* ts / first seen
Bound :sha. Unknown sha -> "not registered" + a link back.
WHY THE SHA IS SHOWN IN FULL: it is what the universal resolver (#193, DONE, tools/resolver.py)
takes as input — `resolver.py <forge_sha>` probes 'forge-artifacts' and returns every place that
blob lives. The page and the resolver are two doors on the same registry. State that in the
docstring so the next reader knows the sha is a live handle, not decoration.
DO NOT: add a nav/menu entry, touch any other tag, create tables, write rows, or read the filesystem.
This tag DISPLAYS the registry. Registering is #455/#457's job; repairing is nobody's job yet.
ACCEPTANCE:
* ?page=artifacts renders TODAY against the EMPTY table: a clear "no artifacts registered yet"
(naming #455/#457 as what fills it), HTTP 200, page not crashed. This is the state you can
actually test right now.
* Seed a THROWAWAY copy of the db with a few rows across two kinds and verify: the index groups by
kind with correct counts; each kind links to its filtered list; each row links to the sha detail;
the detail shows all locations sharing that sha. Never seed the live db.
* SECURITY: an artifact whose name contains `<<<TreeView>>>` and `<script>alert(1)</script>` renders
INERT (escaped) — prove it in the throwaway.
* grep the file: zero INSERT/UPDATE/DELETE; every $_GET facet bound, none interpolated.
RUN ON CLAUDE.