@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 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();
@@ -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 ?? '';
@@ -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
- parsed.data[key] = parseFloat(value);
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.
@@ -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
- * Create a new node of the given type with the given slug.
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 async function createNode(graphDir, type, slug, lang) {
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 content = renderTemplate(nodeType, slug, {
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, slug);
55
- // Ensure directory exists
56
- const dir = filePath.substring(0, filePath.lastIndexOf('/'));
57
- if (!fs.existsSync(dir)) {
58
- fs.mkdirSync(dir, { recursive: true });
59
- }
60
- fs.writeFileSync(filePath, content, 'utf-8');
61
- return { id, type: nodeType, path: filePath };
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
- * Add an edge (link) from source to target with the given relation.
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 createEdge(graphDir, source, target, relation) {
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
- // Write back
122
+ // Compute new file content
98
123
  const output = matter.stringify(parsed.content, parsed.data);
99
- fs.writeFileSync(filePath, output);
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;
@@ -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
  }
@@ -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().describe('URL-friendly slug for the node'),
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.0",
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"
@@ -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
@@ -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