whygraph 0.1.0

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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/cli/commands/config.d.ts +14 -0
  4. package/dist/cli/commands/config.js +123 -0
  5. package/dist/cli/commands/down.d.ts +9 -0
  6. package/dist/cli/commands/down.js +46 -0
  7. package/dist/cli/commands/init.d.ts +17 -0
  8. package/dist/cli/commands/init.js +144 -0
  9. package/dist/cli/commands/issues.d.ts +10 -0
  10. package/dist/cli/commands/issues.js +376 -0
  11. package/dist/cli/commands/mcp.d.ts +2 -0
  12. package/dist/cli/commands/mcp.js +9 -0
  13. package/dist/cli/commands/restart.d.ts +11 -0
  14. package/dist/cli/commands/restart.js +43 -0
  15. package/dist/cli/commands/serve.d.ts +14 -0
  16. package/dist/cli/commands/serve.js +132 -0
  17. package/dist/cli/commands/server-utils.d.ts +6 -0
  18. package/dist/cli/commands/server-utils.js +94 -0
  19. package/dist/cli/commands/status.d.ts +11 -0
  20. package/dist/cli/commands/status.js +97 -0
  21. package/dist/cli/commands/up.d.ts +13 -0
  22. package/dist/cli/commands/up.js +62 -0
  23. package/dist/cli/commands/validate.d.ts +14 -0
  24. package/dist/cli/commands/validate.js +88 -0
  25. package/dist/cli/commands/viz.d.ts +7 -0
  26. package/dist/cli/commands/viz.js +97 -0
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.js +33 -0
  29. package/dist/entity/id.d.ts +8 -0
  30. package/dist/entity/id.js +48 -0
  31. package/dist/entity/issues.d.ts +12 -0
  32. package/dist/entity/issues.js +68 -0
  33. package/dist/entity/parser.d.ts +6 -0
  34. package/dist/entity/parser.js +166 -0
  35. package/dist/entity/types.d.ts +54 -0
  36. package/dist/entity/types.js +21 -0
  37. package/dist/entity/validate.d.ts +12 -0
  38. package/dist/entity/validate.js +136 -0
  39. package/dist/entity/writer.d.ts +16 -0
  40. package/dist/entity/writer.js +142 -0
  41. package/dist/frontend/assets/index-ByZzPwVe.css +1 -0
  42. package/dist/frontend/assets/index-F9dxfzD_.js +170 -0
  43. package/dist/frontend/index.html +14 -0
  44. package/dist/graph/cascade.d.ts +10 -0
  45. package/dist/graph/cascade.js +49 -0
  46. package/dist/graph/decisions.d.ts +11 -0
  47. package/dist/graph/decisions.js +27 -0
  48. package/dist/graph/gaps.d.ts +10 -0
  49. package/dist/graph/gaps.js +58 -0
  50. package/dist/graph/nodes.d.ts +20 -0
  51. package/dist/graph/nodes.js +33 -0
  52. package/dist/graph/projection.d.ts +6 -0
  53. package/dist/graph/projection.js +44 -0
  54. package/dist/graph/query.d.ts +15 -0
  55. package/dist/graph/query.js +82 -0
  56. package/dist/graph/search.d.ts +2 -0
  57. package/dist/graph/search.js +23 -0
  58. package/dist/graph/supersede.d.ts +7 -0
  59. package/dist/graph/supersede.js +48 -0
  60. package/dist/graph/temporal.d.ts +13 -0
  61. package/dist/graph/temporal.js +28 -0
  62. package/dist/mcp/index.d.ts +2 -0
  63. package/dist/mcp/index.js +10 -0
  64. package/dist/mcp/server.d.ts +3 -0
  65. package/dist/mcp/server.js +340 -0
  66. package/dist/onboarding/interview.d.ts +22 -0
  67. package/dist/onboarding/interview.js +92 -0
  68. package/dist/onboarding/scan.d.ts +17 -0
  69. package/dist/onboarding/scan.js +106 -0
  70. package/dist/platform/rules.d.ts +8 -0
  71. package/dist/platform/rules.js +229 -0
  72. package/dist/server/core.d.ts +26 -0
  73. package/dist/server/core.js +111 -0
  74. package/dist/server/derived.d.ts +8 -0
  75. package/dist/server/derived.js +13 -0
  76. package/dist/server/etag.d.ts +9 -0
  77. package/dist/server/etag.js +25 -0
  78. package/dist/server/http.d.ts +13 -0
  79. package/dist/server/http.js +131 -0
  80. package/dist/server/pubsub.d.ts +12 -0
  81. package/dist/server/pubsub.js +19 -0
  82. package/dist/server/schema.d.ts +2 -0
  83. package/dist/server/schema.js +362 -0
  84. package/dist/server/stale-refs.d.ts +7 -0
  85. package/dist/server/stale-refs.js +23 -0
  86. package/dist/server/watcher.d.ts +21 -0
  87. package/dist/server/watcher.js +98 -0
  88. package/dist/server/worktree-watcher.d.ts +20 -0
  89. package/dist/server/worktree-watcher.js +79 -0
  90. package/dist/server/worktree.d.ts +22 -0
  91. package/dist/server/worktree.js +84 -0
  92. package/package.json +73 -0
