whygraph 0.1.2 → 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.
|
@@ -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
|
@@ -62,12 +62,20 @@ function generateInstructions(config) {
|
|
|
62
62
|
"### MCP Server (preferred)",
|
|
63
63
|
"",
|
|
64
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`,
|
|
65
67
|
"",
|
|
66
|
-
"### Direct File
|
|
68
|
+
"### Direct File Access (fallback)",
|
|
67
69
|
"",
|
|
68
|
-
"If the MCP server is unreachable or MCP is blocked by your environment
|
|
69
|
-
"
|
|
70
|
-
"
|
|
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.",
|
|
71
79
|
"",
|
|
72
80
|
"### Server Status",
|
|
73
81
|
"",
|