On this page
Metrics report 🔗
codeps status consumes each configured project's source (SemanticDB, jdeps, or an existing codeps export format JSON file) and writes one JSON document to .codeps/out/<project>/report.json. It has a top-level packages report and, where source data contains files, a top-level files report. inspect-cycle and inspect-node read this cached file to render one cycle or node in detail; use --scope files for file detail.
codeps status
codeps findings --project root --limit 10 --format json
Core metrics are computed fresh from the graph's node/edge list on every run, so a report is a pure function of its input. Budgeted cycle-cut analysis exists in the core library but is not currently wired into the CLI or config, so every cycle's cutAnalysis.status is notRequested in practice (see Cycles). Set the SOURCE_DATE_EPOCH env var (epoch seconds) to pin generatedAt for deterministic CI diffs.
Structure 🔗
{
"schemaVersion": 4,
"packages": {
"schemaVersion": 3,
"scope": "packages",
"generatedAt": "<ISO8601 UTC, second precision, e.g. 2026-08-27T10:00:00Z>",
"summary": {
"nodes": 100,
"edges": 214,
"nodesInCycles": 34,
"orphans": 3
},
"cycles": [
{
"id": "scc:cache",
"members": ["cache", "scheduler"],
"witnessCycle": ["cache", "scheduler", "cache"],
"size": 2,
"extFanIn": 5,
"internalEdges": 2,
"incomingEdges": 5,
"outgoingEdges": 3,
"cutAnalysis": {
"status": "notRequested",
"greedyCutEstimate": null,
"solutions": [],
"examinedCandidates": 0
}
}
],
"propagators": [
{ "node": "cache", "fanIn": 3, "fanOut": 2, "score": 2.5 }
],
"surface": [
{
"node": "cache", "fanIn": 3, "fanOut": 2, "ports": 9, "mutPorts": 5, "exposure": 24,
"dependentsPerPublicPort": 0.33, "cycleId": "scc:cache",
"publicSurface": 3, "protectedSurface": 2, "packageSurface": 1, "privateSurface": 4,
"publicMutableSurface": 1, "protectedMutableSurface": 0, "packageMutableSurface": 0,
"privateMutableSurface": 1, "totalDeclaredSurface": 10,
"encapsulationRatio": 0.3, "publicMutableRatio": 0.33
}
],
"orphans": ["DeadUtil.scala"],
"findings": [
{
"id": "cycle:scc:cache",
"kind": "cycle",
"severity": "high",
"subject": "scc:cache",
"evidence": "size=2, extFanIn=5, greedyCutEstimate=none",
"confidence": "high",
"nextAction": "inspect-cycle --id scc:cache"
}
]
},
"files": { "scope": "files", "...": "same report shape" }
}
Summary 🔗
-
The outer
schemaVersionis4; each scope report hasschemaVersion3. -
packagesis always present.filesisnullwhen the input has no file graph, such as jdeps; readers also accept an omittedfilesfield. -
truncationisnullwhen no findings were omitted; otherwisefindingsOmittedrecords the omitted count. -
nodes/edges— size of the scope graph. -
nodesInCycles— total members of all cycles (multi-member strongly connected components). -
orphans— count of nodes with zero fan-in and zero fan-out (dead-code-removal candidates).
JSON Schemas 🔗
Machine-readable contracts use JSON Schema Draft 2020-12. The schemas describe current output; older artifact versions are not supported. Unknown properties are allowed for additive evolution. Relative $ref links share definitions; offline validators need the entire docs/static/schemas/ directory, with each $id mapped to its local file.
| Artifact | Schema |
|---|---|
report.json (outer v4, scope v3) |
report.schema.json |
| Codeps export format | export.schema.json |
| One health history NDJSON line (v6) | history.schema.json |
findings --format json (v1) |
findings.schema.json |
inspect-cycle --format json |
inspect-cycle.schema.json |
inspect-node --format json |
inspect-node.schema.json |
Unavailable files and file node packageId values serialize as explicit null; readers also accept omitted fields. truncation is explicit null when no findings were omitted. Unavailable surface ratios, cycleId, and greedyCutEstimate are explicit null. Export declaration counters default to zero when missing, and edge weight defaults to one. Schemas validate shapes and types; they do not prove that edges reference existing nodes, that cycle paths are valid, or that aggregate counts agree.
Findings 🔗
findings is a deterministic index of actionable diagnostics. Each finding has a stable id, a kind (cycle, propagator, mutableSurface, or structuralUse), severity, and subject, plus human-readable evidence, confidence, and a nextAction. Findings are ranked by severity, then metric score, then id. They cover every reported SCC, every above-average propagator, every node with exposed mutable ports, and every node whose structural-use proxy is below 1.0. The structural-use kind is a graph proxy based on file/package edges; it does not identify individual declarations. JSON serialization bounds findings at 10,000 rows so large projects remain agent-usable. When rows are omitted, truncation.findingsOmitted records the count. Cycles, propagators, surface rows, and orphans are not subject to this JSON inventory bound.
codeps findings returns a bounded selection of this cached index with exact kind and severity filters. Its omitted counts cached matches beyond the requested limit; sourceFindingsOmitted separately carries the scope report's truncation.findingsOmitted. See the CLI contract. Action hints require callers to preserve the selected project and scope when inspecting a subject.
History and dashboard 🔗
.codeps/<project>.ndjson contains one JSON object per recorded snapshot. Each entry owns its commit metadata once and has direct packages and optional files health sections. Scores are stored and displayed to two decimal places. The dashboard's Home tab shows the two-decimal average of the available package and file scores, with both underlying scores visible; it is a compact directional summary, not a replacement for either scope's evidence.
The trend chart spaces snapshots evenly in recording order and labels its axis with short commit IDs. Dirty snapshots retain the -dirty suffix; timestamp-based IDs remain intact. Hovering or focusing a point shows its full ID, timestamp, and metric value. Selecting a point updates its metric details. On long histories the axis shows a subset of labels; every recorded point remains available through pages of up to 20 snapshots. The chart opens on the newest page; Older and Newer navigate history, while Latest snapshot always shows the newest recorded entry for the selected scope. Switching scopes opens that scope's newest page. Snapshot metadata defaults and overrides are defined in the CLI reference.
Cycles 🔗
A cycle is a strongly connected component with more than 1 member (Tarjan's algorithm). Singleton components are just acyclic nodes and are never reported.
id—scc:+ the lexicographically smallest member id. Stable across recomputation after cuts (a counter-based id would renumber unpredictably).members— the exhaustive, sorted list of every node in the SCC. It is never merely a witness path, including when the SCC contains several interlocking cycles.witnessCycle— one deterministic closed cycle path through the smallest member; the first node is repeated at the end. This is a representative cycle, not a membership list.size— full member count of the SCC (may exceed the path length, see above).extFanIn— edges whose target is in the SCC and whose source is outside: how much outside stuff is exposed to the cycle's blast radius. Cycles are ranked bysizefirst,extFanInas tiebreaker.internalEdges— distinct edges whose source and target are both in the SCC.incomingEdges— distinct edges entering the SCC from outside. This is the same count asextFanIn, retained as an explicit directional edge count for inspection consumers.outgoingEdges— distinct edges leaving the SCC to outside nodes.cutAnalysis— the explicit result of optional cut investigation. ItsstatusisnotRequestedwhen cut analysis is not requested,completedExactwhen bounded enumeration finished its complete candidate space,completedHeuristicwhen only the greedy pass was available (including large SCCs), orbudgetExceededwhen its time or candidate limit was reached.budgetExceededis still a successful report result. Every serialized solution is validated against the original SCC; a greedy plan is never emitted if it leaves any cyclic component.greedyCutEstimate— nullable greedy estimate of the total cuts needed to dissolve the cycle. It is present only when the greedy pass completed before the budget was exhausted; it is a heuristic, not a guaranteed-minimum feedback-edge set.solutions— up to 3 complete ways to break the cycle, simplest first. Each solution is a list ofcuts({edge, weight}); removing ALL of them together dissolves the cycle (no multi-member component remains among its members). A solution can be 1 edge or several. Results are ranked by fewest cuts, cheapest total weight, then lexicographic order. Sets containing a smaller working solution are skipped. Partial candidates are never serialized.examinedCandidates— number of candidate simulations started before the budget check stopped the search.
- Cut analysis is not currently exposed through
codeps statusor the config file: nothing enables it, socutAnalysis.statusis alwaysnotRequestedandgreedyCutEstimate/solutionsare always empty in practice. Usemembers,witnessCycle, andextFanInto reason about a cycle by hand instead.
Change propagators 🔗
The JSON index contains every node above the normalized propagation threshold.
score—(fanIn / avgFanIn + fanOut / avgFanOut) / 2, whereavgFanIn/avgFanOutare the graph-wide means (edges / nodes). An exactly average node scores1.0; a hub with twice the average fan-in and no fan-out scores2.0. Only nodes above1.0are listed, sorted by score descending. A graph with no edges has no propagators.fanIn/fanOut— the raw counts (same values as insurface), shown for context.
Surface 🔗
One row per scope node is retained in JSON, using camelCase field names.
fanIn/fanOut— count of distinct edges in/out. Always derived from the edge list, never stored separately.ports— weighted count of exposed members (see Exposed surface).mutPorts— count of exposed mutable members:vars, or vals/defs typed as a mutable collection. A coupling channel that never shows up as a graph edge.exposure—ports + mutPorts * 3; mutable ports weighted 3× because they are a hidden channel on top of being exposed at all. Always look at theports/mutPortsbreakdown, neverexposurealone.dependentsPerPublicPort—fanIn / portswhenfanIn > 0andports > 0, elsenull. This is a structural proxy: file/package edges do not identify which public declarations are used. Anullvalue is meaningful (no consumers or no weighted public ports), not a 0.cycleId— the nullable stable SCC id (scc:plus the lexicographically smallest member) for nodes in a reported cycle;nullfor nodes outside all cycles.
Rows are sorted by dependentsPerPublicPort ascending (nulls last): the most exposed-for-its-use nodes first. Raw declaration counters are retained so consumers can triage public surface, public mutability, and encapsulation independently.
Declaration surface fields 🔗
Each row also carries raw declaration counts, aggregated from the adapter-neutral declarationSurface counters in the export graph:
publicSurface,protectedSurface,packageSurface,privateSurface— declaration counts by visibility (packageSurfacecorresponds toprivate[pkg]).publicMutableSurface,protectedMutableSurface,packageMutableSurface,privateMutableSurface— mutable declaration counts in the same buckets.totalDeclaredSurface— sum of the four visibility counts.encapsulationRatio—publicSurface / totalDeclaredSurfacewhen the denominator is non-zero; otherwisenull. Lower means more of the declaration surface is encapsulated.publicMutableRatio—publicMutableSurface / publicSurfacewhen public declarations exist; otherwisenull.
Exposed surface 🔗
isExposed, ports and mutPorts are resolved per node by the extraction backend (SemanticDB export), using Scala-specific weight rules that never leak into the metrics layer:
3per exposed type/trait/class/object1per exposed function/method/def/val0.5per exposed member that belongs to asealedhierarchy (external code cannot add new subtypes, so the effective surface is smaller)+1flat, once pergiven/implicitinstance (ambiently public via implicit search)1mutPortper exposedvar, or exposed val/def typedscala.collection.mutable.*orscala.Array
private[pkg] and protected members are not exposed; class-private members are omitted from dependency nodes but retained in file declaration counters; var setters are accessors and never count as surface. jdeps data carries no access info, so all its nodes have ports/mutPorts 0 and dependentsPerPublicPort null — a known gap, not silently meaningful.
Orphans 🔗
orphans— node ids withfanIn == 0 AND fanOut == 0, sorted. Step 1 of the improvement loop: dead-code-removal candidates. JSON retains the complete list.
inspect-cycle and inspect-node render one cycle or node from this cached report at a time (as table or json, via --format). findings renders a bounded selection of the findings index; cycles/propagators/surface/orphans have no full-inventory table command. Use the JSON report directly for the full inventories; the generated .codeps/out/<project>/index.html dashboard shows trends and summary evidence. The JSON findings array is capped at 10,000 rows and describes any omissions in truncation; the other graph-derived inventories (cycles, propagators, surface, orphans) are never bounded.