@@ -0,0 +1,94 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import * as yaml from "js-yaml";
5
+ export function findWhygraphDir(startDir) {
6
+ let current = resolve(startDir);
7
+ const root = resolve("/");
8
+ while (current !== root) {
9
+ const candidate = join(current, ".whygraph");
10
+ if (existsSync(candidate)) {
11
+ return current;
12
+ }
13
+ const parent = resolve(current, "..");
14
+ /* v8 ignore next 2 */
15
+ if (parent === current)
16
+ break;
17
+ current = parent;
18
+ }
19
+ /* v8 ignore next 3 */
20
+ if (existsSync(join(root, ".whygraph"))) {
21
+ return root;
22
+ }
23
+ return null;
24
+ }
25
+ export function getConfiguredPort(projectDir) {
26
+ const configPath = join(projectDir, ".whygraph", "config.yaml");
27
+ try {
28
+ const content = readFileSync(configPath, "utf-8");
29
+ const config = yaml.load(content);
30
+ return config.serverPort ?? 4777;
31
+ }
32
+ catch {
33
+ return 4777;
34
+ }
35
+ }
36
+ export function killProcessOnPort(port) {
37
+ try {
38
+ const result = execSync(`lsof -ti:${port}`, { encoding: "utf-8" }).trim();
39
+ if (result) {
40
+ const pids = result.split("\n").map((p) => p.trim()).filter(Boolean);
41
+ for (const pid of pids) {
42
+ try {
43
+ execSync(`kill ${pid}`);
44
+ }
45
+ catch {
46
+ // Process may have already exited
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+ }
52
+ catch {
53
+ // No process on that port
54
+ }
55
+ return false;
56
+ }
57
+ export async function waitForPortFree(port, timeoutMs = 3000) {
58
+ const start = Date.now();
59
+ while (Date.now() - start < timeoutMs) {
60
+ try {
61
+ await fetch(`http://localhost:${port}/api/health`);
62
+ await new Promise((r) => setTimeout(r, 200));
63
+ }
64
+ catch {
65
+ return;
66
+ }
67
+ }
68
+ throw new Error(`Port ${port} still in use after ${timeoutMs}ms`);
69
+ }
70
+ export async function waitForServerReady(port, timeoutMs = 5000) {
71
+ const start = Date.now();
72
+ while (Date.now() - start < timeoutMs) {
73
+ try {
74
+ const res = await fetch(`http://localhost:${port}/api/health`);
75
+ /* v8 ignore next 1 */
76
+ if (res.ok)
77
+ return;
78
+ }
79
+ catch {
80
+ // Not ready yet
81
+ }
82
+ await new Promise((r) => setTimeout(r, 200));
83
+ }
84
+ throw new Error(`Server not ready on port ${port} after ${timeoutMs}ms`);
85
+ }
86
+ export function isServerRunning(port) {
87
+ try {
88
+ execSync(`lsof -ti:${port}`, { encoding: "utf-8" });
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
@@ -0,0 +1,11 @@
1
+ import type { Command } from "commander";
2
+ export interface StatusResult {
3
+ serverRunning: boolean;
4
+ serverPort: number;
5
+ entityCount?: number;
6
+ nodeCount?: number;
7
+ decisionCount?: number;
8
+ error?: string;
9
+ }
10
+ export declare function runStatus(targetDir: string): Promise<StatusResult>;
11
+ export declare function registerStatusCommand(program: Command): void;
@@ -0,0 +1,97 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { findWhygraphDir } from "./serve.js";
5
+ // ============================================================
6
+ // Core Logic
7
+ // ============================================================
8
+ export async function runStatus(targetDir) {
9
+ const projectDir = findWhygraphDir(targetDir);
10
+ if (!projectDir) {
11
+ throw new Error(`.whygraph/ not found. Run "whygraph init" first.`);
12
+ }
13
+ const configPath = join(projectDir, ".whygraph", "config.yaml");
14
+ if (!existsSync(configPath)) {
15
+ throw new Error(`.whygraph/config.yaml not found. Run "whygraph init" first.`);
16
+ }
17
+ const raw = readFileSync(configPath, "utf-8");
18
+ const config = yaml.load(raw);
19
+ const port = config.serverPort;
20
+ // Try to connect to the server
21
+ try {
22
+ const healthRes = await fetch(`http://localhost:${port}/api/health`);
23
+ if (!healthRes.ok) {
24
+ return { serverRunning: false, serverPort: port, error: "Health check failed" };
25
+ }
26
+ // Query GraphQL for status
27
+ const gqlRes = await fetch(`http://localhost:${port}/api/graphql`, {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify({
31
+ query: `{ status { running entityCount nodeCount decisionCount } }`,
32
+ }),
33
+ });
34
+ if (!gqlRes.ok) {
35
+ return { serverRunning: true, serverPort: port, error: "GraphQL query failed" };
36
+ }
37
+ const gqlData = (await gqlRes.json());
38
+ const status = gqlData.data?.status;
39
+ if (!status) {
40
+ return { serverRunning: true, serverPort: port, error: "Unexpected GraphQL response" };
41
+ }
42
+ return {
43
+ serverRunning: true,
44
+ serverPort: port,
45
+ entityCount: status.entityCount,
46
+ nodeCount: status.nodeCount,
47
+ decisionCount: status.decisionCount,
48
+ };
49
+ }
50
+ catch {
51
+ return { serverRunning: false, serverPort: port };
52
+ }
53
+ }
54
+ // ============================================================
55
+ // CLI Wiring
56
+ // ============================================================
57
+ export function registerStatusCommand(program) {
58
+ program
59
+ .command("status")
60
+ .description("Check the whygraph server status and entity counts")
61
+ .option("--json", "Output results as JSON")
62
+ .action(async (opts) => {
63
+ try {
64
+ const result = await runStatus(process.cwd());
65
+ if (opts.json) {
66
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
67
+ }
68
+ else {
69
+ if (result.serverRunning) {
70
+ /* v8 ignore start */
71
+ const ec = result.entityCount ?? "unknown";
72
+ const nc = result.nodeCount ?? "unknown";
73
+ const dc = result.decisionCount ?? "unknown";
74
+ /* v8 ignore stop */
75
+ process.stdout.write(`Server: running (port ${result.serverPort})\n` +
76
+ ` Entities: ${ec}\n` +
77
+ ` Nodes: ${nc}\n` +
78
+ ` Decisions: ${dc}\n`);
79
+ }
80
+ else {
81
+ process.stdout.write(`Server: not running (port ${result.serverPort})\n`);
82
+ }
83
+ }
84
+ }
85
+ catch (err) {
86
+ /* v8 ignore next 1 */
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ if (opts.json) {
89
+ process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
90
+ }
91
+ else {
92
+ process.stderr.write(`Error: ${message}\n`);
93
+ }
94
+ process.exitCode = 1;
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,13 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import type { Command } from "commander";
3
+ export declare function runUp(targetDir: string, options?: {
4
+ port?: number;
5
+ json?: boolean;
6
+ spawner?: typeof nodeSpawn;
7
+ waitForReady?: (port: number) => Promise<void>;
8
+ }): Promise<{
9
+ url: string;
10
+ port: number;
11
+ pid: number;
12
+ }>;
13
+ export declare function registerUpCommand(program: Command): void;
@@ -0,0 +1,62 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { findWhygraphDir, getConfiguredPort, isServerRunning, waitForServerReady, } from "./server-utils.js";
5
+ export async function runUp(targetDir, options = {}) {
6
+ const projectDir = findWhygraphDir(targetDir);
7
+ if (!projectDir) {
8
+ throw new Error(`.whygraph/ not found. Run "whygraph init" first.`);
9
+ }
10
+ const port = options.port ?? getConfiguredPort(projectDir);
11
+ if (isServerRunning(port)) {
12
+ throw new Error(`Server already running on port ${port}. Use "whygraph restart" or "whygraph down" first.`);
13
+ }
14
+ // Spawn the serve command as a detached background process
15
+ const spawn = options.spawner ?? nodeSpawn;
16
+ const serveScript = join(dirname(fileURLToPath(import.meta.url)), "..", "index.js");
17
+ const child = spawn(process.execPath, [serveScript, "serve", "--port", String(port)], {
18
+ detached: true,
19
+ stdio: "ignore",
20
+ cwd: projectDir,
21
+ });
22
+ child.unref();
23
+ const pid = child.pid;
24
+ // Wait for the server to be ready
25
+ const waitFn = options.waitForReady ?? waitForServerReady;
26
+ await waitFn(port);
27
+ return {
28
+ url: `http://localhost:${port}`,
29
+ port,
30
+ pid,
31
+ };
32
+ }
33
+ export function registerUpCommand(program) {
34
+ program
35
+ .command("up")
36
+ .description("Start the whygraph server in the background")
37
+ .option("--port <number>", "Port number")
38
+ .option("--json", "Output results as JSON")
39
+ .action(async (opts) => {
40
+ try {
41
+ const port = opts.port ? parseInt(opts.port, 10) : undefined;
42
+ const result = await runUp(process.cwd(), { port, json: opts.json });
43
+ if (opts.json) {
44
+ process.stdout.write(JSON.stringify(result) + "\n");
45
+ }
46
+ else {
47
+ process.stdout.write(`whygraph server running at ${result.url} (pid ${result.pid})\n`);
48
+ }
49
+ }
50
+ catch (err) {
51
+ /* v8 ignore next 1 */
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ if (opts.json) {
54
+ process.stdout.write(JSON.stringify({ error: message }) + "\n");
55
+ }
56
+ else {
57
+ process.stderr.write(`Error: ${message}\n`);
58
+ }
59
+ process.exitCode = 1;
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,14 @@
1
+ import type { Command } from "commander";
2
+ export interface FileError {
3
+ file: string;
4
+ field?: string;
5
+ message: string;
6
+ severity: "error" | "warning";
7
+ }
8
+ export interface ValidateResult {
9
+ filesChecked: number;
10
+ entitiesParsed: number;
11
+ errors: FileError[];
12
+ }
13
+ export declare function runValidate(whygraphDir: string): Promise<ValidateResult>;
14
+ export declare function registerValidateCommand(program: Command): void;
@@ -0,0 +1,88 @@
1
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseEntity } from "../../entity/parser.js";
4
+ import { validateEntity, validateEntityRefs } from "../../entity/validate.js";
5
+ // ============================================================
6
+ // Core Logic
7
+ // ============================================================
8
+ export async function runValidate(whygraphDir) {
9
+ const graphDir = join(whygraphDir, ".whygraph", "graph");
10
+ if (!existsSync(graphDir)) {
11
+ return { filesChecked: 0, entitiesParsed: 0, errors: [] };
12
+ }
13
+ const files = readdirSync(graphDir).filter((f) => f.endsWith(".md"));
14
+ const errors = [];
15
+ const entityMap = new Map();
16
+ const filePathMap = new Map(); // entity id → file path
17
+ // First pass: parse all entities into a map
18
+ for (const file of files) {
19
+ const filePath = join(graphDir, file);
20
+ const content = readFileSync(filePath, "utf-8");
21
+ const entity = parseEntity(content);
22
+ if (entity === null) {
23
+ errors.push({
24
+ file: filePath,
25
+ message: "Failed to parse entity from file",
26
+ severity: "error",
27
+ });
28
+ continue;
29
+ }
30
+ entityMap.set(entity.id, entity);
31
+ filePathMap.set(entity.id, filePath);
32
+ }
33
+ // Second pass: schema + cross-reference validation
34
+ for (const [id, entity] of entityMap) {
35
+ const filePath = filePathMap.get(id);
36
+ const schemaResult = validateEntity(entity);
37
+ for (const err of schemaResult.errors) {
38
+ errors.push({ file: filePath, field: err.field, message: err.message, severity: err.severity });
39
+ }
40
+ const refErrors = validateEntityRefs(entity, entityMap);
41
+ for (const err of refErrors) {
42
+ errors.push({ file: filePath, field: err.field, message: err.message, severity: err.severity });
43
+ }
44
+ }
45
+ return {
46
+ filesChecked: files.length,
47
+ entitiesParsed: entityMap.size,
48
+ errors,
49
+ };
50
+ }
51
+ // ============================================================
52
+ // CLI Wiring
53
+ // ============================================================
54
+ function formatTable(result) {
55
+ const lines = [];
56
+ if (result.errors.length === 0) {
57
+ lines.push(`Checked ${result.filesChecked} file(s), ${result.entitiesParsed} entity(ies) parsed. No errors found.`);
58
+ return lines.join("\n");
59
+ }
60
+ lines.push("FILE FIELD SEVERITY MESSAGE");
61
+ lines.push("─".repeat(90));
62
+ for (const err of result.errors) {
63
+ /* v8 ignore next 1 */
64
+ const fileName = err.file.split("/").pop() ?? err.file;
65
+ const field = err.field ?? "-";
66
+ lines.push(`${fileName.padEnd(30)} ${field.padEnd(12)} ${err.severity.padEnd(9)} ${err.message}`);
67
+ }
68
+ lines.push("");
69
+ lines.push(`Checked ${result.filesChecked} file(s), ${result.entitiesParsed} entity(ies) parsed. ${result.errors.length} error(s) found.`);
70
+ return lines.join("\n");
71
+ }
72
+ export function registerValidateCommand(program) {
73
+ program
74
+ .command("validate")
75
+ .description("Validate all entity files in .whygraph/graph/")
76
+ .option("--json", "Output results as JSON")
77
+ .action(async (opts) => {
78
+ const result = await runValidate(process.cwd());
79
+ if (opts.json) {
80
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
81
+ }
82
+ else {
83
+ process.stdout.write(formatTable(result) + "\n");
84
+ }
85
+ const hasErrors = result.errors.some((e) => e.severity === "error");
86
+ process.exitCode = hasErrors ? 1 : 0;
87
+ });
88
+ }
@@ -0,0 +1,7 @@
1
+ import type { Command } from "commander";
2
+ export declare function runViz(targetDir: string, options?: {
3
+ port?: number;
4
+ }): Promise<{
5
+ url: string;
6
+ }>;
7
+ export declare function registerVizCommand(program: Command): void;
@@ -0,0 +1,97 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ // ============================================================
5
+ // Find .whygraph directory (reuse serve's logic inline)
6
+ // ============================================================
7
+ function findWhygraphDir(startDir) {
8
+ let current = resolve(startDir);
9
+ const root = resolve("/");
10
+ while (current !== root) {
11
+ const candidate = join(current, ".whygraph");
12
+ if (existsSync(candidate)) {
13
+ return current;
14
+ }
15
+ const parent = resolve(current, "..");
16
+ /* v8 ignore next 2 */
17
+ if (parent === current)
18
+ break;
19
+ current = parent;
20
+ }
21
+ /* v8 ignore next 3 */
22
+ if (existsSync(join(root, ".whygraph"))) {
23
+ return root;
24
+ }
25
+ return null;
26
+ }
27
+ // ============================================================
28
+ // Core Logic
29
+ // ============================================================
30
+ export async function runViz(targetDir, options = {}) {
31
+ const projectDir = findWhygraphDir(targetDir);
32
+ if (!projectDir) {
33
+ throw new Error(`.whygraph/ not found. Run "whygraph init" first.`);
34
+ }
35
+ const port = options.port ?? 4777;
36
+ const url = `http://localhost:${port}`;
37
+ // Check if server is running via health check
38
+ let serverRunning = false;
39
+ try {
40
+ const response = await fetch(`${url}/api/health`);
41
+ if (response.ok) {
42
+ serverRunning = true;
43
+ }
44
+ }
45
+ catch {
46
+ // Server not running
47
+ }
48
+ // If not running, start it in background
49
+ if (!serverRunning) {
50
+ const child = spawn("whygraph", ["serve", "--port", String(port)], {
51
+ cwd: projectDir,
52
+ detached: true,
53
+ stdio: "ignore",
54
+ });
55
+ child.unref();
56
+ // Brief wait for server to start
57
+ await new Promise((r) => setTimeout(r, 1000));
58
+ }
59
+ // Open browser
60
+ const open = await import("open");
61
+ await open.default(url);
62
+ return { url };
63
+ }
64
+ // ============================================================
65
+ // CLI Wiring
66
+ // ============================================================
67
+ export function registerVizCommand(program) {
68
+ program
69
+ .command("viz")
70
+ .description("Open the whygraph visualization in a browser")
71
+ .option("--port <number>", "Port number", "4777")
72
+ .option("--json", "Output results as JSON")
73
+ .action(async (opts) => {
74
+ try {
75
+ /* v8 ignore next 1 */
76
+ const port = opts.port ? parseInt(opts.port, 10) : undefined;
77
+ const result = await runViz(process.cwd(), { port });
78
+ if (opts.json) {
79
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
80
+ }
81
+ else {
82
+ process.stdout.write(`Opening whygraph at ${result.url}\n`);
83
+ }
84
+ }
85
+ catch (err) {
86
+ /* v8 ignore next 1 */
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ if (opts.json) {
89
+ process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
90
+ }
91
+ else {
92
+ process.stderr.write(`Error: ${message}\n`);
93
+ }
94
+ process.exitCode = 1;
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Command } from "commander";
4
+ const require = createRequire(import.meta.url);
5
+ const { version } = require("../../package.json");
6
+ import { registerConfigCommand } from "./commands/config.js";
7
+ import { registerDownCommand } from "./commands/down.js";
8
+ import { registerInitCommand } from "./commands/init.js";
9
+ import { registerIssuesCommand } from "./commands/issues.js";
10
+ import { registerMcpCommand } from "./commands/mcp.js";
11
+ import { registerRestartCommand } from "./commands/restart.js";
12
+ import { registerServeCommand } from "./commands/serve.js";
13
+ import { registerStatusCommand } from "./commands/status.js";
14
+ import { registerUpCommand } from "./commands/up.js";
15
+ import { registerValidateCommand } from "./commands/validate.js";
16
+ import { registerVizCommand } from "./commands/viz.js";
17
+ const program = new Command();
18
+ program
19
+ .name("whygraph")
20
+ .description("The graph of why — so your agent knows before it touches anything.")
21
+ .version(version);
22
+ registerConfigCommand(program);
23
+ registerDownCommand(program);
24
+ registerInitCommand(program);
25
+ registerIssuesCommand(program);
26
+ registerMcpCommand(program);
27
+ registerRestartCommand(program);
28
+ registerServeCommand(program);
29
+ registerStatusCommand(program);
30
+ registerUpCommand(program);
31
+ registerValidateCommand(program);
32
+ registerVizCommand(program);
33
+ program.parse();
@@ -0,0 +1,8 @@
1
+ export interface IdOptions {
2
+ prefix?: string;
3
+ length?: number;
4
+ }
5
+ export declare function generateId(options?: IdOptions): string;
6
+ export declare function slugify(title: string): string;
7
+ export declare function toFilename(id: string, title: string): string;
8
+ export declare function parseFilename(filename: string): string | null;
@@ -0,0 +1,48 @@
1
+ import { customAlphabet } from "nanoid";
2
+ const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
3
+ const DEFAULT_PREFIX = "wg-";
4
+ const DEFAULT_LENGTH = 4;
5
+ const MAX_SLUG_LENGTH = 50;
6
+ export function generateId(options) {
7
+ const prefix = options?.prefix ?? DEFAULT_PREFIX;
8
+ const length = options?.length ?? DEFAULT_LENGTH;
9
+ const nanoid = customAlphabet(ALPHABET, length);
10
+ return `${prefix}${nanoid()}`;
11
+ }
12
+ export function slugify(title) {
13
+ let slug = title
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/g, "-")
16
+ .replace(/-+/g, "-")
17
+ .replace(/^-|-$/g, "");
18
+ if (slug.length > MAX_SLUG_LENGTH) {
19
+ slug = slug.slice(0, MAX_SLUG_LENGTH).replace(/-$/, "");
20
+ }
21
+ return slug;
22
+ }
23
+ export function toFilename(id, title) {
24
+ const slug = slugify(title);
25
+ if (!slug) {
26
+ return `${id}.md`;
27
+ }
28
+ return `${id}--${slug}.md`;
29
+ }
30
+ export function parseFilename(filename) {
31
+ // Strip directory path
32
+ const base = filename.includes("/")
33
+ ? filename.slice(filename.lastIndexOf("/") + 1)
34
+ : filename;
35
+ // Must end with .md
36
+ if (!base.endsWith(".md")) {
37
+ return null;
38
+ }
39
+ const withoutExt = base.slice(0, -3);
40
+ // Try double-dash split first
41
+ const ddIndex = withoutExt.indexOf("--");
42
+ const id = ddIndex >= 0 ? withoutExt.slice(0, ddIndex) : withoutExt;
43
+ // Validate ID looks like prefix + nanoid
44
+ if (!/^[a-z]+-[0-9a-z]+$/.test(id)) {
45
+ return null;
46
+ }
47
+ return id;
48
+ }
@@ -0,0 +1,12 @@
1
+ import type { ValidationError } from "./validate.js";
2
+ export interface EntityIssue {
3
+ entityId: string;
4
+ errors: ValidationError[];
5
+ createdAt: string;
6
+ updatedAt: string;
7
+ }
8
+ export declare function writeIssue(whygraphDir: string, entityId: string, errors: ValidationError[]): EntityIssue;
9
+ export declare function readIssue(whygraphDir: string, entityId: string): EntityIssue | null;
10
+ export declare function deleteIssue(whygraphDir: string, entityId: string): boolean;
11
+ export declare function listIssues(whygraphDir: string): EntityIssue[];
12
+ export declare function reconcileIssue(whygraphDir: string, entityId: string, errors: ValidationError[]): EntityIssue | null;
@@ -0,0 +1,68 @@
1
+ import { writeFileSync, readFileSync, readdirSync, unlinkSync, mkdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ function issuesDir(whygraphDir) {
4
+ return join(whygraphDir, "issues");
5
+ }
6
+ function issueFilePath(whygraphDir, entityId) {
7
+ return join(issuesDir(whygraphDir), `${entityId}.json`);
8
+ }
9
+ export function writeIssue(whygraphDir, entityId, errors) {
10
+ const dir = issuesDir(whygraphDir);
11
+ mkdirSync(dir, { recursive: true });
12
+ const now = new Date().toISOString();
13
+ const existing = readIssue(whygraphDir, entityId);
14
+ const issue = {
15
+ entityId,
16
+ errors,
17
+ createdAt: existing?.createdAt ?? now,
18
+ updatedAt: now,
19
+ };
20
+ writeFileSync(issueFilePath(whygraphDir, entityId), JSON.stringify(issue, null, 2) + "\n", "utf-8");
21
+ return issue;
22
+ }
23
+ export function readIssue(whygraphDir, entityId) {
24
+ const filePath = issueFilePath(whygraphDir, entityId);
25
+ if (!existsSync(filePath))
26
+ return null;
27
+ try {
28
+ const raw = readFileSync(filePath, "utf-8");
29
+ return JSON.parse(raw);
30
+ }
31
+ catch {
32
+ /* v8 ignore next 1 */
33
+ return null;
34
+ }
35
+ }
36
+ export function deleteIssue(whygraphDir, entityId) {
37
+ const filePath = issueFilePath(whygraphDir, entityId);
38
+ if (!existsSync(filePath))
39
+ return false;
40
+ unlinkSync(filePath);
41
+ return true;
42
+ }
43
+ export function listIssues(whygraphDir) {
44
+ const dir = issuesDir(whygraphDir);
45
+ if (!existsSync(dir))
46
+ return [];
47
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
48
+ const issues = [];
49
+ for (const file of files) {
50
+ try {
51
+ const raw = readFileSync(join(dir, file), "utf-8");
52
+ issues.push(JSON.parse(raw));
53
+ }
54
+ catch {
55
+ // Skip corrupted issue files
56
+ }
57
+ }
58
+ return issues;
59
+ }
60
+ export function reconcileIssue(whygraphDir, entityId, errors) {
61
+ const hasErrors = errors.some((e) => e.severity === "error");
62
+ const hasWarnings = errors.length > 0;
63
+ if (hasErrors || hasWarnings) {
64
+ return writeIssue(whygraphDir, entityId, errors);
65
+ }
66
+ deleteIssue(whygraphDir, entityId);
67
+ return null;
68
+ }
@@ -0,0 +1,6 @@
1
+ import type { Entity } from "./types.js";
2
+ /**
3
+ * Parse a markdown string with YAML front matter into a typed Entity object.
4
+ * Returns null for unparseable content.
5
+ */
6
+ export declare function parseEntity(content: string): Entity | null;