@beomjk/emdd 0.1.0 → 0.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 +2 -2
- package/dist/cli.js +38 -26
- package/dist/commands/backlog.js +4 -2
- package/dist/commands/update.js +5 -1
- package/dist/graph/operations.d.ts +13 -1
- package/dist/graph/operations.js +54 -20
- package/dist/graph/templates.d.ts +1 -0
- package/dist/graph/templates.js +13 -2
- package/dist/graph/types.d.ts +22 -0
- package/dist/mcp-server/tools/create-node.js +3 -1
- package/package.json +5 -5
- package/dist/rules/emdd-agent.md +0 -40
- package/dist/rules/emdd-rules.md +0 -99
package/README.md
CHANGED
|
@@ -63,13 +63,13 @@ Hypotheses move through `PROPOSED -> TESTING -> SUPPORTED / REFUTED / REVISED`.
|
|
|
63
63
|
## Installation
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
npm install -g emdd
|
|
66
|
+
npm install -g @beomjk/emdd
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Or use directly with npx:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
npx emdd <command>
|
|
72
|
+
npx @beomjk/emdd <command>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
## Quick Start
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,18 @@ import { graphCommand } from './commands/graph.js';
|
|
|
14
14
|
import { backlogCommand } from './commands/backlog.js';
|
|
15
15
|
import { resolveGraphDir } from './graph/loader.js';
|
|
16
16
|
import { startMcpServer } from './mcp-server/index.js';
|
|
17
|
+
function withCliErrorHandling(fn) {
|
|
18
|
+
return async (...args) => {
|
|
19
|
+
try {
|
|
20
|
+
await fn(...args);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
console.error(`Error: ${message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
17
29
|
const program = new Command();
|
|
18
30
|
program
|
|
19
31
|
.name('emdd')
|
|
@@ -24,32 +36,32 @@ program
|
|
|
24
36
|
.description('Initialize EMDD project')
|
|
25
37
|
.option('--lang <locale>', 'Language', 'en')
|
|
26
38
|
.option('--tool <tool>', 'AI tool rules to generate (claude|cursor|windsurf|cline|copilot|all)', 'claude')
|
|
27
|
-
.action((path, options) => {
|
|
39
|
+
.action(withCliErrorHandling(async (path, options) => {
|
|
28
40
|
initCommand(path, options);
|
|
29
|
-
});
|
|
41
|
+
}));
|
|
30
42
|
program
|
|
31
43
|
.command('new <type> <slug>')
|
|
32
44
|
.description('Create a new node')
|
|
33
45
|
.option('--path <path>', 'Project path')
|
|
34
|
-
.action(async (type, slug, options) => {
|
|
46
|
+
.action(withCliErrorHandling(async (type, slug, options) => {
|
|
35
47
|
await newCommand(type, slug, options);
|
|
36
|
-
});
|
|
48
|
+
}));
|
|
37
49
|
program
|
|
38
50
|
.command('lint [path]')
|
|
39
51
|
.description('Validate graph schema and links')
|
|
40
|
-
.action(async (path) => {
|
|
52
|
+
.action(withCliErrorHandling(async (path) => {
|
|
41
53
|
await lintCommand(path);
|
|
42
|
-
});
|
|
54
|
+
}));
|
|
43
55
|
program
|
|
44
56
|
.command('health [path]')
|
|
45
57
|
.description('Show health dashboard')
|
|
46
|
-
.action(async (path) => {
|
|
58
|
+
.action(withCliErrorHandling(async (path) => {
|
|
47
59
|
await healthCommand(path);
|
|
48
|
-
});
|
|
60
|
+
}));
|
|
49
61
|
program
|
|
50
62
|
.command('check [path]')
|
|
51
63
|
.description('Check consolidation triggers')
|
|
52
|
-
.action(async (path) => {
|
|
64
|
+
.action(withCliErrorHandling(async (path) => {
|
|
53
65
|
const graphDir = resolveGraphDir(path);
|
|
54
66
|
const result = await checkCommand(graphDir);
|
|
55
67
|
if (result.triggers.length === 0) {
|
|
@@ -60,11 +72,11 @@ program
|
|
|
60
72
|
console.log(`TRIGGER ${trigger.type} ${trigger.message}`);
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
|
-
});
|
|
75
|
+
}));
|
|
64
76
|
program
|
|
65
77
|
.command('promote [path]')
|
|
66
78
|
.description('Identify findings eligible for promotion')
|
|
67
|
-
.action(async (path) => {
|
|
79
|
+
.action(withCliErrorHandling(async (path) => {
|
|
68
80
|
const graphDir = resolveGraphDir(path);
|
|
69
81
|
const result = await promoteCommand(graphDir);
|
|
70
82
|
if (result.candidates.length === 0) {
|
|
@@ -75,13 +87,13 @@ program
|
|
|
75
87
|
console.log(`CANDIDATE ${c.id} confidence=${c.confidence} supports=${c.supports}`);
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
|
-
});
|
|
90
|
+
}));
|
|
79
91
|
program
|
|
80
92
|
.command('update <node-id>')
|
|
81
93
|
.description('Update frontmatter fields on a node')
|
|
82
94
|
.option('--set <key=value...>', 'Key-value pairs to set')
|
|
83
95
|
.option('--path <path>', 'Project path')
|
|
84
|
-
.action(async (nodeId, options) => {
|
|
96
|
+
.action(withCliErrorHandling(async (nodeId, options) => {
|
|
85
97
|
const graphDir = resolveGraphDir(options.path);
|
|
86
98
|
const updates = {};
|
|
87
99
|
if (options.set) {
|
|
@@ -97,47 +109,47 @@ program
|
|
|
97
109
|
}
|
|
98
110
|
await updateCommand(graphDir, nodeId, updates);
|
|
99
111
|
console.log(`Updated ${nodeId}`);
|
|
100
|
-
});
|
|
112
|
+
}));
|
|
101
113
|
program
|
|
102
114
|
.command('link <source> <target> <relation>')
|
|
103
115
|
.description('Add a link between nodes')
|
|
104
116
|
.option('--path <path>', 'Project path')
|
|
105
|
-
.action(async (source, target, relation, options) => {
|
|
117
|
+
.action(withCliErrorHandling(async (source, target, relation, options) => {
|
|
106
118
|
const graphDir = resolveGraphDir(options.path);
|
|
107
119
|
await linkCommand(graphDir, source, target, relation);
|
|
108
120
|
console.log(`Linked ${source} -> ${target} (${relation})`);
|
|
109
|
-
});
|
|
121
|
+
}));
|
|
110
122
|
program
|
|
111
123
|
.command('done <episode-id> <item>')
|
|
112
124
|
.description('Mark a checklist item as done')
|
|
113
125
|
.option('--path <path>', 'Project path')
|
|
114
|
-
.action(async (episodeId, item, options) => {
|
|
126
|
+
.action(withCliErrorHandling(async (episodeId, item, options) => {
|
|
115
127
|
const graphDir = resolveGraphDir(options.path);
|
|
116
128
|
await doneCommand(graphDir, episodeId, item);
|
|
117
129
|
console.log(`Done: ${item}`);
|
|
118
|
-
});
|
|
130
|
+
}));
|
|
119
131
|
program
|
|
120
132
|
.command('index [path]')
|
|
121
133
|
.description('Generate _index.md for the graph')
|
|
122
|
-
.action(async (path) => {
|
|
134
|
+
.action(withCliErrorHandling(async (path) => {
|
|
123
135
|
const graphDir = resolveGraphDir(path);
|
|
124
136
|
const result = await indexCommand(graphDir);
|
|
125
137
|
console.log(`Index generated: ${result.nodeCount} nodes`);
|
|
126
|
-
});
|
|
138
|
+
}));
|
|
127
139
|
program
|
|
128
140
|
.command('graph [path]')
|
|
129
141
|
.description('Generate _graph.mmd Mermaid diagram')
|
|
130
|
-
.action(async (path) => {
|
|
142
|
+
.action(withCliErrorHandling(async (path) => {
|
|
131
143
|
const graphDir = resolveGraphDir(path);
|
|
132
144
|
const result = await graphCommand(graphDir);
|
|
133
145
|
console.log(`Graph generated: ${result.nodeCount} nodes, ${result.edgeCount} edges`);
|
|
134
|
-
});
|
|
146
|
+
}));
|
|
135
147
|
program
|
|
136
148
|
.command('backlog')
|
|
137
149
|
.description('Show unchecked backlog items')
|
|
138
150
|
.option('--path <path>', 'Project path')
|
|
139
151
|
.option('--status <status>', 'Filter by status')
|
|
140
|
-
.action(async (options) => {
|
|
152
|
+
.action(withCliErrorHandling(async (options) => {
|
|
141
153
|
const graphDir = resolveGraphDir(options.path);
|
|
142
154
|
const result = await backlogCommand(graphDir, options.status);
|
|
143
155
|
if (result.items.length === 0) {
|
|
@@ -148,11 +160,11 @@ program
|
|
|
148
160
|
console.log(`[ ] ${item.episodeId} ${item.text}`);
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
|
-
});
|
|
163
|
+
}));
|
|
152
164
|
program
|
|
153
165
|
.command('mcp')
|
|
154
166
|
.description('Start MCP server over stdio')
|
|
155
|
-
.action(async () => {
|
|
167
|
+
.action(withCliErrorHandling(async () => {
|
|
156
168
|
await startMcpServer();
|
|
157
|
-
});
|
|
169
|
+
}));
|
|
158
170
|
program.parse();
|
package/dist/commands/backlog.js
CHANGED
|
@@ -12,14 +12,16 @@ export async function backlogCommand(graphDir, _statusFilter) {
|
|
|
12
12
|
try {
|
|
13
13
|
content = readFileSync(file, 'utf-8');
|
|
14
14
|
}
|
|
15
|
-
catch {
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.warn(`Warning: Could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
16
17
|
continue;
|
|
17
18
|
}
|
|
18
19
|
let parsed;
|
|
19
20
|
try {
|
|
20
21
|
parsed = matter(content);
|
|
21
22
|
}
|
|
22
|
-
catch {
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.warn(`Warning: Could not parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
25
|
continue;
|
|
24
26
|
}
|
|
25
27
|
const episodeId = parsed.data?.id ?? '';
|
package/dist/commands/update.js
CHANGED
|
@@ -19,7 +19,11 @@ export async function updateCommand(graphDir, nodeId, updates) {
|
|
|
19
19
|
// Apply updates
|
|
20
20
|
for (const [key, value] of Object.entries(updates)) {
|
|
21
21
|
if (key === 'confidence') {
|
|
22
|
-
|
|
22
|
+
const num = parseFloat(value);
|
|
23
|
+
if (isNaN(num) || num < 0 || num > 1) {
|
|
24
|
+
throw new Error(`Invalid confidence value: "${value}" (must be 0-1)`);
|
|
25
|
+
}
|
|
26
|
+
parsed.data[key] = num;
|
|
23
27
|
}
|
|
24
28
|
else {
|
|
25
29
|
parsed.data[key] = value;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Node, NodeFilter, NodeDetail, CreateNodeResult, CreateEdgeResult, HealthReport, CheckResult, PromoteCandidate } from './types.js';
|
|
1
|
+
import type { Node, NodeFilter, NodeDetail, CreateNodeResult, CreateEdgeResult, CreateNodePlan, CreateEdgePlan, FileOp, HealthReport, CheckResult, PromoteCandidate } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* List all nodes in the graph, optionally filtered by type and/or status.
|
|
4
4
|
*/
|
|
@@ -8,11 +8,23 @@ export declare function listNodes(graphDir: string, filter?: NodeFilter): Promis
|
|
|
8
8
|
* Returns null if the node is not found.
|
|
9
9
|
*/
|
|
10
10
|
export declare function readNode(graphDir: string, nodeId: string): Promise<NodeDetail | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Execute a list of file operations (mkdir / write).
|
|
13
|
+
*/
|
|
14
|
+
export declare function executeOps(ops: FileOp[]): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Plan the creation of a new node (pure computation, no I/O).
|
|
17
|
+
*/
|
|
18
|
+
export declare function planCreateNode(graphDir: string, type: string, slug: string, lang?: string): CreateNodePlan;
|
|
11
19
|
/**
|
|
12
20
|
* Create a new node of the given type with the given slug.
|
|
13
21
|
* Returns the created node's ID, type, and file path.
|
|
14
22
|
*/
|
|
15
23
|
export declare function createNode(graphDir: string, type: string, slug: string, lang?: string): Promise<CreateNodeResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Plan the creation of an edge (pure computation after graph load).
|
|
26
|
+
*/
|
|
27
|
+
export declare function planCreateEdge(graphDir: string, source: string, target: string, relation: string): Promise<CreateEdgePlan>;
|
|
16
28
|
/**
|
|
17
29
|
* Add an edge (link) from source to target with the given relation.
|
|
18
30
|
* Validates relation, source existence, and target existence.
|
package/dist/graph/operations.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import matter from 'gray-matter';
|
|
3
4
|
import { loadGraph } from './loader.js';
|
|
4
|
-
import { nextId, renderTemplate, nodePath } from './templates.js';
|
|
5
|
+
import { nextId, renderTemplate, nodePath, sanitizeSlug } from './templates.js';
|
|
5
6
|
import { NODE_TYPES, ALL_VALID_RELATIONS, REVERSE_LABELS } from './types.js';
|
|
6
7
|
// ── listNodes ───────────────────────────────────────────────────────
|
|
7
8
|
/**
|
|
@@ -36,36 +37,61 @@ export async function readNode(graphDir, nodeId) {
|
|
|
36
37
|
body: parsed.content,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
// ── executeOps ──────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Execute a list of file operations (mkdir / write).
|
|
43
|
+
*/
|
|
44
|
+
export async function executeOps(ops) {
|
|
45
|
+
for (const op of ops) {
|
|
46
|
+
switch (op.kind) {
|
|
47
|
+
case 'mkdir':
|
|
48
|
+
if (!fs.existsSync(op.path)) {
|
|
49
|
+
fs.mkdirSync(op.path, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case 'write':
|
|
53
|
+
fs.writeFileSync(op.path, op.content, 'utf-8');
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
// ── createNode ──────────────────────────────────────────────────────
|
|
40
59
|
/**
|
|
41
|
-
*
|
|
42
|
-
* Returns the created node's ID, type, and file path.
|
|
60
|
+
* Plan the creation of a new node (pure computation, no I/O).
|
|
43
61
|
*/
|
|
44
|
-
export
|
|
62
|
+
export function planCreateNode(graphDir, type, slug, lang) {
|
|
45
63
|
if (!NODE_TYPES.includes(type)) {
|
|
46
64
|
throw new Error(`Invalid node type: ${type}. Valid types: ${NODE_TYPES.join(', ')}`);
|
|
47
65
|
}
|
|
48
66
|
const nodeType = type;
|
|
49
67
|
const id = nextId(graphDir, nodeType);
|
|
50
|
-
const
|
|
68
|
+
const sanitized = sanitizeSlug(slug);
|
|
69
|
+
const content = renderTemplate(nodeType, sanitized, {
|
|
51
70
|
id,
|
|
52
71
|
locale: lang ?? 'en',
|
|
53
72
|
});
|
|
54
|
-
const filePath = nodePath(graphDir, nodeType, id,
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
const filePath = nodePath(graphDir, nodeType, id, sanitized);
|
|
74
|
+
const dir = path.dirname(filePath);
|
|
75
|
+
const ops = [
|
|
76
|
+
{ kind: 'mkdir', path: dir },
|
|
77
|
+
{ kind: 'write', path: filePath, content },
|
|
78
|
+
];
|
|
79
|
+
return { id, type: nodeType, path: filePath, ops };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a new node of the given type with the given slug.
|
|
83
|
+
* Returns the created node's ID, type, and file path.
|
|
84
|
+
*/
|
|
85
|
+
export async function createNode(graphDir, type, slug, lang) {
|
|
86
|
+
const plan = planCreateNode(graphDir, type, slug, lang);
|
|
87
|
+
await executeOps(plan.ops);
|
|
88
|
+
return { id: plan.id, type: plan.type, path: plan.path };
|
|
62
89
|
}
|
|
63
90
|
// ── createEdge ──────────────────────────────────────────────────────
|
|
64
91
|
/**
|
|
65
|
-
*
|
|
66
|
-
* Validates relation, source existence, and target existence.
|
|
92
|
+
* Plan the creation of an edge (pure computation after graph load).
|
|
67
93
|
*/
|
|
68
|
-
export async function
|
|
94
|
+
export async function planCreateEdge(graphDir, source, target, relation) {
|
|
69
95
|
// Validate relation
|
|
70
96
|
if (!ALL_VALID_RELATIONS.has(relation)) {
|
|
71
97
|
const valid = [...ALL_VALID_RELATIONS].sort().join(', ');
|
|
@@ -78,7 +104,6 @@ export async function createEdge(graphDir, source, target, relation) {
|
|
|
78
104
|
if (!sourceNode) {
|
|
79
105
|
throw new Error(`Source node not found: ${source}`);
|
|
80
106
|
}
|
|
81
|
-
// Validate target exists (new behavior not in original link.ts)
|
|
82
107
|
const targetNode = graph.nodes.get(target);
|
|
83
108
|
if (!targetNode) {
|
|
84
109
|
throw new Error(`Target node not found: ${target}`);
|
|
@@ -94,10 +119,19 @@ export async function createEdge(graphDir, source, target, relation) {
|
|
|
94
119
|
parsed.data.links.push({ target, relation: canonical });
|
|
95
120
|
// Auto-update the `updated` field
|
|
96
121
|
parsed.data.updated = new Date().toISOString().slice(0, 10);
|
|
97
|
-
//
|
|
122
|
+
// Compute new file content
|
|
98
123
|
const output = matter.stringify(parsed.content, parsed.data);
|
|
99
|
-
|
|
100
|
-
return { source, target, relation: canonical };
|
|
124
|
+
const ops = [{ kind: 'write', path: filePath, content: output }];
|
|
125
|
+
return { source, target, relation: canonical, ops };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Add an edge (link) from source to target with the given relation.
|
|
129
|
+
* Validates relation, source existence, and target existence.
|
|
130
|
+
*/
|
|
131
|
+
export async function createEdge(graphDir, source, target, relation) {
|
|
132
|
+
const plan = await planCreateEdge(graphDir, source, target, relation);
|
|
133
|
+
await executeOps(plan.ops);
|
|
134
|
+
return { source: plan.source, target: plan.target, relation: plan.relation };
|
|
101
135
|
}
|
|
102
136
|
// ── getHealth ───────────────────────────────────────────────────────
|
|
103
137
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NodeType } from './types.js';
|
|
2
2
|
import type { Locale } from '../i18n/index.js';
|
|
3
|
+
export declare function sanitizeSlug(slug: string): string;
|
|
3
4
|
export declare function renderTemplate(type: NodeType, slug: string, options?: {
|
|
4
5
|
locale?: Locale;
|
|
5
6
|
user?: string;
|
package/dist/graph/templates.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { NODE_TYPE_DIRS, ID_PREFIXES, VALID_STATUSES } from './types.js';
|
|
4
|
+
// ── sanitizeSlug ──────────────────────────────────────────────────
|
|
5
|
+
export function sanitizeSlug(slug) {
|
|
6
|
+
let s = slug.replace(/[\/\\\.]+/g, '-');
|
|
7
|
+
s = s.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
8
|
+
s = s.replace(/-{2,}/g, '-');
|
|
9
|
+
s = s.replace(/^-+|-+$/g, '');
|
|
10
|
+
s = s.slice(0, 80);
|
|
11
|
+
if (s.length === 0)
|
|
12
|
+
throw new Error('Slug is empty after sanitization');
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
4
15
|
// ── Body templates per type and locale ─────────────────────────────
|
|
5
16
|
const BODY_TEMPLATES = {
|
|
6
17
|
en: {
|
|
@@ -57,7 +68,7 @@ export function renderTemplate(type, slug, options) {
|
|
|
57
68
|
if (data.id)
|
|
58
69
|
lines.push(`id: ${data.id}`);
|
|
59
70
|
lines.push(`type: ${data.type}`);
|
|
60
|
-
lines.push(`title: "${data.title}"`);
|
|
71
|
+
lines.push(`title: "${String(data.title).replace(/"/g, '\\"')}"`);
|
|
61
72
|
lines.push(`status: ${data.status}`);
|
|
62
73
|
if (confidence !== undefined) {
|
|
63
74
|
lines.push(`confidence: ${data.confidence}`);
|
|
@@ -98,5 +109,5 @@ export function nextId(graphDir, type) {
|
|
|
98
109
|
// ── nodePath ───────────────────────────────────────────────────────
|
|
99
110
|
export function nodePath(graphDir, type, id, slug) {
|
|
100
111
|
const typeDir = NODE_TYPE_DIRS[type];
|
|
101
|
-
return path.join(graphDir, typeDir, `${id}-${slug}.md`);
|
|
112
|
+
return path.join(graphDir, typeDir, `${id}-${sanitizeSlug(slug)}.md`);
|
|
102
113
|
}
|
package/dist/graph/types.d.ts
CHANGED
|
@@ -69,3 +69,25 @@ export interface PromoteCandidate {
|
|
|
69
69
|
confidence: number;
|
|
70
70
|
supports: number;
|
|
71
71
|
}
|
|
72
|
+
export interface WriteFileOp {
|
|
73
|
+
kind: 'write';
|
|
74
|
+
path: string;
|
|
75
|
+
content: string;
|
|
76
|
+
}
|
|
77
|
+
export interface MkdirOp {
|
|
78
|
+
kind: 'mkdir';
|
|
79
|
+
path: string;
|
|
80
|
+
}
|
|
81
|
+
export type FileOp = WriteFileOp | MkdirOp;
|
|
82
|
+
export interface CreateNodePlan {
|
|
83
|
+
id: string;
|
|
84
|
+
type: NodeType;
|
|
85
|
+
path: string;
|
|
86
|
+
ops: FileOp[];
|
|
87
|
+
}
|
|
88
|
+
export interface CreateEdgePlan {
|
|
89
|
+
source: string;
|
|
90
|
+
target: string;
|
|
91
|
+
relation: string;
|
|
92
|
+
ops: FileOp[];
|
|
93
|
+
}
|
|
@@ -5,7 +5,9 @@ export function registerCreateNode(server) {
|
|
|
5
5
|
server.tool('create-node', 'Create a new node of the given type with the given slug', {
|
|
6
6
|
graphDir: z.string().describe('Path to the EMDD graph directory'),
|
|
7
7
|
type: z.string().describe('Node type (hypothesis, experiment, finding, knowledge, question, decision, episode)'),
|
|
8
|
-
slug: z.string().
|
|
8
|
+
slug: z.string().min(1).max(80)
|
|
9
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, 'Slug must be alphanumeric with hyphens/underscores')
|
|
10
|
+
.describe('URL-friendly slug for the node'),
|
|
9
11
|
lang: z.string().optional().describe('Language locale (default: en)'),
|
|
10
12
|
}, async ({ graphDir, type, slug, lang }) => withErrorHandling(async () => {
|
|
11
13
|
const result = await createNode(graphDir, type, slug, lang);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beomjk/emdd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "CLI for Evolving Mindmap-Driven Development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"test": "vitest",
|
|
20
20
|
"test:run": "vitest run --passWithNoTests",
|
|
21
21
|
"lint": "tsc --noEmit",
|
|
22
|
-
"demo:record": "terminal-demo play docs/assets/demo.md --record docs/assets/demo.cast && svg-term --in docs/assets/demo.cast --out docs/assets/demo.svg --window --width 80 --height 24"
|
|
22
|
+
"demo:record": "npx terminal-demo play docs/assets/demo.md --record docs/assets/demo.cast && npx svg-term --in docs/assets/demo.cast --out docs/assets/demo.svg --window --width 80 --height 24"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"emdd",
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"cli"
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
32
35
|
"dependencies": {
|
|
33
36
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
34
37
|
"chalk": "^5.6.2",
|
|
@@ -41,9 +44,6 @@
|
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"@types/node": "^25.5.0",
|
|
44
|
-
"react": "^16.14.0",
|
|
45
|
-
"svg-term-cli": "^2.1.1",
|
|
46
|
-
"terminal-demo": "^0.1.11",
|
|
47
47
|
"tsx": "^4.21.0",
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
49
|
"vitest": "^4.1.0"
|
package/dist/rules/emdd-agent.md
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# EMDD Agent Behavior Guidelines
|
|
2
|
-
|
|
3
|
-
## Session Workflow
|
|
4
|
-
|
|
5
|
-
1. **Session Start**: Read the latest Episode's "What's Next" section. Load prerequisite nodes.
|
|
6
|
-
2. **During Work**: Execute experiments, write code, take notes. Mark surprises with [!].
|
|
7
|
-
3. **Session End**: Write a new Episode. Record what was tried, create Findings, list next steps.
|
|
8
|
-
|
|
9
|
-
## Intervention Rules
|
|
10
|
-
|
|
11
|
-
- During deep work: do not interrupt unless a kill criterion is hit or a crash occurs
|
|
12
|
-
- Suggestions are a menu, not an order — the researcher selects what to pursue
|
|
13
|
-
- After a suggestion is rejected: 24-hour cooldown before re-suggesting
|
|
14
|
-
- Never negate an idea still forming in early exploration stages
|
|
15
|
-
|
|
16
|
-
## Authority Scope
|
|
17
|
-
|
|
18
|
-
**No approval needed:**
|
|
19
|
-
- Recording experiment metrics as Finding/Result nodes
|
|
20
|
-
- Updating Experiment status (PLANNED -> RUNNING -> COMPLETED)
|
|
21
|
-
- Time-based attribute updates (updated field)
|
|
22
|
-
|
|
23
|
-
**Approval required:**
|
|
24
|
-
- Changing Hypothesis confidence
|
|
25
|
-
- Creating new Hypothesis or Question nodes
|
|
26
|
-
- Adding or deleting edges
|
|
27
|
-
- Changing Knowledge status (DISPUTED/RETRACTED)
|
|
28
|
-
|
|
29
|
-
**Forbidden:**
|
|
30
|
-
- Deleting any node (archive/deprecate instead)
|
|
31
|
-
- Creating Decision nodes
|
|
32
|
-
- Modifying kill criteria
|
|
33
|
-
|
|
34
|
-
## Graph Maintenance Tasks
|
|
35
|
-
|
|
36
|
-
- After experiments: update related node statuses and confidence scores (with approval)
|
|
37
|
-
- Check Consolidation triggers after creating Episodes or Findings
|
|
38
|
-
- Identify orphan nodes (nodes with no outgoing links)
|
|
39
|
-
- Detect stale nodes (untested hypotheses older than 3 days)
|
|
40
|
-
- Flag structural gaps between clusters
|
package/dist/rules/emdd-rules.md
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# EMDD — Evolving Mindmap-Driven Development
|
|
2
|
-
|
|
3
|
-
You are working in a project that uses the EMDD methodology. EMDD organizes research and exploration as a knowledge graph stored in `graph/` with Markdown + YAML frontmatter files, tracked by Git.
|
|
4
|
-
|
|
5
|
-
## Graph Structure
|
|
6
|
-
|
|
7
|
-
The graph contains 7 node types, each in its own subdirectory:
|
|
8
|
-
|
|
9
|
-
| Node Type | Directory | Purpose |
|
|
10
|
-
|-----------|-----------|---------|
|
|
11
|
-
| Hypothesis | `graph/hypotheses/` | Testable claims with confidence scores |
|
|
12
|
-
| Experiment | `graph/experiments/` | Units of work that test hypotheses |
|
|
13
|
-
| Finding | `graph/findings/` | Observations from experiments |
|
|
14
|
-
| Knowledge | `graph/knowledge/` | Established facts promoted from findings |
|
|
15
|
-
| Question | `graph/questions/` | Open questions driving exploration |
|
|
16
|
-
| Decision | `graph/decisions/` | Recorded choices with rationale |
|
|
17
|
-
| Episode | `graph/episodes/` | Session logs linking work to the graph |
|
|
18
|
-
|
|
19
|
-
Nodes are connected by typed edges (supports, contradicts, spawns, produces, tests, depends_on, extends, promotes, answers, etc.) declared in YAML frontmatter `links:` arrays.
|
|
20
|
-
|
|
21
|
-
## Node File Format
|
|
22
|
-
|
|
23
|
-
Every node is a Markdown file with YAML frontmatter:
|
|
24
|
-
|
|
25
|
-
```yaml
|
|
26
|
-
---
|
|
27
|
-
id: hyp-001
|
|
28
|
-
type: hypothesis
|
|
29
|
-
status: PROPOSED
|
|
30
|
-
confidence: 0.4
|
|
31
|
-
created: 2026-03-15
|
|
32
|
-
updated: 2026-03-15
|
|
33
|
-
created_by: human:yourname
|
|
34
|
-
tags: [topic]
|
|
35
|
-
links:
|
|
36
|
-
- target: know-001
|
|
37
|
-
relation: depends_on
|
|
38
|
-
---
|
|
39
|
-
# Title here
|
|
40
|
-
Body content...
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Required fields vary by type. All nodes need: `id`, `type`, `status`, `created`, `updated`. Hypotheses and findings also need `confidence` (0.0-1.0).
|
|
44
|
-
|
|
45
|
-
## Node ID Convention
|
|
46
|
-
|
|
47
|
-
IDs use type prefix + sequential number: `hyp-001`, `exp-003`, `fnd-012`, `knw-005`, `qst-002`, `dec-001`, `epi-007`.
|
|
48
|
-
|
|
49
|
-
## Episode Writing Protocol
|
|
50
|
-
|
|
51
|
-
Episodes are the primary mechanism for maintaining research continuity. Write an Episode at the end of each work session.
|
|
52
|
-
|
|
53
|
-
**Mandatory sections:**
|
|
54
|
-
- **What I Tried** — what was done this session
|
|
55
|
-
- **What's Next** — planned next steps with prerequisite reading nodes
|
|
56
|
-
|
|
57
|
-
**Optional sections:**
|
|
58
|
-
- What Got Stuck — blockers or wrong turns
|
|
59
|
-
- What Was Deliberately Not Done — deferred items with reasons
|
|
60
|
-
- Questions That Arose — new questions for the graph
|
|
61
|
-
|
|
62
|
-
Each "What's Next" item should list prerequisite reading: the node IDs to load before starting that task. This curates context for the next session.
|
|
63
|
-
|
|
64
|
-
## Consolidation Protocol
|
|
65
|
-
|
|
66
|
-
Consolidation is a mandatory maintenance ceremony. Check triggers after creating Episodes or Findings.
|
|
67
|
-
|
|
68
|
-
**Triggers (run if ANY apply):**
|
|
69
|
-
- 5 or more Finding nodes added since last Consolidation
|
|
70
|
-
- 3 or more Episode nodes added since last Consolidation
|
|
71
|
-
- 0 open Questions (the illusion that research is "done")
|
|
72
|
-
- An Experiment has 5+ Findings attached
|
|
73
|
-
|
|
74
|
-
**Consolidation steps:**
|
|
75
|
-
1. **Promotion** — promote established Findings to Knowledge nodes
|
|
76
|
-
2. **Splitting** — split bloated Experiments into meaningful units
|
|
77
|
-
3. **Question generation** — convert Episode questions into Question nodes
|
|
78
|
-
4. **Hypothesis update** — update confidence based on evidence
|
|
79
|
-
5. **Orphan cleanup** — add connections to unlinked Findings
|
|
80
|
-
|
|
81
|
-
Consolidation is an obligation, not optional. Do not record Consolidation as an Episode. Do not start new exploration during Consolidation.
|
|
82
|
-
|
|
83
|
-
## Key Principles
|
|
84
|
-
|
|
85
|
-
1. **Graph is source of truth** — the graph, not code, is the project's knowledge structure
|
|
86
|
-
2. **Minimum viable structure** — add structure only when needed; if it feels like bureaucracy, reduce it
|
|
87
|
-
3. **Gap-driven exploration** — the most valuable information is in the empty spaces between nodes
|
|
88
|
-
4. **Temporal evolution** — never delete wrong paths; deprecate them. The history of why something failed is itself knowledge
|
|
89
|
-
5. **Riskiest-first** — validate the most uncertain hypotheses first
|
|
90
|
-
6. **Archive, don't delete** — change status to REFUTED/RETRACTED/SUPERSEDED instead of removing nodes
|
|
91
|
-
|
|
92
|
-
## AI Agent Role
|
|
93
|
-
|
|
94
|
-
You are a **gardener** of the graph:
|
|
95
|
-
- Maintain connections, detect duplicates, identify orphans
|
|
96
|
-
- Detect patterns and potential connections the researcher missed
|
|
97
|
-
- Suggest exploration directions based on structural gaps
|
|
98
|
-
- Automate routine tasks (literature search, result summarization)
|
|
99
|
-
- Never make judgment calls — suggest, don't decide
|