diff --git a/crates/arc-attractor/tests/kilroy_compat.rs b/crates/arc-attractor/tests/kilroy_compat.rs new file mode 100644 index 000000000..f4413bdbc --- /dev/null +++ b/crates/arc-attractor/tests/kilroy_compat.rs @@ -0,0 +1,164 @@ +use std::path::Path; + +use arc_attractor::parser::parse; + +fn parse_kilroy_dot(filename: &str) -> Result { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../test/kilroy") + .join(filename); + let content = + std::fs::read_to_string(&path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; + parse(&content).map_err(|e| format!("failed to parse {filename}: {e}")) +} + +// --------------------------------------------------------------------------- +// Parsing tests: every kilroy DOT file must parse without error +// --------------------------------------------------------------------------- + +#[test] +fn parse_kilroy_simple_example() { + let graph = parse_kilroy_dot("simple_example.dot").unwrap(); + assert_eq!(graph.name, "Simple"); + assert_eq!(graph.goal(), "Run tests and report"); + assert_eq!(graph.nodes.len(), 4); + assert_eq!(graph.edges.len(), 3); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); +} + +// The batch_*.dot files use unquoted values with hyphens/dots (e.g., `llm_model=gpt-5.2`) +// which is valid in kilroy's more lenient Go parser but not in arc's strict DOT parser. +// These tests document the parser gap: arc requires quoting such values. +#[test] +fn parse_kilroy_batch_clean_requires_quoted_model_values() { + let err = parse_kilroy_dot("batch_clean.dot").unwrap_err(); + assert!( + err.contains("grammar error"), + "expected grammar error for unquoted `gpt-5.2`, got: {err}" + ); +} + +#[test] +fn parse_kilroy_batch_has_errors_requires_quoted_model_values() { + let err = parse_kilroy_dot("batch_has_errors.dot").unwrap_err(); + assert!( + err.contains("grammar error"), + "expected grammar error for unquoted `gpt-5.2`, got: {err}" + ); +} + +#[test] +fn parse_kilroy_batch_warnings_only_requires_quoted_model_values() { + let err = parse_kilroy_dot("batch_warnings_only.dot").unwrap_err(); + assert!( + err.contains("grammar error"), + "expected grammar error for unquoted `gpt-5.2`, got: {err}" + ); +} + +#[test] +fn parse_kilroy_solitaire_fast() { + let graph = parse_kilroy_dot("solitaire_fast.dot").unwrap(); + assert_eq!(graph.name, "solitaire"); + assert_eq!( + graph.goal(), + "Build a terminal-based solitaire (Klondike) game" + ); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); + // Large workflow: 2 control + many work nodes + diamond gates + assert!( + graph.nodes.len() > 15, + "expected >15 nodes, got {}", + graph.nodes.len() + ); + assert!( + graph.edges.len() > 20, + "expected >20 edges, got {}", + graph.edges.len() + ); +} + +#[test] +fn parse_kilroy_consensus_task() { + let graph = parse_kilroy_dot("consensus_task.dot").unwrap(); + assert_eq!(graph.name, "Workflow"); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); + // Multi-model consensus: many parallel branches + assert!(graph.nodes.len() > 10); +} + +#[test] +fn parse_kilroy_semport() { + let graph = parse_kilroy_dot("semport.dot").unwrap(); + assert_eq!(graph.name, "Workflow"); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); + // Loop-based workflow with conditional routing + assert!(graph.edges.len() > 5); +} + +#[test] +fn parse_kilroy_reference_template() { + let graph = parse_kilroy_dot("reference_template.dot").unwrap(); + assert_eq!(graph.name, "reference_template"); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); + // Kitchen-sink template: subgraphs, fan-out, parallel, loops + assert!(graph.nodes.len() > 30); + assert!(graph.edges.len() > 30); + // Verify subgraph-derived classes are applied + assert!( + graph.nodes.contains_key("implement"), + "should contain implement node" + ); +} + +#[test] +fn parse_kilroy_green_test_moderate() { + let graph = parse_kilroy_dot("green_test_moderate.dot").unwrap(); + assert_eq!(graph.name, "linkcheck"); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); +} + +#[test] +fn parse_kilroy_green_test_complex() { + let graph = parse_kilroy_dot("green_test_complex.dot").unwrap(); + assert_eq!(graph.name, "dttf"); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); + // Very large workflow (40+ stages) + assert!(graph.nodes.len() > 40); +} + +#[test] +fn parse_kilroy_green_test_vague() { + let graph = parse_kilroy_dot("green_test_vague.dot").unwrap(); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); +} + +#[test] +fn parse_kilroy_refactor_test_moderate() { + let graph = parse_kilroy_dot("refactor_test_moderate.dot").unwrap(); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); +} + +#[test] +fn parse_kilroy_refactor_test_complex() { + let graph = parse_kilroy_dot("refactor_test_complex.dot").unwrap(); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); + // Large workflow + assert!(graph.nodes.len() > 30); +} + +#[test] +fn parse_kilroy_refactor_test_vague() { + let graph = parse_kilroy_dot("refactor_test_vague.dot").unwrap(); + assert!(graph.find_start_node().is_some()); + assert!(graph.find_exit_node().is_some()); +} diff --git a/test/kilroy/batch_clean.dot b/test/kilroy/batch_clean.dot new file mode 100644 index 000000000..ff05b692b --- /dev/null +++ b/test/kilroy/batch_clean.dot @@ -0,0 +1,6 @@ +digraph G { + start [shape=Mdiamond] + exit [shape=Msquare] + work [shape=box, llm_provider=openai, llm_model=gpt-5.2, prompt="Do the work."] + start -> work -> exit +} diff --git a/test/kilroy/batch_has_errors.dot b/test/kilroy/batch_has_errors.dot new file mode 100644 index 000000000..321ba89ad --- /dev/null +++ b/test/kilroy/batch_has_errors.dot @@ -0,0 +1,6 @@ +digraph G { + start [shape=Mdiamond] + exit [shape=Msquare] + work [shape=box, llm_model=gpt-5.2, prompt="Do the work."] + start -> work -> exit +} diff --git a/test/kilroy/batch_warnings_only.dot b/test/kilroy/batch_warnings_only.dot new file mode 100644 index 000000000..bccdb8e92 --- /dev/null +++ b/test/kilroy/batch_warnings_only.dot @@ -0,0 +1,6 @@ +digraph G { + start [shape=Mdiamond] + exit [shape=Msquare] + work [shape=box, llm_provider=openai, llm_model=gpt-5.2] + start -> work -> exit +} diff --git a/test/kilroy/consensus_task.dot b/test/kilroy/consensus_task.dot new file mode 100644 index 000000000..47f473dc8 --- /dev/null +++ b/test/kilroy/consensus_task.dot @@ -0,0 +1,158 @@ +digraph Workflow { + graph [ + label="Consensus Task Workflow", + goal="$task", + rankdir="LR", + context_fidelity_default="truncate", + context_thread_default="consensus-task", + default_max_retry="3", + retry_target="CheckDoD", + fallback_retry_target="Start" + ]; + + Start [ + node_type="start", label="Start", shape="Mdiamond", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="4", timeout="300" + ]; + + CheckDoD [ + node_type="stack.steer", label="Check DoD", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="2", timeout="120", + llm_prompt="Check if definition of done provided.\n\nTASK: $task\nDOD: $definition_of_done\n\nWrite status.json with outcome=needs_dod if DOD is empty or just a placeholder, else outcome=has_dod if a real DOD was provided." + ]; + + DefineDoD_Gemini [ + node_type="stack.observe", label="Define DoD (Gemini)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", + llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_gemini.md" + ]; + + DefineDoD_GPT [ + node_type="stack.observe", label="Define DoD (GPT)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", reasoning_effort="high", + llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_gpt.md" + ]; + + DefineDoD_Opus [ + node_type="stack.observe", label="Define DoD (Opus)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", + llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_opus.md" + ]; + + ConsolidateDoD [ + node_type="stack.observe", label="Consolidate DoD", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="8", timeout="420", + llm_prompt="Synthesize three DoD proposals.\n\nTASK: $task\n\nRead .ai/dod_*.md, write consensus to .ai/definition_of_done.md" + ]; + + PlanGemini [ + node_type="stack.observe", label="Plan (Gemini)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="8", timeout="420", + llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_gemini.md" + ]; + + PlanGPT [ + node_type="stack.observe", label="Plan (GPT)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="8", timeout="420", reasoning_effort="high", + llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_gpt.md" + ]; + + PlanOpus [ + node_type="stack.observe", label="Plan (Opus)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="8", timeout="420", + llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_opus.md" + ]; + + DebateConsolidate [ + node_type="stack.observe", label="Debate & Consolidate", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="10", timeout="600", + llm_prompt="Synthesize three plans.\n\nTASK: $task\n\nRead .ai/plan_*.md, write final to .ai/plan_final.md" + ]; + + Implement [ + node_type="stack.observe", label="Implement (Opus)", shape="box", style="rounded,filled", + is_codergen="true", allow_partial="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="25", timeout="1200", + llm_prompt="Execute plan.\n\nTASK: $task\n\nFollow .ai/plan_final.md. Log to .ai/implementation_log.md" + ]; + + ReviewGemini [ + node_type="stack.observe", label="Review (Gemini)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", + llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_gemini.md with PASS/FAIL" + ]; + + ReviewGPT [ + node_type="stack.observe", label="Review (GPT)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", reasoning_effort="high", + llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_gpt.md with PASS/FAIL" + ]; + + ReviewOpus [ + node_type="stack.observe", label="Review (Opus)", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", + llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_opus.md with PASS/FAIL" + ]; + + ReviewConsensus [ + node_type="stack.steer", label="Review Consensus", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="6", timeout="300", + llm_prompt="Reach consensus.\n\nRead .ai/review_*.md\nWrite to .ai/review_consensus.md\n\noutcome=yes if PASS, outcome=retry if FAIL. Set preferred_next_label to \"\" (empty) or to \"yes\"/\"retry\"; do not use \"No\"." + ]; + + Postmortem [ + node_type="stack.observe", label="Postmortem", shape="box", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="8", timeout="420", + llm_prompt="Failure postmortem.\n\nTASK: $task\n\nWrite to .ai/postmortem_NN.md" + ]; + + Exit [ + node_type="exit", label="Exit", shape="Msquare", style="rounded,filled", + is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex", + max_agent_turns="4", timeout="120" + ]; + + Start -> CheckDoD; + CheckDoD -> DefineDoD_Gemini [condition="outcome=needs_dod"]; + CheckDoD -> DefineDoD_GPT [condition="outcome=needs_dod"]; + CheckDoD -> DefineDoD_Opus [condition="outcome=needs_dod"]; + CheckDoD -> PlanGemini [condition="outcome=has_dod"]; + CheckDoD -> PlanGPT [condition="outcome=has_dod"]; + CheckDoD -> PlanOpus [condition="outcome=has_dod"]; + DefineDoD_Gemini -> ConsolidateDoD; + DefineDoD_GPT -> ConsolidateDoD; + DefineDoD_Opus -> ConsolidateDoD; + ConsolidateDoD -> PlanGemini; + ConsolidateDoD -> PlanGPT; + ConsolidateDoD -> PlanOpus; + PlanGemini -> DebateConsolidate; + PlanGPT -> DebateConsolidate; + PlanOpus -> DebateConsolidate; + DebateConsolidate -> Implement; + Implement -> ReviewGemini; + Implement -> ReviewGPT; + Implement -> ReviewOpus; + ReviewGemini -> ReviewConsensus; + ReviewGPT -> ReviewConsensus; + ReviewOpus -> ReviewConsensus; + ReviewConsensus -> Exit [condition="outcome=yes"]; + ReviewConsensus -> Postmortem; + Postmortem -> PlanGemini [loop_restart="true"]; + Postmortem -> PlanGPT [loop_restart="true"]; + Postmortem -> PlanOpus [loop_restart="true"]; +} diff --git a/test/kilroy/green_test_complex.dot b/test/kilroy/green_test_complex.dot new file mode 100644 index 000000000..f6cc27b8f --- /dev/null +++ b/test/kilroy/green_test_complex.dot @@ -0,0 +1,1075 @@ +digraph dttf { + graph [ + goal="Build DTTF: a bitmap-to-TrueType font converter with custom quadratic Bezier tracer", + rankdir=LR, + default_max_retry=3, + retry_target="impl_setup", + model_stylesheet=" + * { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } + .hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + .verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } + .review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + " + ] + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + // Project setup + impl_setup [ + shape=box, + timeout=600, + max_retries=2, + prompt="Read specs/dttf-v1.md. Create Go project structure for DTTF. + +Create: +- go.mod with module github.com/kilroy/dttf +- cmd/dttf/main.go (stub that prints version) +- pkg/dttf/ directory structure +- .ai/implementation_plan.md documenting the build order + +Run: go build ./... + +Write status.json: outcome=success if project builds, outcome=fail with error otherwise." + ] + + verify_setup [ + shape=box, + class="verify", + timeout=300, + prompt="Verify project setup. + +Run: +1. go build ./... +2. go vet ./... +3. Check that go.mod exists with correct module path + +Write results to .ai/verify_setup.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_setup [shape=diamond, label="Setup OK?"] + + // Core types and interfaces + impl_types [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 7 (Data Structures). + +Implement core types in pkg/dttf/types.go: +- GlyphBitmap (codepoint, pixels, width, height) +- Point (X, Y int16, OnCurve bool) +- Contour (Points []Point) +- TracedGlyph (codepoint, contours, metrics) +- FontMetadata (family, style, units_per_em, ascender, descender) +- Options, TraceOptions, RasterizeOptions structs + +Add comprehensive godoc comments. Include validation methods where appropriate. + +Run: go build ./... && go test ./pkg/dttf/... + +Write status.json: outcome=success if builds and tests pass, outcome=fail otherwise." + ] + + verify_types [ + shape=box, + class="verify", + timeout=300, + prompt="Verify core types implementation. + +Run: +1. go build ./... +2. go vet ./... +3. go test ./pkg/dttf/... -v +4. Check that all types from section 7 are present + +Write results to .ai/verify_types.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_types [shape=diamond, label="Types OK?"] + + // PNG loader and filename parser + impl_loader [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md sections 1.1-1.5 (Input format). + +Implement in pkg/dttf/loader.go: +- LoadGlyphs(inputDir string) ([]GlyphBitmap, *FontMetadata, error) +- ParseFilename(name string) (codepoint rune, ok bool) +- LoadFontMetadata(dir string) (*FontMetadata, error) + +Handle: +- PNG loading with image/png +- Filename parsing (U+XXXX pattern, case-insensitive) +- Optional font.json sidecar with defaults +- Error handling per section 9 + +Create tests in pkg/dttf/loader_test.go with sample PNGs. + +Run: go test ./pkg/dttf/... -run TestLoad + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_loader [ + shape=box, + class="verify", + timeout=300, + prompt="Verify PNG loader implementation. + +Run: +1. go build ./... +2. go test ./pkg/dttf/... -run TestLoad -v +3. Check error handling for missing files +4. Verify font.json defaults are applied + +Write results to .ai/verify_loader.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_loader [shape=diamond, label="Loader OK?"] + + // Image processing (grayscale + threshold) + impl_imageproc [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 1.4 (Image Requirements). + +Implement in pkg/dttf/imageproc.go: +- ToGrayscale(img image.Image) [][]uint8 +- Threshold(grayscale [][]uint8, threshold uint8) [][]bool +- Helper functions for pixel format conversion + +Handle RGB/RGBA/Gray input formats. Output: row-major 2D arrays. + +Create tests with synthetic images and known thresholds. + +Run: go test ./pkg/dttf/... -run TestImage + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_imageproc [ + shape=box, + class="verify", + timeout=300, + prompt="Verify image processing implementation. + +Run: +1. go test ./pkg/dttf/... -run TestImage -v +2. Check grayscale conversion preserves intensity +3. Check threshold produces binary output + +Write results to .ai/verify_imageproc.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_imageproc [shape=diamond, label="ImageProc OK?"] + + // Tracer Phase 1: Path decomposition + impl_tracer_phase1 [ + shape=box, + class="hard", + timeout=1800, + max_retries=2, + prompt="Read specs/dttf-v1.md section 3.2 Phase 1 (Path Decomposition). + +Implement in pkg/dttf/tracer/contour.go: +- DecomposePaths(binary [][]bool) []RawPath +- RawPath struct (points as pixel coordinates) +- Contour-following algorithm on 1-bit raster +- Separate outer contours from inner contours/counters + +This is complex boundary-tracing logic. Reference standard contour-following algorithms. + +Create tests with simple shapes (square, circle, letter O with counter). + +Run: go test ./pkg/dttf/tracer/... -run TestDecompose + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_tracer_phase1 [ + shape=box, + class="verify", + timeout=300, + prompt="Verify tracer phase 1 (path decomposition). + +Run: +1. go test ./pkg/dttf/tracer/... -run TestDecompose -v +2. Check that outer/inner contours are distinguished +3. Verify closed paths return to origin + +Write results to .ai/verify_tracer_phase1.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_tracer_phase1 [shape=diamond, label="Phase1 OK?"] + + // Tracer Phase 2: Optimal polygon + impl_tracer_phase2 [ + shape=box, + class="hard", + timeout=1800, + max_retries=2, + prompt="Read specs/dttf-v1.md section 3.2 Phase 2 (Optimal Polygon). + +Implement in pkg/dttf/tracer/polygon.go: +- OptimalPolygon(path RawPath) []PixelPoint +- Convert pixel staircase to minimal straight-line segments +- Preserve shape fidelity + +Use polygon approximation algorithm (Douglas-Peucker or similar). + +Create tests comparing input/output vertex counts. + +Run: go test ./pkg/dttf/tracer/... -run TestPolygon + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_tracer_phase2 [ + shape=box, + class="verify", + timeout=300, + prompt="Verify tracer phase 2 (optimal polygon). + +Run: +1. go test ./pkg/dttf/tracer/... -run TestPolygon -v +2. Check polygon has fewer points than raw path +3. Verify shape is preserved + +Write results to .ai/verify_tracer_phase2.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_tracer_phase2 [shape=diamond, label="Phase2 OK?"] + + // Tracer Phase 3: Quadratic Bezier fitting + impl_tracer_phase3 [ + shape=box, + class="hard", + timeout=2400, + max_retries=3, + prompt="Read specs/dttf-v1.md section 3.2 Phase 3 (Quadratic Bezier Fitting). + +Implement in pkg/dttf/tracer/bezier.go: +- FitQuadraticBezier(polygon []Point, tolerance float64) []Point +- Quadratic curve fitting (NOT cubic) +- Output: Points with OnCurve flags +- Minimize point count while staying within tolerance + +This is the core tracer algorithm. Complex curve fitting math. + +Create tests with known curves. + +Run: go test ./pkg/dttf/tracer/... -run TestBezier + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_tracer_phase3 [ + shape=box, + class="verify", + timeout=300, + prompt="Verify tracer phase 3 (quadratic Bezier fitting). + +Run: +1. go test ./pkg/dttf/tracer/... -run TestBezier -v +2. Check output has OnCurve flags set correctly +3. Verify curves are quadratic (TrueType format) + +Write results to .ai/verify_tracer_phase3.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_tracer_phase3 [shape=diamond, label="Phase3 OK?"] + + // Tracer Phase 4: Font-specific optimization + impl_tracer_phase4 [ + shape=box, + class="hard", + timeout=1800, + max_retries=2, + prompt="Read specs/dttf-v1.md section 3.2 Phase 4 (Font-Aware Optimization). + +Implement in pkg/dttf/tracer/optimize.go: +- OptimizeForFont(contours []Contour, opts TraceOptions) []Contour +- Insert points at extrema (required by OpenType) +- Ensure correct winding direction (clockwise outer, CCW inner) +- Remove self-intersections +- Eliminate short segments (< 2 units) +- Enforce max 1000 control points per glyph + +Create tests validating each constraint. + +Run: go test ./pkg/dttf/tracer/... -run TestOptimize + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_tracer_phase4 [ + shape=box, + class="verify", + timeout=300, + prompt="Verify tracer phase 4 (font optimization). + +Run: +1. go test ./pkg/dttf/tracer/... -run TestOptimize -v +2. Check winding direction is correct +3. Verify no self-intersections +4. Check point count cap + +Write results to .ai/verify_tracer_phase4.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_tracer_phase4 [shape=diamond, label="Phase4 OK?"] + + // Coordinate mapping + impl_coordinates [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 3.3 (Coordinate Mapping). + +Implement in pkg/dttf/tracer/coordinates.go: +- PixelToFontUnits(pixelX, pixelY, imageWidth, imageHeight, advanceWidth, ascender, descender int) (int16, int16) +- FontUnitsToPixel (inverse for testing) + +Handle the coordinate system transformation. All output coordinates are integers. + +Create tests with known transformations. + +Run: go test ./pkg/dttf/tracer/... -run TestCoordinates + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_coordinates [ + shape=box, + class="verify", + timeout=300, + prompt="Verify coordinate mapping. + +Run: +1. go test ./pkg/dttf/tracer/... -run TestCoordinates -v +2. Check round-trip accuracy +3. Verify integer output + +Write results to .ai/verify_coordinates.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_coordinates [shape=diamond, label="Coords OK?"] + + // Main tracer integration + impl_tracer_main [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 4.2 (API Surface). + +Implement in pkg/dttf/tracer.go: +- TraceGlyph(bitmap GlyphBitmap, opts TraceOptions) ([]Contour, error) +- Integrate all 4 tracer phases plus coordinate mapping +- Handle parallelization across glyphs (coordinate for later) + +Read types from pkg/dttf/types.go. +Read phase implementations from pkg/dttf/tracer/*.go. + +Create integration tests. + +Run: go test ./pkg/dttf/... -run TestTrace + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_tracer_main [ + shape=box, + class="verify", + timeout=300, + prompt="Verify main tracer integration. + +Run: +1. go test ./pkg/dttf/... -run TestTrace -v +2. Check end-to-end: bitmap in, contours out +3. Verify all phases are called + +Write results to .ai/verify_tracer_main.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_tracer_main [shape=diamond, label="Tracer OK?"] + + // Metrics computation + impl_metrics [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 2.6 (Glyph Metrics). + +Implement in pkg/dttf/metrics.go: +- ComputeMetrics(contours []Contour, opts Options) (advanceWidth uint16, lsb int16, bbox BoundingBox) +- BoundingBox calculation from contour points +- Advance width = bbox width + sidebearings +- Sidebearing strategy per spec + +Create tests with known glyph shapes. + +Run: go test ./pkg/dttf/... -run TestMetrics + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_metrics [ + shape=box, + class="verify", + timeout=300, + prompt="Verify metrics computation. + +Run: +1. go test ./pkg/dttf/... -run TestMetrics -v +2. Check bounding box accuracy +3. Verify sidebearing calculations + +Write results to .ai/verify_metrics.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_metrics [shape=diamond, label="Metrics OK?"] + + // TrueType table builders + impl_tables [ + shape=box, + class="hard", + timeout=2400, + max_retries=3, + prompt="Read specs/dttf-v1.md sections 2.2, 8.1-8.5 (TrueType Tables). + +Implement in pkg/dttf/ttf/: +- BuildHeadTable(meta *FontMetadata) []byte +- BuildMaxpTable(glyphs []TracedGlyph) []byte +- BuildHheaTable(meta *FontMetadata, glyphs []TracedGlyph) []byte +- BuildHmtxTable(glyphs []TracedGlyph) []byte +- BuildOS2Table(meta *FontMetadata, glyphs []TracedGlyph) []byte +- BuildNameTable(meta *FontMetadata) []byte +- BuildPostTable() []byte (format 3.0) +- BuildCmapTable(glyphs []TracedGlyph) []byte (format 4) +- BuildGlyfTable(glyphs []TracedGlyph) []byte +- BuildLocaTable(glyphOffsets []uint32) []byte +- BuildGaspTable() []byte + +Each table builder is a separate function. Follow TrueType spec exactly. + +Create tests validating table structure. + +Run: go test ./pkg/dttf/ttf/... -v + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_tables [ + shape=box, + class="verify", + timeout=300, + prompt="Verify TrueType table builders. + +Run: +1. go test ./pkg/dttf/ttf/... -v +2. Check all 11 tables build without errors +3. Verify checksums are computed correctly + +Write results to .ai/verify_tables.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_tables [shape=diamond, label="Tables OK?"] + + // Font assembler + impl_assembler [ + shape=box, + class="hard", + timeout=1800, + max_retries=2, + prompt="Read specs/dttf-v1.md sections 4.2, 8.1-8.3 (Font Assembly). + +Implement in pkg/dttf/assembler.go: +- AssembleFont(glyphs []TracedGlyph, meta *FontMetadata) (*Font, error) +- Font struct (table directory + table data) +- Build all tables using pkg/dttf/ttf/* builders +- Compute table checksums +- Alphabetical table ordering +- Offset table construction + +Run: go test ./pkg/dttf/... -run TestAssemble + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_assembler [ + shape=box, + class="verify", + timeout=300, + prompt="Verify font assembler. + +Run: +1. go test ./pkg/dttf/... -run TestAssemble -v +2. Check table directory is correct +3. Verify offset calculations + +Write results to .ai/verify_assembler.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_assembler [shape=diamond, label="Assembler OK?"] + + // Font writer + impl_writer [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md sections 4.2, 8.3 (File Writing). + +Implement in pkg/dttf/writer.go: +- WriteFont(font *Font, outputPath string) error +- Write offset table, table directory, table data +- 4-byte padding +- Compute head.checksumAdjustment: 0xB1B0AFBA - file_checksum + +Create tests writing to temp files. + +Run: go test ./pkg/dttf/... -run TestWrite + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_writer [ + shape=box, + class="verify", + timeout=300, + prompt="Verify font writer. + +Run: +1. go test ./pkg/dttf/... -run TestWrite -v +2. Check output file is valid binary +3. Verify checksum calculation + +Write results to .ai/verify_writer.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_writer [shape=diamond, label="Writer OK?"] + + // Pipeline integration + impl_pipeline [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md sections 4.2, 4.3 (Main Pipeline). + +Implement in pkg/dttf/dttf.go: +- Build(inputDir string, outputPath string, opts Options) error +- Integrate: LoadGlyphs -> TraceGlyph (parallel) -> ComputeMetrics -> AssembleFont -> WriteFont +- Use goroutines for parallel glyph tracing + +Create end-to-end test with sample input directory. + +Run: go test ./pkg/dttf/... -run TestBuild + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_pipeline [ + shape=box, + class="verify", + timeout=300, + prompt="Verify main pipeline integration. + +Run: +1. go test ./pkg/dttf/... -run TestBuild -v +2. Check full pipeline: PNG dir -> .ttf file +3. Verify parallelization works + +Write results to .ai/verify_pipeline.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_pipeline [shape=diamond, label="Pipeline OK?"] + + // Validator + impl_validator [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 5.1 (Font Validity). + +Implement in pkg/dttf/validator.go: +- Validate(fontPath string) (ValidityReport, error) +- ValidityReport struct (checks + pass/fail) +- Check: loadable by golang.org/x/image/font/sfnt +- Check: contours closed +- Check: correct winding direction +- Check: no self-intersections +- Check: points at extrema + +Create tests with valid and invalid fonts. + +Run: go test ./pkg/dttf/... -run TestValidate + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_validator [ + shape=box, + class="verify", + timeout=300, + prompt="Verify font validator. + +Run: +1. go test ./pkg/dttf/... -run TestValidate -v +2. Check that valid fonts pass all checks +3. Check that invalid fonts fail appropriately + +Write results to .ai/verify_validator.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_validator [shape=diamond, label="Validator OK?"] + + // Rasterizer + impl_rasterizer [ + shape=box, + class="hard", + timeout=1800, + max_retries=2, + prompt="Read specs/dttf-v1.md section 11 (Rasterizer). + +Implement in pkg/dttf/rasterizer.go: +- Rasterize(fontPath string, outputDir string, opts RasterizeOptions) error +- Use golang.org/x/image/font/opentype for rendering +- Charset selection (ASCII, all, chars, ranges) +- Generate PNGs named per DTTF format +- Write font.json with extracted metrics +- Handle space glyph (U+0020) as white image + +Create tests with a simple font. + +Run: go test ./pkg/dttf/... -run TestRasterize + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_rasterizer [ + shape=box, + class="verify", + timeout=300, + prompt="Verify rasterizer implementation. + +Run: +1. go test ./pkg/dttf/... -run TestRasterize -v +2. Check PNG output format is correct +3. Verify font.json is written + +Write results to .ai/verify_rasterizer.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_rasterizer [shape=diamond, label="Rasterizer OK?"] + + // SSIM computation + impl_ssim [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md sections 5.2, 5.3 (SSIM Quality Metric). + +Implement in pkg/dttf/quality/ssim.go: +- SSIM(img1, img2 image.Image) float64 +- Structural Similarity Index implementation +- Window-based comparison +- Return value 0.0-1.0 + +Create tests with identical/different images. + +Run: go test ./pkg/dttf/quality/... -run TestSSIM + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_ssim [ + shape=box, + class="verify", + timeout=300, + prompt="Verify SSIM implementation. + +Run: +1. go test ./pkg/dttf/quality/... -run TestSSIM -v +2. Check identical images return 1.0 +3. Check different images return < 1.0 + +Write results to .ai/verify_ssim.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_ssim [shape=diamond, label="SSIM OK?"] + + // Test harness + impl_test_harness [ + shape=box, + class="hard", + timeout=2400, + max_retries=3, + prompt="Read specs/dttf-v1.md section 6 (Test Harness). + +Implement in pkg/dttf/test/harness.go: +- RoundTripTest(referenceFontPath string, opts TestOptions) (*TestReport, error) +- TestReport struct (per-glyph scores, aggregate, failures) +- Pipeline: render reference -> trace -> render output -> compute SSIM +- Multi-scale testing (12, 16, 24, 48, 96 px) +- Download reference fonts if needed + +Create tests with a known font. + +Run: go test ./pkg/dttf/test/... -run TestHarness + +Write status.json: outcome=success if tests pass, outcome=fail otherwise." + ] + + verify_test_harness [ + shape=box, + class="verify", + timeout=300, + prompt="Verify test harness implementation. + +Run: +1. go test ./pkg/dttf/test/... -run TestHarness -v +2. Check round-trip completes +3. Verify SSIM scores are computed + +Write results to .ai/verify_test_harness.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_test_harness [shape=diamond, label="Harness OK?"] + + // CLI build command + impl_cli_build [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 12 (CLI Reference). + +Implement in cmd/dttf/build.go: +- BuildCommand using a CLI framework (cobra or stdlib flag) +- Flags: -o/--output, --family, --style, --units-per-em, --threshold, etc. +- Call pkg/dttf.Build() with parsed options + +Update cmd/dttf/main.go to register command. + +Run: go build ./cmd/dttf && ./cmd/dttf build --help + +Write status.json: outcome=success if help displays, outcome=fail otherwise." + ] + + verify_cli_build [ + shape=box, + class="verify", + timeout=300, + prompt="Verify CLI build command. + +Run: +1. go build ./cmd/dttf +2. ./cmd/dttf build --help +3. Check all flags are present + +Write results to .ai/verify_cli_build.md. +Write status.json: outcome=success if help works, outcome=fail with details." + ] + + check_cli_build [shape=diamond, label="CLI Build OK?"] + + // CLI rasterize command + impl_cli_rasterize [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 12 (CLI Reference). + +Implement in cmd/dttf/rasterize.go: +- RasterizeCommand +- Flags: -o/--output-dir, --ascii, --all, --chars, --range, --height +- Call pkg/dttf.Rasterize() with parsed options + +Update cmd/dttf/main.go to register command. + +Run: go build ./cmd/dttf && ./cmd/dttf rasterize --help + +Write status.json: outcome=success if help displays, outcome=fail otherwise." + ] + + verify_cli_rasterize [ + shape=box, + class="verify", + timeout=300, + prompt="Verify CLI rasterize command. + +Run: +1. go build ./cmd/dttf +2. ./cmd/dttf rasterize --help +3. Check all flags are present + +Write results to .ai/verify_cli_rasterize.md. +Write status.json: outcome=success if help works, outcome=fail with details." + ] + + check_cli_rasterize [shape=diamond, label="CLI Rasterize OK?"] + + // CLI validate command + impl_cli_validate [ + shape=box, + timeout=900, + max_retries=2, + prompt="Read specs/dttf-v1.md section 12 (CLI Reference). + +Implement in cmd/dttf/validate.go: +- ValidateCommand +- Call pkg/dttf.Validate() +- Exit 0 if valid, 1 if invalid + +Update cmd/dttf/main.go to register command. + +Run: go build ./cmd/dttf && ./cmd/dttf validate --help + +Write status.json: outcome=success if help displays, outcome=fail otherwise." + ] + + verify_cli_validate [ + shape=box, + class="verify", + timeout=300, + prompt="Verify CLI validate command. + +Run: +1. go build ./cmd/dttf +2. ./cmd/dttf validate --help + +Write results to .ai/verify_cli_validate.md. +Write status.json: outcome=success if help works, outcome=fail with details." + ] + + check_cli_validate [shape=diamond, label="CLI Validate OK?"] + + // CLI test command + impl_cli_test [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read specs/dttf-v1.md section 12 (CLI Reference). + +Implement in cmd/dttf/test.go: +- TestCommand +- Flags: --reference, --reference-dir, --sizes, --threshold, --output-dir +- Call pkg/dttf/test.RoundTripTest() +- Handle single font or directory of fonts + +Update cmd/dttf/main.go to register command. + +Run: go build ./cmd/dttf && ./cmd/dttf test --help + +Write status.json: outcome=success if help displays, outcome=fail otherwise." + ] + + verify_cli_test [ + shape=box, + class="verify", + timeout=300, + prompt="Verify CLI test command. + +Run: +1. go build ./cmd/dttf +2. ./cmd/dttf test --help +3. Check all flags are present + +Write results to .ai/verify_cli_test.md. +Write status.json: outcome=success if help works, outcome=fail with details." + ] + + check_cli_test [shape=diamond, label="CLI Test OK?"] + + // Integration test + impl_integration [ + shape=box, + class="hard", + timeout=2400, + max_retries=3, + goal_gate=true, + prompt="Read specs/dttf-v1.md sections 6.1-6.4 (Test Harness). + +Create integration test in test/integration_test.go: +1. Download or use embedded simple reference font (e.g., Roboto subset) +2. Run: dttf rasterize reference.ttf -o testdata/input/ +3. Run: dttf build testdata/input/ -o testdata/output.ttf +4. Run: dttf validate testdata/output.ttf +5. Run: dttf test --reference reference.ttf +6. Check SSIM > 0.90 + +This is an end-to-end test of the full pipeline. + +Run: go test ./test/... -v -timeout 5m + +Write status.json: outcome=success if all steps pass and SSIM > 0.90, outcome=fail with details." + ] + + verify_integration [ + shape=box, + class="verify", + timeout=600, + prompt="Verify integration test. + +Run: +1. go test ./test/... -v -timeout 5m +2. Check all pipeline stages completed +3. Verify output font is valid +4. Check SSIM threshold met + +Write results to .ai/verify_integration.md. +Write status.json: outcome=success if all pass, outcome=fail with details." + ] + + check_integration [shape=diamond, label="Integration OK?"] + + // Final review + review [ + shape=box, + class="review", + timeout=900, + goal_gate=true, + prompt="Read specs/dttf-v1.md in full. + +Review the complete DTTF implementation: +1. Check all required components from spec are implemented +2. Verify the 4-phase tracer produces quadratic Beziers +3. Check all 11 TrueType tables are built correctly +4. Verify CLI has all 4 commands (build, rasterize, validate, test) +5. Check error handling per section 9 +6. Verify quality metrics (SSIM) are computed +7. Run full test suite + +Run: +1. go build ./... +2. go test ./... -v +3. go vet ./... + +Write a review report to .ai/review.md. +Write status.json: outcome=success if complete and correct, outcome=fail with missing/broken items." + ] + + check_review [shape=diamond, label="Review OK?"] + + // Flow + start -> impl_setup -> verify_setup -> check_setup + check_setup -> impl_types [condition="outcome=success"] + check_setup -> impl_setup [condition="outcome=fail", label="retry"] + + impl_types -> verify_types -> check_types + check_types -> impl_loader [condition="outcome=success"] + check_types -> impl_types [condition="outcome=fail", label="retry"] + + impl_loader -> verify_loader -> check_loader + check_loader -> impl_imageproc [condition="outcome=success"] + check_loader -> impl_loader [condition="outcome=fail", label="retry"] + + impl_imageproc -> verify_imageproc -> check_imageproc + check_imageproc -> impl_tracer_phase1 [condition="outcome=success"] + check_imageproc -> impl_imageproc [condition="outcome=fail", label="retry"] + + impl_tracer_phase1 -> verify_tracer_phase1 -> check_tracer_phase1 + check_tracer_phase1 -> impl_tracer_phase2 [condition="outcome=success"] + check_tracer_phase1 -> impl_tracer_phase1 [condition="outcome=fail", label="retry"] + + impl_tracer_phase2 -> verify_tracer_phase2 -> check_tracer_phase2 + check_tracer_phase2 -> impl_tracer_phase3 [condition="outcome=success"] + check_tracer_phase2 -> impl_tracer_phase2 [condition="outcome=fail", label="retry"] + + impl_tracer_phase3 -> verify_tracer_phase3 -> check_tracer_phase3 + check_tracer_phase3 -> impl_tracer_phase4 [condition="outcome=success"] + check_tracer_phase3 -> impl_tracer_phase3 [condition="outcome=fail", label="retry"] + + impl_tracer_phase4 -> verify_tracer_phase4 -> check_tracer_phase4 + check_tracer_phase4 -> impl_coordinates [condition="outcome=success"] + check_tracer_phase4 -> impl_tracer_phase4 [condition="outcome=fail", label="retry"] + + impl_coordinates -> verify_coordinates -> check_coordinates + check_coordinates -> impl_tracer_main [condition="outcome=success"] + check_coordinates -> impl_coordinates [condition="outcome=fail", label="retry"] + + impl_tracer_main -> verify_tracer_main -> check_tracer_main + check_tracer_main -> impl_metrics [condition="outcome=success"] + check_tracer_main -> impl_tracer_main [condition="outcome=fail", label="retry"] + + impl_metrics -> verify_metrics -> check_metrics + check_metrics -> impl_tables [condition="outcome=success"] + check_metrics -> impl_metrics [condition="outcome=fail", label="retry"] + + impl_tables -> verify_tables -> check_tables + check_tables -> impl_assembler [condition="outcome=success"] + check_tables -> impl_tables [condition="outcome=fail", label="retry"] + + impl_assembler -> verify_assembler -> check_assembler + check_assembler -> impl_writer [condition="outcome=success"] + check_assembler -> impl_assembler [condition="outcome=fail", label="retry"] + + impl_writer -> verify_writer -> check_writer + check_writer -> impl_pipeline [condition="outcome=success"] + check_writer -> impl_writer [condition="outcome=fail", label="retry"] + + impl_pipeline -> verify_pipeline -> check_pipeline + check_pipeline -> impl_validator [condition="outcome=success"] + check_pipeline -> impl_pipeline [condition="outcome=fail", label="retry"] + + impl_validator -> verify_validator -> check_validator + check_validator -> impl_rasterizer [condition="outcome=success"] + check_validator -> impl_validator [condition="outcome=fail", label="retry"] + + impl_rasterizer -> verify_rasterizer -> check_rasterizer + check_rasterizer -> impl_ssim [condition="outcome=success"] + check_rasterizer -> impl_rasterizer [condition="outcome=fail", label="retry"] + + impl_ssim -> verify_ssim -> check_ssim + check_ssim -> impl_test_harness [condition="outcome=success"] + check_ssim -> impl_ssim [condition="outcome=fail", label="retry"] + + impl_test_harness -> verify_test_harness -> check_test_harness + check_test_harness -> impl_cli_build [condition="outcome=success"] + check_test_harness -> impl_test_harness [condition="outcome=fail", label="retry"] + + impl_cli_build -> verify_cli_build -> check_cli_build + check_cli_build -> impl_cli_rasterize [condition="outcome=success"] + check_cli_build -> impl_cli_build [condition="outcome=fail", label="retry"] + + impl_cli_rasterize -> verify_cli_rasterize -> check_cli_rasterize + check_cli_rasterize -> impl_cli_validate [condition="outcome=success"] + check_cli_rasterize -> impl_cli_rasterize [condition="outcome=fail", label="retry"] + + impl_cli_validate -> verify_cli_validate -> check_cli_validate + check_cli_validate -> impl_cli_test [condition="outcome=success"] + check_cli_validate -> impl_cli_validate [condition="outcome=fail", label="retry"] + + impl_cli_test -> verify_cli_test -> check_cli_test + check_cli_test -> impl_integration [condition="outcome=success"] + check_cli_test -> impl_cli_test [condition="outcome=fail", label="retry"] + + impl_integration -> verify_integration -> check_integration + check_integration -> review [condition="outcome=success"] + check_integration -> impl_integration [condition="outcome=fail", label="retry"] + + review -> check_review + check_review -> exit [condition="outcome=success"] + check_review -> impl_setup [condition="outcome=fail", label="fix from start"] +} diff --git a/test/kilroy/green_test_moderate.dot b/test/kilroy/green_test_moderate.dot new file mode 100644 index 000000000..79134522c --- /dev/null +++ b/test/kilroy/green_test_moderate.dot @@ -0,0 +1,592 @@ +digraph linkcheck { + graph [ + goal="Build a Go CLI tool that crawls URLs, checks links for HTTP status, and reports broken links with robots.txt support", + rankdir=LR, + default_max_retry=3, + retry_target="impl_setup", + model_stylesheet=" + * { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } + .hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + .verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; } + .review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; } + " + ] + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + // Phase 1: Requirements expansion + expand_spec [ + shape=box, + timeout=600, + prompt="Given the requirements: Build a Go CLI tool called 'linkcheck' that takes a URL as input, crawls the page, finds all links, checks each link for HTTP status, and outputs a report of broken links (4xx/5xx). It should respect robots.txt, have a configurable crawl depth (default 1), and output in both human-readable and JSON formats. + +Expand this into a detailed spec covering: +- CLI interface (flags, arguments, usage) +- Package structure (cmd/, pkg/) +- Core types and interfaces (URL, Link, Report, etc.) +- Robots.txt parsing and compliance +- HTTP client configuration (timeouts, retries) +- Crawl algorithm (depth limits, visited tracking) +- Link extraction from HTML +- Status code categorization (2xx=ok, 4xx/5xx=broken) +- Output formats (human-readable table, JSON) +- Error handling strategy +- Test plan for each component + +Write the expanded spec to .ai/spec.md. + +Write status.json: outcome=success" + ] + + // Phase 2: Project setup + impl_setup [ + shape=box, + timeout=600, + max_retries=2, + prompt="Read .ai/spec.md. + +Create the Go project structure: +- go.mod (module name: linkcheck) +- cmd/linkcheck/main.go (stub with basic CLI parsing) +- pkg/crawler/ directory +- pkg/robotstxt/ directory +- pkg/checker/ directory +- pkg/report/ directory + +Run: go build ./... + +Write status.json: outcome=success if the project builds, outcome=fail with failure_reason otherwise." + ] + + verify_setup [ + shape=box, + class="verify", + timeout=300, + prompt="Verify project setup. + +Run: +1. go build ./... +2. go vet ./... + +Check that: +- go.mod exists with correct module name +- cmd/linkcheck/main.go exists +- pkg/ directories are created + +Write results to .ai/verify_setup.md. +Write status.json: outcome=success if all checks pass, outcome=fail with details otherwise." + ] + + check_setup [shape=diamond, label="Setup OK?"] + + // Phase 3: Core types and interfaces + impl_types [ + shape=box, + timeout=900, + max_retries=2, + prompt="Read .ai/spec.md. + +Implement core types and interfaces per the spec. + +Create pkg/crawler/types.go with: +- URL type +- Link type (URL, source, depth, status code) +- CrawlConfig struct (max depth, user agent, timeout) +- Crawler interface + +Create pkg/report/types.go with: +- Report struct (total links, broken links, results slice) +- LinkResult struct (URL, status, error) +- Formatter interface + +Include comprehensive documentation for all exported types. + +Run: go build ./... + +Write status.json: outcome=success if builds, outcome=fail with failure_reason otherwise." + ] + + verify_types [ + shape=box, + class="verify", + timeout=300, + prompt="Verify core types implementation. + +Run: +1. go build ./... +2. go vet ./... + +Check that: +- pkg/crawler/types.go defines all required types +- pkg/report/types.go defines report types +- All types have godoc comments +- No compilation errors + +Write results to .ai/verify_types.md. +Write status.json: outcome=success if all pass, outcome=fail with details otherwise." + ] + + check_types [shape=diamond, label="Types OK?"] + + // Phase 4: Robots.txt parser + impl_robotstxt [ + shape=box, + class="hard", + timeout=1200, + max_retries=2, + prompt="Read .ai/spec.md section on robots.txt compliance. + +Implement robots.txt parser in pkg/robotstxt/: +- parser.go: Parse robots.txt files per RFC 9309 +- matcher.go: Check if a URL is allowed for a given user-agent +- cache.go: Cache parsed robots.txt files to avoid repeated fetches + +Handle: +- User-agent matching (specific agent, wildcards, default *) +- Allow/Disallow rules with path matching +- Crawl-delay directive +- Missing robots.txt (allow all) +- Malformed robots.txt (be permissive) + +Create pkg/robotstxt/parser_test.go with tests for: +- Various robots.txt formats +- User-agent matching +- Path matching edge cases + +Read: pkg/crawler/types.go for interfaces. + +Run: go test ./pkg/robotstxt/... + +Write status.json: outcome=success if all tests pass, outcome=fail with failure_reason otherwise." + ] + + verify_robotstxt [ + shape=box, + class="verify", + timeout=300, + prompt="Verify robots.txt parser implementation. + +Run: +1. go build ./... +2. go vet ./pkg/robotstxt/... +3. go test ./pkg/robotstxt/... -v + +Check test coverage: +4. go test ./pkg/robotstxt/... -cover + +Write results to .ai/verify_robotstxt.md. +Write status.json: outcome=success if all pass and coverage > 70%, outcome=fail with details otherwise." + ] + + check_robotstxt [shape=diamond, label="Robots.txt OK?"] + + // Phase 5: HTTP checker + impl_checker [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read .ai/spec.md section on HTTP checking. + +Implement HTTP link checker in pkg/checker/: +- checker.go: Check URL status codes +- Configure HTTP client with reasonable timeouts +- Follow redirects (up to a limit) +- Categorize status codes (2xx/3xx=ok, 4xx/5xx=broken) +- Handle network errors gracefully +- Support HEAD requests (fallback to GET if HEAD fails) + +Create pkg/checker/checker_test.go with: +- Tests for various HTTP status codes +- Mock HTTP server for testing +- Timeout handling tests +- Redirect handling tests + +Read: pkg/crawler/types.go for types. + +Run: go test ./pkg/checker/... + +Write status.json: outcome=success if all tests pass, outcome=fail with failure_reason otherwise." + ] + + verify_checker [ + shape=box, + class="verify", + timeout=300, + prompt="Verify HTTP checker implementation. + +Run: +1. go build ./... +2. go vet ./pkg/checker/... +3. go test ./pkg/checker/... -v + +Check: +- Tests cover success, failure, and error cases +- No race conditions: go test ./pkg/checker/... -race + +Write results to .ai/verify_checker.md. +Write status.json: outcome=success if all pass, outcome=fail with details otherwise." + ] + + check_checker [shape=diamond, label="Checker OK?"] + + // Phase 6: HTML link extractor + impl_extractor [ + shape=box, + timeout=1200, + max_retries=2, + prompt="Read .ai/spec.md section on link extraction. + +Implement HTML link extractor in pkg/crawler/: +- extractor.go: Parse HTML and extract all links +- Find links in , , ,