@chrisdudek/yg 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.
package/dist/bin.js ADDED
@@ -0,0 +1,3294 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/init.ts
7
+ import { mkdir as mkdir2, writeFile as writeFile2, readdir, readFile as readFile2, stat } from "fs/promises";
8
+ import path2 from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ // src/templates/default-config.ts
12
+ var DEFAULT_CONFIG = `name: ""
13
+
14
+ stack:
15
+ language: ""
16
+ runtime: ""
17
+
18
+ standards: ""
19
+
20
+ tags: []
21
+
22
+ node_types:
23
+ - module
24
+ - service
25
+ - library
26
+
27
+ artifacts:
28
+ responsibility.md:
29
+ required: always
30
+ description: "What this node is responsible for, and what it is not"
31
+ interface.md:
32
+ required:
33
+ when: has_incoming_relations
34
+ description: "Public API \u2014 methods, parameters, return types, contracts"
35
+ structural_context: true
36
+ constraints.md:
37
+ required: never
38
+ description: "Validation rules, business rules, invariants"
39
+ errors.md:
40
+ required:
41
+ when: has_incoming_relations
42
+ description: "Error conditions, codes, recovery behavior"
43
+ structural_context: true
44
+ state.md:
45
+ required: never
46
+ description: "State machines, lifecycle, transitions"
47
+ decisions.md:
48
+ required: never
49
+ description: "Local design decisions and rationale"
50
+
51
+ knowledge_categories:
52
+ - name: decisions
53
+ description: "Global semantic decisions and their rationale"
54
+ - name: patterns
55
+ description: "Implementation conventions with examples"
56
+ - name: invariants
57
+ description: "System truths that must never be violated"
58
+
59
+ quality:
60
+ min_artifact_length: 50
61
+ max_direct_relations: 10
62
+ context_budget:
63
+ warning: 5000
64
+ error: 10000
65
+ knowledge_staleness_days: 90
66
+ `;
67
+
68
+ // src/templates/platform.ts
69
+ import { readFile, writeFile, mkdir } from "fs/promises";
70
+ import path from "path";
71
+
72
+ // src/templates/rules.ts
73
+ var AGENT_RULES_CONTENT = `# Yggdrasil - System Semantic Memory (Operating Manual)
74
+
75
+ You are working in a repository managed by Yggdrasil.
76
+ Yggdrasil is a persistent, structured semantic memory graph stored in \`.yggdrasil/\`. It maps the repository, dictates system rules, and assembles implementation contexts.
77
+
78
+ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strictly.
79
+
80
+ ---
81
+
82
+ ## 1. CORE PRINCIPLES (NON-NEGOTIABLE)
83
+
84
+ 1. **Graph First, Always:** Before answering a question, modifying code, or planning a feature, you MUST consult the graph.
85
+ 2. **Context is Sufficient:** If you feel the need to randomly explore source files to understand what a node should do, the graph is incomplete. **Fix the graph** (add decisions, interface details, constraints). Do not bypass the graph by reading raw code.
86
+ 3. **Graph is Intended Truth:** If the code and graph diverge, the graph is the truth. If a code change is deliberate, update the graph to match.
87
+ 4. **Exhaustive Coverage:** Every source file MUST belong to exactly one graph node. No orphaned files.
88
+ 5. **Tools Read, You Write:** The \`yg\` CLI tools only read, validate, and manage metadata. YOU must create and edit graph directories, \`.yaml\` files, and \`.md\` artifacts manually.
89
+ 6. **English Only for Artifacts:** All graph artifact files (filenames from \`config.artifacts\`, in the same directory as \`node.yaml\`) MUST be written in English. Conversation can be in the user's language.
90
+ 7. **Never Touch Operational Metadata:** NEVER manually edit \`.yggdrasil/.drift-state\` or \`.yggdrasil/.journal.yaml\`.
91
+
92
+ ---
93
+
94
+ ## 2. CONVERSATION LIFECYCLE (YOUR HABITS)
95
+
96
+ You do not need explicit "session" commands. Follow these conversational triggers:
97
+
98
+ ### A. Preflight (First message of the conversation)
99
+ Always execute these commands before doing anything else:
100
+ 1. \`yg journal-read\` -> If entries exist, consolidate them into the graph, then \`yg journal-archive\`.
101
+ 2. \`yg drift\` -> If divergence is detected, present states (\`ok\`, \`drift\`, \`missing\`, \`unmaterialized\`). Ask the user: Absorb (update graph) or Reject (re-materialize code from graph)?
102
+ 3. \`yg status\` -> Report graph health.
103
+ 4. \`yg validate\` -> If W008 stale-knowledge appears, update the knowledge artifacts to reflect current node state.
104
+
105
+ ### B. Wrap-up (User signals closing the topic)
106
+ Triggered by phrases like: "ko\u0144czymy", "wrap up", "to tyle", "gotowe".
107
+ **Note: The graph should ALREADY be up to date. Do not wait for wrap-up to update graph artifacts.**
108
+ 1. If iterative journal mode was used: consolidate notes to the graph, then \`yg journal-archive\`.
109
+ 2. \`yg drift\` -> Check if files changed manually during the conversation.
110
+ 3. \`yg validate\` -> Fix any structural errors.
111
+ 4. Report exactly what nodes and files were changed.
112
+
113
+ ---
114
+
115
+ ## 3. WORKFLOW: MODIFYING OR CREATING FILES
116
+
117
+ You are NOT ALLOWED to edit or create source code without establishing graph coverage first.
118
+
119
+ **Step 1: Check coverage** -> Run \`yg owner --file <path>\`
120
+
121
+ **Step 2: If Owner FOUND (The Execution Checklist)**
122
+ Whenever you write or edit source code, you MUST output this exact checklist in your response to the user, and execute each step BEFORE finishing your turn. This forces you to remember the graph:
123
+
124
+ - [x] 1. Read Specification (ran \`yg build-context\`)
125
+ - [x] 2. Modify Source Code
126
+ - [x] 3. Sync Graph Artifacts (manually edit the node's artifact files \u2014 filenames from \`config.artifacts\` \u2014 IMMEDIATELY to match new code behavior)
127
+ - [x] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
128
+
129
+ *If you do not print this checklist and check off step 3, you have failed the core directive of Yggdrasil.*
130
+
131
+ **Step 3: If Owner NOT FOUND (Uncovered Area)**
132
+ STOP. Do not modify the code. First determine: **Is this greenfield (empty or new code to be created)?**
133
+
134
+ * **If GREENFIELD (empty directory, new project, code not yet written):** Do NOT offer blackbox. Use Option 1 only \u2014 create proper nodes (reverse engineering or upfront design) before implementing. Blackbox is forbidden for new code.
135
+ * **If EXISTING CODE (legacy, third-party, shipped-but-unmapped):** Present the user with 3 options and wait for their decision:
136
+ * **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
137
+ * **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node at a user-chosen granularity to establish ownership without deep semantic exploration.
138
+ * **Option 3: Abort/Change Plan:** Do not touch the file.
139
+
140
+
141
+ ---
142
+
143
+ ## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS
144
+
145
+ When adding features or changing architecture, update the graph FIRST.
146
+
147
+ **DO NOT DEFER GRAPH UPDATES:**
148
+ * **DO NOT wait for the user to confirm if a change is "final".** The graph must evolve continuously with your code edits.
149
+ * **Default Behavior:** If iterative journal mode is OFF, you MUST write structural and semantic changes directly to the graph files (\`node.yaml\`, artifacts or other files like aspects or flows, etc.) IMMEDIATELY. Suppress your innate safety bias to wait for permission.
150
+
151
+ 1. **Check Blast Radius:** Before modifying a node that others depend on, run \`yg impact --node <node_path> --simulate\`. Report the impact to the user.
152
+ 2. **Read Config & Templates:**
153
+ * Check \`.yggdrasil/config.yaml\` for allowed \`node_types\` and \`tags\`.
154
+ * **CRITICAL:** ALWAYS read the required schema files in \`.yggdrasil/templates/\` (e.g., \`node.yaml\`, \`service.yaml\`) to know the exact fields and structure before creating or editing any graph file.
155
+ 3. **Validate & Fix:** Run \`yg validate\`. You must fix all E-codes (Errors).
156
+ 4. **Token Economy & W-codes (Warnings):**
157
+ * If you see \`W005 budget-warning\` or \`W006 budget-error\`, the context package is too large. You MUST consider splitting the node or reducing dependencies.
158
+ * If you see \`W008 stale-knowledge\`, the semantic memory is outdated compared to the code. Update the knowledge artifacts.
159
+ * **Smallest Viable Scope:** Prefer \`scope: nodes\` over \`scope: tags\`. Prefer tags over \`scope: global\`. Global scope costs token budget in EVERY context package.
160
+
161
+ **Journaling (Iterative Mode):**
162
+ * **Default:** Write changes directly to graph files immediately.
163
+ * **Opt-in:** ONLY if the user says "tryb iteracyjny" or "u\u017Cyj journala", use \`yg journal-add --note "..."\` to buffer intent during fast ping-pong changes.
164
+
165
+ ---
166
+
167
+ ## 5. PATH CONVENTIONS (CRITICAL)
168
+
169
+ To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
170
+ * **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
171
+ * **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
172
+ * **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
173
+
174
+ ---
175
+
176
+ ## 6. GRAPH STRUCTURE, CONFIG & TEMPLATES CHEAT SHEET
177
+
178
+ The graph lives entirely under \`.yggdrasil/\`. You NEVER guess structure. You MUST ALWAYS read the corresponding schema reference in \`.yggdrasil/templates/\` before creating or editing any graph file.
179
+
180
+ * **\`.yggdrasil/config.yaml\`**: The ONLY config file. Defines \`node_types\`, \`tags\`, \`artifacts\`, \`knowledge_categories\`, and quality thresholds. Read this before any graph work.
181
+ * **\`.yggdrasil/templates/\`**: The SINGLE place for all templates and schemas.
182
+ * Contains node-type templates (e.g., \`service.yaml\`, \`module.yaml\`) with suggested artifacts and guidance.
183
+ * Contains schema references (\`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`) showing exact file structures.
184
+ * **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files (filenames from \`config.artifacts\`; required ones depend on config).
185
+ * **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` (binds via \`tag: <name>\`) and \`.md\` content.
186
+ * **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` (lists \`nodes: [paths]\` and \`knowledge: [paths]\`) and \`.md\` content.
187
+ * **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom (\`decisions/\`, \`patterns/\`, \`invariants/\`). Directory contains \`knowledge.yaml\` and \`.md\` content.
188
+
189
+ ---
190
+
191
+ ## 7. CLI TOOLS REFERENCE (\`yg\`)
192
+
193
+ Always use these exact commands.
194
+
195
+ * \`yg owner --file <file_path>\` -> Find owning node.
196
+ * \`yg build-context --node <node_path>\` -> Assemble strict specification.
197
+ * \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
198
+ * \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
199
+ * \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
200
+ * \`yg status\` -> Graph health metrics.
201
+ * \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
202
+ * \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
203
+ * \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
204
+
205
+ *(Iterative mode only)*
206
+ * \`yg journal-read\`
207
+ * \`yg journal-add --note "<content>" [--target <node_path>]\`
208
+ * \`yg journal-archive\`
209
+ `;
210
+
211
+ // src/templates/platform.ts
212
+ var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
213
+ var YGGDRASIL_START = "<!-- yggdrasil:start -->";
214
+ var YGGDRASIL_END = "<!-- yggdrasil:end -->";
215
+ var YGGDRASIL_SECTION = `## Yggdrasil
216
+
217
+ ${AGENT_RULES_CONTENT}`;
218
+ var YGGDRASIL_BLOCK = `${YGGDRASIL_START}
219
+ ${YGGDRASIL_SECTION}
220
+ ${YGGDRASIL_END}`;
221
+ var PLATFORMS = [
222
+ "cursor",
223
+ "claude-code",
224
+ "copilot",
225
+ "cline",
226
+ "roocode",
227
+ "codex",
228
+ "windsurf",
229
+ "aider",
230
+ "gemini",
231
+ "amp",
232
+ "generic"
233
+ ];
234
+ async function installRulesForPlatform(projectRoot, platform) {
235
+ const agentRulesPath = path.join(projectRoot, ".yggdrasil", "agent-rules.md");
236
+ switch (platform) {
237
+ case "cursor":
238
+ return installForCursor(projectRoot);
239
+ case "claude-code":
240
+ return installForClaudeCode(projectRoot, agentRulesPath);
241
+ case "copilot":
242
+ return installForCopilot(projectRoot);
243
+ case "cline":
244
+ return installForCline(projectRoot);
245
+ case "roocode":
246
+ return installForRooCode(projectRoot);
247
+ case "codex":
248
+ return installForCodex(projectRoot);
249
+ case "windsurf":
250
+ return installForWindsurf(projectRoot);
251
+ case "aider":
252
+ return installForAider(projectRoot, agentRulesPath);
253
+ case "gemini":
254
+ return installForGemini(projectRoot, agentRulesPath);
255
+ case "amp":
256
+ return installForAmp(projectRoot, agentRulesPath);
257
+ case "generic":
258
+ default:
259
+ return installForGeneric(projectRoot);
260
+ }
261
+ }
262
+ async function ensureAgentRules(agentRulesPath) {
263
+ await mkdir(path.dirname(agentRulesPath), { recursive: true });
264
+ await writeFile(agentRulesPath, AGENT_RULES_CONTENT, "utf-8");
265
+ }
266
+ async function installForCursor(projectRoot) {
267
+ const dir = path.join(projectRoot, ".cursor", "rules");
268
+ await mkdir(dir, { recursive: true });
269
+ const filePath = path.join(dir, "yggdrasil.mdc");
270
+ const content = `---
271
+ description: Yggdrasil \u2014 semantic memory of the repository
272
+ alwaysApply: true
273
+ ---
274
+
275
+ ${AGENT_RULES_CONTENT}`;
276
+ await writeFile(filePath, content, "utf-8");
277
+ return filePath;
278
+ }
279
+ async function installForClaudeCode(projectRoot, agentRulesPath) {
280
+ await ensureAgentRules(agentRulesPath);
281
+ const filePath = path.join(projectRoot, "CLAUDE.md");
282
+ let existing = "";
283
+ try {
284
+ existing = await readFile(filePath, "utf-8");
285
+ } catch {
286
+ }
287
+ const importLine = AGENT_RULES_IMPORT;
288
+ if (existing.includes(importLine)) {
289
+ return agentRulesPath;
290
+ }
291
+ const content = existing.trimEnd() ? `${existing.trimEnd()}
292
+ ${importLine}
293
+ ` : `${importLine}
294
+ `;
295
+ await writeFile(filePath, content, "utf-8");
296
+ return agentRulesPath;
297
+ }
298
+ async function installForCopilot(projectRoot) {
299
+ const dir = path.join(projectRoot, ".github");
300
+ await mkdir(dir, { recursive: true });
301
+ const filePath = path.join(dir, "copilot-instructions.md");
302
+ let existing = "";
303
+ try {
304
+ existing = await readFile(filePath, "utf-8");
305
+ } catch {
306
+ }
307
+ let content;
308
+ if (existing.includes(YGGDRASIL_START) && existing.includes(YGGDRASIL_END)) {
309
+ content = existing.replace(
310
+ new RegExp(`${escapeRegex(YGGDRASIL_START)}[\\s\\S]*?${escapeRegex(YGGDRASIL_END)}`, "g"),
311
+ YGGDRASIL_BLOCK
312
+ );
313
+ } else {
314
+ content = existing.trimEnd() ? `${existing.trimEnd()}
315
+
316
+ ${YGGDRASIL_BLOCK}
317
+ ` : `${YGGDRASIL_BLOCK}
318
+ `;
319
+ }
320
+ await writeFile(filePath, content, "utf-8");
321
+ return filePath;
322
+ }
323
+ async function installForCline(projectRoot) {
324
+ const dir = path.join(projectRoot, ".clinerules");
325
+ await mkdir(dir, { recursive: true });
326
+ const filePath = path.join(dir, "yggdrasil.md");
327
+ await writeFile(filePath, AGENT_RULES_CONTENT, "utf-8");
328
+ return filePath;
329
+ }
330
+ async function installForRooCode(projectRoot) {
331
+ const dir = path.join(projectRoot, ".roo", "rules");
332
+ await mkdir(dir, { recursive: true });
333
+ const filePath = path.join(dir, "yggdrasil.md");
334
+ await writeFile(filePath, AGENT_RULES_CONTENT, "utf-8");
335
+ return filePath;
336
+ }
337
+ async function installForCodex(projectRoot) {
338
+ const filePath = path.join(projectRoot, "AGENTS.md");
339
+ let existing = "";
340
+ try {
341
+ existing = await readFile(filePath, "utf-8");
342
+ } catch {
343
+ }
344
+ let content;
345
+ if (existing.includes(YGGDRASIL_START) && existing.includes(YGGDRASIL_END)) {
346
+ content = existing.replace(
347
+ new RegExp(`${escapeRegex(YGGDRASIL_START)}[\\s\\S]*?${escapeRegex(YGGDRASIL_END)}`, "g"),
348
+ YGGDRASIL_BLOCK
349
+ );
350
+ } else {
351
+ content = existing.trimEnd() ? `${existing.trimEnd()}
352
+
353
+ ${YGGDRASIL_BLOCK}
354
+ ` : `${YGGDRASIL_BLOCK}
355
+ `;
356
+ }
357
+ await writeFile(filePath, content, "utf-8");
358
+ return filePath;
359
+ }
360
+ async function installForWindsurf(projectRoot) {
361
+ const dir = path.join(projectRoot, ".windsurf", "rules");
362
+ await mkdir(dir, { recursive: true });
363
+ const filePath = path.join(dir, "yggdrasil.md");
364
+ await writeFile(filePath, AGENT_RULES_CONTENT, "utf-8");
365
+ return filePath;
366
+ }
367
+ async function installForAider(projectRoot, agentRulesPath) {
368
+ await ensureAgentRules(agentRulesPath);
369
+ const filePath = path.join(projectRoot, ".aider.conf.yml");
370
+ const entry = ".yggdrasil/agent-rules.md";
371
+ let existing = "";
372
+ try {
373
+ existing = await readFile(filePath, "utf-8");
374
+ } catch {
375
+ }
376
+ if (existing.includes(entry)) {
377
+ return agentRulesPath;
378
+ }
379
+ const content = appendAiderReadEntry(existing, entry);
380
+ await writeFile(filePath, content, "utf-8");
381
+ return agentRulesPath;
382
+ }
383
+ function appendAiderReadEntry(existing, entry) {
384
+ const newItem = ` - ${entry} # added by yg init
385
+ `;
386
+ const readBlock = /^read:\s*\n((?:\s+-\s+[^\n]+\n)*)/m;
387
+ const match = existing.match(readBlock);
388
+ if (match) {
389
+ return existing.replace(match[0], `read:
390
+ ${match[1]}${newItem}`);
391
+ }
392
+ const readEmpty = /^read:\s*$/m;
393
+ if (readEmpty.test(existing)) {
394
+ return existing.replace(readEmpty, `read:
395
+ ${newItem}`);
396
+ }
397
+ const trimmed = existing.trimEnd();
398
+ return trimmed ? `${trimmed}
399
+
400
+ read:
401
+ ${newItem}` : `read:
402
+ ${newItem}`;
403
+ }
404
+ async function installForGemini(projectRoot, agentRulesPath) {
405
+ await ensureAgentRules(agentRulesPath);
406
+ const filePath = path.join(projectRoot, "GEMINI.md");
407
+ let existing = "";
408
+ try {
409
+ existing = await readFile(filePath, "utf-8");
410
+ } catch {
411
+ }
412
+ const importLine = AGENT_RULES_IMPORT;
413
+ if (existing.includes(importLine)) {
414
+ return agentRulesPath;
415
+ }
416
+ const content = existing.trimEnd() ? `${existing.trimEnd()}
417
+ ${importLine}
418
+ ` : `${importLine}
419
+ `;
420
+ await writeFile(filePath, content, "utf-8");
421
+ return agentRulesPath;
422
+ }
423
+ async function installForAmp(projectRoot, agentRulesPath) {
424
+ await ensureAgentRules(agentRulesPath);
425
+ const filePath = path.join(projectRoot, "AGENTS.md");
426
+ let existing = "";
427
+ try {
428
+ existing = await readFile(filePath, "utf-8");
429
+ } catch {
430
+ }
431
+ const importLine = AGENT_RULES_IMPORT;
432
+ if (existing.includes(importLine)) {
433
+ return agentRulesPath;
434
+ }
435
+ const content = existing.trimEnd() ? `${existing.trimEnd()}
436
+ ${importLine}
437
+ ` : `${importLine}
438
+ `;
439
+ await writeFile(filePath, content, "utf-8");
440
+ return agentRulesPath;
441
+ }
442
+ async function installForGeneric(projectRoot) {
443
+ const filePath = path.join(projectRoot, ".yggdrasil", "agent-rules.md");
444
+ await ensureAgentRules(filePath);
445
+ return filePath;
446
+ }
447
+ function escapeRegex(s) {
448
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
449
+ }
450
+
451
+ // src/cli/init.ts
452
+ function getGraphTemplatesDir() {
453
+ const currentDir = path2.dirname(fileURLToPath(import.meta.url));
454
+ const packageRoot = path2.join(currentDir, "..");
455
+ return path2.join(packageRoot, "graph-templates");
456
+ }
457
+ var GITIGNORE_CONTENT = `.journal.yaml
458
+ journals-archive/
459
+ `;
460
+ function registerInitCommand(program2) {
461
+ program2.command("init").description("Initialize Yggdrasil graph in current project").option(
462
+ "--platform <name>",
463
+ "Agent platform: cursor, claude-code, copilot, cline, roocode, codex, windsurf, aider, gemini, amp, generic",
464
+ "generic"
465
+ ).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
466
+ const projectRoot = process.cwd();
467
+ const yggRoot = path2.join(projectRoot, ".yggdrasil");
468
+ let upgradeMode = false;
469
+ try {
470
+ const statResult = await stat(yggRoot);
471
+ if (!statResult.isDirectory()) {
472
+ process.stderr.write("Error: .yggdrasil exists but is not a directory.\n");
473
+ process.exit(1);
474
+ }
475
+ if (options.upgrade) {
476
+ upgradeMode = true;
477
+ } else {
478
+ process.stderr.write(
479
+ "Error: .yggdrasil/ already exists. Use --upgrade to refresh rules only.\n"
480
+ );
481
+ process.exit(1);
482
+ }
483
+ } catch {
484
+ }
485
+ const platform = options.platform ?? "generic";
486
+ if (!PLATFORMS.includes(platform)) {
487
+ process.stderr.write(
488
+ `Error: Unknown platform '${platform}'. Use: ${PLATFORMS.join(", ")}
489
+ `
490
+ );
491
+ process.exit(1);
492
+ }
493
+ if (upgradeMode) {
494
+ const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
495
+ process.stdout.write("\u2713 Rules refreshed.\n");
496
+ process.stdout.write(` ${path2.relative(projectRoot, rulesPath2)}
497
+ `);
498
+ return;
499
+ }
500
+ await mkdir2(path2.join(yggRoot, "model"), { recursive: true });
501
+ await mkdir2(path2.join(yggRoot, "aspects"), { recursive: true });
502
+ await mkdir2(path2.join(yggRoot, "flows"), { recursive: true });
503
+ await mkdir2(path2.join(yggRoot, "knowledge", "decisions"), { recursive: true });
504
+ await mkdir2(path2.join(yggRoot, "knowledge", "patterns"), { recursive: true });
505
+ await mkdir2(path2.join(yggRoot, "knowledge", "invariants"), { recursive: true });
506
+ const templatesDir = path2.join(yggRoot, "templates");
507
+ await mkdir2(templatesDir, { recursive: true });
508
+ const graphTemplatesDir = getGraphTemplatesDir();
509
+ try {
510
+ const entries = await readdir(graphTemplatesDir, { withFileTypes: true });
511
+ const templateFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
512
+ for (const file of templateFiles) {
513
+ const srcPath = path2.join(graphTemplatesDir, file);
514
+ const content = await readFile2(srcPath, "utf-8");
515
+ await writeFile2(path2.join(templatesDir, file), content, "utf-8");
516
+ }
517
+ } catch (err) {
518
+ process.stderr.write(
519
+ `Warning: Could not copy graph templates from ${graphTemplatesDir}: ${err.message}
520
+ `
521
+ );
522
+ }
523
+ await writeFile2(path2.join(yggRoot, "config.yaml"), DEFAULT_CONFIG, "utf-8");
524
+ await writeFile2(path2.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
525
+ const rulesPath = await installRulesForPlatform(projectRoot, platform);
526
+ process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
527
+ process.stdout.write("Created:\n");
528
+ process.stdout.write(" .yggdrasil/config.yaml\n");
529
+ process.stdout.write(" .yggdrasil/.gitignore\n");
530
+ process.stdout.write(" .yggdrasil/model/\n");
531
+ process.stdout.write(" .yggdrasil/aspects/\n");
532
+ process.stdout.write(" .yggdrasil/flows/\n");
533
+ process.stdout.write(" .yggdrasil/knowledge/ (decisions, patterns, invariants)\n");
534
+ process.stdout.write(
535
+ " .yggdrasil/templates/ (module, service, library, node, aspect, flow, knowledge)\n"
536
+ );
537
+ process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
538
+
539
+ `);
540
+ process.stdout.write("Next steps:\n");
541
+ process.stdout.write(" 1. Edit .yggdrasil/config.yaml \u2014 set name, stack, standards\n");
542
+ process.stdout.write(" 2. Create nodes under .yggdrasil/model/\n");
543
+ process.stdout.write(" 3. Run: yg validate\n");
544
+ });
545
+ }
546
+
547
+ // src/core/graph-loader.ts
548
+ import { readdir as readdir3 } from "fs/promises";
549
+ import path5 from "path";
550
+
551
+ // src/io/config-parser.ts
552
+ import { readFile as readFile3 } from "fs/promises";
553
+ import { parse as parseYaml } from "yaml";
554
+ var DEFAULT_QUALITY = {
555
+ min_artifact_length: 50,
556
+ max_direct_relations: 10,
557
+ context_budget: { warning: 5e3, error: 1e4 },
558
+ knowledge_staleness_days: 90
559
+ };
560
+ async function parseConfig(filePath) {
561
+ const content = await readFile3(filePath, "utf-8");
562
+ const raw = parseYaml(content);
563
+ if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
564
+ throw new Error(`config.yaml: missing or invalid 'name' field`);
565
+ }
566
+ const nodeTypes = raw.node_types;
567
+ if (!Array.isArray(nodeTypes) || nodeTypes.length === 0) {
568
+ throw new Error(`config.yaml: 'node_types' must be a non-empty array`);
569
+ }
570
+ const artifacts = raw.artifacts;
571
+ if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
572
+ throw new Error(`config.yaml: 'artifacts' must be a non-empty object`);
573
+ }
574
+ const artifactsMap = {};
575
+ for (const [key, val] of Object.entries(artifacts)) {
576
+ if (key === "node") {
577
+ throw new Error(`config.yaml: artifact name 'node' is reserved`);
578
+ }
579
+ const a = val;
580
+ const required = a.required;
581
+ if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
582
+ throw new Error(`config.yaml: artifact '${key}' has invalid 'required' field`);
583
+ }
584
+ if (typeof required === "object" && required && "when" in required) {
585
+ const when = required.when;
586
+ const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && when.startsWith("has_tag:");
587
+ if (!validWhen) {
588
+ throw new Error(
589
+ `config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_tag:<name>`
590
+ );
591
+ }
592
+ }
593
+ artifactsMap[key] = {
594
+ required,
595
+ description: a.description ?? "",
596
+ structural_context: a.structural_context ?? false
597
+ };
598
+ }
599
+ if (!("knowledge_categories" in raw)) {
600
+ throw new Error(
601
+ `config.yaml: missing 'knowledge_categories' field (required, may be empty list)`
602
+ );
603
+ }
604
+ const knowledgeCategoriesRaw = raw.knowledge_categories;
605
+ if (!Array.isArray(knowledgeCategoriesRaw)) {
606
+ throw new Error(`config.yaml: 'knowledge_categories' must be an array`);
607
+ }
608
+ const knowledgeCategories = knowledgeCategoriesRaw;
609
+ const categoryNames = /* @__PURE__ */ new Set();
610
+ for (const kc of knowledgeCategories) {
611
+ if (!kc?.name || typeof kc.name !== "string") continue;
612
+ if (categoryNames.has(kc.name)) {
613
+ throw new Error(`config.yaml: duplicate knowledge category '${kc.name}'`);
614
+ }
615
+ categoryNames.add(kc.name);
616
+ }
617
+ const qualityRaw = raw.quality;
618
+ const quality = qualityRaw ? {
619
+ min_artifact_length: qualityRaw.min_artifact_length ?? DEFAULT_QUALITY.min_artifact_length,
620
+ max_direct_relations: qualityRaw.max_direct_relations ?? DEFAULT_QUALITY.max_direct_relations,
621
+ context_budget: {
622
+ warning: qualityRaw.context_budget?.warning ?? DEFAULT_QUALITY.context_budget.warning,
623
+ error: qualityRaw.context_budget?.error ?? DEFAULT_QUALITY.context_budget.error
624
+ },
625
+ knowledge_staleness_days: qualityRaw.knowledge_staleness_days ?? DEFAULT_QUALITY.knowledge_staleness_days
626
+ } : DEFAULT_QUALITY;
627
+ if (quality.context_budget.error < quality.context_budget.warning) {
628
+ throw new Error(
629
+ `config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
630
+ );
631
+ }
632
+ if (!("tags" in raw)) {
633
+ throw new Error(`config.yaml: missing 'tags' field (required, may be empty list)`);
634
+ }
635
+ const tags = raw.tags;
636
+ if (!Array.isArray(tags)) {
637
+ throw new Error(`config.yaml: 'tags' must be an array`);
638
+ }
639
+ const tagsList = tags.filter((t) => typeof t === "string");
640
+ return {
641
+ name: raw.name.trim(),
642
+ stack: raw.stack ?? {},
643
+ standards: typeof raw.standards === "string" ? raw.standards : "",
644
+ tags: tagsList,
645
+ node_types: nodeTypes,
646
+ artifacts: artifactsMap,
647
+ knowledge_categories: knowledgeCategories.filter((kc) => kc?.name),
648
+ quality
649
+ };
650
+ }
651
+
652
+ // src/io/node-parser.ts
653
+ import { readFile as readFile4 } from "fs/promises";
654
+ import { parse as parseYaml2 } from "yaml";
655
+ var RELATION_TYPES = [
656
+ "uses",
657
+ "calls",
658
+ "extends",
659
+ "implements",
660
+ "emits",
661
+ "listens"
662
+ ];
663
+ function isValidRelationType(t) {
664
+ return typeof t === "string" && RELATION_TYPES.includes(t);
665
+ }
666
+ async function parseNodeYaml(filePath) {
667
+ const content = await readFile4(filePath, "utf-8");
668
+ const raw = parseYaml2(content);
669
+ if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
670
+ throw new Error(`node.yaml at ${filePath}: missing or empty 'name'`);
671
+ }
672
+ if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
673
+ throw new Error(`node.yaml at ${filePath}: missing or empty 'type'`);
674
+ }
675
+ const relations = parseRelations(raw.relations, filePath);
676
+ const mapping = parseMapping(raw.mapping, filePath);
677
+ return {
678
+ name: raw.name.trim(),
679
+ type: raw.type.trim(),
680
+ tags: parseStringArray(raw.tags),
681
+ blackbox: raw.blackbox ?? false,
682
+ relations: relations.length > 0 ? relations : void 0,
683
+ knowledge: parseStringArray(raw.knowledge),
684
+ mapping
685
+ };
686
+ }
687
+ function parseStringArray(val) {
688
+ if (!Array.isArray(val)) return void 0;
689
+ const arr = val.filter((v) => typeof v === "string");
690
+ return arr.length > 0 ? arr : void 0;
691
+ }
692
+ function parseRelations(raw, filePath) {
693
+ if (raw === void 0) return [];
694
+ if (!Array.isArray(raw)) {
695
+ throw new Error(`node.yaml at ${filePath}: 'relations' must be an array`);
696
+ }
697
+ const result = [];
698
+ for (let index = 0; index < raw.length; index++) {
699
+ const r = raw[index];
700
+ if (typeof r !== "object" || r === null) {
701
+ throw new Error(`node.yaml at ${filePath}: relations[${index}] must be an object`);
702
+ }
703
+ const obj = r;
704
+ const target = obj.target;
705
+ const type = obj.type;
706
+ if (typeof target !== "string" || target.trim() === "") {
707
+ throw new Error(
708
+ `node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
709
+ );
710
+ }
711
+ if (!isValidRelationType(type)) {
712
+ throw new Error(`node.yaml at ${filePath}: relations[${index}].type is invalid`);
713
+ }
714
+ const rel = {
715
+ target: target.trim(),
716
+ type
717
+ };
718
+ if (Array.isArray(obj.consumes)) {
719
+ rel.consumes = obj.consumes.filter((c) => typeof c === "string");
720
+ }
721
+ if (typeof obj.failure === "string") {
722
+ rel.failure = obj.failure;
723
+ }
724
+ if (typeof obj.event_name === "string" && obj.event_name.trim()) {
725
+ rel.event_name = obj.event_name.trim();
726
+ }
727
+ result.push(rel);
728
+ }
729
+ return result;
730
+ }
731
+ function validateRelativePath(pathValue, filePath, fieldName) {
732
+ const normalized = pathValue.trim();
733
+ if (normalized === "") {
734
+ throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
735
+ }
736
+ if (normalized.startsWith("/")) {
737
+ throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
738
+ }
739
+ return normalized;
740
+ }
741
+ function parseMapping(rawMapping, filePath) {
742
+ if (!rawMapping || typeof rawMapping !== "object") return void 0;
743
+ const obj = rawMapping;
744
+ if (Array.isArray(obj.paths) && obj.paths.length > 0) {
745
+ const paths = obj.paths.filter((p) => typeof p === "string").map((p) => validateRelativePath(p, filePath, "mapping.paths[]"));
746
+ if (paths.length === 0) {
747
+ throw new Error(`node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
748
+ }
749
+ return { paths };
750
+ }
751
+ if (obj.paths !== void 0 || obj.type !== void 0 || obj.path !== void 0) {
752
+ throw new Error(
753
+ `node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
754
+ );
755
+ }
756
+ return void 0;
757
+ }
758
+
759
+ // src/io/aspect-parser.ts
760
+ import { readFile as readFile6 } from "fs/promises";
761
+ import { parse as parseYaml3 } from "yaml";
762
+
763
+ // src/io/artifact-reader.ts
764
+ import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
765
+ import path3 from "path";
766
+ async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles) {
767
+ const entries = await readdir2(dirPath, { withFileTypes: true });
768
+ const artifacts = [];
769
+ const includeSet = includeFiles && includeFiles.length > 0 ? new Set(includeFiles) : null;
770
+ for (const entry of entries) {
771
+ if (!entry.isFile()) continue;
772
+ if (excludeFiles.includes(entry.name)) continue;
773
+ if (includeSet && !includeSet.has(entry.name)) continue;
774
+ const filePath = path3.join(dirPath, entry.name);
775
+ const content = await readFile5(filePath, "utf-8");
776
+ artifacts.push({ filename: entry.name, content });
777
+ }
778
+ artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
779
+ return artifacts;
780
+ }
781
+
782
+ // src/io/aspect-parser.ts
783
+ async function parseAspect(aspectDir, aspectYamlPath) {
784
+ const content = await readFile6(aspectYamlPath, "utf-8");
785
+ const raw = parseYaml3(content);
786
+ if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
787
+ throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
788
+ }
789
+ if (!raw.tag || typeof raw.tag !== "string" || raw.tag.trim() === "") {
790
+ throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'tag'`);
791
+ }
792
+ const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
793
+ return {
794
+ name: raw.name.trim(),
795
+ tag: raw.tag.trim(),
796
+ artifacts
797
+ };
798
+ }
799
+
800
+ // src/io/flow-parser.ts
801
+ import { readFile as readFile7 } from "fs/promises";
802
+ import { parse as parseYaml4 } from "yaml";
803
+ async function parseFlow(flowDir, flowYamlPath) {
804
+ const content = await readFile7(flowYamlPath, "utf-8");
805
+ const raw = parseYaml4(content);
806
+ if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
807
+ throw new Error(`flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
808
+ }
809
+ const nodes = raw.nodes;
810
+ if (!Array.isArray(nodes) || nodes.length === 0) {
811
+ throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
812
+ }
813
+ const nodePaths = nodes.filter((n) => typeof n === "string");
814
+ if (nodePaths.length === 0) {
815
+ throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
816
+ }
817
+ const knowledge = Array.isArray(raw.knowledge) ? raw.knowledge.filter((k) => typeof k === "string") : void 0;
818
+ const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
819
+ return {
820
+ name: raw.name.trim(),
821
+ nodes: nodePaths,
822
+ knowledge,
823
+ artifacts
824
+ };
825
+ }
826
+
827
+ // src/io/knowledge-parser.ts
828
+ import { readFile as readFile8 } from "fs/promises";
829
+ import { parse as parseYaml5 } from "yaml";
830
+ async function parseKnowledge(knowledgeDir, knowledgeYamlPath, category, relativePath) {
831
+ const content = await readFile8(knowledgeYamlPath, "utf-8");
832
+ const raw = parseYaml5(content);
833
+ if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
834
+ throw new Error(`knowledge.yaml at ${knowledgeYamlPath}: missing or empty 'name'`);
835
+ }
836
+ const scope = parseScope(raw.scope, knowledgeYamlPath);
837
+ const artifacts = await readArtifacts(knowledgeDir, ["knowledge.yaml"]);
838
+ return {
839
+ name: raw.name.trim(),
840
+ scope,
841
+ category,
842
+ path: relativePath,
843
+ artifacts
844
+ };
845
+ }
846
+ function parseScope(raw, filePath) {
847
+ if (raw === "global") {
848
+ return "global";
849
+ }
850
+ if (raw && typeof raw === "object") {
851
+ const obj = raw;
852
+ if (Array.isArray(obj.tags)) {
853
+ const tags = obj.tags.filter((t) => typeof t === "string");
854
+ if (tags.length === 0) {
855
+ throw new Error(`knowledge.yaml at ${filePath}: scope.tags must be a non-empty array`);
856
+ }
857
+ return { tags };
858
+ }
859
+ if (Array.isArray(obj.nodes)) {
860
+ const nodes = obj.nodes.filter((n) => typeof n === "string");
861
+ if (nodes.length === 0) {
862
+ throw new Error(`knowledge.yaml at ${filePath}: scope.nodes must be a non-empty array`);
863
+ }
864
+ return { nodes };
865
+ }
866
+ }
867
+ throw new Error(`knowledge.yaml at ${filePath}: invalid 'scope' value`);
868
+ }
869
+
870
+ // src/io/template-parser.ts
871
+ import { readFile as readFile9 } from "fs/promises";
872
+ import { parse as parseYaml6 } from "yaml";
873
+ async function parseTemplate(filePath) {
874
+ const content = await readFile9(filePath, "utf-8");
875
+ const raw = parseYaml6(content);
876
+ if (!raw.node_type || typeof raw.node_type !== "string" || raw.node_type.trim() === "") {
877
+ throw new Error(`template at ${filePath}: missing or empty 'node_type'`);
878
+ }
879
+ const suggestedArtifacts = Array.isArray(raw.suggested_artifacts) ? raw.suggested_artifacts.filter((a) => typeof a === "string") : void 0;
880
+ return {
881
+ nodeType: raw.node_type.trim(),
882
+ suggestedArtifacts: suggestedArtifacts && suggestedArtifacts.length > 0 ? suggestedArtifacts : void 0,
883
+ guidance: typeof raw.guidance === "string" ? raw.guidance : void 0
884
+ };
885
+ }
886
+
887
+ // src/utils/paths.ts
888
+ import path4 from "path";
889
+ import { fileURLToPath as fileURLToPath2 } from "url";
890
+ import { stat as stat2 } from "fs/promises";
891
+ async function findYggRoot(projectRoot) {
892
+ let current = path4.resolve(projectRoot);
893
+ const root = path4.parse(current).root;
894
+ while (true) {
895
+ const yggPath = path4.join(current, ".yggdrasil");
896
+ try {
897
+ const st = await stat2(yggPath);
898
+ if (!st.isDirectory()) {
899
+ throw new Error(
900
+ `.yggdrasil exists but is not a directory (${yggPath}). Run 'yg init' in a clean location.`
901
+ );
902
+ }
903
+ return yggPath;
904
+ } catch (err) {
905
+ if (err.code === "ENOENT") {
906
+ if (current === root) {
907
+ throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
908
+ }
909
+ current = path4.dirname(current);
910
+ continue;
911
+ }
912
+ throw err;
913
+ }
914
+ }
915
+ }
916
+ function normalizeMappingPaths(mapping) {
917
+ if (!mapping?.paths?.length) return [];
918
+ return mapping.paths.map((p) => p.trim()).filter(Boolean);
919
+ }
920
+ function normalizeProjectRelativePath(projectRoot, rawPath) {
921
+ const normalizedInput = rawPath.trim().replace(/\\/g, "/");
922
+ if (normalizedInput.length === 0) {
923
+ throw new Error("Path cannot be empty");
924
+ }
925
+ const absolute = path4.resolve(projectRoot, normalizedInput);
926
+ const relative = path4.relative(projectRoot, absolute);
927
+ const isOutside = relative.startsWith("..") || path4.isAbsolute(relative);
928
+ if (isOutside) {
929
+ throw new Error(`Path is outside project root: ${rawPath}`);
930
+ }
931
+ return relative.split(path4.sep).join("/");
932
+ }
933
+
934
+ // src/core/graph-loader.ts
935
+ function toModelPath(absolutePath, modelDir) {
936
+ return path5.relative(modelDir, absolutePath).split(path5.sep).join("/");
937
+ }
938
+ var FALLBACK_CONFIG = {
939
+ name: "",
940
+ stack: {},
941
+ standards: "",
942
+ tags: [],
943
+ node_types: [],
944
+ artifacts: {},
945
+ knowledge_categories: []
946
+ };
947
+ async function loadGraph(projectRoot, options = {}) {
948
+ const yggRoot = await findYggRoot(projectRoot);
949
+ let configError;
950
+ let config = FALLBACK_CONFIG;
951
+ try {
952
+ config = await parseConfig(path5.join(yggRoot, "config.yaml"));
953
+ } catch (error) {
954
+ if (!options.tolerateInvalidConfig) {
955
+ throw error;
956
+ }
957
+ configError = error.message;
958
+ }
959
+ const modelDir = path5.join(yggRoot, "model");
960
+ const nodes = /* @__PURE__ */ new Map();
961
+ const nodeParseErrors = [];
962
+ const artifactFilenames = Object.keys(config.artifacts ?? {});
963
+ try {
964
+ await scanModelDirectory(modelDir, modelDir, null, nodes, nodeParseErrors, artifactFilenames);
965
+ } catch (err) {
966
+ if (err.code === "ENOENT") {
967
+ throw new Error(`Directory .yggdrasil/model/ does not exist. Run 'yg init' first.`, {
968
+ cause: err
969
+ });
970
+ }
971
+ throw err;
972
+ }
973
+ const aspects = await loadAspects(path5.join(yggRoot, "aspects"));
974
+ const flows = await loadFlows(path5.join(yggRoot, "flows"));
975
+ const knowledge = await loadKnowledge(
976
+ path5.join(yggRoot, "knowledge"),
977
+ config.knowledge_categories
978
+ );
979
+ const templates = await loadTemplates(path5.join(yggRoot, "templates"));
980
+ return {
981
+ config,
982
+ configError,
983
+ nodeParseErrors: nodeParseErrors.length > 0 ? nodeParseErrors : void 0,
984
+ nodes,
985
+ aspects,
986
+ flows,
987
+ knowledge,
988
+ templates,
989
+ rootPath: yggRoot
990
+ };
991
+ }
992
+ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErrors, artifactFilenames) {
993
+ const entries = await readdir3(dirPath, { withFileTypes: true });
994
+ const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
995
+ if (!hasNodeYaml && dirPath !== modelDir) {
996
+ return;
997
+ }
998
+ if (hasNodeYaml) {
999
+ const graphPath = toModelPath(dirPath, modelDir);
1000
+ let meta;
1001
+ try {
1002
+ meta = await parseNodeYaml(path5.join(dirPath, "node.yaml"));
1003
+ } catch (err) {
1004
+ nodeParseErrors.push({
1005
+ nodePath: graphPath,
1006
+ message: err.message
1007
+ });
1008
+ return;
1009
+ }
1010
+ const artifacts = await readArtifacts(dirPath, ["node.yaml"], artifactFilenames);
1011
+ const node = {
1012
+ path: graphPath,
1013
+ meta,
1014
+ artifacts,
1015
+ children: [],
1016
+ parent
1017
+ };
1018
+ nodes.set(graphPath, node);
1019
+ if (parent) {
1020
+ parent.children.push(node);
1021
+ }
1022
+ for (const entry of entries) {
1023
+ if (!entry.isDirectory()) continue;
1024
+ if (entry.name.startsWith(".")) continue;
1025
+ await scanModelDirectory(
1026
+ path5.join(dirPath, entry.name),
1027
+ modelDir,
1028
+ node,
1029
+ nodes,
1030
+ nodeParseErrors,
1031
+ artifactFilenames
1032
+ );
1033
+ }
1034
+ } else {
1035
+ for (const entry of entries) {
1036
+ if (!entry.isDirectory()) continue;
1037
+ if (entry.name.startsWith(".")) continue;
1038
+ await scanModelDirectory(
1039
+ path5.join(dirPath, entry.name),
1040
+ modelDir,
1041
+ null,
1042
+ nodes,
1043
+ nodeParseErrors,
1044
+ artifactFilenames
1045
+ );
1046
+ }
1047
+ }
1048
+ }
1049
+ async function loadAspects(aspectsDir) {
1050
+ try {
1051
+ const entries = await readdir3(aspectsDir, { withFileTypes: true });
1052
+ const aspects = [];
1053
+ for (const entry of entries) {
1054
+ if (!entry.isDirectory()) continue;
1055
+ const aspectYamlPath = path5.join(aspectsDir, entry.name, "aspect.yaml");
1056
+ const aspect = await parseAspect(path5.join(aspectsDir, entry.name), aspectYamlPath);
1057
+ aspects.push(aspect);
1058
+ }
1059
+ return aspects;
1060
+ } catch {
1061
+ return [];
1062
+ }
1063
+ }
1064
+ async function loadFlows(flowsDir) {
1065
+ try {
1066
+ const entries = await readdir3(flowsDir, { withFileTypes: true });
1067
+ const flows = [];
1068
+ for (const entry of entries) {
1069
+ if (!entry.isDirectory()) continue;
1070
+ const flowYamlPath = path5.join(flowsDir, entry.name, "flow.yaml");
1071
+ const flow = await parseFlow(path5.join(flowsDir, entry.name), flowYamlPath);
1072
+ flows.push(flow);
1073
+ }
1074
+ return flows;
1075
+ } catch {
1076
+ return [];
1077
+ }
1078
+ }
1079
+ async function loadKnowledge(knowledgeDir, categories) {
1080
+ const items = [];
1081
+ const categorySet = new Set(categories.map((c) => c.name));
1082
+ try {
1083
+ const catEntries = await readdir3(knowledgeDir, { withFileTypes: true });
1084
+ for (const catEntry of catEntries) {
1085
+ if (!catEntry.isDirectory()) continue;
1086
+ if (!categorySet.has(catEntry.name)) continue;
1087
+ const catPath = path5.join(knowledgeDir, catEntry.name);
1088
+ const itemEntries = await readdir3(catPath, { withFileTypes: true });
1089
+ for (const itemEntry of itemEntries) {
1090
+ if (!itemEntry.isDirectory()) continue;
1091
+ const itemDir = path5.join(catPath, itemEntry.name);
1092
+ const knowledgeYamlPath = path5.join(itemDir, "knowledge.yaml");
1093
+ const relativePath = `${catEntry.name}/${itemEntry.name}`;
1094
+ const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
1095
+ items.push(item);
1096
+ }
1097
+ }
1098
+ } catch {
1099
+ }
1100
+ return items;
1101
+ }
1102
+ async function loadTemplates(templatesDir) {
1103
+ try {
1104
+ const entries = await readdir3(templatesDir, { withFileTypes: true });
1105
+ const templates = [];
1106
+ for (const entry of entries) {
1107
+ if (!entry.isFile()) continue;
1108
+ if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
1109
+ const t = await parseTemplate(path5.join(templatesDir, entry.name));
1110
+ templates.push(t);
1111
+ }
1112
+ return templates;
1113
+ } catch {
1114
+ return [];
1115
+ }
1116
+ }
1117
+
1118
+ // src/core/context-builder.ts
1119
+ import { readFile as readFile10 } from "fs/promises";
1120
+ import path6 from "path";
1121
+
1122
+ // src/utils/tokens.ts
1123
+ function estimateTokens(text) {
1124
+ return Math.ceil(text.length / 4);
1125
+ }
1126
+
1127
+ // src/core/context-builder.ts
1128
+ var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
1129
+ var EVENT_RELATION_TYPES = /* @__PURE__ */ new Set(["emits", "listens"]);
1130
+ async function buildContext(graph, nodePath) {
1131
+ const node = graph.nodes.get(nodePath);
1132
+ if (!node) {
1133
+ throw new Error(`Node not found: ${nodePath}`);
1134
+ }
1135
+ const nodeTags = new Set(node.meta.tags ?? []);
1136
+ const seenKnowledge = /* @__PURE__ */ new Set();
1137
+ const layers = [];
1138
+ layers.push(buildGlobalLayer(graph.config));
1139
+ for (const k of collectKnowledgeItems(graph, nodePath, nodeTags, seenKnowledge)) {
1140
+ layers.push(buildKnowledgeLayer(k));
1141
+ }
1142
+ const ancestors = collectAncestors(node);
1143
+ for (const ancestor of ancestors) {
1144
+ layers.push(buildHierarchyLayer(ancestor, graph.config));
1145
+ }
1146
+ layers.push(await buildOwnLayer(node, graph.config, graph.rootPath));
1147
+ for (const relation of node.meta.relations ?? []) {
1148
+ const target = graph.nodes.get(relation.target);
1149
+ if (!target) {
1150
+ throw new Error(`Broken relation: ${nodePath} -> ${relation.target} (target not found)`);
1151
+ }
1152
+ if (STRUCTURAL_RELATION_TYPES.has(relation.type)) {
1153
+ layers.push(buildStructuralRelationLayer(target, relation, graph.config));
1154
+ } else if (EVENT_RELATION_TYPES.has(relation.type)) {
1155
+ layers.push(buildEventRelationLayer(target, relation));
1156
+ }
1157
+ }
1158
+ for (const tag of nodeTags) {
1159
+ for (const aspect of graph.aspects) {
1160
+ if (aspect.tag === tag) {
1161
+ layers.push(buildAspectLayer(aspect));
1162
+ }
1163
+ }
1164
+ }
1165
+ for (const flow of collectParticipatingFlows(graph, nodePath)) {
1166
+ layers.push(buildFlowLayer(flow));
1167
+ for (const kPath of flow.knowledge ?? []) {
1168
+ const norm = kPath.replace(/\/$/, "");
1169
+ const k = graph.knowledge.find((item) => item.path === norm || item.path === kPath);
1170
+ if (k && !seenKnowledge.has(k.path)) {
1171
+ seenKnowledge.add(k.path);
1172
+ layers.push(buildKnowledgeLayer(k, true));
1173
+ }
1174
+ }
1175
+ }
1176
+ const fullText = layers.map((l) => l.content).join("\n\n");
1177
+ const tokenCount = estimateTokens(fullText);
1178
+ const mapping = normalizeMappingPaths(node.meta.mapping);
1179
+ const sections = buildSections(layers, mapping.length > 0 ? mapping : null);
1180
+ return {
1181
+ nodePath,
1182
+ nodeName: node.meta.name,
1183
+ layers,
1184
+ sections,
1185
+ mapping: mapping.length > 0 ? mapping : null,
1186
+ tokenCount
1187
+ };
1188
+ }
1189
+ function collectKnowledgeItems(graph, nodePath, nodeTags, seenKnowledge) {
1190
+ const result = [];
1191
+ for (const k of graph.knowledge) {
1192
+ if (k.scope === "global" && !seenKnowledge.has(k.path)) {
1193
+ seenKnowledge.add(k.path);
1194
+ result.push(k);
1195
+ }
1196
+ }
1197
+ for (const k of graph.knowledge) {
1198
+ if (typeof k.scope === "object" && "tags" in k.scope) {
1199
+ const overlap = k.scope.tags.some((t) => nodeTags.has(t));
1200
+ if (overlap && !seenKnowledge.has(k.path)) {
1201
+ seenKnowledge.add(k.path);
1202
+ result.push(k);
1203
+ }
1204
+ }
1205
+ }
1206
+ for (const k of graph.knowledge) {
1207
+ if (typeof k.scope === "object" && "nodes" in k.scope) {
1208
+ if (k.scope.nodes.includes(nodePath) && !seenKnowledge.has(k.path)) {
1209
+ seenKnowledge.add(k.path);
1210
+ result.push(k);
1211
+ }
1212
+ }
1213
+ }
1214
+ const node = graph.nodes.get(nodePath);
1215
+ if (node?.meta.knowledge) {
1216
+ for (const kPath of node.meta.knowledge) {
1217
+ const norm = kPath.replace(/\/$/, "");
1218
+ const k = graph.knowledge.find((item) => item.path === norm || item.path === kPath);
1219
+ if (k && !seenKnowledge.has(k.path)) {
1220
+ seenKnowledge.add(k.path);
1221
+ result.push(k);
1222
+ }
1223
+ }
1224
+ }
1225
+ return result;
1226
+ }
1227
+ function collectParticipatingFlows(graph, nodePath) {
1228
+ return graph.flows.filter((f) => f.nodes.includes(nodePath));
1229
+ }
1230
+ function buildGlobalLayer(config) {
1231
+ let content = `**Project:** ${config.name}
1232
+
1233
+ `;
1234
+ content += `**Stack:**
1235
+ `;
1236
+ for (const [key, value] of Object.entries(config.stack)) {
1237
+ content += `- ${key}: ${value}
1238
+ `;
1239
+ }
1240
+ content += `
1241
+ **Standards:**
1242
+ ${config.standards || "(none)"}
1243
+ `;
1244
+ return { type: "global", label: "Global Context", content };
1245
+ }
1246
+ function buildKnowledgeLayer(k, fromFlow) {
1247
+ const categoryLabel = k.category.charAt(0).toUpperCase() + k.category.slice(1);
1248
+ const content = k.artifacts.map((a) => `### ${a.filename}
1249
+ ${a.content}`).join("\n\n");
1250
+ const label = fromFlow ? `Long-term Memory (from flow): ${k.name}` : `${categoryLabel}: ${k.name}`;
1251
+ return {
1252
+ type: "knowledge",
1253
+ label,
1254
+ content
1255
+ };
1256
+ }
1257
+ function filterArtifactsByConfig(artifacts, config) {
1258
+ const allowed = new Set(Object.keys(config.artifacts ?? {}));
1259
+ return artifacts.filter((a) => allowed.has(a.filename));
1260
+ }
1261
+ function buildHierarchyLayer(ancestor, config) {
1262
+ const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
1263
+ const content = filtered.map((a) => `### ${a.filename}
1264
+ ${a.content}`).join("\n\n");
1265
+ return {
1266
+ type: "hierarchy",
1267
+ label: `Module Context (${ancestor.path}/)`,
1268
+ content
1269
+ };
1270
+ }
1271
+ async function buildOwnLayer(node, config, graphRootPath) {
1272
+ const parts = [];
1273
+ const nodeYamlPath = path6.join(graphRootPath, "model", node.path, "node.yaml");
1274
+ try {
1275
+ const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
1276
+ parts.push(`### node.yaml
1277
+ ${nodeYamlContent.trim()}`);
1278
+ } catch {
1279
+ parts.push(`### node.yaml
1280
+ (not found)`);
1281
+ }
1282
+ const filtered = filterArtifactsByConfig(node.artifacts, config);
1283
+ for (const a of filtered) {
1284
+ parts.push(`### ${a.filename}
1285
+ ${a.content}`);
1286
+ }
1287
+ const content = parts.join("\n\n");
1288
+ return {
1289
+ type: "own",
1290
+ label: `Node: ${node.meta.name}`,
1291
+ content
1292
+ };
1293
+ }
1294
+ function buildStructuralRelationLayer(target, relation, config) {
1295
+ let content = "";
1296
+ if (relation.consumes?.length) {
1297
+ content += `Consumes: ${relation.consumes.join(", ")}
1298
+
1299
+ `;
1300
+ }
1301
+ if (relation.failure) {
1302
+ content += `On failure: ${relation.failure}
1303
+
1304
+ `;
1305
+ }
1306
+ const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
1307
+ const structuralArts = structuralArtifactFilenames.map((filename) => {
1308
+ const art = target.artifacts.find((a) => a.filename === filename);
1309
+ return art ? { filename: art.filename, content: art.content } : null;
1310
+ }).filter((a) => a !== null);
1311
+ if (structuralArts.length > 0) {
1312
+ content += structuralArts.map((a) => `### ${a.filename}
1313
+ ${a.content}`).join("\n\n");
1314
+ } else {
1315
+ const filtered = filterArtifactsByConfig(target.artifacts, config);
1316
+ content += filtered.map((a) => `### ${a.filename}
1317
+ ${a.content}`).join("\n\n");
1318
+ }
1319
+ return {
1320
+ type: "relational",
1321
+ label: `Dependency: ${target.meta.name} (${relation.type}) \u2014 ${target.path}`,
1322
+ content: content.trim()
1323
+ };
1324
+ }
1325
+ function buildEventRelationLayer(target, relation) {
1326
+ const eventName = relation.event_name ?? target.meta.name;
1327
+ const isEmit = relation.type === "emits";
1328
+ let content = isEmit ? `Target: ${target.path}
1329
+ You publish ${eventName}.` : `Source: ${target.path}
1330
+ You listen for ${eventName}.`;
1331
+ if (relation.consumes?.length) {
1332
+ content += `
1333
+ Consumes: ${relation.consumes.join(", ")}`;
1334
+ }
1335
+ return {
1336
+ type: "relational",
1337
+ label: `Event: ${eventName} [${relation.type}]`,
1338
+ content
1339
+ };
1340
+ }
1341
+ function buildAspectLayer(aspect) {
1342
+ const content = aspect.artifacts.map((a) => `### ${a.filename}
1343
+ ${a.content}`).join("\n\n");
1344
+ return {
1345
+ type: "aspects",
1346
+ label: `${aspect.name} (tag: ${aspect.tag})`,
1347
+ content
1348
+ };
1349
+ }
1350
+ function buildFlowLayer(flow) {
1351
+ const content = flow.artifacts.map((a) => `### ${a.filename}
1352
+ ${a.content}`).join("\n\n");
1353
+ return {
1354
+ type: "flows",
1355
+ label: `Flow: ${flow.name}`,
1356
+ content: content || "(no artifacts)"
1357
+ };
1358
+ }
1359
+ function buildSections(layers, mapping) {
1360
+ const ownLayers = layers.filter((layer) => layer.type === "own");
1361
+ if (mapping && mapping.length > 0) {
1362
+ ownLayers.push({
1363
+ type: "own",
1364
+ label: "Materialization Target",
1365
+ content: mapping.join(", ")
1366
+ });
1367
+ }
1368
+ return [
1369
+ { key: "Global", layers: layers.filter((l) => l.type === "global") },
1370
+ { key: "Knowledge", layers: layers.filter((l) => l.type === "knowledge") },
1371
+ { key: "Hierarchy", layers: layers.filter((l) => l.type === "hierarchy") },
1372
+ { key: "OwnArtifacts", layers: ownLayers },
1373
+ { key: "Dependencies", layers: layers.filter((l) => l.type === "relational") },
1374
+ { key: "Aspects", layers: layers.filter((l) => l.type === "aspects") },
1375
+ { key: "Flows", layers: layers.filter((l) => l.type === "flows") }
1376
+ ];
1377
+ }
1378
+ function collectAncestors(node) {
1379
+ const ancestors = [];
1380
+ let current = node.parent;
1381
+ while (current) {
1382
+ ancestors.unshift(current);
1383
+ current = current.parent;
1384
+ }
1385
+ return ancestors;
1386
+ }
1387
+
1388
+ // src/core/validator.ts
1389
+ import { readdir as readdir4 } from "fs/promises";
1390
+ import path8 from "path";
1391
+
1392
+ // src/utils/git.ts
1393
+ import { execSync } from "child_process";
1394
+ import path7 from "path";
1395
+ function getLastCommitTimestamp(projectRoot, relativePath) {
1396
+ const normalized = path7.normalize(relativePath).replace(/\\/g, "/");
1397
+ try {
1398
+ const out = execSync(`git log -1 --format=%ct -- "${normalized}"`, {
1399
+ cwd: projectRoot,
1400
+ encoding: "utf-8",
1401
+ stdio: ["pipe", "pipe", "pipe"]
1402
+ });
1403
+ const ts = parseInt(out.trim(), 10);
1404
+ return Number.isNaN(ts) ? null : ts;
1405
+ } catch {
1406
+ return null;
1407
+ }
1408
+ }
1409
+
1410
+ // src/core/validator.ts
1411
+ var RESERVED_DIRS = /* @__PURE__ */ new Set();
1412
+ async function validate(graph, scope = "all") {
1413
+ const issues = [];
1414
+ if (graph.configError) {
1415
+ issues.push({
1416
+ severity: "error",
1417
+ code: "E012",
1418
+ rule: "invalid-config",
1419
+ message: graph.configError
1420
+ });
1421
+ }
1422
+ for (const { nodePath, message } of graph.nodeParseErrors ?? []) {
1423
+ issues.push({
1424
+ severity: "error",
1425
+ code: "E001",
1426
+ rule: "invalid-node-yaml",
1427
+ message,
1428
+ nodePath
1429
+ });
1430
+ }
1431
+ if (!graph.configError) {
1432
+ issues.push(...checkNodeTypes(graph));
1433
+ issues.push(...checkTagsDefined(graph));
1434
+ issues.push(...checkAspectTags(graph));
1435
+ issues.push(...checkAspectTagUniqueness(graph));
1436
+ issues.push(...checkRequiredArtifacts(graph));
1437
+ issues.push(...await checkUnknownKnowledgeCategories(graph));
1438
+ issues.push(...checkInvalidArtifactConditions(graph));
1439
+ issues.push(...checkScopeTagsDefined(graph));
1440
+ issues.push(...await checkMissingPatternExamples(graph));
1441
+ issues.push(...await checkContextBudget(graph));
1442
+ issues.push(...checkHighFanOut(graph));
1443
+ issues.push(...await checkStaleKnowledge(graph));
1444
+ issues.push(...checkTemplates(graph));
1445
+ }
1446
+ issues.push(...checkRelationTargets(graph));
1447
+ issues.push(...checkNoCycles(graph));
1448
+ issues.push(...checkMappingOverlap(graph));
1449
+ issues.push(...checkBrokenKnowledgeRefs(graph));
1450
+ issues.push(...checkBrokenFlowRefs(graph));
1451
+ issues.push(...checkBrokenScopeRefs(graph));
1452
+ issues.push(...await checkDirectoriesHaveNodeYaml(graph));
1453
+ issues.push(...await checkShallowArtifacts(graph));
1454
+ issues.push(...await checkUnreachableKnowledge(graph));
1455
+ issues.push(...checkUnpairedEvents(graph));
1456
+ let filtered = issues;
1457
+ let nodesScanned = graph.nodes.size;
1458
+ if (scope !== "all" && scope.trim()) {
1459
+ if (!graph.nodes.has(scope)) {
1460
+ return {
1461
+ issues: [{ severity: "error", rule: "invalid-scope", message: `Node not found: ${scope}` }],
1462
+ nodesScanned: 0
1463
+ };
1464
+ }
1465
+ filtered = issues.filter((i) => !i.nodePath || i.nodePath === scope);
1466
+ nodesScanned = 1;
1467
+ }
1468
+ return { issues: filtered, nodesScanned };
1469
+ }
1470
+ function checkNodeTypes(graph) {
1471
+ const issues = [];
1472
+ const allowedTypes = new Set(graph.config.node_types ?? []);
1473
+ for (const [nodePath, node] of graph.nodes) {
1474
+ if (!allowedTypes.has(node.meta.type)) {
1475
+ issues.push({
1476
+ severity: "error",
1477
+ code: "E002",
1478
+ rule: "unknown-node-type",
1479
+ message: `Node type '${node.meta.type}' not in config.node_types (${[...allowedTypes].join(", ")})`,
1480
+ nodePath
1481
+ });
1482
+ }
1483
+ }
1484
+ return issues;
1485
+ }
1486
+ function findSimilar(target, candidates) {
1487
+ if (candidates.length === 0) return null;
1488
+ let best = null;
1489
+ let bestScore = -1;
1490
+ for (const c of candidates) {
1491
+ if (c === target) return c;
1492
+ const targetParts = target.split("/");
1493
+ const candParts = c.split("/");
1494
+ let score = 0;
1495
+ for (let i = 0; i < Math.min(targetParts.length, candParts.length); i++) {
1496
+ if (targetParts[i] === candParts[i]) score++;
1497
+ else break;
1498
+ }
1499
+ if (score > bestScore && score > 0) {
1500
+ bestScore = score;
1501
+ best = c;
1502
+ }
1503
+ }
1504
+ return best;
1505
+ }
1506
+ function checkRelationTargets(graph) {
1507
+ const issues = [];
1508
+ const nodePaths = [...graph.nodes.keys()];
1509
+ for (const [nodePath, node] of graph.nodes) {
1510
+ for (const rel of node.meta.relations ?? []) {
1511
+ if (!graph.nodes.has(rel.target)) {
1512
+ const suggestion = findSimilar(rel.target, nodePaths);
1513
+ const parts = rel.target.split("/");
1514
+ const parentPrefix = parts.length > 1 ? parts.slice(0, -1).join("/") + "/" : "";
1515
+ const existingInParent = nodePaths.filter((p) => p.startsWith(parentPrefix) && p !== rel.target).map((p) => {
1516
+ const rest = p.slice(parentPrefix.length);
1517
+ return rest.split("/")[0];
1518
+ }).filter((v, i, a) => a.indexOf(v) === i).sort();
1519
+ const existingLine = existingInParent.length > 0 ? `
1520
+ Existing nodes in ${parentPrefix || "model/"}: ${existingInParent.join(", ")}` : "";
1521
+ const hint = suggestion ? `
1522
+ Did you mean '${suggestion}'?` : "";
1523
+ issues.push({
1524
+ severity: "error",
1525
+ code: "E004",
1526
+ rule: "broken-relation",
1527
+ message: `Relation target '${rel.target}' does not exist${existingLine}${hint}`,
1528
+ nodePath
1529
+ });
1530
+ }
1531
+ }
1532
+ }
1533
+ return issues;
1534
+ }
1535
+ function checkTagsDefined(graph) {
1536
+ const issues = [];
1537
+ const definedTags = new Set(graph.config.tags ?? []);
1538
+ for (const [nodePath, node] of graph.nodes) {
1539
+ for (const tag of node.meta.tags ?? []) {
1540
+ if (!definedTags.has(tag)) {
1541
+ issues.push({
1542
+ severity: "error",
1543
+ code: "E003",
1544
+ rule: "unknown-tag",
1545
+ message: `Tag '${tag}' not defined in config.yaml`,
1546
+ nodePath
1547
+ });
1548
+ }
1549
+ }
1550
+ }
1551
+ return issues;
1552
+ }
1553
+ function checkAspectTags(graph) {
1554
+ const issues = [];
1555
+ const definedTags = new Set(graph.config.tags ?? []);
1556
+ for (const aspect of graph.aspects) {
1557
+ if (!definedTags.has(aspect.tag)) {
1558
+ issues.push({
1559
+ severity: "error",
1560
+ code: "E007",
1561
+ rule: "broken-aspect-tag",
1562
+ message: `Aspect '${aspect.name}' references undefined tag '${aspect.tag}'`
1563
+ });
1564
+ }
1565
+ }
1566
+ return issues;
1567
+ }
1568
+ function checkAspectTagUniqueness(graph) {
1569
+ const issues = [];
1570
+ const byTag = /* @__PURE__ */ new Map();
1571
+ for (const aspect of graph.aspects) {
1572
+ const names = byTag.get(aspect.tag) ?? [];
1573
+ names.push(aspect.name);
1574
+ byTag.set(aspect.tag, names);
1575
+ }
1576
+ for (const [tag, names] of byTag) {
1577
+ if (names.length <= 1) continue;
1578
+ issues.push({
1579
+ severity: "error",
1580
+ code: "E014",
1581
+ rule: "duplicate-aspect-binding",
1582
+ message: `Tag '${tag}' is bound to multiple aspects (${names.join(", ")})`
1583
+ });
1584
+ }
1585
+ return issues;
1586
+ }
1587
+ function checkNoCycles(graph) {
1588
+ const WHITE = 0;
1589
+ const GRAY = 1;
1590
+ const BLACK = 2;
1591
+ const color = /* @__PURE__ */ new Map();
1592
+ for (const p of graph.nodes.keys()) color.set(p, WHITE);
1593
+ const issues = [];
1594
+ function dfs(nodePath, pathSegments) {
1595
+ color.set(nodePath, GRAY);
1596
+ const node = graph.nodes.get(nodePath);
1597
+ const structuralTypes = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
1598
+ for (const rel of node.meta.relations ?? []) {
1599
+ const targetNode = graph.nodes.get(rel.target);
1600
+ if (!targetNode) continue;
1601
+ if (!structuralTypes.has(rel.type)) continue;
1602
+ if (color.get(rel.target) === GRAY) {
1603
+ const cyclePath = [...pathSegments, nodePath, rel.target];
1604
+ const cycleNodes = pathSegments.slice(pathSegments.indexOf(rel.target)).concat(nodePath);
1605
+ const hasBlackboxInCycle = cycleNodes.some(
1606
+ (p) => graph.nodes.get(p)?.meta.blackbox === true
1607
+ );
1608
+ if (!hasBlackboxInCycle) {
1609
+ issues.push({
1610
+ severity: "error",
1611
+ code: "E010",
1612
+ rule: "structural-cycle",
1613
+ message: `Circular dependency: ${cyclePath.join(" -> ")}`
1614
+ });
1615
+ }
1616
+ return true;
1617
+ }
1618
+ if (color.get(rel.target) === WHITE) {
1619
+ if (dfs(rel.target, [...pathSegments, nodePath])) return true;
1620
+ }
1621
+ }
1622
+ color.set(nodePath, BLACK);
1623
+ return false;
1624
+ }
1625
+ for (const nodePath of graph.nodes.keys()) {
1626
+ if (color.get(nodePath) === WHITE) {
1627
+ dfs(nodePath, []);
1628
+ }
1629
+ }
1630
+ return issues;
1631
+ }
1632
+ function normalizePathForCompare(mappingPath) {
1633
+ return mappingPath.replace(/\\/g, "/").replace(/\/+$/, "");
1634
+ }
1635
+ function arePathsOverlapping(pathA, pathB) {
1636
+ if (pathA === pathB) return true;
1637
+ return pathA.startsWith(pathB + "/") || pathB.startsWith(pathA + "/");
1638
+ }
1639
+ function checkMappingOverlap(graph) {
1640
+ const issues = [];
1641
+ const ownership = [];
1642
+ for (const [nodePath, node] of graph.nodes) {
1643
+ const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizePathForCompare).filter((mappingPath) => mappingPath.length > 0);
1644
+ for (const mappingPath of mappingPaths) {
1645
+ ownership.push({ nodePath, mappingPath });
1646
+ }
1647
+ }
1648
+ for (let index = 0; index < ownership.length; index++) {
1649
+ const current = ownership[index];
1650
+ for (let nestedIndex = index + 1; nestedIndex < ownership.length; nestedIndex++) {
1651
+ const candidate = ownership[nestedIndex];
1652
+ if (current.nodePath === candidate.nodePath) continue;
1653
+ if (!arePathsOverlapping(current.mappingPath, candidate.mappingPath)) continue;
1654
+ issues.push({
1655
+ severity: "error",
1656
+ code: "E009",
1657
+ rule: "overlapping-mapping",
1658
+ message: `Mapping paths '${current.mappingPath}' (${current.nodePath}) and '${candidate.mappingPath}' (${candidate.nodePath}) overlap. Keep one owner mapping and model other concerns via relations.`,
1659
+ nodePath: candidate.nodePath
1660
+ });
1661
+ }
1662
+ }
1663
+ return issues;
1664
+ }
1665
+ function getIncomingRelationSources(graph, nodePath) {
1666
+ const sources = [];
1667
+ for (const [srcPath, node] of graph.nodes) {
1668
+ for (const rel of node.meta.relations ?? []) {
1669
+ if (rel.target === nodePath) sources.push(srcPath);
1670
+ }
1671
+ }
1672
+ return sources;
1673
+ }
1674
+ function artifactRequiredReason(graph, nodePath, node, required) {
1675
+ if (required === "never") return null;
1676
+ if (required === "always") {
1677
+ return node.meta.blackbox ? null : "required: always";
1678
+ }
1679
+ const when = required.when;
1680
+ if (when === "has_incoming_relations") {
1681
+ const sources = getIncomingRelationSources(graph, nodePath);
1682
+ return sources.length > 0 ? `${sources.length} incoming relation(s): ${sources.join(", ")}` : null;
1683
+ }
1684
+ if (when === "has_outgoing_relations") {
1685
+ const count = node.meta.relations?.length ?? 0;
1686
+ return count > 0 ? `${count} outgoing relation(s)` : null;
1687
+ }
1688
+ if (when.startsWith("has_tag:")) {
1689
+ const tag = when.slice(8);
1690
+ return (node.meta.tags ?? []).includes(tag) ? `node has tag '${tag}'` : null;
1691
+ }
1692
+ return null;
1693
+ }
1694
+ function getIncomingRelations(graph, nodePath) {
1695
+ const incoming = [];
1696
+ for (const [fromPath, node] of graph.nodes) {
1697
+ for (const rel of node.meta.relations ?? []) {
1698
+ if (rel.target === nodePath) {
1699
+ incoming.push(fromPath);
1700
+ break;
1701
+ }
1702
+ }
1703
+ }
1704
+ return incoming.sort();
1705
+ }
1706
+ function checkRequiredArtifacts(graph) {
1707
+ const issues = [];
1708
+ const artifacts = graph.config.artifacts ?? {};
1709
+ for (const [nodePath, node] of graph.nodes) {
1710
+ for (const [filename, config] of Object.entries(artifacts)) {
1711
+ const hasArtifact = node.artifacts.some((a) => a.filename === filename);
1712
+ const reason = artifactRequiredReason(graph, nodePath, node, config.required);
1713
+ if (reason && !hasArtifact) {
1714
+ const action = config.description ?? "";
1715
+ const incoming = getIncomingRelations(graph, nodePath);
1716
+ const incomingStr = incoming.length > 0 ? ` Node has ${incoming.length} incoming relation(s): ${incoming.slice(0, 5).join(", ")}${incoming.length > 5 ? "..." : ""}.` : "";
1717
+ const msg = action ? `Missing required artifact '${filename}' (${reason}).${incomingStr} ${action}` : `Missing required artifact '${filename}' (${reason}).${incomingStr}`;
1718
+ issues.push({
1719
+ severity: "warning",
1720
+ code: "W001",
1721
+ rule: "missing-artifact",
1722
+ message: msg.trim(),
1723
+ nodePath
1724
+ });
1725
+ }
1726
+ }
1727
+ }
1728
+ return issues;
1729
+ }
1730
+ function checkBrokenKnowledgeRefs(graph) {
1731
+ const issues = [];
1732
+ const knowledgePaths = new Set(graph.knowledge.map((k) => k.path));
1733
+ for (const [nodePath, node] of graph.nodes) {
1734
+ for (const kPath of node.meta.knowledge ?? []) {
1735
+ const norm = kPath.replace(/\/$/, "");
1736
+ if (!knowledgePaths.has(norm) && !knowledgePaths.has(kPath)) {
1737
+ issues.push({
1738
+ severity: "error",
1739
+ code: "E005",
1740
+ rule: "broken-knowledge-ref",
1741
+ message: `Knowledge ref '${kPath}' does not resolve to existing knowledge item`,
1742
+ nodePath
1743
+ });
1744
+ }
1745
+ }
1746
+ }
1747
+ return issues;
1748
+ }
1749
+ function checkBrokenFlowRefs(graph) {
1750
+ const issues = [];
1751
+ const nodePaths = new Set(graph.nodes.keys());
1752
+ const knowledgePaths = new Set(graph.knowledge.map((k) => k.path));
1753
+ for (const flow of graph.flows) {
1754
+ for (const n of flow.nodes) {
1755
+ if (!nodePaths.has(n)) {
1756
+ issues.push({
1757
+ severity: "error",
1758
+ code: "E006",
1759
+ rule: "broken-flow-ref",
1760
+ message: `Flow '${flow.name}' references non-existent node '${n}'`
1761
+ });
1762
+ }
1763
+ }
1764
+ for (const kPath of flow.knowledge ?? []) {
1765
+ const norm = kPath.replace(/\/$/, "");
1766
+ if (!knowledgePaths.has(norm) && !knowledgePaths.has(kPath)) {
1767
+ issues.push({
1768
+ severity: "error",
1769
+ code: "E005",
1770
+ rule: "broken-knowledge-ref",
1771
+ message: `Flow '${flow.name}' references non-existent knowledge '${kPath}'`,
1772
+ nodePath: `flows/${flow.name}`
1773
+ });
1774
+ }
1775
+ }
1776
+ }
1777
+ return issues;
1778
+ }
1779
+ function checkBrokenScopeRefs(graph) {
1780
+ const issues = [];
1781
+ const nodePaths = new Set(graph.nodes.keys());
1782
+ for (const k of graph.knowledge) {
1783
+ if (typeof k.scope === "object" && "nodes" in k.scope) {
1784
+ for (const n of k.scope.nodes) {
1785
+ if (!nodePaths.has(n)) {
1786
+ issues.push({
1787
+ severity: "error",
1788
+ code: "E008",
1789
+ rule: "broken-scope-ref",
1790
+ message: `Knowledge '${k.path}' scope references non-existent node '${n}'`
1791
+ });
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ return issues;
1797
+ }
1798
+ function checkScopeTagsDefined(graph) {
1799
+ const issues = [];
1800
+ const definedTags = new Set(graph.config.tags ?? []);
1801
+ for (const k of graph.knowledge) {
1802
+ if (typeof k.scope !== "object" || !("tags" in k.scope)) continue;
1803
+ for (const tag of k.scope.tags) {
1804
+ if (definedTags.has(tag)) continue;
1805
+ issues.push({
1806
+ severity: "error",
1807
+ code: "E008",
1808
+ rule: "broken-scope-ref",
1809
+ message: `Knowledge '${k.path}' scope references undefined tag '${tag}'`
1810
+ });
1811
+ }
1812
+ }
1813
+ return issues;
1814
+ }
1815
+ async function checkUnknownKnowledgeCategories(graph) {
1816
+ const issues = [];
1817
+ const categorySet = new Set((graph.config.knowledge_categories ?? []).map((c) => c.name));
1818
+ const knowledgeDir = path8.join(graph.rootPath, "knowledge");
1819
+ const existingDirs = /* @__PURE__ */ new Set();
1820
+ try {
1821
+ const entries = await readdir4(knowledgeDir, { withFileTypes: true });
1822
+ for (const e of entries) {
1823
+ if (!e.isDirectory()) continue;
1824
+ if (e.name.startsWith(".")) continue;
1825
+ existingDirs.add(e.name);
1826
+ if (!categorySet.has(e.name)) {
1827
+ issues.push({
1828
+ severity: "error",
1829
+ code: "E011",
1830
+ rule: "unknown-knowledge-category",
1831
+ message: `Directory knowledge/${e.name}/ does not match any config.knowledge_categories`
1832
+ });
1833
+ }
1834
+ }
1835
+ } catch {
1836
+ }
1837
+ for (const cat of graph.config.knowledge_categories ?? []) {
1838
+ if (!existingDirs.has(cat.name)) {
1839
+ issues.push({
1840
+ severity: "error",
1841
+ code: "E017",
1842
+ rule: "missing-knowledge-category-dir",
1843
+ message: `Category '${cat.name}' in config has no knowledge/${cat.name}/ directory`
1844
+ });
1845
+ }
1846
+ }
1847
+ return issues;
1848
+ }
1849
+ function checkInvalidArtifactConditions(graph) {
1850
+ const issues = [];
1851
+ const definedTags = new Set(graph.config.tags ?? []);
1852
+ const artifacts = graph.config.artifacts ?? {};
1853
+ for (const [artifactName, config] of Object.entries(artifacts)) {
1854
+ const required = config.required;
1855
+ if (typeof required === "object" && required && "when" in required) {
1856
+ const when = required.when;
1857
+ if (when.startsWith("has_tag:")) {
1858
+ const tag = when.slice(8);
1859
+ if (!definedTags.has(tag)) {
1860
+ issues.push({
1861
+ severity: "error",
1862
+ code: "E013",
1863
+ rule: "invalid-artifact-condition",
1864
+ message: `Artifact '${artifactName}' condition has_tag:${tag} references undefined tag`
1865
+ });
1866
+ }
1867
+ }
1868
+ }
1869
+ }
1870
+ return issues;
1871
+ }
1872
+ async function checkShallowArtifacts(graph) {
1873
+ const issues = [];
1874
+ const minLen = graph.config.quality?.min_artifact_length ?? 50;
1875
+ for (const [nodePath, node] of graph.nodes) {
1876
+ for (const art of node.artifacts) {
1877
+ if (art.content.trim().length < minLen) {
1878
+ issues.push({
1879
+ severity: "warning",
1880
+ code: "W002",
1881
+ rule: "shallow-artifact",
1882
+ message: `Artifact '${art.filename}' is below minimum length (${art.content.length} < ${minLen})`,
1883
+ nodePath
1884
+ });
1885
+ }
1886
+ }
1887
+ }
1888
+ return issues;
1889
+ }
1890
+ async function checkUnreachableKnowledge(graph) {
1891
+ const issues = [];
1892
+ const nodePaths = new Set(graph.nodes.keys());
1893
+ const nodeTags = /* @__PURE__ */ new Map();
1894
+ for (const [p, n] of graph.nodes) {
1895
+ nodeTags.set(p, new Set(n.meta.tags ?? []));
1896
+ }
1897
+ const knowledgeReachable = /* @__PURE__ */ new Set();
1898
+ for (const k of graph.knowledge) {
1899
+ if (k.scope === "global") {
1900
+ knowledgeReachable.add(k.path);
1901
+ continue;
1902
+ }
1903
+ if (typeof k.scope === "object" && "tags" in k.scope) {
1904
+ for (const [, tags] of nodeTags) {
1905
+ if (k.scope.tags.some((t) => tags.has(t))) {
1906
+ knowledgeReachable.add(k.path);
1907
+ break;
1908
+ }
1909
+ }
1910
+ }
1911
+ if (typeof k.scope === "object" && "nodes" in k.scope) {
1912
+ if (k.scope.nodes.some((n) => nodePaths.has(n))) {
1913
+ knowledgeReachable.add(k.path);
1914
+ }
1915
+ }
1916
+ }
1917
+ for (const [, node] of graph.nodes) {
1918
+ for (const kPath of node.meta.knowledge ?? []) {
1919
+ const k = graph.knowledge.find(
1920
+ (i) => i.path === kPath || i.path === kPath.replace(/\/$/, "")
1921
+ );
1922
+ if (k) knowledgeReachable.add(k.path);
1923
+ }
1924
+ }
1925
+ for (const flow of graph.flows) {
1926
+ for (const kPath of flow.knowledge ?? []) {
1927
+ const k = graph.knowledge.find(
1928
+ (i) => i.path === kPath || i.path === kPath.replace(/\/$/, "")
1929
+ );
1930
+ if (k) knowledgeReachable.add(k.path);
1931
+ }
1932
+ }
1933
+ for (const k of graph.knowledge) {
1934
+ if (!knowledgeReachable.has(k.path)) {
1935
+ issues.push({
1936
+ severity: "warning",
1937
+ code: "W003",
1938
+ rule: "unreachable-knowledge",
1939
+ message: `Knowledge '${k.path}' does not reach any context package`
1940
+ });
1941
+ }
1942
+ }
1943
+ return issues;
1944
+ }
1945
+ async function checkMissingPatternExamples(graph) {
1946
+ const issues = [];
1947
+ const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
1948
+ if (!hasPatterns) return issues;
1949
+ const patternsDir = path8.join(graph.rootPath, "knowledge", "patterns");
1950
+ try {
1951
+ const entries = await readdir4(patternsDir, { withFileTypes: true });
1952
+ const exampleExtensions = /* @__PURE__ */ new Set([
1953
+ ".ts",
1954
+ ".js",
1955
+ ".tsx",
1956
+ ".jsx",
1957
+ ".py",
1958
+ ".go",
1959
+ ".rs",
1960
+ ".java",
1961
+ ".kt"
1962
+ ]);
1963
+ for (const e of entries) {
1964
+ if (!e.isDirectory()) continue;
1965
+ const itemDir = path8.join(patternsDir, e.name);
1966
+ const itemEntries = await readdir4(itemDir, { withFileTypes: true });
1967
+ const hasExample = itemEntries.some(
1968
+ (f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path8.extname(f.name).toLowerCase()))
1969
+ );
1970
+ if (!hasExample) {
1971
+ issues.push({
1972
+ severity: "warning",
1973
+ code: "W004",
1974
+ rule: "missing-example",
1975
+ message: `Pattern 'patterns/${e.name}' has no example file`
1976
+ });
1977
+ }
1978
+ }
1979
+ } catch {
1980
+ }
1981
+ return issues;
1982
+ }
1983
+ function checkHighFanOut(graph) {
1984
+ const issues = [];
1985
+ const maxRel = graph.config.quality?.max_direct_relations ?? 10;
1986
+ for (const [nodePath, node] of graph.nodes) {
1987
+ const count = node.meta.relations?.length ?? 0;
1988
+ if (count > maxRel) {
1989
+ issues.push({
1990
+ severity: "warning",
1991
+ code: "W007",
1992
+ rule: "high-fan-out",
1993
+ message: `Node has ${count} direct relations (max: ${maxRel})`,
1994
+ nodePath
1995
+ });
1996
+ }
1997
+ }
1998
+ return issues;
1999
+ }
2000
+ function getNodesInScope(k, graph) {
2001
+ if (k.scope === "global") {
2002
+ return [...graph.nodes.keys()];
2003
+ }
2004
+ if (typeof k.scope === "object" && "nodes" in k.scope && k.scope.nodes) {
2005
+ return k.scope.nodes.filter((p) => graph.nodes.has(p));
2006
+ }
2007
+ if (typeof k.scope === "object" && "tags" in k.scope && k.scope.tags) {
2008
+ const tagSet = new Set(k.scope.tags);
2009
+ return [...graph.nodes.keys()].filter((p) => {
2010
+ const node = graph.nodes.get(p);
2011
+ return (node.meta.tags ?? []).some((t) => tagSet.has(t));
2012
+ });
2013
+ }
2014
+ return [];
2015
+ }
2016
+ async function checkStaleKnowledge(graph) {
2017
+ const issues = [];
2018
+ const stalenessDays = graph.config.quality?.knowledge_staleness_days ?? 90;
2019
+ const projectRoot = path8.dirname(graph.rootPath);
2020
+ const yggRel = path8.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
2021
+ for (const k of graph.knowledge) {
2022
+ const scopeNodes = getNodesInScope(k, graph);
2023
+ if (scopeNodes.length === 0) continue;
2024
+ const kPath = `${yggRel}/knowledge/${k.path}`;
2025
+ const tK = getLastCommitTimestamp(projectRoot, kPath);
2026
+ if (tK === null) continue;
2027
+ let maxTp = 0;
2028
+ let latestNode = "";
2029
+ for (const nodePath of scopeNodes) {
2030
+ const nodePathRel = `${yggRel}/model/${nodePath}`;
2031
+ const tP = getLastCommitTimestamp(projectRoot, nodePathRel);
2032
+ if (tP !== null && tP > maxTp) {
2033
+ maxTp = tP;
2034
+ latestNode = nodePath;
2035
+ }
2036
+ }
2037
+ if (maxTp === 0) continue;
2038
+ const diffDays = (maxTp - tK) / (60 * 60 * 24);
2039
+ if (diffDays > stalenessDays) {
2040
+ issues.push({
2041
+ severity: "warning",
2042
+ code: "W008",
2043
+ rule: "stale-knowledge",
2044
+ message: `Knowledge '${k.path}' may be stale: node '${latestNode}' modified ${Math.floor(diffDays)} days later (Git commits)`,
2045
+ nodePath: latestNode
2046
+ });
2047
+ }
2048
+ }
2049
+ return issues;
2050
+ }
2051
+ function checkUnpairedEvents(graph) {
2052
+ const issues = [];
2053
+ const emitsTo = /* @__PURE__ */ new Map();
2054
+ const listensFrom = /* @__PURE__ */ new Map();
2055
+ for (const [nodePath, node] of graph.nodes) {
2056
+ for (const rel of node.meta.relations ?? []) {
2057
+ if (rel.type === "emits") {
2058
+ const set = emitsTo.get(nodePath) ?? /* @__PURE__ */ new Set();
2059
+ set.add(rel.target);
2060
+ emitsTo.set(nodePath, set);
2061
+ }
2062
+ if (rel.type === "listens") {
2063
+ const set = listensFrom.get(nodePath) ?? /* @__PURE__ */ new Set();
2064
+ set.add(rel.target);
2065
+ listensFrom.set(nodePath, set);
2066
+ }
2067
+ }
2068
+ }
2069
+ for (const [emitter, targets] of emitsTo) {
2070
+ for (const target of targets) {
2071
+ const listenerSet = listensFrom.get(target);
2072
+ if (!listenerSet?.has(emitter)) {
2073
+ issues.push({
2074
+ severity: "warning",
2075
+ code: "W009",
2076
+ rule: "unpaired-event",
2077
+ message: `Node '${emitter}' emits to '${target}' but '${target}' has no listens from '${emitter}'`,
2078
+ nodePath: emitter
2079
+ });
2080
+ }
2081
+ }
2082
+ }
2083
+ for (const [listener, sources] of listensFrom) {
2084
+ for (const source of sources) {
2085
+ const emitterSet = emitsTo.get(source);
2086
+ if (!emitterSet?.has(listener)) {
2087
+ issues.push({
2088
+ severity: "warning",
2089
+ code: "W009",
2090
+ rule: "unpaired-event",
2091
+ message: `Node '${listener}' listens from '${source}' but '${source}' has no emits to '${listener}'`,
2092
+ nodePath: listener
2093
+ });
2094
+ }
2095
+ }
2096
+ }
2097
+ return issues;
2098
+ }
2099
+ function checkTemplates(graph) {
2100
+ const issues = [];
2101
+ const allowedTypes = new Set(graph.config.node_types ?? []);
2102
+ const allowedArtifacts = new Set(Object.keys(graph.config.artifacts ?? {}));
2103
+ const typeToTemplate = /* @__PURE__ */ new Map();
2104
+ for (const template of graph.templates) {
2105
+ if (!allowedTypes.has(template.nodeType)) {
2106
+ issues.push({
2107
+ severity: "error",
2108
+ code: "E002",
2109
+ rule: "unknown-node-type",
2110
+ message: `Template for '${template.nodeType}' references node_type not in config.node_types (${[...allowedTypes].join(", ")})`
2111
+ });
2112
+ }
2113
+ for (const artifact of template.suggestedArtifacts ?? []) {
2114
+ if (!allowedArtifacts.has(artifact)) {
2115
+ issues.push({
2116
+ severity: "warning",
2117
+ code: "W001",
2118
+ rule: "missing-artifact",
2119
+ message: `Template for '${template.nodeType}' suggests artifact '${artifact}' not defined in config.artifacts`
2120
+ });
2121
+ }
2122
+ }
2123
+ const existing = typeToTemplate.get(template.nodeType);
2124
+ if (existing) {
2125
+ issues.push({
2126
+ severity: "error",
2127
+ code: "E016",
2128
+ rule: "duplicate-template",
2129
+ message: `Multiple templates for node_type '${template.nodeType}'`
2130
+ });
2131
+ } else {
2132
+ typeToTemplate.set(template.nodeType, template.nodeType);
2133
+ }
2134
+ }
2135
+ return issues;
2136
+ }
2137
+ async function checkDirectoriesHaveNodeYaml(graph) {
2138
+ const issues = [];
2139
+ const modelDir = path8.join(graph.rootPath, "model");
2140
+ async function scanDir(dirPath, segments) {
2141
+ const entries = await readdir4(dirPath, { withFileTypes: true });
2142
+ const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
2143
+ const dirName = path8.basename(dirPath);
2144
+ if (RESERVED_DIRS.has(dirName)) return;
2145
+ const hasContent = entries.some((e) => e.isFile()) || entries.some((e) => e.isDirectory());
2146
+ const graphPath = segments.join("/");
2147
+ if (hasContent && !hasNodeYaml && graphPath !== "") {
2148
+ issues.push({
2149
+ severity: "error",
2150
+ code: "E015",
2151
+ rule: "missing-node-yaml",
2152
+ message: `Directory '${graphPath}' has content but no node.yaml`,
2153
+ nodePath: graphPath
2154
+ });
2155
+ }
2156
+ for (const entry of entries) {
2157
+ if (!entry.isDirectory()) continue;
2158
+ if (RESERVED_DIRS.has(entry.name)) continue;
2159
+ if (entry.name.startsWith(".")) continue;
2160
+ await scanDir(path8.join(dirPath, entry.name), [...segments, entry.name]);
2161
+ }
2162
+ }
2163
+ try {
2164
+ const rootEntries = await readdir4(modelDir, { withFileTypes: true });
2165
+ for (const entry of rootEntries) {
2166
+ if (!entry.isDirectory()) continue;
2167
+ if (entry.name.startsWith(".")) continue;
2168
+ await scanDir(path8.join(modelDir, entry.name), [entry.name]);
2169
+ }
2170
+ } catch {
2171
+ }
2172
+ return issues;
2173
+ }
2174
+ async function checkContextBudget(graph) {
2175
+ const issues = [];
2176
+ const warningThreshold = graph.config.quality?.context_budget.warning ?? 5e3;
2177
+ const errorThreshold = graph.config.quality?.context_budget.error ?? 1e4;
2178
+ for (const [nodePath, node] of graph.nodes) {
2179
+ if (node.meta.blackbox) continue;
2180
+ try {
2181
+ const pkg = await buildContext(graph, nodePath);
2182
+ if (pkg.tokenCount >= errorThreshold) {
2183
+ issues.push({
2184
+ severity: "warning",
2185
+ code: "W006",
2186
+ rule: "budget-error",
2187
+ message: `Context is ${pkg.tokenCount.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}) \u2014 blocks materialization, node must be split`,
2188
+ nodePath
2189
+ });
2190
+ } else if (pkg.tokenCount >= warningThreshold) {
2191
+ issues.push({
2192
+ severity: "warning",
2193
+ code: "W005",
2194
+ rule: "budget-warning",
2195
+ message: `Context is ${pkg.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Consider splitting the node or reducing dependencies.`,
2196
+ nodePath
2197
+ });
2198
+ }
2199
+ } catch {
2200
+ }
2201
+ }
2202
+ return issues;
2203
+ }
2204
+
2205
+ // src/formatters/markdown.ts
2206
+ function formatContextMarkdown(pkg) {
2207
+ let md = "";
2208
+ md += `# Context Package: ${pkg.nodeName}
2209
+ `;
2210
+ md += `# Path: ${pkg.nodePath}
2211
+ `;
2212
+ md += `# Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
2213
+
2214
+ `;
2215
+ md += `---
2216
+
2217
+ `;
2218
+ for (const section of pkg.sections) {
2219
+ if (section.layers.length === 0) continue;
2220
+ md += `## ${section.key}
2221
+
2222
+ `;
2223
+ for (const layer of section.layers) {
2224
+ md += `### ${layer.label}
2225
+
2226
+ `;
2227
+ md += layer.content;
2228
+ md += `
2229
+
2230
+ `;
2231
+ }
2232
+ md += `---
2233
+
2234
+ `;
2235
+ }
2236
+ md += `Context size: ${pkg.tokenCount.toLocaleString()} tokens
2237
+ `;
2238
+ md += `Layers: ${pkg.layers.map((l) => l.type).join(", ")}
2239
+ `;
2240
+ return md;
2241
+ }
2242
+
2243
+ // src/cli/build-context.ts
2244
+ function registerBuildCommand(program2) {
2245
+ program2.command("build-context").description("Assemble a context package for one node").requiredOption("--node <node-path>", "Node path relative to .yggdrasil/model/").action(async (options) => {
2246
+ try {
2247
+ const graph = await loadGraph(process.cwd());
2248
+ const validationResult = await validate(graph, "all");
2249
+ const structuralErrors = validationResult.issues.filter(
2250
+ (issue) => issue.severity === "error"
2251
+ );
2252
+ if (structuralErrors.length > 0) {
2253
+ process.stderr.write(
2254
+ `Error: build-context requires a structurally valid graph (${structuralErrors.length} errors found).
2255
+ `
2256
+ );
2257
+ process.exit(1);
2258
+ }
2259
+ const nodePath = options.node.trim().replace(/\/$/, "");
2260
+ const pkg = await buildContext(graph, nodePath);
2261
+ const warningThreshold = graph.config.quality?.context_budget.warning ?? 5e3;
2262
+ const errorThreshold = graph.config.quality?.context_budget.error ?? 1e4;
2263
+ const budgetStatus = pkg.tokenCount >= errorThreshold ? "error" : pkg.tokenCount >= warningThreshold ? "warning" : "ok";
2264
+ let output = formatContextMarkdown(pkg);
2265
+ output += `Budget status: ${budgetStatus}
2266
+ `;
2267
+ process.stdout.write(output);
2268
+ if (budgetStatus === "error") {
2269
+ process.stderr.write(
2270
+ `Error: context package exceeds error budget (${pkg.tokenCount} >= ${errorThreshold}).
2271
+ `
2272
+ );
2273
+ process.exit(1);
2274
+ }
2275
+ } catch (error) {
2276
+ process.stderr.write(`Error: ${error.message}
2277
+ `);
2278
+ process.exit(1);
2279
+ }
2280
+ });
2281
+ }
2282
+
2283
+ // src/cli/validate.ts
2284
+ import chalk from "chalk";
2285
+ function registerValidateCommand(program2) {
2286
+ program2.command("validate").description("Validate graph structural integrity and completeness signals").option("--scope <scope>", "Scope: all or node-path (default: all)", "all").action(async (options) => {
2287
+ try {
2288
+ const graph = await loadGraph(process.cwd(), { tolerateInvalidConfig: true });
2289
+ const scope = (options.scope ?? "all").trim() || "all";
2290
+ const result = await validate(graph, scope);
2291
+ process.stdout.write(`${result.nodesScanned} nodes scanned
2292
+
2293
+ `);
2294
+ const errors = result.issues.filter((i) => i.severity === "error");
2295
+ const warnings = result.issues.filter((i) => i.severity === "warning");
2296
+ for (const issue of errors) {
2297
+ const code = issue.code ?? "";
2298
+ const loc = issue.nodePath ?? "";
2299
+ const prefix = loc ? `${code} ${loc} -> ` : `${code} `;
2300
+ process.stdout.write(chalk.red(`\u2717 ${prefix}${issue.message}
2301
+ `));
2302
+ }
2303
+ for (const issue of warnings) {
2304
+ const code = issue.code ?? "";
2305
+ const loc = issue.nodePath ?? "";
2306
+ const prefix = loc ? `${code} ${loc} -> ` : `${code} `;
2307
+ process.stdout.write(chalk.yellow(`\u26A0 ${prefix}${issue.message}
2308
+ `));
2309
+ }
2310
+ if (errors.length === 0 && warnings.length === 0) {
2311
+ process.stdout.write(chalk.green("\u2713 No issues found.\n"));
2312
+ } else {
2313
+ process.stdout.write(`
2314
+ ${errors.length} errors, ${warnings.length} warnings.
2315
+ `);
2316
+ }
2317
+ const hasErrors = result.issues.some((i) => i.severity === "error");
2318
+ process.exit(hasErrors ? 1 : 0);
2319
+ } catch (error) {
2320
+ process.stderr.write(`Error: ${error.message}
2321
+ `);
2322
+ process.exit(1);
2323
+ }
2324
+ });
2325
+ }
2326
+
2327
+ // src/cli/drift.ts
2328
+ import chalk2 from "chalk";
2329
+
2330
+ // src/io/drift-state-store.ts
2331
+ import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
2332
+ import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
2333
+ import path9 from "path";
2334
+ var DRIFT_STATE_FILE = ".drift-state";
2335
+ function getCanonicalHash(entry) {
2336
+ return typeof entry === "string" ? entry : entry.hash;
2337
+ }
2338
+ function getFileHashes(entry) {
2339
+ return typeof entry === "object" ? entry.files : void 0;
2340
+ }
2341
+ async function readDriftState(yggRoot) {
2342
+ const filePath = path9.join(yggRoot, DRIFT_STATE_FILE);
2343
+ try {
2344
+ const content = await readFile11(filePath, "utf-8");
2345
+ const raw = parseYaml7(content);
2346
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
2347
+ const result = {};
2348
+ for (const [k, v] of Object.entries(raw)) {
2349
+ if (typeof k === "string" && typeof v === "string") {
2350
+ result[k] = v;
2351
+ } else if (typeof k === "string" && typeof v === "object" && v !== null && "hash" in v) {
2352
+ result[k] = v;
2353
+ }
2354
+ }
2355
+ return result;
2356
+ }
2357
+ return {};
2358
+ } catch {
2359
+ return {};
2360
+ }
2361
+ }
2362
+ async function writeDriftState(yggRoot, state) {
2363
+ const filePath = path9.join(yggRoot, DRIFT_STATE_FILE);
2364
+ const content = stringifyYaml(state);
2365
+ await writeFile3(filePath, content, "utf-8");
2366
+ }
2367
+
2368
+ // src/utils/hash.ts
2369
+ import { readFile as readFile12, readdir as readdir5, stat as stat3 } from "fs/promises";
2370
+ import path10 from "path";
2371
+ import { createHash } from "crypto";
2372
+ import { createRequire } from "module";
2373
+ var require2 = createRequire(import.meta.url);
2374
+ var ignoreFactory = require2("ignore");
2375
+ async function hashFile(filePath) {
2376
+ const content = await readFile12(filePath);
2377
+ return createHash("sha256").update(content).digest("hex");
2378
+ }
2379
+ async function hashPath(targetPath, options = {}) {
2380
+ const projectRoot = options.projectRoot ? path10.resolve(options.projectRoot) : void 0;
2381
+ const gitignoreMatcher = await loadGitignoreMatcher(projectRoot);
2382
+ const targetStat = await stat3(targetPath);
2383
+ if (targetStat.isFile()) {
2384
+ if (isIgnoredPath(targetPath, projectRoot, gitignoreMatcher)) {
2385
+ return hashString("");
2386
+ }
2387
+ return hashFile(targetPath);
2388
+ }
2389
+ if (targetStat.isDirectory()) {
2390
+ const fileHashes = await collectDirectoryFileHashes(targetPath, targetPath, {
2391
+ projectRoot,
2392
+ gitignoreMatcher
2393
+ });
2394
+ const digestInput = fileHashes.sort((a, b) => a.path.localeCompare(b.path)).map((entry) => `${entry.path}:${entry.hash}`).join("\n");
2395
+ return hashString(digestInput);
2396
+ }
2397
+ throw new Error(`Unsupported mapping path type: ${targetPath}`);
2398
+ }
2399
+ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, options) {
2400
+ const entries = await readdir5(directoryPath, { withFileTypes: true });
2401
+ const result = [];
2402
+ for (const entry of entries) {
2403
+ const absoluteChildPath = path10.join(directoryPath, entry.name);
2404
+ if (isIgnoredPath(absoluteChildPath, options.projectRoot, options.gitignoreMatcher)) {
2405
+ continue;
2406
+ }
2407
+ if (entry.isDirectory()) {
2408
+ const nested = await collectDirectoryFileHashes(
2409
+ absoluteChildPath,
2410
+ rootDirectoryPath,
2411
+ options
2412
+ );
2413
+ for (const nestedEntry of nested) {
2414
+ result.push({
2415
+ path: path10.relative(rootDirectoryPath, path10.join(absoluteChildPath, nestedEntry.path)),
2416
+ hash: nestedEntry.hash
2417
+ });
2418
+ }
2419
+ continue;
2420
+ }
2421
+ if (!entry.isFile()) {
2422
+ continue;
2423
+ }
2424
+ result.push({
2425
+ path: path10.relative(rootDirectoryPath, absoluteChildPath),
2426
+ hash: await hashFile(absoluteChildPath)
2427
+ });
2428
+ }
2429
+ return result;
2430
+ }
2431
+ async function loadGitignoreMatcher(projectRoot) {
2432
+ if (!projectRoot) {
2433
+ return void 0;
2434
+ }
2435
+ try {
2436
+ const gitignorePath = path10.join(projectRoot, ".gitignore");
2437
+ const gitignoreContent = await readFile12(gitignorePath, "utf-8");
2438
+ const matcher = ignoreFactory();
2439
+ matcher.add(gitignoreContent);
2440
+ return matcher;
2441
+ } catch {
2442
+ return void 0;
2443
+ }
2444
+ }
2445
+ function isIgnoredPath(candidatePath, projectRoot, matcher) {
2446
+ if (!projectRoot || !matcher) {
2447
+ return false;
2448
+ }
2449
+ const relativePath = path10.relative(projectRoot, candidatePath);
2450
+ if (relativePath === "" || relativePath.startsWith("..")) {
2451
+ return false;
2452
+ }
2453
+ return matcher.ignores(relativePath) || matcher.ignores(relativePath + "/");
2454
+ }
2455
+ function hashString(content) {
2456
+ return createHash("sha256").update(content).digest("hex");
2457
+ }
2458
+ async function perFileHashes(projectRoot, mapping) {
2459
+ const root = path10.resolve(projectRoot);
2460
+ const paths = mapping.paths ?? [];
2461
+ if (paths.length === 0) return [];
2462
+ const result = [];
2463
+ const gitignoreMatcher = await loadGitignoreMatcher(root);
2464
+ for (const p of paths) {
2465
+ const absPath = path10.join(root, p);
2466
+ const st = await stat3(absPath);
2467
+ if (st.isFile()) {
2468
+ result.push({ path: p, hash: await hashFile(absPath) });
2469
+ } else if (st.isDirectory()) {
2470
+ const hashes = await collectDirectoryFileHashes(absPath, absPath, {
2471
+ projectRoot: root,
2472
+ gitignoreMatcher
2473
+ });
2474
+ for (const h of hashes) {
2475
+ result.push({
2476
+ path: path10.join(p, h.path).split(path10.sep).join("/"),
2477
+ hash: h.hash
2478
+ });
2479
+ }
2480
+ }
2481
+ }
2482
+ return result;
2483
+ }
2484
+ async function hashForMapping(projectRoot, mapping) {
2485
+ const root = path10.resolve(projectRoot);
2486
+ const paths = mapping.paths ?? [];
2487
+ if (paths.length === 0) throw new Error("Invalid mapping for hash: no paths");
2488
+ const pairs = [];
2489
+ for (const p of paths) {
2490
+ const absPath = path10.join(root, p);
2491
+ const st = await stat3(absPath);
2492
+ if (st.isFile()) {
2493
+ pairs.push({ path: p, hash: await hashFile(absPath) });
2494
+ } else if (st.isDirectory()) {
2495
+ const dirHash = await hashPath(absPath, { projectRoot: root });
2496
+ pairs.push({ path: p, hash: dirHash });
2497
+ }
2498
+ }
2499
+ const digestInput = pairs.sort((a, b) => a.path.localeCompare(b.path)).map((e) => `${e.path}:${e.hash}`).join("\n");
2500
+ return createHash("sha256").update(digestInput).digest("hex");
2501
+ }
2502
+
2503
+ // src/core/drift-detector.ts
2504
+ import { access } from "fs/promises";
2505
+ import path11 from "path";
2506
+ async function detectDrift(graph, filterNodePath) {
2507
+ const projectRoot = path11.dirname(graph.rootPath);
2508
+ const driftState = await readDriftState(graph.rootPath);
2509
+ const entries = [];
2510
+ for (const [nodePath, node] of graph.nodes) {
2511
+ if (filterNodePath && nodePath !== filterNodePath) continue;
2512
+ const mapping = node.meta.mapping;
2513
+ if (!mapping) continue;
2514
+ const mappingPaths = normalizeMappingPaths(mapping);
2515
+ if (mappingPaths.length === 0) continue;
2516
+ const storedEntry = driftState[nodePath];
2517
+ if (!storedEntry) {
2518
+ const allMissing = await allPathsMissing(projectRoot, mappingPaths);
2519
+ entries.push({
2520
+ nodePath,
2521
+ mappingPaths,
2522
+ status: allMissing ? "unmaterialized" : "drift",
2523
+ details: allMissing ? "No drift state recorded, files do not exist" : "No drift state recorded, files exist (run drift-sync after materialization)"
2524
+ });
2525
+ continue;
2526
+ }
2527
+ const storedHash = getCanonicalHash(storedEntry);
2528
+ let status = "ok";
2529
+ let details = "";
2530
+ try {
2531
+ const currentHash = await hashForMapping(projectRoot, mapping);
2532
+ if (currentHash !== storedHash) {
2533
+ status = "drift";
2534
+ const changedFiles = await diagnoseChangedFiles(
2535
+ projectRoot,
2536
+ mapping,
2537
+ getFileHashes(storedEntry)
2538
+ );
2539
+ details = changedFiles.length > 0 ? `Changed files: ${changedFiles.join(", ")}` : "File(s) modified since last sync";
2540
+ }
2541
+ } catch {
2542
+ status = "missing";
2543
+ details = "Mapped path(s) do not exist";
2544
+ }
2545
+ entries.push({ nodePath, mappingPaths, status, details });
2546
+ }
2547
+ return {
2548
+ entries,
2549
+ totalChecked: entries.length,
2550
+ okCount: entries.filter((e) => e.status === "ok").length,
2551
+ driftCount: entries.filter((e) => e.status === "drift").length,
2552
+ missingCount: entries.filter((e) => e.status === "missing").length,
2553
+ unmaterializedCount: entries.filter((e) => e.status === "unmaterialized").length
2554
+ };
2555
+ }
2556
+ async function diagnoseChangedFiles(projectRoot, mapping, storedFileHashes) {
2557
+ try {
2558
+ const currentHashes = await perFileHashes(projectRoot, mapping);
2559
+ if (!storedFileHashes) {
2560
+ return currentHashes.map((h) => h.path).sort();
2561
+ }
2562
+ const changed = [];
2563
+ const storedPaths = new Set(Object.keys(storedFileHashes));
2564
+ for (const { path: filePath, hash } of currentHashes) {
2565
+ const stored = storedFileHashes[filePath];
2566
+ if (!stored || stored !== hash) {
2567
+ changed.push(filePath);
2568
+ }
2569
+ storedPaths.delete(filePath);
2570
+ }
2571
+ for (const removed of storedPaths) {
2572
+ changed.push(`${removed} (deleted)`);
2573
+ }
2574
+ return changed.sort();
2575
+ } catch {
2576
+ return [];
2577
+ }
2578
+ }
2579
+ async function allPathsMissing(projectRoot, mappingPaths) {
2580
+ for (const mp of mappingPaths) {
2581
+ const absPath = path11.join(projectRoot, mp);
2582
+ try {
2583
+ await access(absPath);
2584
+ return false;
2585
+ } catch {
2586
+ }
2587
+ }
2588
+ return true;
2589
+ }
2590
+ async function syncDriftState(graph, nodePath) {
2591
+ const projectRoot = path11.dirname(graph.rootPath);
2592
+ const node = graph.nodes.get(nodePath);
2593
+ if (!node) throw new Error(`Node not found: ${nodePath}`);
2594
+ const mapping = node.meta.mapping;
2595
+ if (!mapping) throw new Error(`Node has no mapping: ${nodePath}`);
2596
+ const currentHash = await hashForMapping(projectRoot, mapping);
2597
+ const driftState = await readDriftState(graph.rootPath);
2598
+ const previousEntry = driftState[nodePath];
2599
+ const previousHash = previousEntry ? getCanonicalHash(previousEntry) : void 0;
2600
+ const fileHashes = await perFileHashes(projectRoot, mapping);
2601
+ const files = {};
2602
+ for (const fh of fileHashes) {
2603
+ files[fh.path] = fh.hash;
2604
+ }
2605
+ const newEntry = { hash: currentHash, files };
2606
+ driftState[nodePath] = newEntry;
2607
+ await writeDriftState(graph.rootPath, driftState);
2608
+ return { previousHash, currentHash };
2609
+ }
2610
+
2611
+ // src/cli/drift.ts
2612
+ function registerDriftCommand(program2) {
2613
+ program2.command("drift").description("Detect divergence between graph and code").option("--scope <scope>", "Scope: all or node-path (default: all)", "all").action(async (options) => {
2614
+ try {
2615
+ const graph = await loadGraph(process.cwd());
2616
+ const scope = (options.scope ?? "all").trim() || "all";
2617
+ if (scope && scope !== "all" && !graph.nodes.has(scope)) {
2618
+ process.stderr.write(`Error: Node not found: ${scope}
2619
+ `);
2620
+ process.exit(1);
2621
+ }
2622
+ if (scope && scope !== "all") {
2623
+ const scopedNode = graph.nodes.get(scope);
2624
+ if (!scopedNode.meta.mapping) {
2625
+ process.stderr.write(
2626
+ `Error: Node has no mapping (does not participate in drift detection): ${options.scope}
2627
+ `
2628
+ );
2629
+ process.exit(1);
2630
+ }
2631
+ }
2632
+ const scopeNode = scope === "all" ? void 0 : scope;
2633
+ const report = await detectDrift(graph, scopeNode);
2634
+ process.stdout.write("Drift:\n");
2635
+ for (const entry of report.entries) {
2636
+ const paths = entry.mappingPaths.join(", ");
2637
+ switch (entry.status) {
2638
+ case "ok":
2639
+ process.stdout.write(chalk2.green(` ok ${entry.nodePath} -> ${paths}
2640
+ `));
2641
+ break;
2642
+ case "drift":
2643
+ process.stdout.write(chalk2.red(` drift ${entry.nodePath} -> ${paths}
2644
+ `));
2645
+ if (entry.details) process.stdout.write(` ${entry.details}
2646
+ `);
2647
+ break;
2648
+ case "missing":
2649
+ process.stdout.write(chalk2.yellow(` missing ${entry.nodePath} -> ${paths}
2650
+ `));
2651
+ break;
2652
+ case "unmaterialized":
2653
+ process.stdout.write(chalk2.dim(` unmat. ${entry.nodePath} -> ${paths}
2654
+ `));
2655
+ break;
2656
+ }
2657
+ }
2658
+ process.stdout.write(
2659
+ `
2660
+ Summary: ${report.driftCount} drift, ${report.missingCount} missing, ${report.unmaterializedCount} unmaterialized, ${report.okCount} ok
2661
+ `
2662
+ );
2663
+ if (report.driftCount > 0 || report.missingCount > 0 || report.unmaterializedCount > 0) {
2664
+ process.exit(1);
2665
+ }
2666
+ process.exit(0);
2667
+ } catch (error) {
2668
+ process.stderr.write(`Error: ${error.message}
2669
+ `);
2670
+ process.exit(1);
2671
+ }
2672
+ });
2673
+ }
2674
+
2675
+ // src/cli/drift-sync.ts
2676
+ import chalk3 from "chalk";
2677
+ function registerDriftSyncCommand(program2) {
2678
+ program2.command("drift-sync").description("Record current file hash after resolving drift").requiredOption("--node <path>", "Node path to sync").action(async (options) => {
2679
+ try {
2680
+ const graph = await loadGraph(process.cwd());
2681
+ const nodePath = options.node.trim().replace(/\/$/, "");
2682
+ const { previousHash, currentHash } = await syncDriftState(graph, nodePath);
2683
+ process.stdout.write(chalk3.green(`Synchronized: ${nodePath}
2684
+ `));
2685
+ process.stdout.write(
2686
+ ` Hash: ${previousHash ? previousHash.slice(0, 8) : "none"} -> ${currentHash.slice(0, 8)}
2687
+ `
2688
+ );
2689
+ } catch (error) {
2690
+ process.stderr.write(`Error: ${error.message}
2691
+ `);
2692
+ process.exit(1);
2693
+ }
2694
+ });
2695
+ }
2696
+
2697
+ // src/cli/status.ts
2698
+ function registerStatusCommand(program2) {
2699
+ program2.command("status").description("Show graph summary").action(async () => {
2700
+ try {
2701
+ const graph = await loadGraph(process.cwd());
2702
+ const typeCounts = /* @__PURE__ */ new Map();
2703
+ let blackboxCount = 0;
2704
+ for (const node of graph.nodes.values()) {
2705
+ typeCounts.set(node.meta.type, (typeCounts.get(node.meta.type) ?? 0) + 1);
2706
+ if (node.meta.blackbox) blackboxCount++;
2707
+ }
2708
+ let structuralRelations = 0;
2709
+ let eventRelations = 0;
2710
+ const structuralTypes = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2711
+ for (const node of graph.nodes.values()) {
2712
+ for (const rel of node.meta.relations ?? []) {
2713
+ if (structuralTypes.has(rel.type)) structuralRelations += 1;
2714
+ else eventRelations += 1;
2715
+ }
2716
+ }
2717
+ const flowCount = graph.flows.length;
2718
+ const knowledgeCount = graph.knowledge.length;
2719
+ const drift = await detectDrift(graph);
2720
+ const validation = await validate(graph, "all");
2721
+ const errorCount = validation.issues.filter((issue) => issue.severity === "error").length;
2722
+ const warningCount = validation.issues.filter(
2723
+ (issue) => issue.severity === "warning"
2724
+ ).length;
2725
+ process.stdout.write(`Graph: ${graph.config.name}
2726
+ `);
2727
+ const pluralize = (word, count) => count === 1 ? word : word.endsWith("y") ? word.slice(0, -1) + "ies" : word + "s";
2728
+ const typeStr = [...typeCounts.entries()].map(([t, c]) => `${c} ${pluralize(t, c)}`).join(", ");
2729
+ process.stdout.write(
2730
+ `Nodes: ${graph.nodes.size} (${typeStr}) + ${blackboxCount} blackbox
2731
+ `
2732
+ );
2733
+ process.stdout.write(
2734
+ `Relations: ${structuralRelations} structural, ${eventRelations} event
2735
+ `
2736
+ );
2737
+ process.stdout.write(
2738
+ `Aspects: ${graph.aspects.length} Flows: ${flowCount} Knowledge: ${knowledgeCount}
2739
+ `
2740
+ );
2741
+ process.stdout.write(
2742
+ `Drift: ${drift.driftCount} drift, ${drift.missingCount} missing, ${drift.unmaterializedCount} unmaterialized, ${drift.okCount} ok
2743
+ `
2744
+ );
2745
+ process.stdout.write(`Validation: ${errorCount} errors, ${warningCount} warnings
2746
+ `);
2747
+ } catch (error) {
2748
+ process.stderr.write(`Error: ${error.message}
2749
+ `);
2750
+ process.exit(1);
2751
+ }
2752
+ });
2753
+ }
2754
+
2755
+ // src/cli/tree.ts
2756
+ function registerTreeCommand(program2) {
2757
+ program2.command("tree").description("Display graph structure as a tree").option("--root <path>", "Show only subtree rooted at this path").option("--depth <n>", "Maximum depth", (v) => parseInt(v, 10)).action(async (options) => {
2758
+ try {
2759
+ const graph = await loadGraph(process.cwd());
2760
+ let roots;
2761
+ let showProjectName;
2762
+ if (options.root?.trim()) {
2763
+ const path15 = options.root.trim().replace(/\/$/, "");
2764
+ const node = graph.nodes.get(path15);
2765
+ if (!node) {
2766
+ process.stderr.write(`Error: path '${path15}' not found
2767
+ `);
2768
+ process.exit(1);
2769
+ }
2770
+ roots = [node];
2771
+ showProjectName = false;
2772
+ } else {
2773
+ roots = [...graph.nodes.values()].filter((n) => n.parent === null).sort((a, b) => a.path.localeCompare(b.path));
2774
+ showProjectName = true;
2775
+ }
2776
+ if (showProjectName) {
2777
+ process.stdout.write("model/\n");
2778
+ }
2779
+ for (let i = 0; i < roots.length; i++) {
2780
+ const isLast = i === roots.length - 1;
2781
+ printNode(roots[i], "", isLast, 1, options.depth);
2782
+ }
2783
+ } catch (error) {
2784
+ process.stderr.write(`Error: ${error.message}
2785
+ `);
2786
+ process.exit(1);
2787
+ }
2788
+ });
2789
+ }
2790
+ function printNode(node, prefix, isLast, depth, maxDepth) {
2791
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
2792
+ const name = node.path.split("/").pop() ?? node.path;
2793
+ const type = `[${node.meta.type}]`;
2794
+ const tags = node.meta.tags?.length ? ` tags:${node.meta.tags.join(",")}` : "";
2795
+ const blackbox = node.meta.blackbox ? " \u25A0 blackbox" : "";
2796
+ const relationCount = node.meta.relations?.length ?? 0;
2797
+ process.stdout.write(
2798
+ `${prefix}${connector}${name}/ ${type}${tags}${blackbox} -> ${relationCount} relations
2799
+ `
2800
+ );
2801
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
2802
+ if (maxDepth !== void 0 && depth >= maxDepth) return;
2803
+ const children = [...node.children].sort((a, b) => a.path.localeCompare(b.path));
2804
+ for (let i = 0; i < children.length; i++) {
2805
+ printNode(children[i], childPrefix, i === children.length - 1, depth + 1, maxDepth);
2806
+ }
2807
+ }
2808
+
2809
+ // src/cli/owner.ts
2810
+ function normalizeForMatch(inputPath) {
2811
+ return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
2812
+ }
2813
+ function findOwner(graph, projectRoot, rawPath) {
2814
+ const file = normalizeForMatch(normalizeProjectRelativePath(projectRoot, rawPath));
2815
+ let best = null;
2816
+ for (const [nodePath, node] of graph.nodes) {
2817
+ const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizeForMatch).filter((mappingPath) => mappingPath.length > 0);
2818
+ for (const mappingPath of mappingPaths) {
2819
+ if (file === mappingPath) {
2820
+ return { file, nodePath, mappingPath };
2821
+ }
2822
+ if (file.startsWith(mappingPath + "/")) {
2823
+ if (!best || best && mappingPath.length > best.mappingPath.length) {
2824
+ best = { nodePath, mappingPath, exact: false };
2825
+ }
2826
+ }
2827
+ }
2828
+ }
2829
+ return best ? { file, nodePath: best.nodePath, mappingPath: best.mappingPath } : { file, nodePath: null };
2830
+ }
2831
+ function registerOwnerCommand(program2) {
2832
+ program2.command("owner").description("Find which graph node owns a source file").requiredOption("--file <path>", "File path (relative to repository root)").action(async (options) => {
2833
+ try {
2834
+ const projectRoot = process.cwd();
2835
+ const graph = await loadGraph(projectRoot);
2836
+ const result = findOwner(graph, projectRoot, options.file);
2837
+ if (!result.nodePath) {
2838
+ process.stdout.write(`${result.file} -> no graph coverage
2839
+ `);
2840
+ } else {
2841
+ process.stdout.write(`${result.file} -> ${result.nodePath}
2842
+ `);
2843
+ }
2844
+ } catch (error) {
2845
+ process.stderr.write(`Error: ${error.message}
2846
+ `);
2847
+ process.exit(1);
2848
+ }
2849
+ });
2850
+ }
2851
+
2852
+ // src/core/dependency-resolver.ts
2853
+ import { execSync as execSync2 } from "child_process";
2854
+ import path12 from "path";
2855
+ var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2856
+ var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
2857
+ function filterRelationType(relType, filter) {
2858
+ if (filter === "all") return true;
2859
+ if (filter === "structural") return STRUCTURAL_RELATION_TYPES2.has(relType);
2860
+ if (filter === "event") return EVENT_RELATION_TYPES2.has(relType);
2861
+ return false;
2862
+ }
2863
+ function buildDependencyTree(graph, nodePath, options = {}) {
2864
+ const node = graph.nodes.get(nodePath);
2865
+ if (!node) {
2866
+ throw new Error(`Node not found: ${nodePath}`);
2867
+ }
2868
+ const maxDepth = options.depth ?? Infinity;
2869
+ const typeFilter = options.relationType ?? "all";
2870
+ function buildChildren(fromPath, currentDepth, branch) {
2871
+ if (currentDepth >= maxDepth) return [];
2872
+ const fromNode = graph.nodes.get(fromPath);
2873
+ const children = [];
2874
+ for (const rel of fromNode.meta.relations ?? []) {
2875
+ if (!filterRelationType(rel.type, typeFilter)) continue;
2876
+ if (!graph.nodes.has(rel.target)) continue;
2877
+ if (branch.has(rel.target)) continue;
2878
+ const targetNode = graph.nodes.get(rel.target);
2879
+ const nextBranch = new Set(branch);
2880
+ nextBranch.add(rel.target);
2881
+ children.push({
2882
+ nodePath: rel.target,
2883
+ relationType: rel.type,
2884
+ relationTarget: fromPath,
2885
+ blackbox: targetNode.meta.blackbox ?? false,
2886
+ children: buildChildren(rel.target, currentDepth + 1, nextBranch)
2887
+ });
2888
+ }
2889
+ return children;
2890
+ }
2891
+ return buildChildren(nodePath, 0, /* @__PURE__ */ new Set([nodePath]));
2892
+ }
2893
+ function formatDependencyTree(graph, nodePath, options = {}) {
2894
+ const roots = buildDependencyTree(graph, nodePath, options);
2895
+ const lines = [nodePath];
2896
+ function formatNode(node, prefix, isLast) {
2897
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
2898
+ const blackbox = node.blackbox ? " \u25A0 blackbox" : "";
2899
+ lines.push(`${prefix}${connector}${node.relationType} ${node.nodePath}${blackbox}`);
2900
+ const childPrefix = prefix + (isLast ? " " : "\u2502 ");
2901
+ const lastIdx = node.children.length - 1;
2902
+ node.children.forEach((c, i) => formatNode(c, childPrefix, i === lastIdx));
2903
+ }
2904
+ roots.forEach((r, i) => formatNode(r, "", i === roots.length - 1));
2905
+ return lines.join("\n");
2906
+ }
2907
+
2908
+ // src/cli/deps.ts
2909
+ function registerDepsCommand(program2) {
2910
+ program2.command("deps").description("Show direct and transitive node dependencies").requiredOption("--node <path>", "Node path relative to .yggdrasil/model/").option("--depth <n>", "Maximum depth for tree (when using --node)", (v) => parseInt(v, 10)).option("--type <type>", "Relation type filter: structural, event, all (default: all)", "all").action(async (options) => {
2911
+ try {
2912
+ const graph = await loadGraph(process.cwd());
2913
+ const typeFilter = options.type === "structural" || options.type === "event" || options.type === "all" ? options.type : "all";
2914
+ const nodePath = options.node.trim().replace(/\/$/, "");
2915
+ const text = formatDependencyTree(graph, nodePath, {
2916
+ depth: options.depth,
2917
+ relationType: typeFilter
2918
+ });
2919
+ process.stdout.write(text + "\n");
2920
+ } catch (error) {
2921
+ process.stderr.write(`Error: ${error.message}
2922
+ `);
2923
+ process.exit(1);
2924
+ }
2925
+ });
2926
+ }
2927
+
2928
+ // src/core/graph-from-git.ts
2929
+ import { mkdtemp, rm } from "fs/promises";
2930
+ import { tmpdir } from "os";
2931
+ import path13 from "path";
2932
+ import { execSync as execSync3 } from "child_process";
2933
+ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
2934
+ const yggPath = ".yggdrasil";
2935
+ let tmpDir = null;
2936
+ try {
2937
+ execSync3(`git rev-parse ${ref}`, { cwd: projectRoot, stdio: "pipe" });
2938
+ } catch {
2939
+ return null;
2940
+ }
2941
+ try {
2942
+ tmpDir = await mkdtemp(path13.join(tmpdir(), "ygg-git-"));
2943
+ const archivePath = path13.join(tmpDir, "archive.tar");
2944
+ execSync3(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
2945
+ cwd: projectRoot,
2946
+ stdio: "pipe"
2947
+ });
2948
+ execSync3(`tar -xf "${archivePath}" -C "${tmpDir}"`, { stdio: "pipe" });
2949
+ const graph = await loadGraph(tmpDir);
2950
+ return graph;
2951
+ } catch {
2952
+ return null;
2953
+ } finally {
2954
+ if (tmpDir) {
2955
+ await rm(tmpDir, { recursive: true, force: true });
2956
+ }
2957
+ }
2958
+ }
2959
+
2960
+ // src/cli/impact.ts
2961
+ var STRUCTURAL_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2962
+ function collectReverseDependents(graph, targetNode) {
2963
+ const reverse = /* @__PURE__ */ new Map();
2964
+ const relationFrom = /* @__PURE__ */ new Map();
2965
+ for (const [nodePath, node] of graph.nodes) {
2966
+ for (const rel of node.meta.relations ?? []) {
2967
+ if (!STRUCTURAL_TYPES.has(rel.type)) continue;
2968
+ const deps = reverse.get(rel.target) ?? /* @__PURE__ */ new Set();
2969
+ deps.add(nodePath);
2970
+ reverse.set(rel.target, deps);
2971
+ relationFrom.set(`${nodePath}->${rel.target}`, {
2972
+ type: rel.type,
2973
+ consumes: rel.consumes
2974
+ });
2975
+ }
2976
+ }
2977
+ const direct = [...reverse.get(targetNode) ?? []].sort();
2978
+ const seen = new Set(direct);
2979
+ const queue = [...direct];
2980
+ while (queue.length > 0) {
2981
+ const current = queue.shift();
2982
+ for (const next of reverse.get(current) ?? []) {
2983
+ if (seen.has(next)) continue;
2984
+ seen.add(next);
2985
+ queue.push(next);
2986
+ }
2987
+ }
2988
+ return {
2989
+ direct,
2990
+ transitive: [...seen].sort(),
2991
+ reverse,
2992
+ relationFrom
2993
+ };
2994
+ }
2995
+ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
2996
+ const directSet = new Set(direct);
2997
+ const transitiveOnly = transitive.filter((t) => !directSet.has(t));
2998
+ if (transitiveOnly.length === 0) return [];
2999
+ const parent = /* @__PURE__ */ new Map();
3000
+ const queue = [targetNode];
3001
+ const visited = /* @__PURE__ */ new Set([targetNode]);
3002
+ while (queue.length > 0) {
3003
+ const current = queue.shift();
3004
+ for (const next of reverse.get(current) ?? []) {
3005
+ if (visited.has(next)) continue;
3006
+ visited.add(next);
3007
+ parent.set(next, current);
3008
+ queue.push(next);
3009
+ }
3010
+ }
3011
+ const chains = [];
3012
+ for (const node of transitiveOnly) {
3013
+ const path15 = [];
3014
+ let current = node;
3015
+ while (current) {
3016
+ path15.unshift(current);
3017
+ current = parent.get(current);
3018
+ }
3019
+ if (path15.length >= 2) {
3020
+ chains.push(path15.map((p) => `<- ${p}`).join(" "));
3021
+ }
3022
+ }
3023
+ return chains.sort();
3024
+ }
3025
+ function registerImpactCommand(program2) {
3026
+ program2.command("impact").description("Show reverse dependency impact for a node").requiredOption("--node <path>", "Node path relative to .yggdrasil/model/").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(async (options) => {
3027
+ try {
3028
+ const graph = await loadGraph(process.cwd());
3029
+ const nodePath = options.node.trim().replace(/\/$/, "");
3030
+ if (!graph.nodes.has(nodePath)) {
3031
+ process.stderr.write(`Node not found: ${nodePath}
3032
+ `);
3033
+ process.exit(1);
3034
+ }
3035
+ const { direct, transitive, reverse, relationFrom } = collectReverseDependents(
3036
+ graph,
3037
+ nodePath
3038
+ );
3039
+ const chains = buildTransitiveChains(nodePath, direct, transitive, reverse);
3040
+ const flows = [];
3041
+ for (const flow of graph.flows) {
3042
+ if (flow.nodes.includes(nodePath)) {
3043
+ flows.push(flow.name);
3044
+ }
3045
+ }
3046
+ const aspectsInScope = [];
3047
+ const targetNode = graph.nodes.get(nodePath);
3048
+ const targetTags = new Set(targetNode.meta.tags ?? []);
3049
+ for (const aspect of graph.aspects) {
3050
+ if (targetTags.has(aspect.tag)) {
3051
+ aspectsInScope.push(aspect.name);
3052
+ }
3053
+ }
3054
+ const knowledgeInScope = [];
3055
+ for (const k of graph.knowledge) {
3056
+ if (k.scope === "global") {
3057
+ knowledgeInScope.push(k.path);
3058
+ continue;
3059
+ }
3060
+ if (typeof k.scope === "object" && "tags" in k.scope) {
3061
+ if (k.scope.tags.some((t) => targetTags.has(t))) {
3062
+ knowledgeInScope.push(k.path);
3063
+ }
3064
+ continue;
3065
+ }
3066
+ if (typeof k.scope === "object" && "nodes" in k.scope) {
3067
+ if (k.scope.nodes.includes(nodePath)) {
3068
+ knowledgeInScope.push(k.path);
3069
+ }
3070
+ }
3071
+ }
3072
+ const budget = graph.config.quality?.context_budget ?? { warning: 5e3, error: 1e4 };
3073
+ process.stdout.write(`Impact of changes in ${nodePath}:
3074
+
3075
+ `);
3076
+ process.stdout.write("Directly dependent:\n");
3077
+ if (direct.length === 0) {
3078
+ process.stdout.write(" (none)\n");
3079
+ } else {
3080
+ for (const dep of direct) {
3081
+ const rel = relationFrom.get(`${dep}->${nodePath}`);
3082
+ const annot = rel?.consumes?.length ? ` (${rel.type}, you consume: ${rel.consumes.join(", ")})` : rel ? ` (${rel.type})` : "";
3083
+ process.stdout.write(` <- ${dep}${annot}
3084
+ `);
3085
+ }
3086
+ }
3087
+ process.stdout.write("\nTransitively dependent:\n");
3088
+ if (chains.length === 0) {
3089
+ process.stdout.write(" (none)\n");
3090
+ } else {
3091
+ for (const chain of chains) {
3092
+ process.stdout.write(` ${chain}
3093
+ `);
3094
+ }
3095
+ }
3096
+ process.stdout.write(`
3097
+ Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
3098
+ `);
3099
+ process.stdout.write(
3100
+ `Aspects (scope covers node): ${aspectsInScope.length > 0 ? aspectsInScope.join(", ") : "(none)"}
3101
+ `
3102
+ );
3103
+ process.stdout.write(
3104
+ `Knowledge (scope covers node): ${knowledgeInScope.length > 0 ? knowledgeInScope.join(", ") : "(none)"}
3105
+ `
3106
+ );
3107
+ process.stdout.write(
3108
+ `
3109
+ Total scope: ${transitive.length} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects, ${knowledgeInScope.length} knowledge
3110
+ `
3111
+ );
3112
+ if (options.simulate && transitive.length > 0) {
3113
+ process.stdout.write("\nChanges in context packages:\n\n");
3114
+ const baselineGraph = await loadGraphFromRef(process.cwd(), "HEAD");
3115
+ const driftReport = await detectDrift(graph);
3116
+ const driftByNode = new Map(driftReport.entries.map((e) => [e.nodePath, e]));
3117
+ for (const dep of transitive) {
3118
+ try {
3119
+ const pkg = await buildContext(graph, dep);
3120
+ const status = pkg.tokenCount >= budget.error ? "error" : pkg.tokenCount >= budget.warning ? "warning" : "ok";
3121
+ let baselineTokens = null;
3122
+ if (baselineGraph?.nodes.has(dep)) {
3123
+ try {
3124
+ const baselinePkg = await buildContext(baselineGraph, dep);
3125
+ baselineTokens = baselinePkg.tokenCount;
3126
+ } catch {
3127
+ }
3128
+ }
3129
+ const hasDepOnTarget = graph.nodes.get(dep)?.meta.relations?.some(
3130
+ (r) => r.target === nodePath && STRUCTURAL_TYPES.has(r.type)
3131
+ );
3132
+ const changedLine = hasDepOnTarget ? ` + Changed dependency interface: ${nodePath}
3133
+ ` : "";
3134
+ const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${pkg.tokenCount} tokens (${status})
3135
+ ` : ` Budget: ${pkg.tokenCount} tokens (${status})
3136
+ `;
3137
+ const driftEntry = driftByNode.get(dep);
3138
+ const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}
3139
+ ` : driftEntry ? ` Mapped files (on-disk): ok
3140
+ ` : "";
3141
+ process.stdout.write(`${dep}:
3142
+ ${changedLine}${budgetLine}${driftLine}
3143
+ `);
3144
+ } catch {
3145
+ process.stdout.write(`${dep}:
3146
+ failed to build context
3147
+
3148
+ `);
3149
+ }
3150
+ }
3151
+ }
3152
+ } catch (error) {
3153
+ process.stderr.write(`Error: ${error.message}
3154
+ `);
3155
+ process.exit(1);
3156
+ }
3157
+ });
3158
+ }
3159
+
3160
+ // src/io/journal-store.ts
3161
+ import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access2 } from "fs/promises";
3162
+ import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
3163
+ import path14 from "path";
3164
+ var JOURNAL_FILE = ".journal.yaml";
3165
+ var ARCHIVE_DIR = "journals-archive";
3166
+ async function readJournal(yggRoot) {
3167
+ const filePath = path14.join(yggRoot, JOURNAL_FILE);
3168
+ try {
3169
+ const content = await readFile13(filePath, "utf-8");
3170
+ const raw = parseYaml8(content);
3171
+ const entries = raw.entries ?? [];
3172
+ return Array.isArray(entries) ? entries : [];
3173
+ } catch {
3174
+ return [];
3175
+ }
3176
+ }
3177
+ async function appendJournalEntry(yggRoot, note, target) {
3178
+ const entries = await readJournal(yggRoot);
3179
+ const at = (/* @__PURE__ */ new Date()).toISOString();
3180
+ const entry = target ? { at, target, note } : { at, note };
3181
+ entries.push(entry);
3182
+ const filePath = path14.join(yggRoot, JOURNAL_FILE);
3183
+ const content = stringifyYaml2({ entries });
3184
+ await writeFile4(filePath, content, "utf-8");
3185
+ return entry;
3186
+ }
3187
+ async function archiveJournal(yggRoot) {
3188
+ const journalPath = path14.join(yggRoot, JOURNAL_FILE);
3189
+ try {
3190
+ await access2(journalPath);
3191
+ } catch {
3192
+ return null;
3193
+ }
3194
+ const entries = await readJournal(yggRoot);
3195
+ if (entries.length === 0) return null;
3196
+ const archiveDir = path14.join(yggRoot, ARCHIVE_DIR);
3197
+ await mkdir3(archiveDir, { recursive: true });
3198
+ const now = /* @__PURE__ */ new Date();
3199
+ const timestamp = `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}-${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`;
3200
+ const archiveName = `.journal.${timestamp}.yaml`;
3201
+ const archivePath = path14.join(archiveDir, archiveName);
3202
+ await rename(journalPath, archivePath);
3203
+ return { archiveName, entryCount: entries.length };
3204
+ }
3205
+
3206
+ // src/cli/journal-add.ts
3207
+ function registerJournalAddCommand(program2) {
3208
+ program2.command("journal-add").description("Add a note to the session journal").requiredOption("--note <text>", "Note content").option("--target <node-path>", "Node path this note relates to").action(async (options) => {
3209
+ try {
3210
+ const projectRoot = process.cwd();
3211
+ const yggRoot = await findYggRoot(projectRoot);
3212
+ await appendJournalEntry(yggRoot, options.note, options.target);
3213
+ const entries = await readJournal(yggRoot);
3214
+ process.stdout.write(`Note added to journal (${entries.length} entries total)
3215
+ `);
3216
+ } catch (error) {
3217
+ process.stderr.write(`Error: ${error.message}
3218
+ `);
3219
+ process.exit(1);
3220
+ }
3221
+ });
3222
+ }
3223
+
3224
+ // src/cli/journal-read.ts
3225
+ function registerJournalReadCommand(program2) {
3226
+ program2.command("journal-read").description("List pending journal entries").action(async () => {
3227
+ try {
3228
+ const projectRoot = process.cwd();
3229
+ const yggRoot = await findYggRoot(projectRoot);
3230
+ const entries = await readJournal(yggRoot);
3231
+ if (entries.length === 0) {
3232
+ process.stdout.write("Session journal: empty (clean state)\n");
3233
+ return;
3234
+ }
3235
+ process.stdout.write(`Session journal (${entries.length} entries):
3236
+
3237
+ `);
3238
+ for (const e of entries) {
3239
+ const date = e.at.slice(0, 19).replace("T", " ");
3240
+ const target = e.target ? ` ${e.target}` : "";
3241
+ process.stdout.write(`[${date}]${target}
3242
+ ${e.note}
3243
+
3244
+ `);
3245
+ }
3246
+ } catch (error) {
3247
+ process.stderr.write(`Error: ${error.message}
3248
+ `);
3249
+ process.exit(1);
3250
+ }
3251
+ });
3252
+ }
3253
+
3254
+ // src/cli/journal-archive.ts
3255
+ function registerJournalArchiveCommand(program2) {
3256
+ program2.command("journal-archive").description("Archive journal after consolidating notes to graph").action(async () => {
3257
+ try {
3258
+ const projectRoot = process.cwd();
3259
+ const yggRoot = await findYggRoot(projectRoot);
3260
+ const result = await archiveJournal(yggRoot);
3261
+ if (!result) {
3262
+ process.stdout.write("No active journal - nothing to archive.\n");
3263
+ return;
3264
+ }
3265
+ process.stdout.write(
3266
+ `Archived journal (${result.entryCount} entries) -> journals-archive/${result.archiveName}
3267
+ `
3268
+ );
3269
+ } catch (error) {
3270
+ process.stderr.write(`Error: ${error.message}
3271
+ `);
3272
+ process.exit(1);
3273
+ }
3274
+ });
3275
+ }
3276
+
3277
+ // src/bin.ts
3278
+ var program = new Command();
3279
+ program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version("0.1.0");
3280
+ registerInitCommand(program);
3281
+ registerBuildCommand(program);
3282
+ registerValidateCommand(program);
3283
+ registerDriftCommand(program);
3284
+ registerDriftSyncCommand(program);
3285
+ registerStatusCommand(program);
3286
+ registerTreeCommand(program);
3287
+ registerOwnerCommand(program);
3288
+ registerDepsCommand(program);
3289
+ registerImpactCommand(program);
3290
+ registerJournalAddCommand(program);
3291
+ registerJournalReadCommand(program);
3292
+ registerJournalArchiveCommand(program);
3293
+ program.parse();
3294
+ //# sourceMappingURL=bin.js.map