* feat(#1825): configurable graphify graph location (graphify.graph_path) Add a graphify.graph_path config key (.planning/config.json) that overrides where /gsd-graphify query|status|diff read the knowledge graph, so one curated umbrella-level cross-repo graph can serve multiple sibling projects without N drifting ~5 MB mirror copies. Previously the graph location was hardcoded to <cwd>/.planning/graphs/. - src/graphify.cts: resolveGraphLocation(cwd, planningDir) honors the key (resolved relative to project root; absolute paths honored via path.resolve); falls back to the historical .planning/graphs/graph.json when unset/blank/ non-string (byte-identical). Wired into graphifyQuery, graphifyStatus, graphifyDiff (snapshot travels with the configured graph via dirname), and writeSnapshot. Configured-but-missing -> actionable error naming the path. Build stays project-scoped (skill hardcodes the cp dest); umbrella graph is built in the umbrella project, sub-projects only READ it. - config-schema.manifest.json: register graphify.graph_path in validKeys. - tests/graphify-graph-path.test.cjs: boundary matrix (unset byte-identical, set+present reads configured graph not default, set+missing actionable error, relative resolved vs project root, blank treated as unset, snapshot alongside configured graph, diff from configured dir, build project-scoped) + VALID_CONFIG_KEYS registration. - docs: CONFIGURATION.md row, FEATURES.md REQ-GRAPH-06, CONTEXT.md module note, .changeset (Added). Closes #1825 * docs(#1825): backfill changeset pr number 2013
This commit is contained in:
@@ -369,6 +369,39 @@ function countCommitsBetween(cwd: string, from: string, to: string): number | nu
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
// ─── Graph location resolution (#1825) ───────────────────────────────────────
|
||||
//
|
||||
// `graphify.graph_path` (in .planning/config.json) lets one umbrella-level graph
|
||||
// serve multiple sibling projects without a per-project mirror. When set, query /
|
||||
// status / diff read the configured graph, and the diff snapshot travels with it
|
||||
// (alongside graph.json). When unset/blank/non-string, behaviour is byte-identical
|
||||
// to the historical `<planningDir>/graphs/graph.json`.
|
||||
|
||||
const GRAPH_FILENAME = 'graph.json';
|
||||
const SNAPSHOT_FILENAME = '.last-build-snapshot.json';
|
||||
|
||||
interface GraphLocation {
|
||||
/** Absolute path to graph.json. */
|
||||
graphPath: string;
|
||||
/** True iff graphify.graph_path was explicitly configured (non-empty string). */
|
||||
configured: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the absolute graph.json location. Honors `graphify.graph_path` in
|
||||
* config.json (resolved relative to the project root, `cwd`); falls back to the
|
||||
* default `<planningDir>/graphs/graph.json` when unset/blank/non-string.
|
||||
*/
|
||||
function resolveGraphLocation(cwd: string, planningDir: string): GraphLocation {
|
||||
const config = safeReadJson(path.join(planningDir, 'config.json'));
|
||||
const graphify = (config && (config as Record<string, unknown>)['graphify']) as Record<string, unknown> | undefined;
|
||||
const configuredValue = graphify && graphify['graph_path'];
|
||||
if (typeof configuredValue === 'string' && configuredValue.trim().length > 0) {
|
||||
return { graphPath: path.resolve(cwd, configuredValue), configured: true };
|
||||
}
|
||||
return { graphPath: path.join(planningDir, 'graphs', GRAPH_FILENAME), configured: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the knowledge graph for nodes matching a term, with optional budget cap.
|
||||
* Uses seed-then-expand BFS traversal (D-01).
|
||||
@@ -377,9 +410,11 @@ function graphifyQuery(cwd: string, term: string, options: { budget?: number | n
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
if (!isCapabilityActive('graphify', cwd)) return disabledResponse();
|
||||
|
||||
const graphPath = path.join(planningDir, 'graphs', 'graph.json');
|
||||
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
||||
if (!fs.existsSync(graphPath)) {
|
||||
return { error: 'No graph built yet. Run graphify build first.' };
|
||||
return { error: configured
|
||||
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
|
||||
: 'No graph built yet. Run graphify build first.' };
|
||||
}
|
||||
|
||||
const graph = safeReadJson(graphPath);
|
||||
@@ -416,9 +451,11 @@ function graphifyStatus(cwd: string): unknown {
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
if (!isCapabilityActive('graphify', cwd)) return disabledResponse();
|
||||
|
||||
const graphPath = path.join(planningDir, 'graphs', 'graph.json');
|
||||
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
||||
if (!fs.existsSync(graphPath)) {
|
||||
return { exists: false, message: 'No graph built yet. Run graphify build to create one.' };
|
||||
return { exists: false, message: configured
|
||||
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
|
||||
: 'No graph built yet. Run graphify build to create one.' };
|
||||
}
|
||||
|
||||
const stat = fs.statSync(graphPath);
|
||||
@@ -479,8 +516,8 @@ function graphifyDiff(cwd: string): unknown {
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
if (!isCapabilityActive('graphify', cwd)) return disabledResponse();
|
||||
|
||||
const snapshotPath = path.join(planningDir, 'graphs', '.last-build-snapshot.json');
|
||||
const graphPath = path.join(planningDir, 'graphs', 'graph.json');
|
||||
const { graphPath } = resolveGraphLocation(cwd, planningDir);
|
||||
const snapshotPath = path.join(path.dirname(graphPath), SNAPSHOT_FILENAME);
|
||||
|
||||
if (!fs.existsSync(snapshotPath)) {
|
||||
return { no_baseline: true, message: 'No previous snapshot. Run graphify build first, then build again to generate a diff baseline.' };
|
||||
@@ -540,7 +577,11 @@ function graphifyBuild(cwd: string): unknown {
|
||||
|
||||
const version = checkGraphifyVersion();
|
||||
|
||||
// Ensure output directory exists (D-05)
|
||||
// Ensure output directory exists (D-05). Build stays project-scoped: the build
|
||||
// skill cp's artifacts into `<planningDir>/graphs/` regardless of graph_path, so
|
||||
// graphs_dir reflects that real destination (not the configured read location).
|
||||
// A shared umbrella graph is built in the umbrella project; sub-projects only
|
||||
// READ it via graphify.graph_path (#1825).
|
||||
const graphsDir = path.join(planningDir, 'graphs');
|
||||
fs.mkdirSync(graphsDir, { recursive: true });
|
||||
|
||||
@@ -573,7 +614,8 @@ interface SnapshotResult {
|
||||
* using platformWriteSync for crash safety.
|
||||
*/
|
||||
function writeSnapshot(cwd: string): SnapshotResult | { error: string } {
|
||||
const graphPath = path.join(cwd, '.planning', 'graphs', 'graph.json');
|
||||
const planningDir = path.join(cwd, '.planning');
|
||||
const { graphPath } = resolveGraphLocation(cwd, planningDir);
|
||||
const graph = safeReadJson(graphPath);
|
||||
if (!graph) return { error: 'Cannot write snapshot: graph.json not parseable' };
|
||||
|
||||
@@ -584,7 +626,7 @@ function writeSnapshot(cwd: string): SnapshotResult | { error: string } {
|
||||
edges: graph.edges || graph.links || [],
|
||||
};
|
||||
|
||||
const snapshotPath = path.join(cwd, '.planning', 'graphs', '.last-build-snapshot.json');
|
||||
const snapshotPath = path.join(path.dirname(graphPath), SNAPSHOT_FILENAME);
|
||||
platformWriteSync(snapshotPath, JSON.stringify(snapshot, null, 2));
|
||||
return {
|
||||
saved: true,
|
||||
|
||||
Reference in New Issue
Block a user