repo-graph
Paths.
analysis-results/…andprogress-tracker/…in this skill are the default workspace layout. They resolve throughlocations.yamlin$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:
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).Run the builder:
python3 harnessing/repo-graph/scripts/build_repo_graph.py $ARGUMENTSThe 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 formetadata.repository(preferring the sibling*-findings-current.jsoncumulative report from thetrack-findingsskill when present — findings nodes then carrydispositions,resolved_findings,open_findings, andfalse_positivesattributes, 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.Report the summary. Read
repo-graph-stats.mdback to the user and call out:- node/edge totals and coverage %
- the top-3 hub repos
repos_without_findingscount — these are gap targets forsecure-code-audit.
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.