Skip to content

feat: add pks graph subcommand (deterministic JSON dependency-graph export) - #44

Draft
benleegusto wants to merge 3 commits into
rubyatscale:mainfrom
benleegusto:graph-dump-subcommand
Draft

feat: add pks graph subcommand (deterministic JSON dependency-graph export)#44
benleegusto wants to merge 3 commits into
rubyatscale:mainfrom
benleegusto:graph-dump-subcommand

Conversation

@benleegusto

@benleegusto benleegusto commented Aug 12, 2026

Copy link
Copy Markdown

What

Adds a read-only pks graph subcommand that prints the whole-repo pack dependency graph as compact JSON:

{"nodes":[{"name":"packs/foo","layer":"...","owner":"..."}],"edges":[{"from":"packs/foo","to":"packs/baz","kind":"declared"}]}

Edges are tagged by kind:

  • declared — from dependencies: in package.yml
  • ignored — from ignored_dependencies:
  • todo — a recorded violation in the source pack's package_todo.yml

Why

Tools that want to analyze the dependency graph (cycle/SCC decomposition, keystone/leverage ranking, visualization) currently re-parse package.yml/package_todo.yml and re-implement resolution, which drifts from how pks actually resolves dependencies. This exposes the graph pks already builds as a stable, machine-readable artifact those tools can consume directly.

Scope (deliberately minimal)

  • Raw output only — no cycle detection, SCC, keystone ranking, or simulation. pks keeps owning enforcement (validate) and constant resolution (list-definitions); this is just the graph.
  • Deterministic — nodes ordered by name, edges by (from, to, kind), serialized compactly, so repeated runs on unchanged config are byte-identical.
  • No new dependencies — reuses the already-present serde/serde_json and the parsed PackSet.

Conventions

Follows the JSON conventions established for pks check -o json:

  • Committed schema/graph-output.json (draft-07, additionalProperties:false, required, $defs + $ref, kind enum), mirroring schema/check-output.json.
  • Compact serde_json::to_writer (raw data; consumers format as needed).
  • EdgeKind enum (declared/ignored/todo); module doc-comment references the schema.

Verified the emitted output validates against schema/graph-output.json (draft-07) across the simple_app, contains_package_todo, and app_with_ignored_dependency fixtures.

Tests

  • Unit (src/packs/graph.rs): determinism, node/edge ordering, declared edges, todo edges.
  • Integration (tests/graph_test.rs, assert_cmd): declared / todo / ignored edges end-to-end + CLI-level determinism (two runs => byte-identical stdout).

cargo fmt, cargo clippy, and the full cargo test suite are green.

Possible follow-ups (out of scope here)

  • pks references (per-edge call sites): a command that enumerates the actual references composing an edge — including legal/declared edges, not just violations — by exposing the reference_extractor pks already runs. Motivation: forming a modularization plan needs every call site of an edge to migrate, and rg/grep miss references that use non-fully-qualified constant names (relative consts, nested-module lookups). check -o json surfaces references only for violation edges; list-definitions gives constant authority but not usages. This would close that gap and let downstream planning tools resolve call sites the way pks does rather than re-implementing (and drifting from) constant resolution.
  • Schema drift-guard: optionally add a test that validates sample graph output against schema/graph-output.json. Left out for now to match how check treats its schema (documentation, not a test-enforced gate) and to avoid adding a JSON-schema validator dev-dependency — flagging in case maintainers prefer an enforced guard.
  • summary object: check output includes a summary (counts + success); omitted here to keep the graph dump raw/lean (counts are trivially derivable), but happy to add for structural parity.
  • Field naming: edges use from/to and nodes use name (graph vocabulary) rather than check's referencing_pack_name/defining_pack_name — open to aligning if consistency is preferred over graph idiom.

Draft: opening for visibility; will socialize internally before requesting review.

🤖 Generated with Claude Code

Adds a read-only `pks graph` command that serializes the whole-repo pack
dependency graph — data pks already parses — as deterministic JSON: nodes
(name + optional layer/owner) and directed edges tagged by kind (`declared`,
`ignored`, `todo`). Fully ordered (nodes by name, edges by from/to/kind) so
repeated runs on unchanged config are byte-identical.

No analysis (no cycle/SCC detection or simulation) — that is left to downstream
tools consuming the raw graph. No new dependencies (reuses serde/serde_json).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
benleegusto and others added 2 commits August 12, 2026 15:45
End-to-end coverage mirroring the other subcommands' tests/*_test.rs: asserts
declared, todo, and ignored edges over the simple_app / contains_package_todo /
app_with_ignored_dependency fixtures, plus CLI-level determinism (two runs =>
byte-identical stdout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the conventions MOD-122 established for `pks check -o json`:
- Serialize compact via serde_json::to_writer (was pretty)
- Model edge kind as an EdgeKind enum (declared/ignored/todo)
- Add schema/graph-output.json (draft-07), mirroring schema/check-output.json
  (additionalProperties:false, required lists, $defs + $ref, enum)
- Extract write_graph<W: Write> for a testable writer boundary; doc-comment
  references the schema file (like json.rs)

Determinism preserved (ordered nodes/edges); tests updated for compact output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/packs/graph.rs
) -> anyhow::Result<()> {
// Compact, raw structured data (matches `pks check -o json`); consumers format as needed.
serde_json::to_writer(writer, &build(configuration))?;
Ok(())

@perryqh perryqh Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serde_json::to_writer emits no trailing newline. All other pks text output ends with \n, and a missing newline causes the shell prompt to render on the same line in some terminals. Consider:

let json = serde_json::to_string(&build(configuration))?;
writeln!(writer, "{json}")?;

or use to_writer_pretty if pretty-printing is ever desired — though compact is fine for machine consumption.

Comment thread src/packs/graph.rs
assert_eq!(
json_bytes(&configuration),
json_bytes(&configuration),
"graph JSON must be byte-identical across runs"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is a tautology: both calls run in the same process on the same Configuration object. Rust's HashMap randomizes iteration order per process startup (via RandomState), not per call, so the two json_bytes calls will always agree within one run — regardless of whether the sort is present. The test passes even if you strip every sort_by call from build().

The integration test graph_cli_output_is_deterministic in tests/graph_test.rs is the real check (separate processes, separate seeds). Consider dropping this unit test or replacing it with one that builds two separate Configuration instances from the same fixture path.

Comment thread src/packs/graph.rs
"expected todo edge packs/foo -> packs/bar"
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test covers layer and owner appearing in node output when set. If a future refactor accidentally drops those fields or changes the skip_serializing_if predicate, nothing here would catch it. Consider adding a test that calls build() against a fixture that has a pack with layer or owner configured and asserts the node's fields are Some(...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

2 participants