# Repo Graph

> Build or refresh the portfolio repository graph. Reads every segment of the inputs inventory (`locations.inputs`) by the kind declared in `<inputs>/inventory.yaml` (groups / release-payload / catalog / services; ansible/ excluded by default), cross-references every repo against analysis-results/findings/ coverage (audit/triage/threat-model + severity), and emits repo-graph.{json,dot,gexf,html} + repo-graph-stats.md into analysis-results/graph/. Use when asked to "build the repo graph", "update the portfolio graph", "map repos to findings", "which repos have no coverage", or "what ships kube-rbac-proxy".

- Skill: `openshift/repo-graph` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add openshift/repo-graph`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openshift/repo-graph/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: openshift (https://skillmd.com/u/openshift)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/openshift/repo-graph

---


# repo-graph

> **Paths.** `analysis-results/…` and `progress-tracker/…` in this skill are the
> default workspace layout. They resolve through `locations.yaml` in
> `$TRAUST_CONFIG_HOME` (`docs/setup.md`, Storage locations); substitute your
> configured roots.


Build a directed graph of the entire Hybrid Platforms source-code portfolio
and overlay security-findings coverage on it. One command, five artefacts.

## What it produces

Written to `--out` (default `../analysis-results/graph/`):

| File | Purpose |
|---|---|
| `repo-graph.json` | Full-fidelity `{nodes,edges,stats}`. Canonical machine-readable output. |
| `repo-graph.dot` | GraphViz source. Render with `dot -Tsvg -Ksfdp` (large — expect minutes). |
| `repo-graph.gexf` | Gephi import for interactive layout / community detection. |
| `repo-graph.html` | Self-contained vis-network page: pan/zoom, text filter, segment-focus dropdown, double-click-to-open. Product-version and category nodes are collapsed into tooltips so the ~10k-node graph stays interactive. |
| `repo-graph-stats.md` | Node/edge counts, coverage %, top-20 hub repos by degree. |

`repo-graph-stats.md` and `repo-graph.html` embed the standard population block (python3 -m traust.cli corpus) for cross-dashboard reconciliation.

## Node types

`segment` → `release` / `product` → `product-version` → `category` → `repo` → `findings`
plus `owner-team` → `repo` (dashed) and `repo` → `repo-ref` (ref layer, below).

Repo nodes are coloured by max triaged severity across all linked findings
dirs (`CRITICAL` red · `HIGH` orange · `MEDIUM` yellow · `LOW` green · none
grey) and carry `attrs.tp_total`, `attrs.findings_dirs`,
`attrs.findings ∈ {audit,triage,tm,none,…}`.

## Edge relations

| rel | meaning |
|---|---|
| `contains` | hierarchy: segment→release/product, product→version, release/version→category |
| `ships` | inventory row: this release/version/category/product ships this repo |
| `owned-by` | owners.csv: this Owner Team owns this repo |
| `has-findings` | this repo has a findings directory under `analysis-results/findings/` (any depth — `<product>/<slug>/` or shallow `<slug>/`) |
| `has_ref` | ref layer: repo → its `repo-ref` node |
| `ships_ref` | ref layer: the same shipper as the branch-carrying `ships` edge, targeted at the `repo-ref` node |

## Ref layer (branch-awareness Phase 2)

Where an inventory CSV row declares a Source Branch, the builder ALSO emits
an optional first-class ref node — id `ref:<host>/<org>/<name>@<branch>`,
kind `repo-ref`, label `<org>/<name>@<branch>`, `attrs.branch` — deduplicated
to one node per unique (repo, branch) regardless of how many rows mention it.
Refs exist only where inventories declare branches (release-payload and
catalog segments carry `Source Branch`; groups/services rows normally do not). The layer is
strictly additive: repo-node identity, all pre-existing edges/attrs, hub
degrees in stats.md, and the HTML view are unchanged — which is why the
parallel edge uses the distinct rel `ships_ref` instead of reusing `ships`
(every existing `ships` consumer stays byte-identical and can never
double-count a branch-carrying row).

Queries this unlocks:

    # repos shipping release-4.19 from a (non-default) inventory branch
    jq -r '.nodes[]|select(.type=="repo-ref" and .attrs.branch=="release-4.19")
           |.label' repo-graph.json

    # every branch an inventory declares for one repo
    jq -r --arg r github.com/openshift/api \
      '.edges[]|select(.rel=="has_ref" and .from=="repo:"+$r)|.to' repo-graph.json

    # which release/version/category ships a specific repo@branch
    jq -r '.edges[]|select(.rel=="ships_ref" and
           .to=="ref:github.com/openshift/api@release-4.19")|.from' repo-graph.json

Per-ref enrichment (dependencies, PQC posture at the ref) is Phase 3 —
`repo-ref` nodes pass through portfolio-graph spine ingest as inert
`kind='repo-ref'` rows; every existing enrichment stage filters
`kind='repo'` and ignores them.

## Procedure

`$ARGUMENTS` are passed straight through to `build_repo_graph.py`. From the
`traust/` checkout:

1. Resolve paths. If the user supplied `--inputs` / `--findings` / `--out`,
   honour them; otherwise use defaults relative to the harness checkout
   (the inputs inventory, `../analysis-results/findings`,
   `../analysis-results/graph`).

2. Run the builder:

       python3 harnessing/repo-graph/scripts/build_repo_graph.py $ARGUMENTS

   The script discovers audit reports through the corpus resolver
   (python3 -m traust.cli corpus, the single definition of report discovery):
   a depth-tolerant walk of the findings tree that includes shallow
   `findings/<repo>/` dirs and excludes symlink aliases. It hard-fails if
   the resolver cannot load. Each report's JSON is indexed for
   `metadata.repository` (preferring the sibling `*-findings-current.json`
   cumulative report from the `track-findings` skill when present — findings
   nodes then carry `dispositions`, `resolved_findings`, `open_findings`,
   and `false_positives` attributes, and stats.md reports ledger coverage),
   ingests all inventory CSVs from the non-excluded segments, links repos
   to findings (exact URL match, then a guarded name-only fallback that
   refuses dirs whose recorded URL points at a different repo), and writes
   all five outputs.

3. Report the summary. Read `repo-graph-stats.md` back to the user and call
   out:
   - node/edge totals and coverage %
   - the top-3 hub repos
   - `repos_without_findings` count — these are gap targets for
     `secure-code-audit`.

4. If the user asked to "open" or "view" the graph, run
   `open <out>/repo-graph.html`.

## Answering questions from the graph

Once `repo-graph.json` exists, common queries:

    # repos with no findings coverage
    jq -r '.nodes[]|select(.type=="repo" and .attrs.findings=="none")|.label' repo-graph.json

    # every product/release that ships a given repo
    jq -r --arg r github.com/openshift/kube-rbac-proxy \
      '.edges[]|select(.rel=="ships" and .to=="repo:"+$r)|.from' repo-graph.json

    # repos owned by a team, sorted by TP
    jq -r --arg t "OpenShift Storage" \
      '[.edges[]|select(.rel=="owned-by" and .from=="owner-team:"+$t)|.to] as $r
       | .nodes[]|select(.id as $i|$r|index($i))
       | "\(.attrs.tp_total // 0)\t\(.label)"' repo-graph.json | sort -rn

## Excluding segments

`--exclude` is repeatable; default is `ansible`. To also drop a catalog
segment named `operators`: `--exclude ansible --exclude operators`.

## Segment kinds

Which layout a segment uses is declared in `<inputs>/inventory.yaml`
(`traust.inventory`), never inferred from the directory name. An undeclared
segment is `groups` — the layout `/add-inputs` writes. `release-payload`
segments become `release` nodes, `catalog` segments `product` →
`product-version` nodes, `groups`/`services` segments `product` nodes per
group directory.

## Integrations

`repo-graph.json` is the spine `/portfolio-graph` builds on (L0), and
the graph's `ships` edges drive per-product reporting in the PQC and
findings dashboards. `/drift-watch` flags the graph stale when
`the inputs inventory` HEAD moves. The DOT/GEXF/HTML renders are
human-terminal.

