Skip to content

Commit 3cb4260

Browse files
authored
refactor: adopt dead helpers across codebase (#895)
* refactor: adopt dead helpers across codebase Wire up extracted helpers from Titan runs that existed but were never consumed, reducing boilerplate and improving error specificity. - Adopt named_child_text across 27 sites in 11 Rust extractors - Migrate cpp.rs from hand-rolled find_cpp_parent_class to find_enclosing_type_name - Add toSymbolRef helper in shared/normalize.ts, adopt at 15 mapping sites - Wire ParseError in parser.ts for structured PARSE_FAILED error codes - Wire BoundaryError in boundaries.ts to distinguish config/DB failures from clean results - Add --modules/--threshold flags to codegraph structure command - Wire batchQuery in CLI batch command, removing duplicated routing logic - Route detect-changes pending analysis through unified runAnalyses engine * fix: address review feedback on dead helper adoption (#895) - manifesto.ts: report 'warn' instead of 'pass' when boundary check throws - structure.ts: validate --threshold flag rejects non-numeric input - dependencies.ts: clarify intentional skip of toSymbolRef for callers * feat(skill): add /titan-grind phase and wire into /titan-run pipeline Forge extracts helpers but never completes the adoption loop — dead symbol count inflates with every run. Grind closes the gap by finding dead helpers from forge, classifying them (adopt/re-export/promote/ false-positive/remove), wiring them into consumers, and gating on a non-positive dead-symbol delta. Pipeline is now: recon → gauntlet → sync → forge → grind → close * fix(skill): add resilience and codegraph usage to /titan-grind - Track currentTarget, processedTargets, failedTargets in state for mid-run resume after interruption - Persist grind classifications to grind-targets.ndjson (append-only) so re-runs skip already-analyzed targets - Write titan-state.json after every target, not just at phase end - Add interrupted-mid-target recovery logic in edge cases - Use codegraph audit/context/fn-impact/where/query/ast before edits - Add codegraph diff-impact --staged before commits - Add codegraph build after edits to keep graph current - Add --target flag for retrying individual failures
1 parent 056f116 commit 3cb4260

30 files changed

Lines changed: 713 additions & 180 deletions

File tree

.claude/skills/titan-grind/SKILL.md

Lines changed: 447 additions & 0 deletions
Large diffs are not rendered by default.

.claude/skills/titan-run/SKILL.md

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: titan-run
3-
description: Run the full Titan Paradigm pipeline end-to-end by dispatching each phase to sub-agents with fresh context windows. Orchestrates recon → gauntlet → sync → forge automatically.
4-
argument-hint: <path (default: .)> <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge> <--gauntlet-batch-size 5> <--yes>
3+
description: Run the full Titan Paradigm pipeline end-to-end by dispatching each phase to sub-agents with fresh context windows. Orchestrates recon → gauntlet → sync → forge → grind automatically.
4+
argument-hint: <path (default: .)> <--skip-recon> <--skip-gauntlet> <--start-from recon|gauntlet|sync|forge|grind> <--gauntlet-batch-size 5> <--yes>
55
allowed-tools: Agent, Read, Bash, Glob, Write, Edit
66
---
77

@@ -16,7 +16,7 @@ You are the **orchestrator** for the full Titan Paradigm pipeline. Your job is t
1616
- `<path>` → target path (passed to recon)
1717
- `--skip-recon` → skip recon (assumes artifacts exist)
1818
- `--skip-gauntlet` → skip gauntlet (assumes artifacts exist)
19-
- `--start-from <phase>` → jump to phase: `recon`, `gauntlet`, `sync`, `forge`
19+
- `--start-from <phase>` → jump to phase: `recon`, `gauntlet`, `sync`, `forge`, `grind`
2020
- `--gauntlet-batch-size <N>` → batch size for gauntlet (default: 5)
2121
- `--yes` → skip all confirmation prompts in the orchestrator (pre-pipeline, forge checkpoint, and resume prompts) and in forge (per-phase confirmation)
2222

@@ -62,11 +62,11 @@ You are the **orchestrator** for the full Titan Paradigm pipeline. Your job is t
6262
```bash
6363
npm install -g @optave/codegraph@latest
6464
```
65-
Log the installed version (skip if codegraph is not available):
65+
Log the installed version:
6666
```bash
67-
codegraph --version || true
67+
codegraph --version
6868
```
69-
If the install fails or `codegraph` is not found, warn the user but continue — the sub-agents may still work if a project-local version is available.
69+
If the install fails, warn the user but continue with whichever version is currently available.
7070

7171
5. **Sync with main** (once, before any sub-agent runs):
7272
```bash
@@ -81,9 +81,10 @@ You are the **orchestrator** for the full Titan Paradigm pipeline. Your job is t
8181
Starting from: <phase>
8282
Gauntlet batch size: <N>
8383
84-
Phases: recon → gauntlet (loop) → sync → [PAUSE] → forge (loop)
84+
Phases: recon → gauntlet (loop) → sync → [PAUSE] → forge (loop) → grind (loop) → close
8585
Each phase runs in a sub-agent with a fresh context window.
8686
Forge requires explicit confirmation (analysis phases are safe to automate).
87+
Grind runs after forge to adopt extracted helpers into consumers.
8788
```
8889

8990
Start immediately — do NOT ask for confirmation before analysis phases. The user invoked `/titan-run`; that is the confirmation. Analysis phases (recon, gauntlet, sync) are read-only and safe to automate. The forge checkpoint (Step 3.5b) still applies unless `--yes` is set.
@@ -141,6 +142,7 @@ For each phase BEFORE `startPhase`, run the corresponding V-checks:
141142
| `recon` | V1 structural fields only (domains, batches, priorityQueue, stats — skip `currentPhase == "recon"` check since later phases advance it), V2 (GLOBAL_ARCH.md), V3 (snapshot exists — WARN if missing), V4 (cross-check counts) |
142143
| `gauntlet` | V5 (coverage ≥ 50%), V6 (entry completeness sample), V7 (summary consistency); also run NDJSON integrity check (2c) |
143144
| `sync` | V8 (sync.json structure), V9 (targets trace to gauntlet), V10 (dependency order) |
145+
| `forge` | V14 (final state consistency), V15 (gate log consistency); execution block must exist in titan-state.json |
144146

145147
If ANY required artifact is **missing** → stop: "Cannot start from `<phase>``<artifact>` is missing. Run the full pipeline or start from an earlier phase."
146148

@@ -632,6 +634,113 @@ Record `phaseTimestamps.forge.completedAt`.
632634
633635
---
634636
637+
## Step 4.5 — GRIND (loop)
638+
639+
Grind runs after forge to close the adoption loop. Forge extracts helpers; grind wires them into consumers and removes dead code. Without grind, the dead symbol count inflates with every forge phase.
640+
641+
**Skip if:** `--start-from` is `close`, or `titan-state.json → grind.completedPhases` already covers all forge phases.
642+
643+
### 4.5a. Pre-loop check
644+
645+
Record `phaseTimestamps.grind.startedAt` (only if not already set — grind may be resuming).
646+
647+
Read `.codegraph/titan/sync.json` → count total phases in `executionOrder`.
648+
Read `.codegraph/titan/titan-state.json` → check `grind.completedPhases` (may not exist yet if grind hasn't started).
649+
650+
### 4.5b. Grind loop
651+
652+
Set `maxIterations = 20` (safety limit — same as forge).
653+
Set `stallCount = 0`, `maxStalls = 2`.
654+
655+
```
656+
previousGrindPhases = grind.completedPhases (or [])
657+
iteration = 0
658+
659+
while iteration < maxIterations:
660+
iteration += 1
661+
662+
# Run Pre-Agent Gate (G1-G4)
663+
664+
# Determine next forge phase to grind
665+
Read .codegraph/titan/titan-state.json
666+
grindCompleted = grind.completedPhases (or [])
667+
forgePhases = execution.completedPhases (or [])
668+
ungroundPhases = forgePhases.filter(p => !grindCompleted.includes(p))
669+
if len(ungroundPhases) == 0 → break
670+
671+
nextPhase = ungroundPhases[0]
672+
673+
headBefore = $(git rev-parse HEAD)
674+
675+
yesFlag = "--yes" if autoConfirm else ""
676+
Agent → "Run /titan-grind --phase <nextPhase> <yesFlag>.
677+
Read .claude/skills/titan-grind/SKILL.md and follow it exactly.
678+
Skip worktree check and main sync — already handled.
679+
680+
For each dead helper from forge phase <nextPhase>:
681+
1. Classify: adopt / re-export / promote / false-positive / intentionally-private / remove
682+
2. For adopt/re-export/promote: wire consumers, stage, run /titan-gate, commit
683+
3. For remove: delete, stage, run /titan-gate, commit
684+
4. Gate on dead-symbol delta at phase end"
685+
686+
# Post-agent checks
687+
headAfter = $(git rev-parse HEAD)
688+
689+
Read .codegraph/titan/titan-state.json
690+
newGrindPhases = grind.completedPhases (or [])
691+
692+
if newGrindPhases == previousGrindPhases:
693+
stallCount += 1
694+
Print: "WARNING: Grind iteration <iteration> made no progress (stall <stallCount>/<maxStalls>)"
695+
if stallCount >= maxStalls:
696+
Stop: "Grind stalled on phase <nextPhase>. Check titan-state.json → grind for details."
697+
else:
698+
stallCount = 0
699+
700+
previousGrindPhases = newGrindPhases
701+
702+
# V16. Commit audit
703+
if headAfter != headBefore:
704+
git log --oneline <headBefore>..<headAfter>
705+
commitCount = number of commits
706+
Print: "Grind phase <nextPhase>: <commitCount> adoption commits"
707+
else:
708+
Print: "Grind phase <nextPhase>: no adoptions needed (forge wired everything correctly)"
709+
710+
# V17. Test suite still green (same as forge V13)
711+
if headAfter != headBefore:
712+
testCmd = <same detection as forge V13>
713+
if testCmd != "NO_TEST_SCRIPT":
714+
Run: <testCmd> 2>&1
715+
if tests fail:
716+
Print: "CRITICAL: Test suite fails after grind phase <nextPhase>. Stopping pipeline."
717+
Print: "Commits from this phase: git log --oneline <headBefore>..<headAfter>"
718+
Stop.
719+
```
720+
721+
### 4.5c. Post-loop validation
722+
723+
**V18. Dead-symbol delta:**
724+
Read `grind.deadSymbolBaseline` and `grind.deadSymbolCurrent` from `titan-state.json`.
725+
- If delta > 10: **WARN** "Grind could not fully adopt forge's helpers. <delta> new dead symbols remain."
726+
- Otherwise: Print summary.
727+
728+
**V19. Grind coverage:**
729+
- Count forge phases processed vs total forge phases
730+
- If < 100%: **WARN** with details
731+
732+
Print grind summary:
733+
```
734+
GRIND complete.
735+
Dead symbols: <baseline><current> (delta: <+/-N>)
736+
Adoptions: <N> helpers wired, <N> removed, <N> false positives logged
737+
Phases ground: <N>/<M>
738+
```
739+
740+
Record `phaseTimestamps.grind.completedAt`.
741+
742+
---
743+
635744
## Step 5 — CLOSE (report + PRs)
636745
637746
After forge completes, dispatch `/titan-close` to produce the final report with before/after metrics and split commits into focused PRs.
@@ -676,7 +785,7 @@ Record `phaseTimestamps.close.completedAt`.
676785
677786
- **You are the orchestrator, not the executor.** Never run codegraph commands, edit source files, or make commits yourself. Only spawn sub-agents and read state files. Exceptions (pure validation/snapshot, no code changes): the post-forge test run (V13), NDJSON integrity checks, the V3 baseline snapshot check (`codegraph snapshot list`), and the pre-forge architectural snapshot capture (Step 3.5a) are run directly by the orchestrator.
678787
- **Run the Pre-Agent Gate (G1-G4) before EVERY sub-agent.** No exceptions.
679-
- **One sub-agent at a time.** Phases are sequential — recon before gauntlet, gauntlet before sync, sync before forge.
788+
- **One sub-agent at a time.** Phases are sequential — recon before gauntlet, gauntlet before sync, sync before forge, forge before grind, grind before close.
680789
- **Fresh context per sub-agent.** This is the whole point — each sub-agent gets a clean context window.
681790
- **Read AND validate state files after every sub-agent.** Trust the on-disk state, not the sub-agent's text output — but verify the state is structurally sound.
682791
- **Back up state before every sub-agent.** The `.bak` file is your safety net against mid-write crashes.

crates/codegraph-core/src/extractors/c.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,11 @@ fn match_c_node(node: &Node, source: &[u8], symbols: &mut FileSymbols, _depth: u
297297
});
298298
}
299299
"field_expression" => {
300-
let name = fn_node.child_by_field_name("field")
301-
.map(|n| node_text(&n, source).to_string())
300+
let name = named_child_text(&fn_node, "field", source)
301+
.map(|s| s.to_string())
302302
.unwrap_or_else(|| node_text(&fn_node, source).to_string());
303-
let receiver = fn_node.child_by_field_name("argument")
304-
.map(|n| node_text(&n, source).to_string());
303+
let receiver = named_child_text(&fn_node, "argument", source)
304+
.map(|s| s.to_string());
305305
symbols.calls.push(Call {
306306
name,
307307
line: start_line(node),

crates/codegraph-core/src/extractors/cpp.rs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -171,21 +171,6 @@ fn extract_cpp_enum_constants(node: &Node, source: &[u8]) -> Vec<Definition> {
171171
constants
172172
}
173173

174-
fn find_cpp_parent_class<'a>(node: &Node<'a>, source: &[u8]) -> Option<String> {
175-
let mut current = node.parent();
176-
while let Some(parent) = current {
177-
match parent.kind() {
178-
"class_specifier" | "struct_specifier" => {
179-
return parent.child_by_field_name("name")
180-
.map(|n| node_text(&n, source).to_string());
181-
}
182-
_ => {}
183-
}
184-
current = parent.parent();
185-
}
186-
None
187-
}
188-
189174
fn extract_cpp_base_classes(node: &Node, source: &[u8], class_name: &str, symbols: &mut FileSymbols) {
190175
for i in 0..node.child_count() {
191176
if let Some(child) = node.child(i) {
@@ -214,7 +199,7 @@ fn extract_cpp_base_classes(node: &Node, source: &[u8], class_name: &str, symbol
214199

215200
fn handle_cpp_function_definition(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
216201
if let Some(name) = extract_cpp_function_name(node, source) {
217-
let parent_class = find_cpp_parent_class(node, source);
202+
let parent_class = find_enclosing_type_name(node, &["class_specifier", "struct_specifier"], source);
218203
let full_name = match &parent_class {
219204
Some(cls) => format!("{}.{}", cls, name),
220205
None => name,
@@ -360,11 +345,11 @@ fn handle_cpp_call_expression(node: &Node, source: &[u8], symbols: &mut FileSymb
360345
});
361346
}
362347
"field_expression" => {
363-
let name = fn_node.child_by_field_name("field")
364-
.map(|n| node_text(&n, source).to_string())
348+
let name = named_child_text(&fn_node, "field", source)
349+
.map(|s| s.to_string())
365350
.unwrap_or_else(|| node_text(&fn_node, source).to_string());
366-
let receiver = fn_node.child_by_field_name("argument")
367-
.map(|n| node_text(&n, source).to_string());
351+
let receiver = named_child_text(&fn_node, "argument", source)
352+
.map(|s| s.to_string());
368353
symbols.calls.push(Call {
369354
name,
370355
line: start_line(node),

crates/codegraph-core/src/extractors/csharp.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ fn handle_invocation_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols)
221221
}
222222
"member_access_expression" => {
223223
if let Some(name) = fn_node.child_by_field_name("name") {
224-
let receiver = fn_node.child_by_field_name("expression")
225-
.map(|expr| node_text(&expr, source).to_string());
224+
let receiver = named_child_text(&fn_node, "expression", source)
225+
.map(|s| s.to_string());
226226
symbols.calls.push(Call {
227227
name: node_text(&name, source).to_string(),
228228
line: start_line(node),

crates/codegraph-core/src/extractors/go.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ fn handle_call_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
207207
}
208208
"selector_expression" => {
209209
if let Some(field) = fn_node.child_by_field_name("field") {
210-
let receiver = fn_node.child_by_field_name("operand")
211-
.map(|op| node_text(&op, source).to_string());
210+
let receiver = named_child_text(&fn_node, "operand", source)
211+
.map(|s| s.to_string());
212212
symbols.calls.push(Call {
213213
name: node_text(&field, source).to_string(),
214214
line: start_line(node),

crates/codegraph-core/src/extractors/helpers.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,8 @@ pub fn find_enclosing_type_name(node: &Node, kinds: &[&str], source: &[u8]) -> O
7575
let mut current = node.parent();
7676
while let Some(parent) = current {
7777
if kinds.contains(&parent.kind()) {
78-
return parent
79-
.child_by_field_name("name")
80-
.map(|n| node_text(&n, source).to_string());
78+
return named_child_text(&parent, "name", source)
79+
.map(|s| s.to_string());
8180
}
8281
current = parent.parent();
8382
}
@@ -541,8 +540,8 @@ fn walk_ast_nodes_with_config_depth(
541540
fn extract_constructor_name(node: &Node, source: &[u8]) -> String {
542541
// Try common field names for the constructed type
543542
for field in &["type", "class", "constructor"] {
544-
if let Some(child) = node.child_by_field_name(field) {
545-
return node_text(&child, source).to_string();
543+
if let Some(text) = named_child_text(node, field, source) {
544+
return text.to_string();
546545
}
547546
}
548547
for i in 0..node.child_count() {
@@ -599,8 +598,8 @@ fn extract_awaited_name(node: &Node, source: &[u8]) -> String {
599598
/// Extract function name from a call node.
600599
fn extract_call_name(node: &Node, source: &[u8]) -> String {
601600
for field in &["function", "method", "name"] {
602-
if let Some(fn_node) = node.child_by_field_name(field) {
603-
return node_text(&fn_node, source).to_string();
601+
if let Some(text) = named_child_text(node, field, source) {
602+
return text.to_string();
604603
}
605604
}
606605
let text = node_text(node, source);

crates/codegraph-core/src/extractors/java.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,8 @@ fn handle_import_decl(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
264264

265265
fn handle_method_invocation(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
266266
if let Some(name_node) = node.child_by_field_name("name") {
267-
let receiver = node.child_by_field_name("object")
268-
.map(|obj| node_text(&obj, source).to_string());
267+
let receiver = named_child_text(node, "object", source)
268+
.map(|s| s.to_string());
269269
symbols.calls.push(Call {
270270
name: node_text(&name_node, source).to_string(),
271271
line: start_line(node),

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ fn extract_new_expr_type_name<'a>(node: &Node<'a>, source: &'a [u8]) -> Option<&
4646
match ctor.kind() {
4747
"identifier" => Some(node_text(&ctor, source)),
4848
"member_expression" => {
49-
ctor.child_by_field_name("property").map(|p| node_text(&p, source))
49+
named_child_text(&ctor, "property", source)
5050
}
5151
_ => None,
5252
}
@@ -905,8 +905,8 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
905905
if prop.kind() == "string" || prop.kind() == "string_fragment" {
906906
let method_name = node_text(&prop, source).replace(&['\'', '"'][..], "");
907907
if !method_name.is_empty() {
908-
let receiver = fn_node.child_by_field_name("object")
909-
.map(|obj| node_text(&obj, source).to_string());
908+
let receiver = named_child_text(&fn_node, "object", source)
909+
.map(|s| s.to_string());
910910
return Some(Call {
911911
name: method_name,
912912
line: start_line(call_node),
@@ -916,8 +916,8 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
916916
}
917917
}
918918

919-
let receiver = fn_node.child_by_field_name("object")
920-
.map(|obj| node_text(&obj, source).to_string());
919+
let receiver = named_child_text(&fn_node, "object", source)
920+
.map(|s| s.to_string());
921921
Some(Call {
922922
name: prop_text.to_string(),
923923
line: start_line(call_node),
@@ -932,8 +932,8 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
932932
let method_name = node_text(&index, source)
933933
.replace(&['\'', '"', '`'][..], "");
934934
if !method_name.is_empty() && !method_name.contains('$') {
935-
let receiver = fn_node.child_by_field_name("object")
936-
.map(|obj| node_text(&obj, source).to_string());
935+
let receiver = named_child_text(&fn_node, "object", source)
936+
.map(|s| s.to_string());
937937
return Some(Call {
938938
name: method_name,
939939
line: start_line(call_node),

crates/codegraph-core/src/extractors/php.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ fn handle_function_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
255255

256256
fn handle_member_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
257257
if let Some(name) = node.child_by_field_name("name") {
258-
let receiver = node.child_by_field_name("object")
259-
.map(|obj| node_text(&obj, source).to_string());
258+
let receiver = named_child_text(node, "object", source)
259+
.map(|s| s.to_string());
260260
symbols.calls.push(Call {
261261
name: node_text(&name, source).to_string(),
262262
line: start_line(node),
@@ -268,8 +268,8 @@ fn handle_member_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
268268

269269
fn handle_scoped_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
270270
if let Some(name) = node.child_by_field_name("name") {
271-
let receiver = node.child_by_field_name("scope")
272-
.map(|s| node_text(&s, source).to_string());
271+
let receiver = named_child_text(node, "scope", source)
272+
.map(|s| s.to_string());
273273
symbols.calls.push(Call {
274274
name: node_text(&name, source).to_string(),
275275
line: start_line(node),

0 commit comments

Comments
 (0)