whygraph 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/crossref.d.ts +8 -0
- package/dist/cli/commands/crossref.js +132 -0
- package/dist/cli/index.js +2 -0
- package/dist/platform/rules.js +24 -11
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
export interface CrossrefResult {
|
|
3
|
+
outputPath: string;
|
|
4
|
+
nodeCount: number;
|
|
5
|
+
decisionCount: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function runCrossref(targetDir: string): CrossrefResult;
|
|
8
|
+
export declare function registerCrossrefCommand(program: Command): void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isStructuralNode, isDecisionNode } from "../../entity/types.js";
|
|
4
|
+
import { parseEntity } from "../../entity/parser.js";
|
|
5
|
+
import { findWhygraphDir } from "./serve.js";
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Core Logic
|
|
8
|
+
// ============================================================
|
|
9
|
+
export function runCrossref(targetDir) {
|
|
10
|
+
const projectDir = findWhygraphDir(targetDir);
|
|
11
|
+
if (!projectDir) {
|
|
12
|
+
throw new Error(`.whygraph/ not found. Run "npx whygraph init" first.`);
|
|
13
|
+
}
|
|
14
|
+
const graphDir = join(projectDir, ".whygraph", "graph");
|
|
15
|
+
const entities = loadEntities(graphDir);
|
|
16
|
+
const nodes = entities.filter(isStructuralNode);
|
|
17
|
+
const decisions = entities.filter(isDecisionNode);
|
|
18
|
+
const idToNode = new Map(nodes.map((n) => [n.id, n]));
|
|
19
|
+
const markdown = buildMarkdown(nodes, decisions, idToNode);
|
|
20
|
+
const outputPath = join(projectDir, ".whygraph", "CONTEXT.md");
|
|
21
|
+
writeFileSync(outputPath, markdown, "utf-8");
|
|
22
|
+
return { outputPath, nodeCount: nodes.length, decisionCount: decisions.length };
|
|
23
|
+
}
|
|
24
|
+
function loadEntities(graphDir) {
|
|
25
|
+
if (!existsSync(graphDir))
|
|
26
|
+
return [];
|
|
27
|
+
return readdirSync(graphDir)
|
|
28
|
+
.filter((f) => f.endsWith(".md"))
|
|
29
|
+
.flatMap((file) => {
|
|
30
|
+
const content = readFileSync(join(graphDir, file), "utf-8");
|
|
31
|
+
const entity = parseEntity(content);
|
|
32
|
+
return entity !== null ? [entity] : [];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function buildMarkdown(nodes, decisions, idToNode) {
|
|
36
|
+
const lines = [
|
|
37
|
+
"# Whygraph Context Snapshot",
|
|
38
|
+
"",
|
|
39
|
+
`_Generated: ${new Date().toISOString()}_`,
|
|
40
|
+
"_This file is auto-generated by `npx whygraph crossref`. Do not edit manually._",
|
|
41
|
+
"",
|
|
42
|
+
];
|
|
43
|
+
// Structural graph section
|
|
44
|
+
lines.push("## Graph Structure", "");
|
|
45
|
+
const apps = nodes.filter((n) => n.label === "App");
|
|
46
|
+
const features = nodes.filter((n) => n.label === "Feature");
|
|
47
|
+
const components = nodes.filter((n) => n.label === "Component");
|
|
48
|
+
for (const app of apps) {
|
|
49
|
+
lines.push(`- **App** \`${app.id}\` — ${app.name}`);
|
|
50
|
+
for (const feature of features.filter((f) => f.parent === app.id)) {
|
|
51
|
+
lines.push(` - **Feature** \`${feature.id}\` — ${feature.name}`);
|
|
52
|
+
for (const comp of components.filter((c) => c.parent === feature.id)) {
|
|
53
|
+
lines.push(` - **Component** \`${comp.id}\` — ${comp.name}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Orphaned features (no matching app)
|
|
58
|
+
for (const feature of features.filter((f) => !f.parent || !idToNode.has(f.parent))) {
|
|
59
|
+
lines.push(`- **Feature** \`${feature.id}\` — ${feature.name} _(unlinked)_`);
|
|
60
|
+
for (const comp of components.filter((c) => c.parent === feature.id)) {
|
|
61
|
+
lines.push(` - **Component** \`${comp.id}\` — ${comp.name}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push("");
|
|
65
|
+
// Decisions section
|
|
66
|
+
lines.push("## Decisions", "");
|
|
67
|
+
if (decisions.length === 0) {
|
|
68
|
+
lines.push("_No decisions recorded yet._", "");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (const d of decisions) {
|
|
72
|
+
lines.push(`### [${d.id}] ${d.title}`);
|
|
73
|
+
lines.push("");
|
|
74
|
+
const affectsLabels = d.affects
|
|
75
|
+
.map((id) => {
|
|
76
|
+
const node = idToNode.get(id);
|
|
77
|
+
return node ? `\`${id}\` (${node.name})` : `\`${id}\``;
|
|
78
|
+
})
|
|
79
|
+
.join(", ");
|
|
80
|
+
lines.push(`- **Status:** ${d.status} | **Date:** ${d.date} | **Tags:** ${d.tags.join(", ")}`);
|
|
81
|
+
if (d.affects.length > 0) {
|
|
82
|
+
lines.push(`- **Affects:** ${affectsLabels}`);
|
|
83
|
+
}
|
|
84
|
+
/* v8 ignore next 3 */
|
|
85
|
+
if (d.supersedes) {
|
|
86
|
+
lines.push(`- **Supersedes:** \`${d.supersedes}\``);
|
|
87
|
+
}
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push(`> **Context:** ${d.context}`);
|
|
90
|
+
lines.push(`> **Decision:** ${d.decision}`);
|
|
91
|
+
lines.push(`> **Tradeoffs:** ${d.tradeoffs}`);
|
|
92
|
+
lines.push(`> **Alternatives:** ${d.alternatives}`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push("---");
|
|
95
|
+
lines.push("");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
// ============================================================
|
|
101
|
+
// CLI Wiring
|
|
102
|
+
// ============================================================
|
|
103
|
+
export function registerCrossrefCommand(program) {
|
|
104
|
+
program
|
|
105
|
+
.command("crossref")
|
|
106
|
+
.description("Generate .whygraph/CONTEXT.md — a graph snapshot for use when MCP is unavailable")
|
|
107
|
+
.option("--json", "Output results as JSON")
|
|
108
|
+
.action((opts) => {
|
|
109
|
+
try {
|
|
110
|
+
const result = runCrossref(process.cwd());
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
process.stdout.write(`Context snapshot written to ${result.outputPath}\n` +
|
|
116
|
+
` Nodes: ${result.nodeCount}\n` +
|
|
117
|
+
` Decisions: ${result.decisionCount}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
/* v8 ignore next 1 */
|
|
122
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
+
if (opts.json) {
|
|
124
|
+
process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
128
|
+
}
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const { version } = require("../../package.json");
|
|
6
6
|
import { registerConfigCommand } from "./commands/config.js";
|
|
7
|
+
import { registerCrossrefCommand } from "./commands/crossref.js";
|
|
7
8
|
import { registerDownCommand } from "./commands/down.js";
|
|
8
9
|
import { registerInitCommand } from "./commands/init.js";
|
|
9
10
|
import { registerIssuesCommand } from "./commands/issues.js";
|
|
@@ -21,6 +22,7 @@ program
|
|
|
21
22
|
.description("The graph of why — so your agent knows before it touches anything.")
|
|
22
23
|
.version(version);
|
|
23
24
|
registerConfigCommand(program);
|
|
25
|
+
registerCrossrefCommand(program);
|
|
24
26
|
registerDownCommand(program);
|
|
25
27
|
registerInitCommand(program);
|
|
26
28
|
registerIssuesCommand(program);
|
package/dist/platform/rules.js
CHANGED
|
@@ -59,10 +59,23 @@ function generateInstructions(config) {
|
|
|
59
59
|
"",
|
|
60
60
|
`Allowed tags: ${tagList}`,
|
|
61
61
|
"",
|
|
62
|
-
"### MCP Server",
|
|
62
|
+
"### MCP Server (preferred)",
|
|
63
63
|
"",
|
|
64
|
-
`Use the \`whygraph\` MCP server for decision capture tools.`,
|
|
65
|
-
|
|
64
|
+
`Use the \`whygraph\` MCP server for decision capture tools when available.`,
|
|
65
|
+
`- \`whygraph_context\` — get existing decisions and structural nodes for a file`,
|
|
66
|
+
`- \`whygraph_create_decision\` — capture a new decision`,
|
|
67
|
+
"",
|
|
68
|
+
"### Direct File Access (fallback)",
|
|
69
|
+
"",
|
|
70
|
+
"If the MCP server is unreachable or MCP is blocked by your environment:",
|
|
71
|
+
"",
|
|
72
|
+
"**For context:** Read `.whygraph/CONTEXT.md` if it exists — this is a pre-built",
|
|
73
|
+
"graph snapshot generated by `npx whygraph crossref`. It lists all structural nodes",
|
|
74
|
+
"and decisions with their `affects` cross-references. If the file is absent, ask",
|
|
75
|
+
"the user to run `npx whygraph crossref` to generate it.",
|
|
76
|
+
"",
|
|
77
|
+
"**For capture:** Write the decision file directly to `.whygraph/graph/`",
|
|
78
|
+
"using the format above. It will be picked up automatically on next server start.",
|
|
66
79
|
"",
|
|
67
80
|
"### Server Status",
|
|
68
81
|
"",
|
|
@@ -89,7 +102,7 @@ function upsertMarkedSection(existing, content) {
|
|
|
89
102
|
// ============================================================
|
|
90
103
|
function registerMcpWithClaude(projectDir) {
|
|
91
104
|
try {
|
|
92
|
-
execSync("claude mcp add --scope project whygraph -- whygraph mcp", {
|
|
105
|
+
execSync("claude mcp add --scope project whygraph -- npx whygraph mcp", {
|
|
93
106
|
cwd: projectDir,
|
|
94
107
|
stdio: "ignore",
|
|
95
108
|
});
|
|
@@ -106,7 +119,7 @@ function registerMcpWithClaude(projectDir) {
|
|
|
106
119
|
}
|
|
107
120
|
const servers = (mcpJson.mcpServers ?? {});
|
|
108
121
|
if (!servers.whygraph) {
|
|
109
|
-
servers.whygraph = { type: "stdio", command: "
|
|
122
|
+
servers.whygraph = { type: "stdio", command: "npx", args: ["whygraph", "mcp"] };
|
|
110
123
|
mcpJson.mcpServers = servers;
|
|
111
124
|
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf-8");
|
|
112
125
|
}
|
|
@@ -129,7 +142,7 @@ function registerMcpWithCursor(projectDir) {
|
|
|
129
142
|
}
|
|
130
143
|
const servers = (mcp.mcpServers ?? {});
|
|
131
144
|
if (!servers.whygraph) {
|
|
132
|
-
servers.whygraph = { command: "
|
|
145
|
+
servers.whygraph = { command: "npx", args: ["whygraph", "mcp"] };
|
|
133
146
|
mcp.mcpServers = servers;
|
|
134
147
|
writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
135
148
|
}
|
|
@@ -150,7 +163,7 @@ function registerMcpWithCopilot(projectDir) {
|
|
|
150
163
|
}
|
|
151
164
|
const servers = (mcp.servers ?? {});
|
|
152
165
|
if (!servers.whygraph) {
|
|
153
|
-
servers.whygraph = { type: "stdio", command: "
|
|
166
|
+
servers.whygraph = { type: "stdio", command: "npx", args: ["whygraph", "mcp"] };
|
|
154
167
|
mcp.servers = servers;
|
|
155
168
|
writeFileSync(mcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
156
169
|
}
|
|
@@ -167,16 +180,16 @@ function writeMcpSetupMd(projectDir, environment) {
|
|
|
167
180
|
const lines = ["# Whygraph MCP Setup", ""];
|
|
168
181
|
switch (environment) {
|
|
169
182
|
case "claude-code":
|
|
170
|
-
lines.push("Run the following command in your project root:", "", "```bash", "claude mcp add --scope project whygraph -- whygraph mcp", "```");
|
|
183
|
+
lines.push("Run the following command in your project root:", "", "```bash", "claude mcp add --scope project whygraph -- npx whygraph mcp", "```");
|
|
171
184
|
break;
|
|
172
185
|
case "cursor":
|
|
173
|
-
lines.push("Add the following to `.cursor/mcp.json` in your project root:", "", "```json", JSON.stringify({ mcpServers: { whygraph: { command: "
|
|
186
|
+
lines.push("Add the following to `.cursor/mcp.json` in your project root:", "", "```json", JSON.stringify({ mcpServers: { whygraph: { command: "npx", args: ["whygraph", "mcp"] } } }, null, 2), "```");
|
|
174
187
|
break;
|
|
175
188
|
case "copilot":
|
|
176
|
-
lines.push("Add the following to `.vscode/mcp.json` in your project root:", "", "```json", JSON.stringify({ servers: { whygraph: { type: "stdio", command: "
|
|
189
|
+
lines.push("Add the following to `.vscode/mcp.json` in your project root:", "", "```json", JSON.stringify({ servers: { whygraph: { type: "stdio", command: "npx", args: ["whygraph", "mcp"] } } }, null, 2), "```");
|
|
177
190
|
break;
|
|
178
191
|
default:
|
|
179
|
-
lines.push("Add the whygraph MCP server to your AI assistant's MCP configuration.", "", "**Command:** `whygraph mcp`", "**Transport:** stdio", "", "Refer to your AI assistant's documentation for how to register MCP servers.");
|
|
192
|
+
lines.push("Add the whygraph MCP server to your AI assistant's MCP configuration.", "", "**Command:** `npx whygraph mcp`", "**Transport:** stdio", "", "Refer to your AI assistant's documentation for how to register MCP servers.");
|
|
180
193
|
}
|
|
181
194
|
writeFileSync(filePath, lines.join("\n") + "\n", "utf-8");
|
|
182
195
|
return filePath;
|