Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit f938dca

Browse files
z23ccclaude
andcommitted
feat: graph typed edges, review-context, incremental hash optimization (v0.1.52)
- Add EdgeKind enum (Calls/Imports/Inherits/References) with classify_edge() - Add `flowctl graph review-context` command with blast-radius risk scoring + test gaps - Hash-based incremental update: skip unchanged files, provenance-based targeted edge rebuild - Version-prefixed graph.bin with clear mismatch error - Configurable find_impact_with_depth() (was hardcoded 3) - 18 new tests (408 total), all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d999825 commit f938dca

10 files changed

Lines changed: 1028 additions & 57 deletions

File tree

.claude-plugin/flowctl-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.1.51
1+
v0.1.52

.claude-plugin/marketplace.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
},
77
"metadata": {
88
"description": "Structured plan-first development plugin for Claude Code with .flow/ task tracking, Agent Teams, and Ralph autonomous mode",
9-
"version": "0.1.51"
9+
"version": "0.1.52"
1010
},
1111
"plugins": [
1212
{
1313
"name": "flow-code",
1414
"description": "Zero-dependency planning + execution with .flow/ task tracking. Three-layer quality (guard + RP plan-review + Codex adversarial). Full-auto, zero questions. Teams auto-parallel, DAG mutation, Codex-driven decisions, auto draft-PR, session summary. 5 prompt templates plus dedicated subagents, slash commands, and skills.",
15-
"version": "0.1.51",
15+
"version": "0.1.52",
1616
"source": "./",
1717
"category": "workflow",
1818
"tags": [
@@ -26,5 +26,5 @@
2626
"strict": true
2727
}
2828
],
29-
"version": "0.1.51"
29+
"version": "0.1.52"
3030
}

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flow-code",
3-
"version": "0.1.51",
3+
"version": "0.1.52",
44
"description": "Zero-dependency planning + execution with .flow/ task tracking and Ralph autonomous mode (multi-model review gates). Worker subagent per task with git worktree isolation for parallel execution. Three-layer quality system (guard + RP plan-review + Codex adversarial). Full-auto by default — AI decides from context, zero questions. Teams-default with file locking, DAG mutation, Codex-driven conflict resolution, auto draft-PR. Auto-detected stack profiles with one-command guard (test/lint/typecheck). Enhanced agent definitions with permissionMode/maxTurns/effort. Lifecycle hooks with state preservation (PreCompact injects .flow state into compaction, TaskCompleted auto-unlocks files, SubagentStart context injection). Memory v2 with atomic entries, dedup, and progressive disclosure. TDD enforcement mode. Multi-epic queue with dependency visualization. Includes dedicated subagents, slash commands, and skills for planning, execution, review, and autonomous operation.",
55
"author": {
66
"name": "z23cc",

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flow-code",
3-
"version": "0.1.51",
3+
"version": "0.1.52",
44
"description": "Zero-dependency planning + execution with .flow/ task tracking and Ralph autonomous mode (multi-model review gates). Worker subagent per task with git worktree isolation for parallel execution. Three-layer quality system (guard + RP plan-review + Codex adversarial). Full-auto by default. Teams-default with file locking, DAG mutation, Codex-driven conflict resolution, auto draft-PR.",
55
"author": {
66
"name": "z23cc",

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to Flow-Code are documented in this file.
44

55
Format follows [Keep a Changelog](https://keepachangelog.com/). Versions use [Semantic Versioning](https://semver.org/).
66

7+
## [0.1.52] - 2026-04-10
8+
9+
### Added
10+
- **Typed edges** in code graph — `EdgeKind{Calls, Imports, Inherits, References}` replaces untyped references
11+
- **`flowctl graph review-context`** command — blast-radius risk scoring + test gap detection from git diff
12+
- **Hash-based incremental update** — skips unchanged files via content hash, provenance-based targeted edge rebuild
13+
- **Edge provenance tracking** — records which file scan produced which edges for precise incremental updates
14+
- **Version-prefixed graph.bin** — 4-byte version header with clear error on format mismatch
15+
- **`find_impact_with_depth()`** — configurable BFS depth (was hardcoded 3)
16+
- **18 new tests** — edge classification, hash skip, version mismatch, review context, deleted file handling, provenance
17+
18+
### Changed
19+
- `flowctl graph status` now shows `typed_edge_counts` breakdown (calls/imports/references)
20+
- `flowctl graph update` no longer rebuilds all edges from scratch — uses provenance for targeted rebuild
21+
- `CodeGraph` struct now derives `Debug`
22+
723
## [0.1.51] - 2026-04-10
824

925
### Added

bin/flowctl

16.4 KB
Binary file not shown.

flowctl/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flowctl/crates/flowctl-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "flowctl-cli"
3-
version = "0.1.51"
3+
version = "0.1.52"
44
description = "CLI entry point for flowctl"
55
edition.workspace = true
66
rust-version.workspace = true

flowctl/crates/flowctl-cli/src/commands/graph.rs

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! `flowctl graph` commands: build, update, status, refs, impact, map.
1+
//! `flowctl graph` commands: build, update, status, refs, impact, map, review-context.
22
//!
33
//! Manages a persistent code graph stored at `.flow/graph.bin`.
44
@@ -36,6 +36,18 @@ pub enum GraphCmd {
3636
#[arg(long, default_value = "0")]
3737
budget: usize,
3838
},
39+
/// Generate blast-radius review context with risk scoring.
40+
ReviewContext {
41+
/// Git ref to diff against (default: HEAD~1).
42+
#[arg(long, default_value = "HEAD~1")]
43+
base: String,
44+
/// Explicit file list (comma-separated). Overrides git diff.
45+
#[arg(long)]
46+
files: Option<String>,
47+
/// BFS depth for impact analysis (default: 3).
48+
#[arg(long, default_value = "3")]
49+
depth: usize,
50+
},
3951
}
4052

4153
// ── Dispatch ───────────────────────────────────────────────────────
@@ -48,6 +60,11 @@ pub fn dispatch(cmd: &GraphCmd, json: bool) {
4860
GraphCmd::Refs { symbol } => cmd_refs(json, symbol),
4961
GraphCmd::Impact { path } => cmd_impact(json, path),
5062
GraphCmd::Map { budget } => cmd_map(json, *budget),
63+
GraphCmd::ReviewContext {
64+
base,
65+
files,
66+
depth,
67+
} => cmd_review_context(json, base, files.as_deref(), *depth),
5168
}
5269
}
5370

@@ -132,6 +149,11 @@ fn load_graph() -> CodeGraph {
132149
}
133150
match CodeGraph::load(&path) {
134151
Ok(g) => g,
152+
Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
153+
error_exit(&format!(
154+
"Graph format outdated: {e}\nRun `flowctl graph build` to rebuild."
155+
));
156+
}
135157
Err(e) => error_exit(&format!("Failed to load graph: {e}")),
136158
}
137159
}
@@ -296,18 +318,31 @@ fn cmd_status(json: bool) {
296318
"symbol_count": stats.symbol_count,
297319
"file_count": stats.file_count,
298320
"edge_count": stats.edge_count,
321+
"typed_edge_counts": stats.typed_edge_counts,
299322
"disk_size_bytes": disk_size,
300323
"built_at_epoch_ms": stats.built_at_epoch_ms,
301324
"path": path.to_string_lossy(),
302325
}));
303326
} else {
327+
let typed_summary: String = if stats.typed_edge_counts.is_empty() {
328+
String::from("(none)")
329+
} else {
330+
let mut parts: Vec<String> = stats
331+
.typed_edge_counts
332+
.iter()
333+
.map(|(k, v)| format!("{k}={v}"))
334+
.collect();
335+
parts.sort();
336+
parts.join(", ")
337+
};
304338
pretty_output(
305339
"graph",
306340
&format!(
307-
"Graph: {} symbols, {} files, {} edges\nOn-disk: {} bytes\nBuilt at: {}\nPath: {}",
341+
"Graph: {} symbols, {} files, {} edges\nTyped edges: {}\nOn-disk: {} bytes\nBuilt at: {}\nPath: {}",
308342
stats.symbol_count,
309343
stats.file_count,
310344
stats.edge_count,
345+
typed_summary,
311346
disk_size,
312347
stats.built_at_epoch_ms,
313348
path.display()
@@ -406,3 +441,125 @@ fn cmd_map(json: bool, budget: usize) {
406441
pretty_output("graph", &map);
407442
}
408443
}
444+
445+
// ── Review Context ────────────────────────────────────────────────
446+
447+
fn cmd_review_context(json: bool, base: &str, files: Option<&str>, depth: usize) {
448+
let graph = load_graph();
449+
let root = project_root();
450+
451+
// Determine changed files: explicit list or git diff.
452+
let changed_files: Vec<String> = if let Some(files_str) = files {
453+
files_str
454+
.split(',')
455+
.map(|f| {
456+
let trimmed = f.trim();
457+
let p = if std::path::Path::new(trimmed).is_absolute() {
458+
trimmed.to_string()
459+
} else {
460+
root.join(trimmed).display().to_string()
461+
};
462+
p
463+
})
464+
.collect()
465+
} else {
466+
// Use git diff to find changed files.
467+
let output = std::process::Command::new("git")
468+
.args(["diff", "--name-only", base])
469+
.current_dir(&root)
470+
.output();
471+
472+
match output {
473+
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
474+
.lines()
475+
.filter(|l| !l.is_empty())
476+
.map(|l| root.join(l).display().to_string())
477+
.collect(),
478+
_ => {
479+
if json {
480+
json_output(json!({
481+
"changed_files": [],
482+
"impacted_files": [],
483+
"test_gaps": [],
484+
"total_risk_score": 0.0,
485+
"message": "No changes detected or git diff failed",
486+
}));
487+
} else {
488+
pretty_output("graph", "No changes detected or git diff failed.");
489+
}
490+
return;
491+
}
492+
}
493+
};
494+
495+
if changed_files.is_empty() {
496+
if json {
497+
json_output(json!({
498+
"changed_files": [],
499+
"impacted_files": [],
500+
"test_gaps": [],
501+
"total_risk_score": 0.0,
502+
}));
503+
} else {
504+
pretty_output("graph", "No changed files detected.");
505+
}
506+
return;
507+
}
508+
509+
let ctx = graph.review_context(&changed_files, depth);
510+
511+
if json {
512+
let impacted: Vec<serde_json::Value> = ctx
513+
.impacted_files
514+
.iter()
515+
.map(|r| {
516+
json!({
517+
"file": r.file,
518+
"risk_score": (r.risk_score * 100.0).round() / 100.0,
519+
"pagerank": (r.pagerank * 10000.0).round() / 10000.0,
520+
"dependent_count": r.dependent_count,
521+
"is_test": r.is_test,
522+
"changed_symbols": r.changed_symbols,
523+
})
524+
})
525+
.collect();
526+
527+
json_output(json!({
528+
"changed_files": ctx.changed_files,
529+
"impacted_files": impacted,
530+
"test_gaps": ctx.test_gaps,
531+
"total_risk_score": (ctx.total_risk_score * 100.0).round() / 100.0,
532+
"impact_depth": depth,
533+
}));
534+
} else {
535+
let mut out = format!(
536+
"Review Context ({} changed, {} impacted, {} test gaps)\n",
537+
ctx.changed_files.len(),
538+
ctx.impacted_files.len(),
539+
ctx.test_gaps.len()
540+
);
541+
out.push_str(&format!(
542+
"Total risk score: {:.1}\n\n",
543+
ctx.total_risk_score
544+
));
545+
546+
if !ctx.impacted_files.is_empty() {
547+
out.push_str("Impacted files (by risk):\n");
548+
for r in &ctx.impacted_files {
549+
out.push_str(&format!(
550+
" [{:.1}] {} (deps={}, test={})\n",
551+
r.risk_score, r.file, r.dependent_count, r.is_test
552+
));
553+
}
554+
}
555+
556+
if !ctx.test_gaps.is_empty() {
557+
out.push_str("\nTest gaps (no test coverage):\n");
558+
for f in &ctx.test_gaps {
559+
out.push_str(&format!(" {f}\n"));
560+
}
561+
}
562+
563+
pretty_output("graph", &out);
564+
}
565+
}

0 commit comments

Comments
 (0)