@dreamtree-org/graphify 1.0.0 → 1.1.1
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/README.md +106 -35
- package/dist/chunk-6JLEILYF.js +207 -0
- package/dist/chunk-6JLEILYF.js.map +1 -0
- package/dist/{chunk-DG5FECXV.js → chunk-EW23BLJC.js} +557 -27
- package/dist/chunk-EW23BLJC.js.map +1 -0
- package/dist/chunk-YT7B6DOD.js +554 -0
- package/dist/chunk-YT7B6DOD.js.map +1 -0
- package/dist/chunk-ZPB37LLQ.js +155 -0
- package/dist/chunk-ZPB37LLQ.js.map +1 -0
- package/dist/cli/index.cjs +1939 -143
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +683 -40
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1131 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -5
- package/dist/index.d.ts +333 -5
- package/dist/index.js +50 -5
- package/dist/mcp/server.cjs +444 -56
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +65 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mysql-EJ6XOWR4.js +8 -0
- package/dist/mysql-EJ6XOWR4.js.map +1 -0
- package/package.json +2 -1
- package/src/skill/SKILL.md +45 -10
- package/dist/chunk-5ANIDX3G.js +0 -364
- package/dist/chunk-5ANIDX3G.js.map +0 -1
- package/dist/chunk-DG5FECXV.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,28 +1,187 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
analyze,
|
|
4
|
+
checkRules,
|
|
4
5
|
cluster,
|
|
5
6
|
exportGraph,
|
|
7
|
+
loadResults,
|
|
8
|
+
mergeGraphs,
|
|
9
|
+
renderLessons,
|
|
6
10
|
renderReport,
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
renderReview,
|
|
12
|
+
reviewRevisions,
|
|
13
|
+
runPipeline,
|
|
14
|
+
saveResult,
|
|
15
|
+
validateRules
|
|
16
|
+
} from "../chunk-EW23BLJC.js";
|
|
9
17
|
import {
|
|
18
|
+
affectedBy,
|
|
19
|
+
buildContextPack,
|
|
10
20
|
explainNode,
|
|
11
21
|
loadGraph,
|
|
12
22
|
queryGraph,
|
|
23
|
+
renderContextPack,
|
|
13
24
|
shortestPath,
|
|
25
|
+
testsForChangedFiles,
|
|
26
|
+
testsForNode
|
|
27
|
+
} from "../chunk-YT7B6DOD.js";
|
|
28
|
+
import {
|
|
29
|
+
validateDsn,
|
|
14
30
|
validateUrl
|
|
15
|
-
} from "../chunk-
|
|
31
|
+
} from "../chunk-6JLEILYF.js";
|
|
16
32
|
|
|
17
33
|
// src/cli/index.ts
|
|
18
|
-
import { execFile } from "child_process";
|
|
19
|
-
import * as
|
|
20
|
-
import * as
|
|
21
|
-
import * as
|
|
22
|
-
import { promisify } from "util";
|
|
34
|
+
import { execFile as execFile2 } from "child_process";
|
|
35
|
+
import * as fs7 from "fs/promises";
|
|
36
|
+
import * as os2 from "os";
|
|
37
|
+
import * as path5 from "path";
|
|
38
|
+
import { promisify as promisify2 } from "util";
|
|
23
39
|
import { watch as watchFiles } from "chokidar";
|
|
24
40
|
import { Command } from "commander";
|
|
25
41
|
|
|
42
|
+
// src/cli/commands/affected.ts
|
|
43
|
+
async function runAffected(nodeName, options = {}) {
|
|
44
|
+
const graph = await loadGraph();
|
|
45
|
+
const result = affectedBy(graph, nodeName, { maxDepth: options.depth, limit: options.limit });
|
|
46
|
+
if (!result) {
|
|
47
|
+
console.log(`No node matched "${nodeName}".`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`Impact of changing ${result.target.label} (${result.target.id}):`);
|
|
51
|
+
if (result.affected.length === 0) {
|
|
52
|
+
console.log(" Nothing in the graph depends on it \u2014 no incoming dependency edges.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let currentDepth = 0;
|
|
56
|
+
for (const node of result.affected) {
|
|
57
|
+
if (node.depth !== currentDepth) {
|
|
58
|
+
currentDepth = node.depth;
|
|
59
|
+
console.log("");
|
|
60
|
+
console.log(currentDepth === 1 ? "Directly affected:" : `Affected at depth ${currentDepth}:`);
|
|
61
|
+
}
|
|
62
|
+
const location = node.sourceLocation ? `:${node.sourceLocation}` : "";
|
|
63
|
+
console.log(
|
|
64
|
+
` ${node.label} \u2014 ${node.sourceFile}${location} (--${node.via.relation}--> ${node.via.dependsOn}, ${node.via.confidence})`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
console.log("");
|
|
68
|
+
const { EXTRACTED, INFERRED, AMBIGUOUS } = result.byConfidence;
|
|
69
|
+
console.log(
|
|
70
|
+
`Blast radius: ${result.affected.length} node(s) within ${result.maxDepth} hop(s) \u2014 ${EXTRACTED} certain (EXTRACTED), ${INFERRED} likely (INFERRED), ${AMBIGUOUS} uncertain (AMBIGUOUS).`
|
|
71
|
+
);
|
|
72
|
+
if (result.truncated) {
|
|
73
|
+
console.log("(List truncated \u2014 raise --limit to see more.)");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/cli/commands/benchmark.ts
|
|
78
|
+
import * as fs from "fs/promises";
|
|
79
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
80
|
+
async function benchmarkQuestion(graph, question, readFile7) {
|
|
81
|
+
const result = queryGraph(graph, question);
|
|
82
|
+
const graphTokens = tokensOf(JSON.stringify(result));
|
|
83
|
+
const sourceFiles = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const node of result.visited) {
|
|
85
|
+
if (node.sourceFile && !node.sourceFile.startsWith("<") && !node.sourceFile.includes("://")) {
|
|
86
|
+
sourceFiles.add(node.sourceFile);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let naiveTokens = 0;
|
|
90
|
+
let filesRead = 0;
|
|
91
|
+
let filesMissing = 0;
|
|
92
|
+
for (const file of [...sourceFiles].sort((a, b) => a.localeCompare(b))) {
|
|
93
|
+
try {
|
|
94
|
+
naiveTokens += tokensOf(await readFile7(file));
|
|
95
|
+
filesRead++;
|
|
96
|
+
} catch {
|
|
97
|
+
filesMissing++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
question,
|
|
102
|
+
graphTokens,
|
|
103
|
+
naiveTokens,
|
|
104
|
+
reduction: graphTokens > 0 && naiveTokens > 0 ? naiveTokens / graphTokens : 0,
|
|
105
|
+
filesRead,
|
|
106
|
+
filesMissing
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function defaultQuestions(graph, count = 5) {
|
|
110
|
+
const entries = [...graph.nodes()].map((id) => ({
|
|
111
|
+
id,
|
|
112
|
+
degree: graph.degree(id),
|
|
113
|
+
label: graph.getNodeAttribute(id, "label") ?? id
|
|
114
|
+
})).filter((e) => e.id.includes("::")).sort((a, b) => b.degree - a.degree || a.id.localeCompare(b.id));
|
|
115
|
+
return entries.slice(0, count).map((e) => `how does ${e.label} work`);
|
|
116
|
+
}
|
|
117
|
+
async function runBenchmark(questions) {
|
|
118
|
+
const graph = await loadGraph();
|
|
119
|
+
const effective = questions.length > 0 ? questions : defaultQuestions(graph);
|
|
120
|
+
if (effective.length === 0) {
|
|
121
|
+
console.log("The graph has no entity nodes to benchmark against \u2014 build it first.");
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const rows = [];
|
|
125
|
+
for (const question of effective) {
|
|
126
|
+
rows.push(await benchmarkQuestion(graph, question, (p) => fs.readFile(p, "utf-8")));
|
|
127
|
+
}
|
|
128
|
+
console.log("Token cost per question \u2014 graph answer vs reading the files it touches:\n");
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
const reduction = row.reduction > 0 ? `${row.reduction.toFixed(1)}x` : "n/a";
|
|
131
|
+
console.log(` "${row.question}"`);
|
|
132
|
+
console.log(
|
|
133
|
+
` graph: ~${row.graphTokens} tokens | files: ~${row.naiveTokens} tokens (${row.filesRead} file(s)${row.filesMissing ? `, ${row.filesMissing} unreadable` : ""}) | reduction: ${reduction}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const scored = rows.filter((r) => r.reduction > 0);
|
|
137
|
+
if (scored.length > 0) {
|
|
138
|
+
const avg = scored.reduce((s, r) => s + r.reduction, 0) / scored.length;
|
|
139
|
+
console.log(`
|
|
140
|
+
Average reduction: ${avg.toFixed(1)}x across ${scored.length} question(s).`);
|
|
141
|
+
} else {
|
|
142
|
+
console.log("\nNo reduction could be measured \u2014 run from the project root so source files are readable.");
|
|
143
|
+
}
|
|
144
|
+
return rows;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/cli/commands/check.ts
|
|
148
|
+
import * as fs2 from "fs/promises";
|
|
149
|
+
import * as path from "path";
|
|
150
|
+
async function runCheck(options = {}) {
|
|
151
|
+
const rulesPath = path.resolve(options.rules ?? "graphify.rules.json");
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
raw = await fs2.readFile(rulesPath, "utf-8");
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`No rules file at ${rulesPath} \u2014 create graphify.rules.json with { "rules": [{ "name": "...", "from": "src/a/**", "disallow": ["src/b/**"] }] }.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const config = validateRules(JSON.parse(raw));
|
|
161
|
+
const graph = await loadGraph();
|
|
162
|
+
const violations = checkRules(graph, config);
|
|
163
|
+
if (violations.length === 0) {
|
|
164
|
+
console.log(`OK \u2014 ${config.rules.length} rule(s), no violations.`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
console.log(`${violations.length} violation(s):`);
|
|
168
|
+
for (const v of violations) {
|
|
169
|
+
console.log(` [${v.rule}] ${v.fromNode} --${v.relation}--> ${v.toNode}`);
|
|
170
|
+
console.log(` ${v.fromFile} must not depend on ${v.toFile}${v.reason ? ` \u2014 ${v.reason}` : ""}`);
|
|
171
|
+
}
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/cli/commands/context.ts
|
|
176
|
+
import * as fs3 from "fs/promises";
|
|
177
|
+
async function runContext(task, options = {}) {
|
|
178
|
+
const graph = await loadGraph();
|
|
179
|
+
const pack = await buildContextPack(graph, task, (p) => fs3.readFile(p, "utf-8"), {
|
|
180
|
+
tokenBudget: options.budget
|
|
181
|
+
});
|
|
182
|
+
console.log(renderContextPack(pack));
|
|
183
|
+
}
|
|
184
|
+
|
|
26
185
|
// src/cli/commands/explain.ts
|
|
27
186
|
async function runExplain(nodeName) {
|
|
28
187
|
const graph = await loadGraph();
|
|
@@ -57,31 +216,372 @@ async function runExplain(nodeName) {
|
|
|
57
216
|
}
|
|
58
217
|
}
|
|
59
218
|
|
|
219
|
+
// src/cli/commands/review.ts
|
|
220
|
+
async function runReview(base, head) {
|
|
221
|
+
const diff = await reviewRevisions(process.cwd(), base, head ?? "HEAD");
|
|
222
|
+
console.log(renderReview(diff));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/cli/commands/tests.ts
|
|
226
|
+
import { execFile } from "child_process";
|
|
227
|
+
import { promisify } from "util";
|
|
228
|
+
var execFileAsync = promisify(execFile);
|
|
229
|
+
async function runTests(nodeQuery, options = {}) {
|
|
230
|
+
const graph = await loadGraph();
|
|
231
|
+
let selection;
|
|
232
|
+
if (options.changed !== void 0 && options.changed !== false) {
|
|
233
|
+
const args = ["diff", "--name-only"];
|
|
234
|
+
if (typeof options.changed === "string") args.push(options.changed);
|
|
235
|
+
const { stdout } = await execFileAsync("git", args);
|
|
236
|
+
const changedFiles = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
237
|
+
if (changedFiles.length === 0) {
|
|
238
|
+
console.log("No changed files in the diff.");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
selection = testsForChangedFiles(graph, changedFiles);
|
|
242
|
+
} else if (nodeQuery) {
|
|
243
|
+
selection = testsForNode(graph, nodeQuery);
|
|
244
|
+
if (!selection) {
|
|
245
|
+
console.log(`No node matched "${nodeQuery}".`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
console.log("Provide a node to select tests for, or --changed for the working-tree diff.");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (selection.testFiles.length === 0) {
|
|
253
|
+
console.log(
|
|
254
|
+
`No test files found in the blast radius of ${selection.target} (${selection.affectedNodeCount} affected node(s) checked) \u2014 either it's untested or the tests reach it through a path the graph does not capture.`
|
|
255
|
+
);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(`Test files for ${selection.target} (most direct first):`);
|
|
259
|
+
for (const test of selection.testFiles) {
|
|
260
|
+
const via = test.depth === 0 ? test.via : `depth ${test.depth}, via ${test.via}`;
|
|
261
|
+
console.log(` ${test.file} (${via})`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/cli/commands/global.ts
|
|
266
|
+
import * as fs4 from "fs/promises";
|
|
267
|
+
import * as os from "os";
|
|
268
|
+
import * as path2 from "path";
|
|
269
|
+
function registryDir() {
|
|
270
|
+
return path2.join(os.homedir(), ".graphify");
|
|
271
|
+
}
|
|
272
|
+
function registryPath() {
|
|
273
|
+
return path2.join(registryDir(), "global.json");
|
|
274
|
+
}
|
|
275
|
+
async function readRegistry() {
|
|
276
|
+
try {
|
|
277
|
+
const raw = await fs4.readFile(registryPath(), "utf-8");
|
|
278
|
+
const parsed = JSON.parse(raw);
|
|
279
|
+
const projects = parsed.projects;
|
|
280
|
+
if (!Array.isArray(projects)) return { projects: [] };
|
|
281
|
+
return { projects: projects.filter((p) => typeof p?.name === "string" && typeof p?.root === "string") };
|
|
282
|
+
} catch {
|
|
283
|
+
return { projects: [] };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function writeRegistry(registry) {
|
|
287
|
+
await fs4.mkdir(registryDir(), { recursive: true });
|
|
288
|
+
await fs4.writeFile(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
289
|
+
}
|
|
290
|
+
async function projectNameFor(root) {
|
|
291
|
+
try {
|
|
292
|
+
const raw = await fs4.readFile(path2.join(root, "package.json"), "utf-8");
|
|
293
|
+
const name = JSON.parse(raw).name;
|
|
294
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
return path2.basename(root);
|
|
298
|
+
}
|
|
299
|
+
async function runGlobalAdd(target = ".") {
|
|
300
|
+
const root = path2.resolve(target);
|
|
301
|
+
const graphJson = path2.join(root, "graphify-out", "graph.json");
|
|
302
|
+
try {
|
|
303
|
+
await fs4.access(graphJson);
|
|
304
|
+
} catch {
|
|
305
|
+
throw new Error(`${root} has no graphify-out/graph.json \u2014 run \`graphify ${target}\` first, then add it.`);
|
|
306
|
+
}
|
|
307
|
+
const name = await projectNameFor(root);
|
|
308
|
+
const registry = await readRegistry();
|
|
309
|
+
const existing = registry.projects.find((p) => p.name === name);
|
|
310
|
+
if (existing) {
|
|
311
|
+
if (existing.root === root) {
|
|
312
|
+
console.log(`${name} is already registered (${root}).`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
throw new Error(
|
|
316
|
+
`A different project is already registered as "${name}" (${existing.root}). Remove it first with \`graphify global remove\`.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
registry.projects.push({ name, root });
|
|
320
|
+
registry.projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
321
|
+
await writeRegistry(registry);
|
|
322
|
+
console.log(`Registered ${name} (${root}). ${registry.projects.length} project(s) in the global graph.`);
|
|
323
|
+
}
|
|
324
|
+
async function runGlobalRemove(name) {
|
|
325
|
+
const registry = await readRegistry();
|
|
326
|
+
const before = registry.projects.length;
|
|
327
|
+
registry.projects = registry.projects.filter((p) => p.name !== name);
|
|
328
|
+
if (registry.projects.length === before) {
|
|
329
|
+
throw new Error(`No project named "${name}" is registered. See \`graphify global list\`.`);
|
|
330
|
+
}
|
|
331
|
+
await writeRegistry(registry);
|
|
332
|
+
console.log(`Removed ${name}. ${registry.projects.length} project(s) remain.`);
|
|
333
|
+
}
|
|
334
|
+
async function runGlobalList() {
|
|
335
|
+
const registry = await readRegistry();
|
|
336
|
+
if (registry.projects.length === 0) {
|
|
337
|
+
console.log("No projects registered. Add one with `graphify global add <path>`.");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
for (const project of registry.projects) {
|
|
341
|
+
console.log(`${project.name} ${project.root}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function loadEntries(sources) {
|
|
345
|
+
const entries = [];
|
|
346
|
+
for (const source of sources) {
|
|
347
|
+
try {
|
|
348
|
+
entries.push({ name: source.name, graph: await loadGraph(source.outDir) });
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.warn(`Skipping ${source.name}: ${error.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return entries;
|
|
354
|
+
}
|
|
355
|
+
async function mergeAndExport(entries, outDir) {
|
|
356
|
+
if (entries.length === 0) {
|
|
357
|
+
throw new Error("Nothing to merge \u2014 no project graphs could be loaded.");
|
|
358
|
+
}
|
|
359
|
+
const merged = mergeGraphs(entries);
|
|
360
|
+
cluster(merged);
|
|
361
|
+
const analysis = analyze(merged);
|
|
362
|
+
const report = renderReport(merged, analysis);
|
|
363
|
+
await exportGraph(merged, { outDir }, report);
|
|
364
|
+
console.log(
|
|
365
|
+
`Merged ${entries.length} project(s) into ${outDir} \u2014 ${merged.order} node(s), ${merged.size} edge(s).`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
async function runGlobalBuild(options = {}) {
|
|
369
|
+
const registry = await readRegistry();
|
|
370
|
+
if (registry.projects.length === 0) {
|
|
371
|
+
throw new Error("No projects registered. Add some with `graphify global add <path>` first.");
|
|
372
|
+
}
|
|
373
|
+
const entries = await loadEntries(
|
|
374
|
+
registry.projects.map((p) => ({ name: p.name, outDir: path2.join(p.root, "graphify-out") }))
|
|
375
|
+
);
|
|
376
|
+
await mergeAndExport(entries, path2.resolve(options.out ?? path2.join(registryDir(), "global-out")));
|
|
377
|
+
}
|
|
378
|
+
async function runMerge(targets, options = {}) {
|
|
379
|
+
if (targets.length < 2) {
|
|
380
|
+
throw new Error("Provide at least two project directories to merge.");
|
|
381
|
+
}
|
|
382
|
+
const sources = [];
|
|
383
|
+
for (const target of targets) {
|
|
384
|
+
const root = path2.resolve(target);
|
|
385
|
+
sources.push({ name: await projectNameFor(root), outDir: path2.join(root, "graphify-out") });
|
|
386
|
+
}
|
|
387
|
+
const names = sources.map((s) => s.name);
|
|
388
|
+
const duplicate = names.find((name, i) => names.indexOf(name) !== i);
|
|
389
|
+
if (duplicate !== void 0) {
|
|
390
|
+
throw new Error(`Two of the given projects resolve to the same name ("${duplicate}") \u2014 rename one.`);
|
|
391
|
+
}
|
|
392
|
+
const entries = await loadEntries(sources);
|
|
393
|
+
await mergeAndExport(entries, path2.resolve(options.out ?? "graphify-merged"));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/cli/commands/hook.ts
|
|
397
|
+
import * as fs5 from "fs/promises";
|
|
398
|
+
import * as path3 from "path";
|
|
399
|
+
var MARKER_BEGIN = "# >>> graphify >>>";
|
|
400
|
+
var MARKER_END = "# <<< graphify <<<";
|
|
401
|
+
var HOOK_NAMES = ["post-commit", "post-merge"];
|
|
402
|
+
var HOOK_BLOCK = `${MARKER_BEGIN}
|
|
403
|
+
# Rebuild the graphify knowledge graph in the background so the hook never
|
|
404
|
+
# slows down the commit. Errors are silenced \u2014 a failed rebuild should never
|
|
405
|
+
# break a git operation. Installed by \`graphify hook install\`.
|
|
406
|
+
(graphify . --update --no-viz >/dev/null 2>&1 &)
|
|
407
|
+
${MARKER_END}`;
|
|
408
|
+
async function hooksDir(cwd) {
|
|
409
|
+
const gitDir = path3.join(cwd, ".git");
|
|
410
|
+
let stats;
|
|
411
|
+
try {
|
|
412
|
+
stats = await fs5.stat(gitDir);
|
|
413
|
+
} catch {
|
|
414
|
+
throw new Error(`${cwd} is not a git repository (no .git directory) \u2014 run this from the repo root.`);
|
|
415
|
+
}
|
|
416
|
+
if (!stats.isDirectory()) {
|
|
417
|
+
const content = (await fs5.readFile(gitDir, "utf-8")).trim();
|
|
418
|
+
const match = /^gitdir:\s*(.+)$/.exec(content);
|
|
419
|
+
if (!match) throw new Error(`${gitDir} exists but is neither a directory nor a gitdir pointer.`);
|
|
420
|
+
return path3.resolve(cwd, match[1], "hooks");
|
|
421
|
+
}
|
|
422
|
+
return path3.join(gitDir, "hooks");
|
|
423
|
+
}
|
|
424
|
+
async function runHookInstall(cwd = process.cwd()) {
|
|
425
|
+
const dir = await hooksDir(cwd);
|
|
426
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
427
|
+
const results = [];
|
|
428
|
+
for (const hook of HOOK_NAMES) {
|
|
429
|
+
const hookPath = path3.join(dir, hook);
|
|
430
|
+
let existing = null;
|
|
431
|
+
try {
|
|
432
|
+
existing = await fs5.readFile(hookPath, "utf-8");
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
if (existing === null) {
|
|
436
|
+
await fs5.writeFile(hookPath, `#!/bin/sh
|
|
437
|
+
${HOOK_BLOCK}
|
|
438
|
+
`, { mode: 493 });
|
|
439
|
+
results.push({ hook, path: hookPath, action: "created" });
|
|
440
|
+
} else if (existing.includes(MARKER_BEGIN)) {
|
|
441
|
+
results.push({ hook, path: hookPath, action: "already-installed" });
|
|
442
|
+
} else {
|
|
443
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
444
|
+
await fs5.writeFile(hookPath, `${existing}${separator}${HOOK_BLOCK}
|
|
445
|
+
`, "utf-8");
|
|
446
|
+
await fs5.chmod(hookPath, 493);
|
|
447
|
+
results.push({ hook, path: hookPath, action: "appended" });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const result of results) {
|
|
451
|
+
console.log(`${result.hook}: ${result.action} (${result.path})`);
|
|
452
|
+
}
|
|
453
|
+
console.log("");
|
|
454
|
+
console.log("The graph will rebuild in the background after each commit and pull.");
|
|
455
|
+
console.log("Remove with `graphify hook uninstall`.");
|
|
456
|
+
return results;
|
|
457
|
+
}
|
|
458
|
+
async function runHookUninstall(cwd = process.cwd()) {
|
|
459
|
+
const dir = await hooksDir(cwd);
|
|
460
|
+
const results = [];
|
|
461
|
+
for (const hook of HOOK_NAMES) {
|
|
462
|
+
const hookPath = path3.join(dir, hook);
|
|
463
|
+
let existing = null;
|
|
464
|
+
try {
|
|
465
|
+
existing = await fs5.readFile(hookPath, "utf-8");
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
if (existing === null || !existing.includes(MARKER_BEGIN)) {
|
|
469
|
+
results.push({ hook, path: hookPath, action: "not-installed" });
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const blockPattern = new RegExp(`\\n?${MARKER_BEGIN}[\\s\\S]*?${MARKER_END}\\n?`);
|
|
473
|
+
const remaining = existing.replace(blockPattern, "\n").trim();
|
|
474
|
+
if (remaining === "" || remaining === "#!/bin/sh") {
|
|
475
|
+
await fs5.unlink(hookPath);
|
|
476
|
+
} else {
|
|
477
|
+
await fs5.writeFile(hookPath, `${remaining}
|
|
478
|
+
`, "utf-8");
|
|
479
|
+
}
|
|
480
|
+
results.push({ hook, path: hookPath, action: "removed" });
|
|
481
|
+
}
|
|
482
|
+
for (const result of results) {
|
|
483
|
+
console.log(`${result.hook}: ${result.action}`);
|
|
484
|
+
}
|
|
485
|
+
return results;
|
|
486
|
+
}
|
|
487
|
+
|
|
60
488
|
// src/cli/commands/install.ts
|
|
61
489
|
import { createRequire } from "module";
|
|
62
|
-
import * as
|
|
63
|
-
import * as
|
|
490
|
+
import * as fs6 from "fs/promises";
|
|
491
|
+
import * as path4 from "path";
|
|
64
492
|
var require2 = createRequire(import.meta.url);
|
|
65
493
|
function packageRoot() {
|
|
66
494
|
const packageJsonPath = require2.resolve("@dreamtree-org/graphify/package.json");
|
|
67
|
-
return
|
|
495
|
+
return path4.dirname(packageJsonPath);
|
|
496
|
+
}
|
|
497
|
+
var MD_MARKER_BEGIN = "<!-- >>> graphify >>> -->";
|
|
498
|
+
var MD_MARKER_END = "<!-- <<< graphify <<< -->";
|
|
499
|
+
async function writeOwnedFile(filePath, content) {
|
|
500
|
+
await fs6.mkdir(path4.dirname(filePath), { recursive: true });
|
|
501
|
+
await fs6.writeFile(filePath, content, "utf-8");
|
|
502
|
+
}
|
|
503
|
+
async function upsertMarkedBlock(filePath, content) {
|
|
504
|
+
const block = `${MD_MARKER_BEGIN}
|
|
505
|
+
${content.trim()}
|
|
506
|
+
${MD_MARKER_END}`;
|
|
507
|
+
let existing = null;
|
|
508
|
+
try {
|
|
509
|
+
existing = await fs6.readFile(filePath, "utf-8");
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
if (existing === null) {
|
|
513
|
+
await fs6.mkdir(path4.dirname(filePath), { recursive: true });
|
|
514
|
+
await fs6.writeFile(filePath, `${block}
|
|
515
|
+
`, "utf-8");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (existing.includes(MD_MARKER_BEGIN)) {
|
|
519
|
+
const pattern = new RegExp(`${MD_MARKER_BEGIN}[\\s\\S]*?${MD_MARKER_END}`);
|
|
520
|
+
await fs6.writeFile(filePath, existing.replace(pattern, block), "utf-8");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
524
|
+
await fs6.writeFile(filePath, `${existing}${separator}${block}
|
|
525
|
+
`, "utf-8");
|
|
68
526
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
527
|
+
var PLATFORMS = {
|
|
528
|
+
claude: async (cwd, content) => {
|
|
529
|
+
const target = path4.join(cwd, ".claude", "skills", "graphify", "SKILL.md");
|
|
530
|
+
await writeOwnedFile(target, content);
|
|
531
|
+
return { host: "Claude Code", path: target };
|
|
532
|
+
},
|
|
533
|
+
cursor: async (cwd, content) => {
|
|
534
|
+
const target = path4.join(cwd, ".cursor", "rules", "graphify.mdc");
|
|
535
|
+
const body = content.replace(/^---[\s\S]*?---\n/, "");
|
|
536
|
+
await writeOwnedFile(
|
|
537
|
+
target,
|
|
538
|
+
`---
|
|
539
|
+
description: Query the graphify knowledge graph for codebase structure questions
|
|
540
|
+
alwaysApply: false
|
|
541
|
+
---
|
|
542
|
+
${body}`
|
|
543
|
+
);
|
|
544
|
+
return { host: "Cursor", path: target };
|
|
545
|
+
},
|
|
546
|
+
windsurf: async (cwd, content) => {
|
|
547
|
+
const target = path4.join(cwd, ".windsurf", "rules", "graphify.md");
|
|
548
|
+
await writeOwnedFile(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
549
|
+
return { host: "Windsurf", path: target };
|
|
550
|
+
},
|
|
551
|
+
cline: async (cwd, content) => {
|
|
552
|
+
const target = path4.join(cwd, ".clinerules", "graphify.md");
|
|
553
|
+
await writeOwnedFile(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
554
|
+
return { host: "Cline", path: target };
|
|
555
|
+
},
|
|
556
|
+
agents: async (cwd, content) => {
|
|
557
|
+
const target = path4.join(cwd, "AGENTS.md");
|
|
558
|
+
await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
559
|
+
return { host: "AGENTS.md agents (Codex, opencode, Amp, ...)", path: target };
|
|
560
|
+
},
|
|
561
|
+
gemini: async (cwd, content) => {
|
|
562
|
+
const target = path4.join(cwd, "GEMINI.md");
|
|
563
|
+
await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
564
|
+
return { host: "Gemini CLI", path: target };
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var SUPPORTED_PLATFORMS = Object.keys(PLATFORMS).sort((a, b) => a.localeCompare(b));
|
|
568
|
+
async function runInstall(platforms = ["claude"], cwd = process.cwd()) {
|
|
569
|
+
const sourcePath = path4.join(packageRoot(), "src", "skill", "SKILL.md");
|
|
570
|
+
const content = await fs6.readFile(sourcePath, "utf-8");
|
|
571
|
+
const requested = platforms.includes("all") ? SUPPORTED_PLATFORMS : platforms;
|
|
72
572
|
const results = [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
573
|
+
for (const platform of requested) {
|
|
574
|
+
const installer = PLATFORMS[platform];
|
|
575
|
+
if (!installer) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Unknown platform "${platform}" \u2014 supported: ${SUPPORTED_PLATFORMS.join(", ")}, or "all".`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
results.push(await installer(cwd, content));
|
|
581
|
+
}
|
|
78
582
|
for (const result of results) {
|
|
79
|
-
console.log(`Installed graphify
|
|
583
|
+
console.log(`Installed graphify for ${result.host} -> ${result.path}`);
|
|
80
584
|
}
|
|
81
|
-
console.log("");
|
|
82
|
-
console.log(
|
|
83
|
-
"Other hosts (Cursor, etc.) are not implemented yet by `graphify install` \u2014 see ARCHITECTURE.md."
|
|
84
|
-
);
|
|
85
585
|
return results;
|
|
86
586
|
}
|
|
87
587
|
|
|
@@ -113,7 +613,7 @@ async function runPath(fromNode, toNode) {
|
|
|
113
613
|
// src/cli/commands/query.ts
|
|
114
614
|
async function runQuery(question, options = {}) {
|
|
115
615
|
const graph = await loadGraph();
|
|
116
|
-
const result = queryGraph(graph, question, { dfs: options.dfs,
|
|
616
|
+
const result = queryGraph(graph, question, { dfs: options.dfs, tokenBudget: options.budget });
|
|
117
617
|
if (result.seeds.length === 0) {
|
|
118
618
|
console.log(
|
|
119
619
|
`No nodes matched "${question}". Try different keywords, or run \`graphify explain <node>\` if you already know the name.`
|
|
@@ -138,15 +638,15 @@ async function runQuery(question, options = {}) {
|
|
|
138
638
|
}
|
|
139
639
|
|
|
140
640
|
// src/cli/index.ts
|
|
141
|
-
var
|
|
641
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
142
642
|
function looksLikeGitUrl(value) {
|
|
143
643
|
if (!/^https?:\/\//i.test(value)) return false;
|
|
144
644
|
return /github\.com|gitlab\.com|bitbucket\.org/i.test(value) || value.endsWith(".git");
|
|
145
645
|
}
|
|
146
646
|
async function cloneRepo(url) {
|
|
147
647
|
const validated = validateUrl(url);
|
|
148
|
-
const dest = await
|
|
149
|
-
await
|
|
648
|
+
const dest = await fs7.mkdtemp(path5.join(os2.tmpdir(), "graphify-clone-"));
|
|
649
|
+
await execFileAsync2("git", ["clone", "--depth", "1", validated, dest]);
|
|
150
650
|
return dest;
|
|
151
651
|
}
|
|
152
652
|
function exportOptionsFrom(options) {
|
|
@@ -159,7 +659,7 @@ function clusterOptionsFrom(options) {
|
|
|
159
659
|
};
|
|
160
660
|
}
|
|
161
661
|
async function runClusterOnly(root, options) {
|
|
162
|
-
const outDir =
|
|
662
|
+
const outDir = path5.join(root, "graphify-out");
|
|
163
663
|
const graph = await loadGraph(outDir);
|
|
164
664
|
cluster(graph, clusterOptionsFrom(options));
|
|
165
665
|
const analysis = analyze(graph);
|
|
@@ -190,12 +690,12 @@ async function watchAndRebuild(root, runOnce) {
|
|
|
190
690
|
});
|
|
191
691
|
}
|
|
192
692
|
var program = new Command();
|
|
193
|
-
program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental
|
|
693
|
+
program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental rebuild \u2014 reuse cached extractions for unchanged files").option("--watch", "rebuild on file change").option("--cluster-only", "rerun clustering on an existing graph").option("--leiden", "use Leiden instead of Louvain for community detection (v2 \u2014 better community quality)").option("--max-iterations <n>", "Leiden only: cap on outer iterations for very large graphs", Number).option("--no-viz", "skip graph.html").option("--svg", "also export graph.svg (not implemented in v1)").option("--graphml", "also export graph.graphml (not implemented in v1)").option("--neo4j", "generate graphify-out/cypher.txt for Neo4j (not implemented in v1)").option("--mcp", "start MCP stdio server instead of running the pipeline").option("--mysql <dsn>", "also extract a MySQL schema (mysql://user:pass@host:port/db) into the graph").action(async (targetArg, options) => {
|
|
194
694
|
try {
|
|
195
695
|
if (options.mcp) {
|
|
196
|
-
const outDir =
|
|
696
|
+
const outDir = path5.resolve(targetArg === "." ? process.cwd() : targetArg, "graphify-out");
|
|
197
697
|
const { startServer } = await import("../mcp/server.js");
|
|
198
|
-
await startServer(
|
|
698
|
+
await startServer(path5.join(outDir, "graph.json"));
|
|
199
699
|
return;
|
|
200
700
|
}
|
|
201
701
|
let root = targetArg;
|
|
@@ -203,12 +703,7 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
|
|
|
203
703
|
console.error(`Cloning ${targetArg} ...`);
|
|
204
704
|
root = await cloneRepo(targetArg);
|
|
205
705
|
}
|
|
206
|
-
root =
|
|
207
|
-
if (options.update) {
|
|
208
|
-
console.error(
|
|
209
|
-
"--update (incremental re-extract) is not implemented yet in v1 \u2014 running the full pipeline instead."
|
|
210
|
-
);
|
|
211
|
-
}
|
|
706
|
+
root = path5.resolve(root);
|
|
212
707
|
if (options.mode) {
|
|
213
708
|
console.error(`--mode ${options.mode} is reserved for a future richer-INFERRED-edges pass \u2014 currently a no-op.`);
|
|
214
709
|
}
|
|
@@ -216,9 +711,17 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
|
|
|
216
711
|
await runClusterOnly(root, options);
|
|
217
712
|
return;
|
|
218
713
|
}
|
|
714
|
+
let extraExtractions;
|
|
715
|
+
if (options.mysql) {
|
|
716
|
+
const { extractMysql } = await import("../mysql-EJ6XOWR4.js");
|
|
717
|
+
console.error(`Extracting MySQL schema from ${validateDsn(options.mysql).safeDisplay} ...`);
|
|
718
|
+
extraExtractions = [await extractMysql(options.mysql)];
|
|
719
|
+
}
|
|
219
720
|
const runOnce = () => runPipeline(root, {
|
|
220
721
|
...exportOptionsFrom(options),
|
|
221
722
|
...clusterOptionsFrom(options),
|
|
723
|
+
extraExtractions,
|
|
724
|
+
update: options.update,
|
|
222
725
|
onProgress: (m) => console.error(m)
|
|
223
726
|
});
|
|
224
727
|
await runOnce();
|
|
@@ -255,13 +758,153 @@ program.command("explain <node>").description("plain-language explanation of a n
|
|
|
255
758
|
process.exitCode = 1;
|
|
256
759
|
}
|
|
257
760
|
});
|
|
258
|
-
program.command("
|
|
761
|
+
program.command("affected <node>").description("reverse impact analysis \u2014 everything that (transitively) depends on a node").option("--depth <n>", "how many reverse hops to follow (default 3)", Number).option("--limit <n>", "cap on affected nodes reported (default 200)", Number).action(async (node, options) => {
|
|
762
|
+
try {
|
|
763
|
+
await runAffected(node, options);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.error(`graphify affected: ${error.message}`);
|
|
766
|
+
process.exitCode = 1;
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
program.command("context <task>").description("token-budgeted working-set pack \u2014 the actual code for a task, in one call").option("--budget <tokens>", "approximate token cap for the pack (default 4000)", Number).action(async (task, options) => {
|
|
770
|
+
try {
|
|
771
|
+
await runContext(task, options);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.error(`graphify context: ${error.message}`);
|
|
774
|
+
process.exitCode = 1;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
program.command("tests [node]").description("structural test selection \u2014 the minimal test files worth running for a change").option("--changed [rev]", "select for the working-tree diff (optionally vs a revision) instead of a node").action(async (node, options) => {
|
|
259
778
|
try {
|
|
260
|
-
await
|
|
779
|
+
await runTests(node, options);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
console.error(`graphify tests: ${error.message}`);
|
|
782
|
+
process.exitCode = 1;
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
program.command("review <base> [head]").description("structural review between two git revisions: added/removed/rewired symbols with blast radius + tests").action(async (base, head) => {
|
|
786
|
+
try {
|
|
787
|
+
await runReview(base, head);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
console.error(`graphify review: ${error.message}`);
|
|
790
|
+
process.exitCode = 1;
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
program.command("check").description("check the graph against dependency rules (graphify.rules.json) \u2014 exits 1 on violations").option("--rules <file>", "rules file (default ./graphify.rules.json)").action(async (options) => {
|
|
794
|
+
try {
|
|
795
|
+
await runCheck(options);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error(`graphify check: ${error.message}`);
|
|
798
|
+
process.exitCode = 1;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
program.command("benchmark [questions...]").description("measure token savings of graph answers vs reading the files they touch").action(async (questions) => {
|
|
802
|
+
try {
|
|
803
|
+
await runBenchmark(questions);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
console.error(`graphify benchmark: ${error.message}`);
|
|
806
|
+
process.exitCode = 1;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
program.command("install").description("install the skill/rules files into local agents (claude, cursor, windsurf, cline, agents, gemini, all)").option("--platform <names...>", "platform(s) to install for", ["claude"]).action(async (options) => {
|
|
810
|
+
try {
|
|
811
|
+
await runInstall(options.platform);
|
|
261
812
|
} catch (error) {
|
|
262
813
|
console.error(`graphify install: ${error.message}`);
|
|
263
814
|
process.exitCode = 1;
|
|
264
815
|
}
|
|
265
816
|
});
|
|
817
|
+
var globalCommand = program.command("global").description("manage the cross-project global graph (registry at ~/.graphify/global.json)");
|
|
818
|
+
globalCommand.command("add [path]").description("register a project (must already have graphify-out/graph.json)").action(async (target) => {
|
|
819
|
+
try {
|
|
820
|
+
await runGlobalAdd(target);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error(`graphify global add: ${error.message}`);
|
|
823
|
+
process.exitCode = 1;
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
globalCommand.command("remove <name>").description("unregister a project by name").action(async (name) => {
|
|
827
|
+
try {
|
|
828
|
+
await runGlobalRemove(name);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
console.error(`graphify global remove: ${error.message}`);
|
|
831
|
+
process.exitCode = 1;
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
globalCommand.command("list").description("list registered projects").action(async () => {
|
|
835
|
+
try {
|
|
836
|
+
await runGlobalList();
|
|
837
|
+
} catch (error) {
|
|
838
|
+
console.error(`graphify global list: ${error.message}`);
|
|
839
|
+
process.exitCode = 1;
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
globalCommand.command("build").description("merge every registered project into one global graph").option("--out <dir>", "output directory (default ~/.graphify/global-out)").action(async (options) => {
|
|
843
|
+
try {
|
|
844
|
+
await runGlobalBuild(options);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
console.error(`graphify global build: ${error.message}`);
|
|
847
|
+
process.exitCode = 1;
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
program.command("merge <dirs...>").description("one-shot merge of two or more built project graphs into one").option("--out <dir>", "output directory (default ./graphify-merged)").action(async (dirs, options) => {
|
|
851
|
+
try {
|
|
852
|
+
await runMerge(dirs, options);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
console.error(`graphify merge: ${error.message}`);
|
|
855
|
+
process.exitCode = 1;
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
program.command("save-result").description("save a Q&A result to graphify-out/memory/ for the graph feedback loop").requiredOption("--question <q>", "the question that was asked").requiredOption("--answer <a>", "the answer that was given").option("--nodes <ids...>", "node ids/labels cited in the answer", []).option("--type <t>", "query|path|explain|affected", "query").option("--outcome <o>", "useful|dead_end|corrected", "useful").option("--correction <text>", "what the right answer was (pairs with --outcome corrected)").action(async (options) => {
|
|
859
|
+
try {
|
|
860
|
+
const memoryDir = path5.join(process.cwd(), "graphify-out", "memory");
|
|
861
|
+
const file = await saveResult(
|
|
862
|
+
{
|
|
863
|
+
question: options.question,
|
|
864
|
+
answer: options.answer,
|
|
865
|
+
nodes: options.nodes,
|
|
866
|
+
type: options.type,
|
|
867
|
+
outcome: options.outcome,
|
|
868
|
+
correction: options.correction
|
|
869
|
+
},
|
|
870
|
+
memoryDir
|
|
871
|
+
);
|
|
872
|
+
console.log(`Saved -> ${file}`);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
console.error(`graphify save-result: ${error.message}`);
|
|
875
|
+
process.exitCode = 1;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
program.command("reflect").description("aggregate graphify-out/memory/ into a recency-weighted lessons doc").option("--half-life-days <n>", "a result loses half its weight every N days (default 30)", Number).option("--out <file>", "output path (default graphify-out/reflections/LESSONS.md)").action(async (options) => {
|
|
879
|
+
try {
|
|
880
|
+
const memoryDir = path5.join(process.cwd(), "graphify-out", "memory");
|
|
881
|
+
const results = await loadResults(memoryDir);
|
|
882
|
+
const lessons = renderLessons(results, { halfLifeDays: options.halfLifeDays });
|
|
883
|
+
const outPath = path5.resolve(options.out ?? path5.join(process.cwd(), "graphify-out", "reflections", "LESSONS.md"));
|
|
884
|
+
await fs7.mkdir(path5.dirname(outPath), { recursive: true });
|
|
885
|
+
await fs7.writeFile(outPath, lessons, "utf-8");
|
|
886
|
+
console.log(`Reflected ${results.length} result(s) -> ${outPath}`);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.error(`graphify reflect: ${error.message}`);
|
|
889
|
+
process.exitCode = 1;
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
var hookCommand = program.command("hook").description("manage the git hooks that auto-rebuild the graph on commit/pull");
|
|
893
|
+
hookCommand.command("install").description("install post-commit and post-merge hooks (preserves existing hook content)").action(async () => {
|
|
894
|
+
try {
|
|
895
|
+
await runHookInstall();
|
|
896
|
+
} catch (error) {
|
|
897
|
+
console.error(`graphify hook install: ${error.message}`);
|
|
898
|
+
process.exitCode = 1;
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
hookCommand.command("uninstall").description("remove the graphify block from the git hooks").action(async () => {
|
|
902
|
+
try {
|
|
903
|
+
await runHookUninstall();
|
|
904
|
+
} catch (error) {
|
|
905
|
+
console.error(`graphify hook uninstall: ${error.message}`);
|
|
906
|
+
process.exitCode = 1;
|
|
907
|
+
}
|
|
908
|
+
});
|
|
266
909
|
program.parseAsync(process.argv);
|
|
267
910
|
//# sourceMappingURL=index.js.map
|