* 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:
5
.changeset/1825-graphify-graph-path.md
Normal file
5
.changeset/1825-graphify-graph-path.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 2013
|
||||
---
|
||||
**`graphify.graph_path` makes the knowledge-graph location configurable so one umbrella graph can serve multiple projects** — a new `.planning/config.json` key (path relative to project root, or absolute) overrides where `/gsd-graphify query|status|diff` read the graph, letting a single curated cross-repo umbrella graph serve every sibling sub-project without N drifting ~5 MB mirror copies. Previously the graph location was hardcoded to `<cwd>/.planning/graphs/` with no override; the only workaround was copying the umbrella `graph.json` into each project (which drifted, wasted disk, and could be silently overwritten by an in-project build). The diff snapshot travels with the configured graph; build stays project-scoped; unset → byte-identical default; a configured-but-missing file yields an actionable error naming the path. (#1825)
|
||||
@@ -277,7 +277,7 @@ Module owning the explicit per-runtime config-mutation dispatch table for the in
|
||||
Module owning the projection of gsd-core's artifact surfaces (`commands`, `agents`, hooks) onto the Claude Code plugin contract (`.claude-plugin/plugin.json` + `hooks/hooks.json`) — the plugin-contract sibling of the Runtime Artifact Layout Module (which projects the same surfaces onto filesystem placements). Defined mapping: `name`=`binName` (drives the `/gsd-core:` command namespace), `repository`/`homepage`=`repoUrl` (Package Identity Module), `version`/`description`/`license` from `package.json` (`version` is required for `claude plugin validate --strict`), `commands`=`./commands/gsd/`, agents via Claude Code's default `agents/` discovery (the explicit string form is schema-rejected), `hooks`=`./hooks/hooks.json`. The hook projection carries ONLY the always-on subset of the Installer Module's Claude `settings.json` wiring (check-update, context-monitor, prompt-guard, read-guard, worktree-path-guard, read-injection-scanner) via `${CLAUDE_PLUGIN_ROOT}`; config-gated opt-in hooks are excluded because a static manifest cannot honor per-project config gates, and plugin-shipped agents cannot carry hook frontmatter (so all plugin-path hook wiring lives in hooks.json). `hooks.json` covers all seven Claude Code lifecycle events: SessionStart, PreToolUse, PostToolUse, SubagentStop, Stop, PreCompact (all wired to context-monitor for context-headroom awareness), and FileChanged (matcher: `config.json` → config-reload, injects `additionalContext` when `.planning/config.json` changes mid-session). Additive — the file-copy path (Runtime Artifact Layout / Install Policy / Installer Modules) is unchanged. Conformance is validated by `claude plugin validate --strict` plus the in-repo drift-guard `tests/issue-766-plugin-manifest.test.cjs`. _Avoid_: "the plugin API", "the plugin file" (when you mean the seam). See ADR-766 and Runtime Artifact Layout Module.
|
||||
|
||||
### Knowledge Graph Module
|
||||
Module owning the graphify integration: tri-state capability gate (`isCapabilityActive('graphify', cwd)` from capability-state.cjs — requires installed AND surfaced AND config-enabled; replaces the former config-only `isGraphifyEnabled` gate, cutover in #1306), disabled response (`disabledResponse`), subprocess helper (`execGraphify`, typed `GRAPHIFY_REASON` enum), presence detection (`checkGraphifyInstalled`), version checking (`checkGraphifyVersion`), query surface (`graphifyQuery` — BFS seed-expand + budget trim), status surface (`graphifyStatus` — node/edge counts, mtime staleness, commit-staleness tri-state via `built_at_commit`/`commits_behind`/`commit_stale`), diff surface (`graphifyDiff` — added/removed/changed nodes+edges), build pre-flight (`graphifyBuild`), snapshot management (`writeSnapshot`). Config leg reads `.planning/config.json:graphify.enabled`; all three legs (install, surface, config) must be active; writes to `.planning/graphs/`. Auto-update hook (`hooks/gsd-graphify-update.sh`) triggers a detached background rebuild after HEAD-advancing git operations on the default branch when `graphify.auto_update=true`. Status file `.planning/graphs/.last-build-status.json` carries `{ ts, status, exit_code, duration_ms, head_at_build, graphify_version }`. Graph IR uses `nodes[]`, `edges[]` (or `links[]` for graphify ≥0.7 compat), `hyperedges[]`, `built_at_commit`. `commit_stale` is tri-state: `false` (known fresh), `true` (stale), `null` (unknown — no git or pre-v0.7 graph). Source: `gsd-core/bin/lib/graphify.cjs`. Skill: `commands/gsd/graphify.md`.
|
||||
Module owning the graphify integration: tri-state capability gate (`isCapabilityActive('graphify', cwd)` from capability-state.cjs — requires installed AND surfaced AND config-enabled; replaces the former config-only `isGraphifyEnabled` gate, cutover in #1306), disabled response (`disabledResponse`), subprocess helper (`execGraphify`, typed `GRAPHIFY_REASON` enum), presence detection (`checkGraphifyInstalled`), version checking (`checkGraphifyVersion`), query surface (`graphifyQuery` — BFS seed-expand + budget trim), status surface (`graphifyStatus` — node/edge counts, mtime staleness, commit-staleness tri-state via `built_at_commit`/`commits_behind`/`commit_stale`), diff surface (`graphifyDiff` — added/removed/changed nodes+edges), build pre-flight (`graphifyBuild`), snapshot management (`writeSnapshot`). Config leg reads `.planning/config.json:graphify.enabled`; all three legs (install, surface, config) must be active; writes to `.planning/graphs/`. Graph location override (#1825): `graphify.graph_path` in `.planning/config.json` (a path relative to the project root, or absolute) redirects where `graphifyQuery`/`graphifyStatus`/`graphifyDiff` read `graph.json` — so one umbrella-level cross-repo graph serves multiple sibling projects without N drifting mirror copies; the diff snapshot (`.last-build-snapshot.json`) travels with the configured graph (same dir); the auto-update status sidecar stays project-local; `writeSnapshot` honors the key (reads the configured graph, writes the snapshot alongside it); build stays project-scoped (`.planning/graphs/`) since the build skill hardcodes that destination — the umbrella graph is built in the umbrella project and sub-projects only READ it. Unset/blank/non-string → byte-identical `.planning/graphs/graph.json` default; a configured-but-missing file yields an actionable error naming the path. The key is registered in `config-schema.manifest.json` `validKeys`. Auto-update hook (`hooks/gsd-graphify-update.sh`) triggers a detached background rebuild after HEAD-advancing git operations on the default branch when `graphify.auto_update=true`. Status file `.planning/graphs/.last-build-status.json` carries `{ ts, status, exit_code, duration_ms, head_at_build, graphify_version }`. Graph IR uses `nodes[]`, `edges[]` (or `links[]` for graphify ≥0.7 compat), `hyperedges[]`, `built_at_commit`. `commit_stale` is tri-state: `false` (known fresh), `true` (stale), `null` (unknown — no git or pre-v0.7 graph). Source: `gsd-core/bin/lib/graphify.cjs`. Skill: `commands/gsd/graphify.md`.
|
||||
|
||||
### Intel Module
|
||||
Module owning the code-intelligence store: tri-state capability gate (`isCapabilityActive('intel', cwd)` from capability-state.cjs — honours installed+surfaced+config-enabled; replaces the former config-only `isIntelEnabled` gate, cutover in #1307; intel has `skills:[]` so installed/surfaced are vacuously true and the effective gate is `intel.enabled` in config), disabled response, query surface (`intelQuery` — full-text search across all intel JSON files), status surface (`intelStatus` — per-file freshness, 24-hour staleness threshold), diff surface (`intelDiff` — added/changed/removed files vs last-refresh snapshot), snapshot management (`saveRefreshSnapshot`/`intelSnapshot`), validation (`intelValidate` — existence, JSON validity, _meta.updated_at recency), api-surface render (`intelApiSurface` — generates `.planning/intel/API-SURFACE.md` from `api-map.json`), plus ungated utilities (`intelPatchMeta` — patches `_meta.updated_at` in any JSON file; `intelExtractExports` — extracts CJS/ESM exports from any JS file). Loop hook rendering gates on `state.active` (not `state.enabled`) so the `activationKey` config gate is honoured even without a per-hook `when` guard (Phase 4 tri-state alignment, #1307). Source: `gsd-core/bin/lib/intel.cjs` (generated from `src/intel.cts`). Router: `gsd-core/bin/lib/intel-command-router.cjs`. See Capability Command Family Module (ADR-959 4d-impl-4) and Loop Extension Point.
|
||||
|
||||
@@ -708,6 +708,7 @@ gsd-tools query config-set mempalace.cross_project_tunnels true
|
||||
| `graphify.enabled` | boolean | `false` | Enable the project knowledge graph. When `true`, `/gsd-graphify` builds and queries a graph in `.planning/graphs/`. Added in v1.36 |
|
||||
| `graphify.build_timeout` | number (seconds) | `300` | Maximum seconds allowed for a `/gsd-graphify build` run before it aborts. Added in v1.36 |
|
||||
| `graphify.auto_update` | boolean | `false` | **Opt-in (issue #3347).** When `true` (and `graphify.enabled` is also `true`), the bundled PostToolUse hook `hooks/gsd-graphify-update.sh` auto-rebuilds the project knowledge graph in a detached background process after `git commit/merge/pull/rebase --continue/cherry-pick` on the default branch (`git.base_branch` override, else `main`/`master`/`trunk`). Hook returns instantly; the rebuild updates `.planning/graphs/{graph.json,graph.html,GRAPH_REPORT.md}` and writes `.planning/graphs/.last-build-status.json` (`{ts, status: "running"\|"ok"\|"failed", exit_code, duration_ms, head_at_build}`). PID-locked, CI-aware (`$CI` env suppresses), bails silently if `graphify` is not on `PATH`. Default `false` so existing behaviour is unchanged after upgrade. |
|
||||
| `graphify.graph_path` | string (path) | _unset_ | **Umbrella/multi-repo support (issue #1825).** Overrides where `/gsd-graphify query\|status\|diff` read the knowledge graph from. Set to a path (relative to the project root, or absolute) pointing at a shared umbrella-level `graph.json` so a single curated cross-repo graph serves every sub-project without N drifting mirror copies. The diff snapshot (`.last-build-snapshot.json`) travels with the configured graph (same directory); the auto-update status sidecar stays project-local. Build stays project-scoped (`.planning/graphs/`) — build the umbrella graph in the umbrella project, then point sub-projects at it. When unset, behaviour is byte-identical to the historical `.planning/graphs/graph.json`. A clear, actionable error is returned when the configured file is missing. |
|
||||
|
||||
#### Multi-developer setup
|
||||
|
||||
|
||||
@@ -2626,8 +2626,9 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
- REQ-GRAPH-03: Build runs within the configurable `graphify.build_timeout` (seconds); exceeding the timeout aborts cleanly without leaving a partial graph.
|
||||
- REQ-GRAPH-04: `graphify.cjs` falls back to `graph.links` when `graph.edges` is absent so older graph artifacts keep rendering.
|
||||
- REQ-GRAPH-05: Graphify is invoked through `gsd-tools.cjs graphify ...` command handlers.
|
||||
- REQ-GRAPH-06: The knowledge-graph location is configurable via `graphify.graph_path` (issue #1825) so one umbrella-level cross-repo graph can serve multiple sibling projects; `query`/`status`/`diff` read the configured graph (relative to project root), with a byte-identical `.planning/graphs/` default when unset.
|
||||
|
||||
**Configuration:** `graphify.enabled`, `graphify.build_timeout`
|
||||
**Configuration:** `graphify.enabled`, `graphify.build_timeout`, `graphify.graph_path`
|
||||
**Reference files:** `commands/gsd/graphify.md`, `bin/lib/graphify.cjs`
|
||||
|
||||
---
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"context_window",
|
||||
"graphify.build_timeout",
|
||||
"graphify.auto_update",
|
||||
"graphify.graph_path",
|
||||
"claude_md_path",
|
||||
"claude_md_assembly.mode",
|
||||
"runtime",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"files": [
|
||||
"graphify-auto-update.slow.test.cjs",
|
||||
"graphify-command-cutover.test.cjs",
|
||||
"graphify-graph-path.test.cjs",
|
||||
"graphify-query.test.cjs",
|
||||
"graphify-visualization.test.cjs",
|
||||
"graphify.test.cjs"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e839e52071d5baa1",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e839e52071d5baa1",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e839e52071d5baa1",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e504d3b82da834ae",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e839e52071d5baa1",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"gsd-core/bin/gsd-tools.cjs": "e839e52071d5baa1",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "7d398e94c44e5268",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "c55b99e5d82f69fb",
|
||||
"gsd-core/bin/shared/model-catalog.json": "ca909d55f7966269",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
|
||||
353
tests/graphify-graph-path.test.cjs
Normal file
353
tests/graphify-graph-path.test.cjs
Normal file
@@ -0,0 +1,353 @@
|
||||
'use strict';
|
||||
|
||||
// Tests for graphify `graph_path` config override (#1825) — a single umbrella
|
||||
// graph serving multiple sibling projects.
|
||||
//
|
||||
// Boundary matrix (per the issue's acceptance criteria + triage outline):
|
||||
// (a) key unset → byte-identical default `.planning/graphs/graph.json`
|
||||
// (b) key set + file present → reads the CONFIGURED graph (not the default)
|
||||
// (c) key set + file missing → actionable error naming the path (no stack trace)
|
||||
// (d) relative path resolved against the project root (cwd)
|
||||
// + snapshot written alongside the configured graph
|
||||
// + build honors the key (graphs_dir = configured dir)
|
||||
// + VALID_CONFIG_KEYS contains graphify.graph_path
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('node:os');
|
||||
const path = require('path');
|
||||
const childProcess = require('child_process');
|
||||
const { mock } = require('node:test');
|
||||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
graphifyQuery,
|
||||
graphifyStatus,
|
||||
graphifyDiff,
|
||||
graphifyBuild,
|
||||
writeSnapshot,
|
||||
} = require('../gsd-core/bin/lib/graphify.cjs');
|
||||
|
||||
const { enableGraphify, SAMPLE_GRAPH } = require('./helpers/graphify.cjs');
|
||||
const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config-schema.cjs');
|
||||
|
||||
// ─── Fixtures (mirrors graphify-query.test.cjs surfaced-config-dir pattern) ────
|
||||
|
||||
function makeSurfacedConfigDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-graph-path-cfg-'));
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '.gsd-surface.json'),
|
||||
JSON.stringify({ baseProfile: 'full', disabledClusters: [], explicitAdds: [], explicitRemoves: [] }, null, 2) + '\n',
|
||||
'utf8',
|
||||
);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function saveSurfacedEnv() {
|
||||
const saved = {
|
||||
GSD_RUNTIME: process.env.GSD_RUNTIME,
|
||||
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||||
GSD_WORKSTREAM: process.env.GSD_WORKSTREAM,
|
||||
GSD_PROJECT: process.env.GSD_PROJECT,
|
||||
};
|
||||
return {
|
||||
restore() {
|
||||
for (const [k, v] of Object.entries(saved)) {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setGraphPath(planningDir, relOrAbs) {
|
||||
const configPath = path.join(planningDir, 'config.json');
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
config.graphify = config.graphify || {};
|
||||
config.graphify.graph_path = relOrAbs;
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
// A graph whose label is distinct from SAMPLE_GRAPH so we can prove which file was read.
|
||||
const UMBRELLA_GRAPH = {
|
||||
nodes: [
|
||||
{ id: 'u1', label: 'UmbrellaService', description: 'only present in the configured umbrella graph', type: 'service' },
|
||||
{ id: 'u2', label: 'CrossRepoEdge', description: 'cross repo', type: 'model' },
|
||||
],
|
||||
edges: [{ source: 'u1', target: 'u2', label: 'calls', confidence: 'EXTRACTED' }],
|
||||
hyperedges: [],
|
||||
};
|
||||
|
||||
// ─── VALID_CONFIG_KEYS registration ───────────────────────────────────────────
|
||||
|
||||
describe('graphify.graph_path config key', () => {
|
||||
test('is registered in VALID_CONFIG_KEYS', () => {
|
||||
assert.ok(VALID_CONFIG_KEYS.has('graphify.graph_path'), 'graphify.graph_path must be a recognised config key');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── query ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('graphify graph_path override — query', () => {
|
||||
let tmpDir, planningDir, cfgDir, env;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
enableGraphify(planningDir);
|
||||
cfgDir = makeSurfacedConfigDir();
|
||||
env = saveSurfacedEnv();
|
||||
process.env.CLAUDE_CONFIG_DIR = cfgDir;
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.GSD_WORKSTREAM;
|
||||
delete process.env.GSD_PROJECT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
cleanup(cfgDir);
|
||||
env.restore();
|
||||
});
|
||||
|
||||
test('(a) unset → reads default .planning/graphs/graph.json', () => {
|
||||
// Write the default graph and query it.
|
||||
fs.mkdirSync(path.join(planningDir, 'graphs'), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDir, 'graphs', 'graph.json'), JSON.stringify(SAMPLE_GRAPH), 'utf8');
|
||||
|
||||
const result = graphifyQuery(tmpDir, 'AuthService');
|
||||
assert.ok(!('error' in result), 'default path must succeed');
|
||||
assert.ok(result.nodes.some((n) => n.label === 'AuthService'));
|
||||
});
|
||||
|
||||
test('(b) set + present → reads the CONFIGURED graph, not the default', () => {
|
||||
// Place the default graph AND a distinct configured graph; prove the configured one wins.
|
||||
fs.mkdirSync(path.join(planningDir, 'graphs'), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDir, 'graphs', 'graph.json'), JSON.stringify(SAMPLE_GRAPH), 'utf8');
|
||||
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-q-'));
|
||||
try {
|
||||
const abs = path.join(umbrellaDir, 'graph.json');
|
||||
fs.writeFileSync(abs, JSON.stringify(UMBRELLA_GRAPH), 'utf8');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = graphifyQuery(tmpDir, 'UmbrellaService');
|
||||
assert.ok(!('error' in result), 'configured path must succeed');
|
||||
assert.ok(result.nodes.some((n) => n.label === 'UmbrellaService'), 'must read the configured graph');
|
||||
assert.ok(!result.nodes.some((n) => n.label === 'AuthService'), 'must NOT read the default graph');
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('(c) set + missing → actionable error naming the configured path (no stack trace)', () => {
|
||||
const abs = path.join(tmpDir, 'does-not-exist.json');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = graphifyQuery(tmpDir, 'anything');
|
||||
assert.ok('error' in result, 'missing configured graph must return an error');
|
||||
assert.ok(result.error.includes(abs), 'error must name the configured absolute path');
|
||||
assert.ok(/graphify.graph_path|graphify build/i.test(result.error), 'error must be actionable');
|
||||
});
|
||||
|
||||
test('(d) relative path 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); // e.g. '../gsd-umbrella-rel-XXX/graph.json'
|
||||
setGraphPath(planningDir, rel);
|
||||
|
||||
const result = graphifyQuery(tmpDir, 'UmbrellaService');
|
||||
assert.ok(!('error' in result), 'relative configured path must resolve and succeed');
|
||||
assert.ok(result.nodes.some((n) => n.label === 'UmbrellaService'));
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('empty-string graph_path is treated as unset (falls back to default)', () => {
|
||||
setGraphPath(planningDir, ' ');
|
||||
fs.mkdirSync(path.join(planningDir, 'graphs'), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDir, 'graphs', 'graph.json'), JSON.stringify(SAMPLE_GRAPH), 'utf8');
|
||||
|
||||
const result = graphifyQuery(tmpDir, 'AuthService');
|
||||
assert.ok(!('error' in result));
|
||||
assert.ok(result.nodes.some((n) => n.label === 'AuthService'), 'blank graph_path falls back to default');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── status ───────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('graphify graph_path override — status', () => {
|
||||
let tmpDir, planningDir, cfgDir, env;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
enableGraphify(planningDir);
|
||||
cfgDir = makeSurfacedConfigDir();
|
||||
env = saveSurfacedEnv();
|
||||
process.env.CLAUDE_CONFIG_DIR = cfgDir;
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.GSD_WORKSTREAM;
|
||||
delete process.env.GSD_PROJECT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
cleanup(cfgDir);
|
||||
env.restore();
|
||||
});
|
||||
|
||||
test('set + present → status reads the configured graph counts', () => {
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-st-'));
|
||||
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.node_count, UMBRELLA_GRAPH.nodes.length);
|
||||
assert.strictEqual(result.edge_count, UMBRELLA_GRAPH.edges.length);
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('set + missing → exists:false with actionable message', () => {
|
||||
const abs = path.join(tmpDir, 'missing.json');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = graphifyStatus(tmpDir);
|
||||
assert.strictEqual(result.exists, false);
|
||||
assert.ok(result.message.includes(abs), 'status message must name the configured path');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── diff + writeSnapshot (snapshot travels with the configured graph) ────────
|
||||
|
||||
describe('graphify graph_path override — diff & snapshot', () => {
|
||||
let tmpDir, planningDir, cfgDir, env;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
enableGraphify(planningDir);
|
||||
cfgDir = makeSurfacedConfigDir();
|
||||
env = saveSurfacedEnv();
|
||||
process.env.CLAUDE_CONFIG_DIR = cfgDir;
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.GSD_WORKSTREAM;
|
||||
delete process.env.GSD_PROJECT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
cleanup(cfgDir);
|
||||
env.restore();
|
||||
});
|
||||
|
||||
test('writeSnapshot reads configured graph and writes the snapshot ALONGSIDE it', () => {
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-snap-'));
|
||||
try {
|
||||
const abs = path.join(umbrellaDir, 'graph.json');
|
||||
fs.writeFileSync(abs, JSON.stringify(UMBRELLA_GRAPH), 'utf8');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = writeSnapshot(tmpDir);
|
||||
assert.strictEqual(result.saved, true);
|
||||
// Snapshot lands next to the configured graph, NOT under the project default.
|
||||
const snapAlongside = path.join(umbrellaDir, '.last-build-snapshot.json');
|
||||
const snapDefault = path.join(planningDir, 'graphs', '.last-build-snapshot.json');
|
||||
assert.ok(fs.existsSync(snapAlongside), 'snapshot must be written alongside the configured graph');
|
||||
assert.ok(!fs.existsSync(snapDefault), 'snapshot must NOT be written under the project default when graph_path is set');
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('diff reads baseline + current from the configured directory', () => {
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-diff-'));
|
||||
try {
|
||||
const abs = path.join(umbrellaDir, 'graph.json');
|
||||
fs.writeFileSync(abs, JSON.stringify(UMBRELLA_GRAPH), 'utf8');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
// First write a snapshot baseline (alongside configured graph).
|
||||
writeSnapshot(tmpDir);
|
||||
|
||||
// Mutate the configured graph (add a node) so diff sees an addition.
|
||||
const evolved = JSON.parse(JSON.stringify(UMBRELLA_GRAPH));
|
||||
evolved.nodes.push({ id: 'u3', label: 'NewNode', description: 'added', type: 'service' });
|
||||
fs.writeFileSync(abs, JSON.stringify(evolved), 'utf8');
|
||||
|
||||
const result = graphifyDiff(tmpDir);
|
||||
assert.ok(!('error' in result) && !result.no_baseline, 'diff must find the baseline alongside the configured graph');
|
||||
assert.strictEqual(result.nodes.added, 1);
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('unset → writeSnapshot writes to the default project graphs dir (byte-identical)', () => {
|
||||
fs.mkdirSync(path.join(planningDir, 'graphs'), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDir, 'graphs', 'graph.json'), JSON.stringify(SAMPLE_GRAPH), 'utf8');
|
||||
|
||||
const result = writeSnapshot(tmpDir);
|
||||
assert.strictEqual(result.saved, true);
|
||||
assert.ok(fs.existsSync(path.join(planningDir, 'graphs', '.last-build-snapshot.json')), 'unset → default snapshot location');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── build (graphs_dir honors the key) ────────────────────────────────────────
|
||||
|
||||
describe('graphify graph_path override — build', () => {
|
||||
let tmpDir, planningDir, cfgDir, env;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
enableGraphify(planningDir);
|
||||
cfgDir = makeSurfacedConfigDir();
|
||||
env = saveSurfacedEnv();
|
||||
process.env.CLAUDE_CONFIG_DIR = cfgDir;
|
||||
delete process.env.GSD_RUNTIME;
|
||||
delete process.env.GSD_WORKSTREAM;
|
||||
delete process.env.GSD_PROJECT;
|
||||
// Mock the graphify subprocess probes so build's pre-flight passes.
|
||||
mock.method(childProcess, 'spawnSync', (_cmd, args) => {
|
||||
if (args && args[0] === '--help') return { status: 0, stdout: 'Usage', stderr: '', error: undefined, signal: null };
|
||||
return { status: 0, stdout: '0.4.3\n', stderr: '', error: undefined, signal: null };
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
cleanup(tmpDir);
|
||||
cleanup(cfgDir);
|
||||
env.restore();
|
||||
});
|
||||
|
||||
test('set → build stays project-scoped (graphs_dir is the default; umbrella graph is built in the umbrella project)', () => {
|
||||
// graphify.graph_path is a READ-path override only (#1825). Build always cp's
|
||||
// into the project's `.planning/graphs/` (the build skill hardcodes that dest),
|
||||
// so graphs_dir must reflect the real destination even when graph_path is set.
|
||||
const umbrellaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-umbrella-build-'));
|
||||
try {
|
||||
const abs = path.join(umbrellaDir, 'graph.json');
|
||||
setGraphPath(planningDir, abs);
|
||||
|
||||
const result = graphifyBuild(tmpDir);
|
||||
assert.strictEqual(result.graphs_dir, path.join(planningDir, 'graphs'));
|
||||
} finally {
|
||||
cleanup(umbrellaDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('unset → graphs_dir is the default .planning/graphs (byte-identical)', () => {
|
||||
const result = graphifyBuild(tmpDir);
|
||||
assert.strictEqual(result.graphs_dir, path.join(planningDir, 'graphs'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user