* enhance(#4836): prefer the graphify CLI for planner and researcher graph queries The planner gets one knowledge-graph query per phase and the researcher two or three, and that single shot decides which modules the plan treats as related — and therefore how tasks are ordered into waves. It was spent on the built-in reader, which seeds by case-insensitive substring match over a node's label and description and then expands a hardcoded two hops. The phase "User Authentication" seeds on `author`, `authoring` and `unauthorized` with the same weight as `authenticate`, and when the inflated payload exceeds `--budget` the trimmer drops edges by confidence tier — so the highest-confidence tier can be discarded to fit a payload that bad seeding inflated in the first place. The graphify CLI is already a hard dependency of /gsd-graphify build, and it ranks seeds (IDF weighting, trigram fuzzy matching) and applies context filters before traversal. Both prompts now prefer it and fall back to the built-in reader, branching on `command -v graphify` — the same degradation shape the repo already uses for Context7 to ctx7. Binary presence is a self-satisfying gate: a graph can only exist if the binary built it, so the fallback covers edge cases (a CI checkout with a committed graph, a binary since removed), not the common path. No new config key and no new tool grant — both agents already have Bash. The planner additionally runs `graphify affected`. The reference states its own goal as "which subsystems may be affected by changes in this phase", which is literally reverse traversal by relation; the built-in reader only approximates it with undirected two-hop expansion and has no equivalent verb, so `affected` is skipped on the fallback path. `graphify status` now reports `graph_path`, the resolved absolute graph location, on both the present and the missing branch. The CLI takes the graph location as `--graph`, and the prompts must not re-derive `.planning/graphs/graph.json` for it: that would point the CLI at a non-existent local mirror in exactly the umbrella multi-repo setup `graphify.graph_path` (#1825) exists to serve. For the same reason the presence gate in both prompts is now the `status` call itself rather than a bare `ls` of the default location, which was already blind to the override. Known limit, stated in both prompts rather than implied: the two paths return different shapes. `graphify query` emits prose and has no `--json` flag; the built-in emits JSON with per-edge confidence tiers and budget_met/budget_estimate. `--budget` also counts rendered output on one and estimated payload bytes on the other (#2738) — same flag name, different unit. Both are read by a model and nothing machine-parses the injected block. With graphify absent from PATH the injected context is byte-identical to before. Closes #4836 Emitted-Drift-Ack-Growth: gsd-phase-researcher.md — the CLI-first branch, the reason it is preferred, and the output-shape warning are the deliverable; a pointer to a part would not be read at the decision point. Emitted-Drift-Ack-Growth: gsd-planner.md — one sentence in the load_graph_context step pointer, so it stops naming the default graph path the reference no longer assumes. * docs(#4836): record the CLI-first graph query in the planner and researcher entries * chore(#4836): add changeset fragment * enhance(#4836): name the full domain word in the planner's query-term examples The reference's own example — phase "User Authentication" → term "auth" — is the exact collision the CLI-first path exists to avoid, and it stays a collision whenever the fallback path runs, since that path matches the term as a substring of label and description. * fix(#4836): surface graph_path on the unparseable-graph status branch graphifyStatus() returned graph_path on the exists:true and exists:false outcomes but not on the third, error, outcome (graph.json present but unparseable). The planner/researcher prompts gate CLI-first dispatch on exists, not on this outcome, so a corrupt graph file made them fall through to the CLI-first branch with the literal <graph> placeholder and no real path to substitute. * docs(#4836): note graph_path's trust boundary at the --graph interpolation graph_path is reflected verbatim into a double-quoted --graph argument the agent executes via Bash. It comes from graphify.graph_path, a config surface already trusted elsewhere, so this isn't a new trust boundary -- but it is a new injection site (no --graph flag existed on this call before). One-line caution for anyone hardening this later. --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -224,6 +224,93 @@ describe('graphify graph_path override — status', () => {
|
||||
assert.strictEqual(result.exists, false);
|
||||
assert.ok(result.message.includes(abs), 'status message must name the configured path');
|
||||
});
|
||||
|
||||
// `graph_path` (#4836). The planner/researcher prompts prefer the `graphify` CLI,
|
||||
// which takes the graph location as `--graph`. They read that value from status
|
||||
// rather than re-deriving `.planning/graphs/graph.json`, which would silently
|
||||
// point the CLI at a non-existent local mirror whenever the umbrella override is
|
||||
// configured — the exact case graph_path exists to serve. So status must emit the
|
||||
// SAME absolute path the built-in reader uses, on both the present and the missing
|
||||
// branch (the missing branch is what makes the error actionable at the call site).
|
||||
|
||||
test('set + present → status emits graph_path = the configured absolute path', () => {
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-gp-'));
|
||||
try {
|
||||
const abs = path.join(umbrellaDir, 'graph.json');
|
||||
fs.writeFileSync(abs, JSON.stringify(UMBRELLA_GRAPH), 'utf8');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, true);
|
||||
assert.strictEqual(result.graph_path, abs);
|
||||
assert.notStrictEqual(
|
||||
result.graph_path,
|
||||
path.join(planningDir, 'graphs', 'graph.json'),
|
||||
'graph_path must be the configured location, not the default mirror',
|
||||
);
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('set + relative → graph_path is resolved against the project root', () => {
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-rel-'));
|
||||
try {
|
||||
const abs = path.join(umbrellaDir, 'graph.json');
|
||||
fs.writeFileSync(abs, JSON.stringify(UMBRELLA_GRAPH), 'utf8');
|
||||
const rel = path.relative(tmpDir, abs);
|
||||
assert.ok(!path.isAbsolute(rel), 'fixture must feed a relative graph_path');
|
||||
setGraphPath(planningDir, rel);
|
||||
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, true);
|
||||
assert.ok(path.isAbsolute(result.graph_path), 'graph_path must be absolute');
|
||||
assert.strictEqual(result.graph_path, abs);
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('set + missing → graph_path still names the configured path', () => {
|
||||
const abs = path.join(tmpDir, 'missing.json');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, false);
|
||||
assert.strictEqual(result.graph_path, abs);
|
||||
});
|
||||
|
||||
test('unset → graph_path is the default .planning/graphs/graph.json', () => {
|
||||
fs.mkdirSync(path.join(planningDir, 'graphs'), { recursive: true });
|
||||
const def = path.join(planningDir, 'graphs', 'graph.json');
|
||||
fs.writeFileSync(def, JSON.stringify(SAMPLE_GRAPH), 'utf8');
|
||||
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, true);
|
||||
assert.strictEqual(result.graph_path, def);
|
||||
});
|
||||
|
||||
test('unset + missing → graph_path is the default path', () => {
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, false);
|
||||
assert.strictEqual(result.graph_path, path.join(planningDir, 'graphs', 'graph.json'));
|
||||
});
|
||||
|
||||
// #4836 review Minor 1: a present-but-unparseable graph.json hits neither the
|
||||
// `exists:false` nor the `exists:true` branch, so the CLI-first prompt gate
|
||||
// ("if exists is false, skip") does not catch it and the CLI call would
|
||||
// interpolate the literal `<graph>` placeholder without a real path to
|
||||
// substitute. graph_path must be present here too.
|
||||
test('present + unparseable → error response still names graph_path', () => {
|
||||
const def = path.join(planningDir, 'graphs', 'graph.json');
|
||||
fs.mkdirSync(path.dirname(def), { recursive: true });
|
||||
fs.writeFileSync(def, '{not valid json', 'utf8');
|
||||
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, undefined);
|
||||
assert.ok(result.error, 'must report the parse failure');
|
||||
assert.strictEqual(result.graph_path, def);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── diff + writeSnapshot (snapshot travels with the configured graph) ────────
|
||||
|
||||
Reference in New Issue
Block a user