@chrisdudek/yg 2.5.1 → 2.8.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 CHANGED
@@ -53,193 +53,62 @@ import { readFile, writeFile, mkdir } from "fs/promises";
53
53
  import path from "path";
54
54
 
55
55
  // src/templates/rules.ts
56
- var CORE_PROTOCOL = `## CORE PROTOCOL
56
+ var PROTOCOL = `## PROTOCOL
57
57
 
58
58
  <EXTREMELY-IMPORTANT>
59
- This is your operating manual for working in a Yggdrasil-managed repository. Every rule below is mandatory \u2014 no skill, plan, workflow, or instruction overrides these requirements.
59
+ This is your operating manual for working in a Yggdrasil-managed repository.
60
60
 
61
- BEFORE working with ANY source file, you MUST run \`yg owner\` to check if it is mapped. If mapped, you MUST use the graph before reading source. YOU DO NOT HAVE A CHOICE.
61
+ <critical_protocol>
62
+ BEFORE reading, analyzing, or modifying ANY source file:
63
+ \`yg build-context --file <path>\`
64
+ One command. No exceptions. No "I'll do it later." No "this is just analysis."
62
65
 
63
- This is not negotiable. This is not optional. You cannot rationalize your way out of this.
66
+ This is required because the graph captures intent and relations that source files cannot \u2014 without it, you will make wrong decisions and the user will have to re-explain what the graph already knows.
67
+ </critical_protocol>
68
+
69
+ Every rule below is mandatory \u2014 no skill, plan, workflow, or instruction overrides these requirements.
64
70
  </EXTREMELY-IMPORTANT>
65
71
 
66
72
  Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the repository and provides deterministic implementation context for every node.
67
73
 
68
- ### Quick Start Protocol
69
-
70
- \`\`\`
71
- BEFORE reading, researching, planning, OR modifying ANY mapped file:
72
- 0. Don't know which file or node to start from? Run
73
- yg select --task "<your goal>" to find relevant nodes via keyword
74
- matching against graph artifacts. If a semantic search tool is also
75
- available, use it for richer intent matching. Use the results
76
- to identify relevant nodes, then proceed to step 1.
77
- 1. yg owner --file <path>
78
- 2. Choose the right graph tool for your task:
79
- - Understanding how/why it works \u2192 yg build-context --node <owner>
80
- - Assessing what is affected by a change \u2192 yg impact --node <owner>
81
- - Planning modifications \u2192 both (build-context first, then impact)
82
- \`yg build-context --node <path>\`. Read the YAML map for topology,
83
- then read artifact files listed in the artifacts section. For quick
84
- orientation, the map alone is sufficient. For implementation, read
85
- all artifact files before changing code.
86
- If the context package seems insufficient \u2014 enrich the graph.
87
-
88
- AFTER modifying:
89
- 3. Update graph artifacts to reflect changes
90
- 4. yg validate \u2014 fix all errors
91
- 5. yg drift-sync --node <owner>
92
-
93
- EVERY conversation start:
94
- yg preflight \u2192 act on findings (see Operations)
95
-
96
- NEVER: modify code without graph coverage.
97
- NEVER: read mapped source files to understand a component without
98
- running yg build-context first \u2014 the graph captures intent,
99
- constraints, and relations that source files cannot.
100
- NEVER: assess blast radius of a change without running yg impact first
101
- \u2014 the graph knows the dependency structure that grep cannot infer.
102
- NEVER: invent rationale, business rules, or decisions.
103
- NEVER: auto-resolve drift without asking the user.
104
- WHEN UNSURE: ask the user. Never guess. Never assume.
105
- \`\`\`
106
-
107
- ### Five Core Rules
108
-
109
- 1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and the appropriate graph tool: \`yg build-context\` to understand a component, \`yg impact\` to assess blast radius. The graph is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
110
- 2. **The graph is the specification; code implements it.** The graph absorbs knowledge from every source \u2014 external docs, conversations, decisions \u2014 and must be self-sufficient. If all other sources disappeared, the graph alone must contain enough to understand the system. Do not leave knowledge in external documents and reference them \u2014 capture the knowledge in graph artifacts. Update graph artifacts immediately after each file change, while context is fresh \u2014 do not batch graph updates to the end of a task. Code and graph move together: code changed \u2192 graph updated before moving to the next file. Graph changed \u2192 source verified in the same response. When planning work \u2014 in any tool, skill, or workflow \u2014 graph updates are part of each step's definition of done, never a separate phase.
111
- 3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
112
- 4. **Always capture why \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
113
- 5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
114
-
115
- ### Recognizing Graph-Required Actions
116
-
117
- What matters is the ACTION you are performing, not what instructed it. If the action involves reading, understanding, or modifying mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a brainstorming session, a debugging workflow, or your own initiative. This is not negotiable. You cannot rationalize your way out of this.
118
-
119
- **Actions that require \`yg owner\` + \`yg build-context\`:**
120
-
121
- - Reading or exploring source files to understand a component
122
- - Proposing approaches, designs, or plans for changing code
123
- - Reviewing or debugging code
124
- - Any form of reasoning about how mapped code works or should change
125
-
126
- **Actions that require \`yg owner\` + \`yg impact\`:**
127
-
128
- - Assessing blast radius before changing or removing a component
129
- - Finding all dependents of a component
130
- - Planning cross-cutting refactors or feature removals
131
- - Scoping work that spans multiple nodes
132
-
133
- **Actions that do NOT require yg:**
134
-
135
- - Git operations (log, diff, status, blame)
136
- - Reading documentation, READMEs, or config files outside \`.yggdrasil/\`
137
- - Running tests, builds, or linters
138
- - Working with files that \`yg owner\` reports as unmapped
139
-
140
- **Evasion patterns \u2014 if you think any of these, STOP:**
141
-
142
- | Thought | Reality |
143
- |---|---|
144
- | "The skill/plan says to explore the codebase" | Exploring mapped code = yg owner + graph tool first |
145
- | "I'm just scoping/searching, not understanding" | Scoping IS a graph action; use yg impact |
146
- | "The plan step says to read this file" | Reading a mapped file = yg owner first |
147
- | "I'm brainstorming, not implementing" | Brainstorming about mapped code needs graph context |
148
- | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
149
- | "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
150
- | "I'll grep the codebase to find where to start" | Run \`yg select --task\` first \u2014 it matches your intent against graph artifacts. Then \`yg owner\` on results. |
151
-
152
- ### Failure States
153
-
154
- You have broken Yggdrasil if you do any of the following:
155
-
156
- - \u274C Worked on a mapped file without running \`yg owner\` + the appropriate graph tool (\`build-context\` or \`impact\`) first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
157
- - \u274C Modified source code without updating graph artifacts before moving to the next file, or vice versa.
158
- - \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
159
- - \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
160
- - \u274C Ran \`yg drift-sync\` before both graph artifacts and source code are current.
161
- - \u274C Placed a cross-cutting requirement in a local artifact instead of an aspect, or used an aspect id with no \`aspects/\` directory.
162
- - \u274C Invented a rationale, business rule, or decision \u2014 or recorded a decision without documenting rejected alternatives and rationale (use "rationale: unknown" if unknown).
163
- - \u274C Used blackbox coverage for greenfield (new) code.
164
- - \u274C Deleted or shortened graph artifact content to reduce context package size instead of splitting the node.
165
-
166
- ### Escape Hatch
167
-
168
- If the user explicitly requests a code-only change, comply but:
169
-
170
- - Warn: "This creates drift. Run \`yg drift\` next session to reconcile."
171
- - Do NOT run \`yg drift-sync\` \u2014 leave the drift visible.
172
-
173
- ### Environment Check
174
-
175
- Before preflight:
176
-
177
- - Verify \`yg\` CLI is available. If not found, inform user and stop.
178
- - If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
179
- - If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
180
-
181
- ### Delegating to Subagents
182
-
183
- When you delegate work to a subagent (any subprocess, tool agent, or spawned assistant), the subagent does NOT inherit your Yggdrasil knowledge. Before any other instruction, the subagent MUST:
184
-
185
- 1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
186
- 2. Follow the Quick Start Protocol from that file before touching any mapped code
187
-
188
- Include this as the FIRST instruction in every subagent prompt:
189
-
190
- \`\`\`
191
- BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
192
- DELIVERABLES \u2014 all required, incomplete work will be rejected:
193
- 1. Working source code
194
- 2. Graph nodes with artifacts for every new/modified source file
195
- 3. \`yg validate\` passing
196
- \`\`\`
197
-
198
- A subagent that delivers code without corresponding graph updates has not completed its task. Before accepting subagent output, verify: are there new or modified source files without corresponding graph coverage? If yes, the work is incomplete.`;
199
- var OPERATIONS = `## OPERATIONS
200
-
201
- ### Conversation Lifecycle
74
+ ### Quick Start
202
75
 
203
76
  \`\`\`
204
- PREFLIGHT (every conversation, before any work):
205
- - [ ] 1. yg preflight \u2192 read unified report
206
- - [ ] 2. If drift: resolve per Drift Resolution, then yg drift-sync per node
207
- - [ ] 3. If validation errors: fix, re-run yg validate
208
- Exception: read-only requests (explain, analyze) \u2014 skip preflight.
209
-
210
- UNDERSTANDING mapped code (questions, research, OR planning):
211
- - [ ] 1. yg owner --file <path>
212
- - [ ] 2. Owner found \u2192 yg build-context --node <path>. Read the YAML map
213
- for topology, then read artifact files from the artifacts section.
214
- For quick orientation, the map alone is sufficient. For implementation,
215
- read all artifact files before changing code.
216
- - [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
217
- Never use grep or raw file reads as primary understanding when graph coverage exists.
218
- Raw reads supplement the context package \u2014 they do not replace it.
77
+ EVERY conversation: yg preflight \u2014 no exceptions.
219
78
 
220
- WRAP-UP (user signals "done", "wrap up", "that's enough"):
221
- - [ ] 1. yg drift --drifted-only \u2192 resolve
222
- - [ ] 2. yg validate \u2192 fix errors
223
- - [ ] 3. Report: which nodes and files were changed
79
+ BEFORE any source file interaction:
80
+ 1. yg build-context --file <path>
81
+ One command: resolves owner, assembles context.
82
+ Read the YAML map \u2014 glossary first (aspect/flow definitions),
83
+ then artifact files listed on each element.
84
+ For blast radius: also run yg impact --file <path>.
85
+ Don't know where to start? yg select --task "<goal>"
224
86
 
225
- BEFORE ENDING ANY RESPONSE (self-audit):
226
- - [ ] Did I interact with mapped code (read, research, or modify)? If yes \u2192 did I use a graph tool BEFORE reading source?
227
- - [ ] Did I modify source code? If yes \u2192 did I update graph artifacts before moving to the next file?
228
- - [ ] If you broke either rule, you have broken the protocol. Do not finish until both are fixed.
87
+ AFTER modifying:
88
+ 2. Update graph artifacts (per file, not batched)
89
+ 3. yg validate \u2014 fix all errors
90
+ 4. yg drift-sync --node <owner>
91
+
92
+ ALWAYS: establish graph coverage before modifying code.
93
+ ALWAYS: run yg build-context --file before reading source.
94
+ ALWAYS: run yg impact before assessing blast radius.
95
+ ALWAYS: ask the user for rationale \u2014 record it, do not invent it.
96
+ ALWAYS: ask before resolving drift or ambiguity.
97
+ WHEN UNSURE: ask the user. Do not guess. Do not assume.
229
98
  \`\`\`
230
99
 
231
100
  ### Modify Source Code
232
101
 
233
102
  You are not allowed to edit or create source code without establishing graph coverage first.
234
103
 
235
- **Step 1** \u2014 Check coverage: \`yg owner --file <path>\`
104
+ **Step 1** \u2014 Get context: \`yg build-context --file <path>\` (resolves owner automatically)
236
105
 
237
106
  **Step 2a** \u2014 Owner found: execute checklist:
238
107
 
239
- - [ ] 1. Read specification: \`yg build-context --node <node_path>\`
108
+ - [ ] 1. Read the context package (already assembled by step 1)
240
109
  - [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
241
110
  - [ ] 3. Modify source code
242
- - [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change)
111
+ - [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change). If the node's purpose changed, update \`description\` in \`yg-node.yaml\`.
243
112
  - [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
244
113
  - [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
245
114
 
@@ -257,103 +126,96 @@ You are not allowed to edit or create source code without establishing graph cov
257
126
 
258
127
  1. Create aspects first (cross-cutting requirements the new code must satisfy)
259
128
  2. Create flows if the code participates in a business process
260
- 3. Create nodes with full artifacts \u2014 responsibility, interface, internals
129
+ 3. Create nodes with full artifacts \u2014 description in \`yg-node.yaml\`, responsibility, interface, internals
261
130
  4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
262
131
  5. Implement code that satisfies the specification
263
132
  6. The graph specifies WHAT and WHY; the code implements HOW (framework APIs, library choices)
264
133
 
265
134
  After the user chooses, return to Step 1 and follow Step 2a.
266
135
 
267
- ### Modify Graph
136
+ ### Example: Correct vs Wrong
268
137
 
269
- - [ ] 1. Read the relevant schema from \`schemas/\` before touching any YAML
270
- - [ ] 2. Before changing an aspect or flow, check scope: \`yg impact --aspect <id>\` or \`yg impact --flow <name>\` \u2014 understand which nodes are affected before modifying shared rules or processes
271
- - [ ] 3. Make changes
272
- - [ ] 4. Run \`yg validate\` immediately \u2014 fix all errors
273
- - [ ] 5. Verify affected source files are consistent \u2014 update if needed
274
- - [ ] 6. Run \`yg drift-sync\` for affected nodes
138
+ <example_correct>
275
139
 
276
- ### Reverse Engineering
277
-
278
- **Order:** aspects (cross-cutting patterns) \u2192 flows (business processes) \u2192 model nodes. Never create nodes before aspects and flows are understood.
140
+ User: "Fix the bug in payment.service.ts"
279
141
 
280
- Per area checklist:
142
+ 1. yg build-context --file src/payment.service.ts \u2192 payment/payment-service
143
+ 2. Read YAML map \u2014 glossary, then artifact files
144
+ 3. Read source file, understand bug in graph context
145
+ 4. Fix bug
146
+ 5. Update payment-service artifacts (responsibility.md, interface.md if API changed)
147
+ 6. yg validate
148
+ 7. yg drift-sync --node payment/payment-service
281
149
 
282
- - [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
283
- - [ ] 2. Determine node granularity \u2014 propose to user if unclear
284
- - [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
285
- - [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
286
- - [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
287
- - [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
288
- - [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add as \`anchors\` in the aspect entry in \`yg-node.yaml\`
289
- - [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
290
- - [ ] 8. \`yg validate\` \u2014 fix errors
291
- - [ ] 9. \`yg drift-sync --node <path>\`
150
+ </example_correct>
292
151
 
293
- **When to ask:**
152
+ <example_wrong>
294
153
 
295
- - Business process unclear: "This code appears to be part of a larger process. Can you describe what it means from a business perspective?"
296
- - Constraint without rationale: "I see [constraint X]. Do you know why this exists? I want to record the reason, not just the rule."
297
- - Unexplained architectural choice: "I see [approach X]. What was the reason for this choice?"
298
- - Decision without alternatives: "You chose [X]. What alternatives did you consider, and why did you reject them?" Record the answer in the Decisions section of \`internals.md\`.
299
- - Decision without known rationale: Record the decision in \`internals.md\` with "rationale: unknown \u2014 inferred from code, not confirmed by developer." A recorded decision with unknown rationale is infinitely more valuable than no record at all, and safer than an invented rationale.
154
+ User: "Fix the bug in payment.service.ts"
300
155
 
301
- ### Bootstrap Mode
156
+ 1. Read src/payment.service.ts \u2190 WRONG: no graph context loaded
157
+ 2. Fix bug
158
+ 3. "I'll update the graph later" \u2190 WRONG: deferred = forgotten
302
159
 
303
- Trigger: \`yg preflight\` shows 0 nodes, or no nodes cover the active work area.
160
+ Result: graph is stale, next agent asks user the same questions
304
161
 
305
- - [ ] 1. Identify the active work area (files the user wants to modify)
306
- - [ ] 2. Scan for cross-cutting patterns \u2192 create aspects
307
- - [ ] 3. Ask user about business processes \u2192 create flows if applicable
308
- - [ ] 4. Propose node structure for the area
309
- - [ ] 5. Create node(s) with initial artifacts, map files
310
- - [ ] 6. \`yg validate\`, \`yg drift-sync\`
311
- - [ ] 7. Proceed with user's original request
162
+ </example_wrong>
312
163
 
313
- Constraint: Do NOT map the entire repository. Focus on the active area. Expand incrementally.
164
+ ### Conversation Lifecycle
314
165
 
315
- ### Drift Resolution
166
+ \`\`\`
167
+ PREFLIGHT (every conversation, before any work):
168
+ - [ ] 1. yg preflight \u2192 read unified report
169
+ - [ ] 2. If drift: resolve per Drift Resolution, then yg drift-sync per node
170
+ - [ ] 3. If validation errors: fix, re-run yg validate
171
+ No exceptions. You cannot know if a file is mapped without running yg.
316
172
 
317
- Always ask the user before resolving drift. Never auto-resolve.
173
+ UNDERSTANDING any source file (questions, research, OR planning):
174
+ - [ ] 1. yg build-context --file <path>
175
+ Mapped \u2192 read the YAML map (glossary first, then artifact files).
176
+ Unmapped \u2192 use file analysis, state it is not graph-backed.
177
+ Never use grep or raw file reads as primary understanding when graph coverage exists.
178
+ Raw reads supplement the context package \u2014 they do not replace it.
318
179
 
319
- - **Source drift** (source files changed) \u2192 update graph artifacts to match source, then \`yg drift-sync\`
320
- - **Graph drift** (graph artifacts changed) \u2192 review affected source, update if needed, then \`yg drift-sync\`
321
- - **Full drift** (both changed) \u2192 present both sides to user, ask which direction wins
322
- - **Missing** \u2192 ask: re-materialize or remove mapping?
323
- - **Unmaterialized** \u2192 ask user how to proceed
180
+ BEFORE reasoning about source code, state which graph context you loaded:
181
+ "graph: <node_path>" if mapped, "graph: unmapped" if not.
182
+ This is a required output step, not optional reflection.
324
183
 
325
- Threshold: >10 drifted nodes \u2192 ask user which area to prioritize. Do not resolve all at once.
184
+ WRAP-UP (user signals "done", "wrap up", "that's enough"):
185
+ - [ ] 1. yg drift --drifted-only \u2192 resolve
186
+ - [ ] 2. yg validate \u2192 fix errors
187
+ - [ ] 3. Report: which nodes and files were changed
326
188
 
327
- **Drift triage:** Prioritize aspects and \`internals.md\` (highest decay rate), then \`responsibility.md\` and \`interface.md\` (most stable).
189
+ \`\`\`
328
190
 
329
- ### Graph Audit
191
+ ### Modify Graph
330
192
 
331
- When reviewing graph quality (triggered by user or quality improvement):
193
+ - [ ] 1. Read the relevant schema from \`schemas/\` before touching any YAML
194
+ - [ ] 2. Before changing an aspect or flow, check scope: \`yg impact --aspect <id>\` or \`yg impact --flow <name>\` \u2014 understand which nodes are affected before modifying shared rules or processes
195
+ - [ ] 3. Make changes
196
+ - [ ] 4. Run \`yg validate\` immediately \u2014 fix all errors
197
+ - [ ] 5. Verify affected source files are consistent \u2014 update if needed
198
+ - [ ] 6. Run \`yg drift-sync\` for affected nodes
332
199
 
333
- **Step 1 \u2014 Consistency** (catches WRONG information):
200
+ ### Delegating to Subagents
334
201
 
335
- - [ ] 1. \`yg build-context --node <path>\`
336
- - [ ] 2. Read mapped source files
337
- - [ ] 3. For each claim in graph: verify against source code
338
- - [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`exceptions\` to the aspect entry in \`yg-node.yaml\`
339
- - [ ] 5. Report inconsistencies
202
+ When you delegate work to a subagent (any subprocess, tool agent, or spawned assistant), the subagent does NOT inherit your Yggdrasil knowledge. Before any other instruction, the subagent MUST:
340
203
 
341
- **Step 2 \u2014 Completeness** (catches MISSING information):
204
+ 1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
205
+ 2. Follow the Quick Start Protocol from that file before touching any mapped code
342
206
 
343
- - [ ] 1. For each public method: is it in \`interface.md\`?
344
- - [ ] 2. For each error path: is it in \`interface.md\` (Failure Modes section)?
345
- - [ ] 3. For each behavioral invariant: is it in the graph?
346
- - [ ] 4. Report omissions separately from inconsistencies
207
+ Include this as the FIRST instruction in every subagent prompt:
347
208
 
348
- ### Error Recovery
209
+ \`\`\`
210
+ BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
211
+ DELIVERABLES \u2014 all required, incomplete work will be rejected:
212
+ 1. Working source code
213
+ 2. Graph nodes with artifacts for every new/modified source file
214
+ 3. \`yg validate\` passing
215
+ \`\`\`
349
216
 
350
- - **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
351
- - **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
352
- - **Budget warning (W005/W006)** \u2192 informational. \`yg validate\` shows a breakdown (own/hierarchy/aspects/flows/dependencies). Large inherited context means the system is complex \u2014 this is not a problem to fix, it is reality to acknowledge. Do not delete knowledge from artifacts. Do not attempt to "reduce" inherited context.
353
- - **Own budget warning (W015)** \u2192 own artifacts are large. Consider splitting this node's responsibilities into child nodes. Redistribute knowledge across children so total knowledge is preserved or increased, never reduced.
354
- - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
355
- - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
356
- var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
217
+ A subagent that delivers code without corresponding graph updates has not completed its task. Before accepting subagent output, verify: are there new or modified source files without corresponding graph coverage? If yes, the work is incomplete.`;
218
+ var REFERENCE = `## REFERENCE
357
219
 
358
220
  ### Graph Structure
359
221
 
@@ -389,9 +251,20 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
389
251
 
390
252
  ### Context Assembly
391
253
 
392
- **Reading context:** \`yg build-context --node <path>\` returns a YAML map with the node's topology (hierarchy, dependencies, aspects, flows) and an \`artifacts\` section listing files to read. All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
254
+ **Reading context:** \`yg build-context --node <path>\` returns a YAML map structured as follows:
255
+
256
+ - **\`glossary\`** (top) \u2014 definitions for every aspect and flow referenced in the map, each with \`files\` listing their artifact paths. Read this first to understand IDs used throughout.
257
+ - **\`node\`** \u2014 the target node with inline \`files\` (its artifact paths). No \`yg-node.yaml\` in file lists.
258
+ - **\`hierarchy\`** \u2014 ancestor and sibling nodes, each with inline \`files\`.
259
+ - **\`dependencies\`** \u2014 dependency nodes, each with inline \`files\`.
260
+ - **\`meta\`** (bottom) \u2014 context assembly metadata.
261
+ - YAML comments before each section guide reading order.
393
262
 
394
- **Default mode (paths-only):** Use for all graph operations. Read the YAML map first to understand topology. Then read artifact files from the \`artifacts\` section using the Read tool. For quick orientation (scoping, blast radius assessment), the map alone is sufficient. For implementation or modification, read all artifact files before changing code.
263
+ All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
264
+
265
+ **Default mode (paths-only):** Use for all graph operations. Read the YAML map first \u2014 start with the \`glossary\` to understand aspects and flows, then the \`node\` section for the target. Read artifact files inline on each element using the Read tool. For quick orientation (scoping, blast radius assessment), the map alone is sufficient. For implementation or modification, read all artifact files before changing code.
266
+
267
+ The glossary at the top defines all aspects and flows \u2014 read it first to understand IDs used throughout.
395
268
 
396
269
  **Full mode (\`--full\`):** Use only when you cannot read files individually \u2014 e.g., when pasting context into a prompt, sharing with a user, or when you have no Read tool available.
397
270
 
@@ -440,7 +313,7 @@ When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are prese
440
313
 
441
314
  - [ ] 1. Read \`schemas/yg-flow.yaml\`
442
315
  - [ ] 2. Create \`flows/<name>/\` directory
443
- - [ ] 3. Write \`yg-flow.yaml\` \u2014 declare nodes (participant list) and flow-level aspects
316
+ - [ ] 3. Write \`yg-flow.yaml\` \u2014 name, description, nodes (participant list), and flow-level aspects
444
317
  - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
445
318
  - [ ] 5. \`yg validate\`
446
319
 
@@ -453,7 +326,8 @@ Test: "Does this describe what happens in the world, or only in the software?" I
453
326
  - **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
454
327
  - **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
455
328
  - **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
456
- - **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
329
+ - **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task. \`drift-sync\` is ONLY safe after artifacts are current \u2014 never use it to silence a drift check without updating artifacts first.
330
+ - **Description maintenance.** Every \`yg-node.yaml\`, \`yg-aspect.yaml\`, and \`yg-flow.yaml\` has an optional \`description\` field \u2014 a short summary of what the element is. Write it when creating new elements. Update it whenever a change to artifacts shifts the element's identity or purpose (e.g., responsibility split, scope change). Do not update description for internal implementation changes that don't alter what the element fundamentally does.
457
331
  - **Completeness test:** Two checks, both required:
458
332
  1. **Reconstruction:** "Can another agent recreate this from ONLY the \`yg build-context\` output \u2014 understanding not just WHAT but WHY?" Test: rejected alternatives, correct algorithm, design arguments.
459
333
  2. **Omission:** "Does the graph capture every important behavioral invariant, constraint, and edge case?" Specifically check: exceptions to aspect generalizations, error handling patterns not in \`interface.md\`, concurrency behaviors not in \`internals.md\`.
@@ -464,8 +338,9 @@ Test: "Does this describe what happens in the world, or only in the software?" I
464
338
 
465
339
  \`\`\`
466
340
  yg preflight [--quick] Unified diagnostic: drift + status + validate.
467
- yg owner --file <path> Find the node that owns this file.
468
- yg build-context --node <path> Assemble context map with artifact paths (default).
341
+ yg owner --file <path> Find the node that owns this file (quick check).
342
+ yg build-context --file <path> Resolve owner + assemble context in one step.
343
+ yg build-context --node <path> Assemble context map for a known node.
469
344
  yg build-context --node <path> --full Same map + file contents appended below separator.
470
345
  yg tree [--root <path>] [--depth N] Print graph structure.
471
346
  yg aspects List aspects with metadata (YAML output).
@@ -474,8 +349,9 @@ yg select --task <description> [--limit <n>]
474
349
  Find graph nodes relevant to a task description.
475
350
  yg deps --node <path> [--depth N] [--type structural|event|all]
476
351
  Show dependencies.
477
- yg impact --node <path> --simulate Simulate blast radius of a planned change.
478
- yg impact --node <path> --method <name> Filter impact to dependents consuming a specific method.
352
+ yg impact --file <path> Resolve owner + show blast radius in one step.
353
+ yg impact --node <path> --simulate Simulate blast radius (works with --file too).
354
+ yg impact --node <path> --method <name> Filter to dependents consuming a method (works with --file too).
479
355
  yg impact --aspect <id> Show all nodes where aspect is effective.
480
356
  yg impact --flow <name> Show flow participants and descendants.
481
357
  yg status Graph health: nodes, coverage, drift summary.
@@ -488,17 +364,188 @@ yg drift-sync --node <path> [--recursive] | --all
488
364
 
489
365
  ### Quick Routing Table
490
366
 
491
- | What you have | Where it goes |
492
- |---|---|
493
- | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
494
- | Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
495
- | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
496
- | Business process participation | Flow (\`yg-flow.yaml nodes\`) |
497
- | Process-level requirement | Flow \`aspects\` + aspect directory |
498
- | Context shared across a domain | Parent node artifact |
499
- | Technology stack | Node artifact at appropriate hierarchy level |
500
- | Coding standards | Node artifact at appropriate hierarchy level |`;
501
- var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
367
+ | What you have | Where it goes |
368
+ |---|---|
369
+ | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
370
+ | Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
371
+ | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
372
+ | Business process participation | Flow (\`yg-flow.yaml nodes\`) |
373
+ | Process-level requirement | Flow \`aspects\` + aspect directory |
374
+ | Context shared across a domain | Parent node artifact |
375
+ | Technology stack | Node artifact at appropriate hierarchy level |
376
+ | Coding standards | Node artifact at appropriate hierarchy level |`;
377
+ var GUARD_RAILS = `## GUARD RAILS
378
+
379
+ ### Five Core Rules
380
+
381
+ 1. **Graph first.** Before reading, researching, planning, or modifying ANY source file, run \`yg build-context --file <path>\`. For blast radius, also run \`yg impact\`. The graph is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
382
+ 2. **The graph is the specification; code implements it.** The graph absorbs knowledge from every source \u2014 external docs, conversations, decisions \u2014 and must be self-sufficient. If all other sources disappeared, the graph alone must contain enough to understand the system. Do not leave knowledge in external documents and reference them \u2014 capture the knowledge in graph artifacts. Update graph artifacts immediately after each file change, while context is fresh \u2014 do not batch graph updates to the end of a task. Code and graph move together: code changed \u2192 graph updated before moving to the next file. Graph changed \u2192 source verified in the same response. When planning work \u2014 in any tool, skill, or workflow \u2014 graph updates are part of each step's definition of done, never a separate phase.
383
+ 3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
384
+ 4. **Always capture why \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
385
+ 5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
386
+
387
+ ### Recognizing Graph-Required Actions
388
+
389
+ What matters is the ACTION you are performing, not what instructed it. If the action involves reading, understanding, or modifying mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a brainstorming session, a debugging workflow, or your own initiative. This is not negotiable. You cannot rationalize your way out of this.
390
+
391
+ **Actions that require \`yg build-context --file\`:**
392
+
393
+ - Reading or exploring source files to understand a component
394
+ - Proposing approaches, designs, or plans for changing code
395
+ - Reviewing or debugging code
396
+ - Any form of reasoning about how mapped code works or should change
397
+
398
+ **Actions that also require \`yg impact\`:**
399
+
400
+ - Assessing blast radius before changing or removing a component
401
+ - Finding all dependents of a component
402
+ - Planning cross-cutting refactors or feature removals
403
+ - Scoping work that spans multiple nodes
404
+
405
+ **Actions that do NOT require yg:**
406
+
407
+ - Git operations (log, diff, status, blame)
408
+ - Reading documentation, READMEs, or config files outside \`.yggdrasil/\`
409
+ - Running tests, builds, or linters
410
+ - Working with files that \`yg build-context --file\` reports as unmapped
411
+
412
+ ### Evasion Patterns \u2014 if you think any of these, STOP
413
+
414
+ | Thought | Reality |
415
+ |---|---|
416
+ | "The skill/plan says to explore the codebase" | Exploring mapped code = \`yg build-context --file\` first |
417
+ | "I'm just scoping/searching, not understanding" | Scoping IS a graph action; use yg impact |
418
+ | "The plan step says to read this file" | Reading any source file = \`yg build-context --file\` first |
419
+ | "I'm brainstorming, not implementing" | Brainstorming about code needs graph context. You proved this by failing at it. |
420
+ | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
421
+ | "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
422
+ | "I'll grep the codebase to find where to start" | Run \`yg select --task\` first, then \`yg build-context --file\` on results. |
423
+ | "Drift is blocking repo-check, let me just sync it" | Drift means artifacts are stale. Update artifacts first, then sync. \`drift-sync\` will warn you (W018). |
424
+ | "The user said work autonomously" | Autonomy amplifies discipline, not relaxes it. More tasks = more graph updates, not fewer. |
425
+ | "Same pattern as the last 5 files, no need to document" | Repetitive patterns hide deviations. Per-node coverage captures what aspects don't. The next agent won't know what you know now. |
426
+ | "I'll batch graph updates at the end" | Batching = never. Context is freshest immediately after the change. Defer = forget. This is a failure state. |
427
+ | "I'm saving context/tool calls by skipping graph" | Graph cost is constant per node. Skipping it creates unbounded future cost \u2014 the user re-explaining what you could have recorded. |
428
+ | "I assumed this file isn't mapped" | You cannot know without running \`yg build-context --file\`. Assume nothing. |
429
+
430
+ ### Failure States
431
+
432
+ You have broken Yggdrasil if you do any of the following:
433
+
434
+ - \u274C Worked on a source file without running \`yg build-context --file\` first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
435
+ - \u274C Modified source code without updating graph artifacts before moving to the next file, or vice versa.
436
+ - \u274C Batched graph updates to "do later" \u2014 deferred = forgotten. Update after EACH file.
437
+ - \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
438
+ - \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
439
+ - \u274C Ran \`yg drift-sync\` before both graph artifacts and source code are current. (CLI will warn you: W018.)
440
+ - \u274C Placed a cross-cutting requirement in a local artifact instead of an aspect, or used an aspect id with no \`aspects/\` directory.
441
+ - \u274C Invented a rationale, business rule, or decision \u2014 or recorded a decision without documenting rejected alternatives and rationale (use "rationale: unknown" if unknown).
442
+ - \u274C Used blackbox coverage for greenfield (new) code.
443
+ - \u274C Deleted or shortened graph artifact content to reduce context package size instead of splitting the node.
444
+ - \u274C Created one wide node for many files instead of granular nodes with focused responsibilities. (CLI will warn you: W017.)
445
+
446
+ ### Reverse Engineering
447
+
448
+ **Order:** aspects (cross-cutting patterns) \u2192 flows (business processes) \u2192 model nodes. Never create nodes before aspects and flows are understood.
449
+
450
+ Per area checklist:
451
+
452
+ - [ ] 1. \`yg build-context --file <path>\` \u2014 confirm no coverage
453
+ - [ ] 2. Determine node granularity \u2014 propose to user if unclear
454
+ - [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
455
+ - [ ] 3b. Write \`description\` in \`yg-node.yaml\` \u2014 a short summary of what the node does
456
+ - [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
457
+ - [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
458
+ - [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
459
+ - [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add as \`anchors\` in the aspect entry in \`yg-node.yaml\`
460
+ - [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
461
+ - [ ] 8. \`yg validate\` \u2014 fix errors
462
+ - [ ] 9. \`yg drift-sync --node <path>\`
463
+
464
+ **When to ask:**
465
+
466
+ - Business process unclear: "This code appears to be part of a larger process. Can you describe what it means from a business perspective?"
467
+ - Constraint without rationale: "I see [constraint X]. Do you know why this exists? I want to record the reason, not just the rule."
468
+ - Unexplained architectural choice: "I see [approach X]. What was the reason for this choice?"
469
+ - Decision without alternatives: "You chose [X]. What alternatives did you consider, and why did you reject them?" Record the answer in the Decisions section of \`internals.md\`.
470
+ - Decision without known rationale: Record the decision in \`internals.md\` with "rationale: unknown \u2014 inferred from code, not confirmed by developer." A recorded decision with unknown rationale is infinitely more valuable than no record at all, and safer than an invented rationale.
471
+
472
+ ### Bootstrap Mode
473
+
474
+ Trigger: \`yg preflight\` shows 0 nodes, or no nodes cover the active work area.
475
+
476
+ - [ ] 1. Identify the active work area (files the user wants to modify)
477
+ - [ ] 2. Scan for cross-cutting patterns \u2192 create aspects
478
+ - [ ] 3. Ask user about business processes \u2192 create flows if applicable
479
+ - [ ] 4. Propose node structure for the area
480
+ - [ ] 5. Create node(s) with initial artifacts, map files
481
+ - [ ] 6. \`yg validate\`, \`yg drift-sync\`
482
+ - [ ] 7. Proceed with user's original request
483
+
484
+ Constraint: Do NOT map the entire repository. Focus on the active area. Expand incrementally.
485
+
486
+ ### Drift Resolution
487
+
488
+ Always ask the user before resolving drift. Never auto-resolve.
489
+
490
+ - **Source drift** (source files changed) \u2192 update graph artifacts to match source, then \`yg drift-sync\`
491
+ - **Graph drift** (graph artifacts changed) \u2192 review affected source, update if needed, then \`yg drift-sync\`
492
+ - **Full drift** (both changed) \u2192 present both sides to user, ask which direction wins
493
+ - **Missing** \u2192 ask: re-materialize or remove mapping?
494
+ - **Unmaterialized** \u2192 ask user how to proceed
495
+
496
+ Threshold: >10 drifted nodes \u2192 ask user which area to prioritize. Do not resolve all at once.
497
+
498
+ **Drift triage:** Prioritize aspects and \`internals.md\` (highest decay rate), then \`responsibility.md\` and \`interface.md\` (most stable).
499
+
500
+ ### Graph Audit
501
+
502
+ When reviewing graph quality (triggered by user or quality improvement):
503
+
504
+ **Step 1 \u2014 Consistency** (catches WRONG information):
505
+
506
+ - [ ] 1. \`yg build-context --node <path>\`
507
+ - [ ] 2. Read mapped source files
508
+ - [ ] 3. For each claim in graph: verify against source code
509
+ - [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`exceptions\` to the aspect entry in \`yg-node.yaml\`
510
+ - [ ] 5. Report inconsistencies
511
+
512
+ **Step 2 \u2014 Completeness** (catches MISSING information):
513
+
514
+ - [ ] 1. For each public method: is it in \`interface.md\`?
515
+ - [ ] 2. For each error path: is it in \`interface.md\` (Failure Modes section)?
516
+ - [ ] 3. For each behavioral invariant: is it in the graph?
517
+ - [ ] 4. Report omissions separately from inconsistencies
518
+
519
+ ### Error Recovery
520
+
521
+ - **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
522
+ - **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
523
+ - **Budget warning (W005/W006)** \u2192 informational. \`yg validate\` shows a breakdown (own/hierarchy/aspects/flows/dependencies). Large inherited context means the system is complex \u2014 this is not a problem to fix, it is reality to acknowledge. Do not delete knowledge from artifacts. Do not attempt to "reduce" inherited context.
524
+ - **Own budget warning (W015)** \u2192 own artifacts are large. Consider splitting this node's responsibilities into child nodes. Redistribute knowledge across children so total knowledge is preserved or increased, never reduced.
525
+ - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
526
+ - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end. But NEVER run \`yg drift-sync\` to silence a failing drift check \u2014 drift is a signal that artifacts need updating. First update artifacts, then sync.
527
+
528
+ ### Escape Hatch
529
+
530
+ If the user explicitly requests a code-only change, comply but:
531
+
532
+ - Warn: "This creates drift. Run \`yg drift\` next session to reconcile."
533
+ - Do NOT run \`yg drift-sync\` \u2014 leave the drift visible.
534
+
535
+ ### Environment Check
536
+
537
+ Before preflight:
538
+
539
+ - Verify \`yg\` CLI is available. If not found, inform user and stop.
540
+ - If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see above).
541
+ - If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
542
+
543
+ <critical_protocol>
544
+ BEFORE reading, analyzing, or modifying ANY source file:
545
+ \`yg build-context --file <path>\`
546
+ One command. No exceptions. No "I'll do it later." No "this is just analysis."
547
+ </critical_protocol>`;
548
+ var AGENT_RULES_CONTENT = [PROTOCOL, REFERENCE, GUARD_RAILS].join("\n\n---\n\n") + "\n";
502
549
 
503
550
  // src/templates/platform.ts
504
551
  var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
@@ -1172,10 +1219,6 @@ function registerInitCommand(program2) {
1172
1219
  });
1173
1220
  }
1174
1221
 
1175
- // src/cli/build-context.ts
1176
- import { readFile as readFile14 } from "fs/promises";
1177
- import path12 from "path";
1178
-
1179
1222
  // src/core/graph-loader.ts
1180
1223
  import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
1181
1224
  import path9 from "path";
@@ -1298,12 +1341,14 @@ async function parseNodeYaml(filePath) {
1298
1341
  if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
1299
1342
  throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
1300
1343
  }
1344
+ const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1301
1345
  const relations = parseRelations(raw.relations, filePath);
1302
1346
  const mapping = parseMapping(raw.mapping, filePath);
1303
1347
  const aspects = parseAspects(raw.aspects, filePath);
1304
1348
  return {
1305
1349
  name: raw.name.trim(),
1306
1350
  type: raw.type.trim(),
1351
+ description,
1307
1352
  aspects,
1308
1353
  blackbox: raw.blackbox ?? false,
1309
1354
  relations: relations.length > 0 ? relations : void 0,
@@ -1509,6 +1554,7 @@ async function parseFlow(flowDir, flowYamlPath) {
1509
1554
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
1510
1555
  throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1511
1556
  }
1557
+ const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1512
1558
  const nodes = raw.nodes ?? raw.participants;
1513
1559
  if (!Array.isArray(nodes) || nodes.length === 0) {
1514
1560
  throw new Error(
@@ -1533,6 +1579,7 @@ async function parseFlow(flowDir, flowYamlPath) {
1533
1579
  return {
1534
1580
  path: path6.basename(flowDir),
1535
1581
  name: raw.name.trim(),
1582
+ description,
1536
1583
  nodes: nodePaths,
1537
1584
  ...aspects !== void 0 && { aspects },
1538
1585
  artifacts
@@ -1779,6 +1826,7 @@ function estimateTokens(text) {
1779
1826
  // src/core/context-builder.ts
1780
1827
  var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
1781
1828
  var EVENT_RELATION_TYPES = /* @__PURE__ */ new Set(["emits", "listens"]);
1829
+ var YG_YAML_FILES = /* @__PURE__ */ new Set(["yg-node.yaml", "yg-aspect.yaml", "yg-flow.yaml"]);
1782
1830
  async function buildContext(graph, nodePath) {
1783
1831
  const node = graph.nodes.get(nodePath);
1784
1832
  if (!node) {
@@ -2137,7 +2185,7 @@ function toContextMapOutput(pkg2, graph) {
2137
2185
  });
2138
2186
  const participatingFlows = collectParticipatingFlows(graph, node);
2139
2187
  const flowRefs = participatingFlows.map((f) => {
2140
- const ref = { path: f.path, name: f.name };
2188
+ const ref = { path: f.path };
2141
2189
  if (f.aspects?.length) ref.aspects = f.aspects;
2142
2190
  return ref;
2143
2191
  });
@@ -2145,7 +2193,7 @@ function toContextMapOutput(pkg2, graph) {
2145
2193
  const hierarchyRefs = ancestors.map((a) => {
2146
2194
  const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
2147
2195
  const expanded = expandAspects(nodeAspectIds, graph.aspects);
2148
- return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2196
+ return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: buildNodeFiles(a, config, `model/${a.path}`) };
2149
2197
  });
2150
2198
  const ancestorPaths = new Set(ancestors.map((a) => a.path));
2151
2199
  const depRefs = [];
@@ -2157,23 +2205,26 @@ function toContextMapOutput(pkg2, graph) {
2157
2205
  const depHierarchy = depAncestors.map((a) => {
2158
2206
  const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
2159
2207
  const expanded = expandAspects(ids, graph.aspects);
2160
- return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2208
+ const ancestorNode = graph.nodes.get(a.path);
2209
+ return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: ancestorNode ? buildDepNodeFiles(ancestorNode, config, `model/${a.path}`) : [] };
2161
2210
  });
2162
2211
  const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
2163
2212
  const ref = {
2164
2213
  path: target.path,
2165
2214
  name: target.meta.name,
2166
2215
  type: target.meta.type,
2216
+ description: target.meta.description,
2167
2217
  relation: relation.type,
2168
2218
  aspects: depEffectiveAspects,
2169
- hierarchy: depHierarchy
2219
+ hierarchy: depHierarchy,
2220
+ files: buildDepNodeFiles(target, config, `model/${target.path}`)
2170
2221
  };
2171
2222
  if (relation.consumes?.length) ref.consumes = relation.consumes;
2172
2223
  if (relation.failure) ref.failure = relation.failure;
2173
2224
  if (relation.event_name) ref["event-name"] = relation.event_name;
2174
2225
  depRefs.push(ref);
2175
2226
  }
2176
- const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2227
+ const glossary = buildGlossary(node, depRefs, graph);
2177
2228
  const breakdown = computeBudgetBreakdown(pkg2, graph);
2178
2229
  const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2179
2230
  const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
@@ -2185,56 +2236,29 @@ function toContextMapOutput(pkg2, graph) {
2185
2236
  path: pkg2.nodePath,
2186
2237
  name: pkg2.nodeName,
2187
2238
  type: node.meta.type,
2239
+ description: node.meta.description,
2188
2240
  mappings: normalizeMappingPaths(node.meta.mapping),
2189
2241
  aspects: nodeAspects,
2190
- flows: flowRefs
2242
+ flows: flowRefs,
2243
+ files: buildNodeFiles(node, config, `model/${pkg2.nodePath}`)
2191
2244
  },
2192
2245
  hierarchy: hierarchyRefs,
2193
2246
  dependencies: depRefs,
2194
- artifacts: registry
2247
+ glossary
2195
2248
  };
2196
2249
  }
2197
- function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2198
- const config = graph.config;
2199
- const configArtifactKeys = new Set(Object.keys(config.artifacts ?? {}));
2200
- const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2201
- const nodes = {};
2250
+ function buildNodeFiles(node, config, prefix) {
2251
+ const configKeys = Object.keys(config.artifacts ?? {});
2252
+ return configKeys.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
2253
+ }
2254
+ function buildDepNodeFiles(node, config, prefix) {
2255
+ const structural = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([f]) => f);
2256
+ const filenames = structural.length > 0 ? structural : Object.keys(config.artifacts ?? {});
2257
+ return filenames.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
2258
+ }
2259
+ function buildGlossary(node, dependencies, graph) {
2202
2260
  const aspects = {};
2203
2261
  const flows = {};
2204
- function addNodeEntry(n, includeYgNodeYaml, filter) {
2205
- if (nodes[n.path]) return;
2206
- const files = [];
2207
- if (includeYgNodeYaml) {
2208
- files.push(`model/${n.path}/yg-node.yaml`);
2209
- }
2210
- for (const filename of filter) {
2211
- if (n.artifacts.some((a) => a.filename === filename)) {
2212
- files.push(`model/${n.path}/${filename}`);
2213
- }
2214
- }
2215
- if (files.length > 0) {
2216
- nodes[n.path] = { files };
2217
- }
2218
- }
2219
- addNodeEntry(node, true, [...configArtifactKeys]);
2220
- for (const ancestor of ancestors) {
2221
- addNodeEntry(ancestor, true, [...configArtifactKeys]);
2222
- }
2223
- const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
2224
- for (const dep of dependencies) {
2225
- const target = graph.nodes.get(dep.path);
2226
- if (target) {
2227
- addNodeEntry(target, false, structuralFilenames);
2228
- }
2229
- for (const ancestor of dep.hierarchy) {
2230
- if (seenDepAncestors.has(ancestor.path)) continue;
2231
- seenDepAncestors.add(ancestor.path);
2232
- const ancestorNode = graph.nodes.get(ancestor.path);
2233
- if (ancestorNode) {
2234
- addNodeEntry(ancestorNode, false, structuralFilenames);
2235
- }
2236
- }
2237
- }
2238
2262
  const allAspectIds = collectEffectiveAspectIds(graph, node.path);
2239
2263
  for (const dep of dependencies) {
2240
2264
  for (const id of dep.aspects) {
@@ -2243,33 +2267,29 @@ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2243
2267
  }
2244
2268
  const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2245
2269
  for (const aspect of resolvedAspects) {
2246
- const files = [];
2247
- files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
2248
- for (const art of aspect.artifacts) {
2249
- files.push(`aspects/${aspect.id}/${art.filename}`);
2250
- }
2270
+ const files = aspect.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `aspects/${aspect.id}/${a.filename}`);
2251
2271
  const entry = {
2252
2272
  name: aspect.name,
2253
2273
  files
2254
2274
  };
2275
+ if (aspect.description) entry.description = aspect.description;
2276
+ if (aspect.stability) entry.stability = aspect.stability;
2255
2277
  if (aspect.implies?.length) entry.implies = aspect.implies;
2256
2278
  aspects[aspect.id] = entry;
2257
2279
  }
2258
2280
  const participatingFlows = collectParticipatingFlows(graph, node);
2259
2281
  for (const flow of participatingFlows) {
2260
- const files = [];
2261
- files.push(`flows/${flow.path}/yg-flow.yaml`);
2262
- for (const art of flow.artifacts) {
2263
- files.push(`flows/${flow.path}/${art.filename}`);
2264
- }
2282
+ const files = flow.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `flows/${flow.path}/${a.filename}`);
2265
2283
  const entry = {
2266
2284
  name: flow.name,
2285
+ participants: flow.nodes,
2267
2286
  files
2268
2287
  };
2288
+ if (flow.description) entry.description = flow.description;
2269
2289
  if (flow.aspects?.length) entry.aspects = flow.aspects;
2270
2290
  flows[flow.path] = entry;
2271
2291
  }
2272
- return { nodes, aspects, flows };
2292
+ return { aspects, flows };
2273
2293
  }
2274
2294
  function collectEffectiveAspectIds(graph, nodePath) {
2275
2295
  const node = graph.nodes.get(nodePath);
@@ -2290,23 +2310,39 @@ function collectEffectiveAspectIds(graph, nodePath) {
2290
2310
  }
2291
2311
 
2292
2312
  // src/formatters/context-text.ts
2293
- import { stringify } from "yaml";
2313
+ import { Document } from "yaml";
2294
2314
  function formatContextYaml(data) {
2295
- const output = {
2296
- meta: {
2297
- "token-count": data.meta.tokenCount,
2298
- "budget-status": data.meta.budgetStatus
2299
- },
2300
- project: data.project,
2301
- node: data.node,
2302
- hierarchy: data.hierarchy.length > 0 ? data.hierarchy : void 0,
2303
- dependencies: data.dependencies.length > 0 ? data.dependencies : void 0,
2304
- artifacts: data.artifacts
2315
+ const output = {};
2316
+ output.project = data.project;
2317
+ output.glossary = data.glossary;
2318
+ output.node = data.node;
2319
+ if (data.hierarchy.length > 0) output.hierarchy = data.hierarchy;
2320
+ if (data.dependencies.length > 0) output.dependencies = data.dependencies;
2321
+ output.meta = {
2322
+ "token-count": data.meta.tokenCount,
2323
+ "budget-status": data.meta.budgetStatus,
2324
+ breakdown: data.meta.breakdown
2305
2325
  };
2306
- for (const key of Object.keys(output)) {
2307
- if (output[key] === void 0) delete output[key];
2326
+ const doc = new Document(output, { aliasDuplicateObjects: false });
2327
+ const map = doc.contents;
2328
+ for (const pair of map.items) {
2329
+ const key = String(pair.key);
2330
+ switch (key) {
2331
+ case "glossary":
2332
+ pair.key.commentBefore = " Glossary: definitions of all aspects and flows referenced in this context.\n Read this first \u2014 IDs below (in node, hierarchy, dependencies) refer to entries here.";
2333
+ break;
2334
+ case "node":
2335
+ pair.key.commentBefore = " Target node: the component you are working on.";
2336
+ break;
2337
+ case "hierarchy":
2338
+ pair.key.commentBefore = " Hierarchy: ancestor modules from root to parent. Context is inherited top-down.";
2339
+ break;
2340
+ case "dependencies":
2341
+ pair.key.commentBefore = " Dependencies: components this node directly depends on.";
2342
+ break;
2343
+ }
2308
2344
  }
2309
- return stringify(output, { lineWidth: 0 });
2345
+ return doc.toString({ lineWidth: 0 });
2310
2346
  }
2311
2347
  function formatFullContent(files) {
2312
2348
  if (files.length === 0) return "";
@@ -2360,6 +2396,7 @@ async function validate(graph, scope = "all") {
2360
2396
  issues.push(...checkInvalidArtifactConditions(graph));
2361
2397
  issues.push(...await checkContextBudget(graph));
2362
2398
  issues.push(...checkHighFanOut(graph));
2399
+ issues.push(...checkMissingDescriptions(graph));
2363
2400
  }
2364
2401
  issues.push(...checkSchemas(graph));
2365
2402
  issues.push(...checkRelationTargets(graph));
@@ -2370,6 +2407,7 @@ async function validate(graph, scope = "all") {
2370
2407
  issues.push(...checkFlowAspectIds(graph));
2371
2408
  issues.push(...await checkDirectoriesHaveNodeYaml(graph));
2372
2409
  issues.push(...await checkShallowArtifacts(graph));
2410
+ issues.push(...await checkWideNodes(graph));
2373
2411
  issues.push(...checkUnpairedEvents(graph));
2374
2412
  let filtered = issues;
2375
2413
  let nodesScanned = graph.nodes.size;
@@ -2851,6 +2889,29 @@ async function checkShallowArtifacts(graph) {
2851
2889
  }
2852
2890
  return issues;
2853
2891
  }
2892
+ async function checkWideNodes(graph) {
2893
+ const issues = [];
2894
+ const maxFiles = graph.config.quality?.max_mapping_source_files ?? 10;
2895
+ const projectRoot = path11.dirname(graph.rootPath);
2896
+ for (const [nodePath, node] of graph.nodes) {
2897
+ if (node.meta.blackbox) continue;
2898
+ const mappingPaths = normalizeMappingPaths(node.meta.mapping);
2899
+ if (mappingPaths.length === 0) continue;
2900
+ const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
2901
+ if (sourceFiles.length <= maxFiles) continue;
2902
+ const filledArtifacts = node.artifacts.filter(
2903
+ (a) => a.content.trim().length >= (graph.config.quality?.min_artifact_length ?? 50)
2904
+ ).length;
2905
+ issues.push({
2906
+ severity: "warning",
2907
+ code: "W017",
2908
+ rule: "wide-node",
2909
+ message: `Node maps ${sourceFiles.length} source files (max: ${maxFiles}) with ${filledArtifacts} artifact(s). Consider splitting into child nodes with focused responsibilities.`,
2910
+ nodePath
2911
+ });
2912
+ }
2913
+ return issues;
2914
+ }
2854
2915
  function checkHighFanOut(graph) {
2855
2916
  const issues = [];
2856
2917
  const maxRel = graph.config.quality?.max_direct_relations ?? 10;
@@ -3092,6 +3153,108 @@ function pct(value, total) {
3092
3153
  if (total === 0) return "0%";
3093
3154
  return `${Math.round(value / total * 100)}%`;
3094
3155
  }
3156
+ function checkMissingDescriptions(graph) {
3157
+ const issues = [];
3158
+ for (const [nodePath, node] of graph.nodes) {
3159
+ if (!node.meta.description?.trim()) {
3160
+ issues.push({
3161
+ severity: "warning",
3162
+ code: "W016",
3163
+ rule: "missing-description",
3164
+ message: `Node has no description`,
3165
+ nodePath
3166
+ });
3167
+ }
3168
+ }
3169
+ for (const aspect of graph.aspects) {
3170
+ if (!aspect.description?.trim()) {
3171
+ issues.push({
3172
+ severity: "warning",
3173
+ code: "W016",
3174
+ rule: "missing-description",
3175
+ message: `Aspect '${aspect.id}' has no description`
3176
+ });
3177
+ }
3178
+ }
3179
+ for (const flow of graph.flows) {
3180
+ if (!flow.description?.trim()) {
3181
+ issues.push({
3182
+ severity: "warning",
3183
+ code: "W016",
3184
+ rule: "missing-description",
3185
+ message: `Flow '${flow.name}' has no description`
3186
+ });
3187
+ }
3188
+ }
3189
+ return issues;
3190
+ }
3191
+
3192
+ // src/cli/owner.ts
3193
+ import path12 from "path";
3194
+ import { access as access2 } from "fs/promises";
3195
+ function normalizeForMatch(inputPath) {
3196
+ return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
3197
+ }
3198
+ function findOwner(graph, projectRoot, rawPath) {
3199
+ const file = normalizeForMatch(normalizeProjectRelativePath(projectRoot, rawPath));
3200
+ let best = null;
3201
+ for (const [nodePath, node] of graph.nodes) {
3202
+ const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizeForMatch).filter((mappingPath) => mappingPath.length > 0);
3203
+ for (const mappingPath of mappingPaths) {
3204
+ if (file === mappingPath) {
3205
+ return { file, nodePath, mappingPath, direct: true };
3206
+ }
3207
+ if (file.startsWith(mappingPath + "/")) {
3208
+ if (!best || best && mappingPath.length > best.mappingPath.length) {
3209
+ best = { nodePath, mappingPath, exact: false };
3210
+ }
3211
+ }
3212
+ }
3213
+ }
3214
+ return best ? { file, nodePath: best.nodePath, mappingPath: best.mappingPath, direct: false } : { file, nodePath: null };
3215
+ }
3216
+ function registerOwnerCommand(program2) {
3217
+ program2.command("owner").description("Find which graph node owns a source file").requiredOption("--file <path>", "File path (relative to repository root)").action(async (options) => {
3218
+ try {
3219
+ const cwd = process.cwd();
3220
+ const graph = await loadGraph(cwd);
3221
+ const repoRoot = projectRootFromGraph(graph.rootPath);
3222
+ const rawPath = options.file.trim();
3223
+ const absolute = path12.resolve(cwd, rawPath);
3224
+ const repoRelative = path12.relative(repoRoot, absolute).split(path12.sep).join("/");
3225
+ const result = findOwner(graph, repoRoot, repoRelative);
3226
+ if (!result.nodePath) {
3227
+ const absPath = path12.resolve(repoRoot, result.file);
3228
+ let exists = true;
3229
+ try {
3230
+ await access2(absPath);
3231
+ } catch {
3232
+ exists = false;
3233
+ }
3234
+ if (exists) {
3235
+ process.stdout.write(`${result.file} -> no graph coverage
3236
+ `);
3237
+ } else {
3238
+ process.stdout.write(`${result.file} -> no graph coverage (file not found)
3239
+ `);
3240
+ }
3241
+ } else {
3242
+ process.stdout.write(`${result.file} -> ${result.nodePath}
3243
+ `);
3244
+ if (result.direct === false && result.mappingPath) {
3245
+ process.stdout.write(
3246
+ ` File has no direct mapping; context comes from ancestor directory ${result.mappingPath}. Use: yg build-context --node ${result.nodePath}
3247
+ `
3248
+ );
3249
+ }
3250
+ }
3251
+ } catch (error) {
3252
+ process.stderr.write(`Error: ${error.message}
3253
+ `);
3254
+ process.exit(1);
3255
+ }
3256
+ });
3257
+ }
3095
3258
 
3096
3259
  // src/cli/build-context.ts
3097
3260
  function collectRelevantNodePaths(graph, nodePath) {
@@ -3114,10 +3277,32 @@ function collectRelevantNodePaths(graph, nodePath) {
3114
3277
  return relevant;
3115
3278
  }
3116
3279
  function registerBuildCommand(program2) {
3117
- program2.command("build-context").description("Assemble a context package for one node").requiredOption("--node <node-path>", "Node path relative to .yggdrasil/model/").option("--full", "Include artifact file contents in output").action(async (options) => {
3280
+ program2.command("build-context").description("Assemble a context package for one node").option("--node <node-path>", "Node path relative to .yggdrasil/model/").option("--file <file-path>", "Source file path \u2014 resolves owner node automatically").option("--full", "Include artifact file contents in output").action(async (options) => {
3118
3281
  try {
3282
+ if (!options.node && !options.file) {
3283
+ process.stderr.write("Error: either '--node <path>' or '--file <path>' is required\n");
3284
+ process.exit(1);
3285
+ }
3286
+ if (options.node && options.file) {
3287
+ process.stderr.write("Error: '--node' and '--file' are mutually exclusive\n");
3288
+ process.exit(1);
3289
+ }
3119
3290
  const graph = await loadGraph(process.cwd());
3120
- const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/$/, "");
3291
+ let nodePath;
3292
+ if (options.file) {
3293
+ const repoRoot = projectRootFromGraph(graph.rootPath);
3294
+ const result = findOwner(graph, repoRoot, options.file.trim());
3295
+ if (!result.nodePath) {
3296
+ process.stderr.write(`${result.file} -> no graph coverage
3297
+ `);
3298
+ process.exit(1);
3299
+ }
3300
+ process.stderr.write(`${result.file} -> ${result.nodePath}
3301
+ `);
3302
+ nodePath = result.nodePath;
3303
+ } else {
3304
+ nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/$/, "");
3305
+ }
3121
3306
  const relevantNodes = collectRelevantNodePaths(graph, nodePath);
3122
3307
  const validationResult = await validate(graph, "all");
3123
3308
  const relevantErrors = validationResult.issues.filter(
@@ -3144,22 +3329,31 @@ function registerBuildCommand(program2) {
3144
3329
  const mapOutput = toContextMapOutput(pkg2, graph);
3145
3330
  let output = formatContextYaml(mapOutput);
3146
3331
  if (options.full) {
3147
- const allFiles = [];
3148
- const allEntries = [
3149
- ...Object.values(mapOutput.artifacts.nodes),
3150
- ...Object.values(mapOutput.artifacts.aspects),
3151
- ...Object.values(mapOutput.artifacts.flows)
3152
- ];
3153
3332
  const seen = /* @__PURE__ */ new Set();
3154
- for (const entry of allEntries) {
3155
- for (const filePath of entry.files) {
3156
- if (seen.has(filePath)) continue;
3157
- seen.add(filePath);
3158
- const content = await findFileContent(filePath, graph);
3159
- if (content !== void 0) {
3160
- allFiles.push({ path: filePath, content });
3161
- }
3333
+ const allFiles = [];
3334
+ async function collectFile(filePath) {
3335
+ if (seen.has(filePath)) return;
3336
+ seen.add(filePath);
3337
+ const content = await findFileContent(filePath, graph);
3338
+ if (content !== void 0) {
3339
+ allFiles.push({ path: filePath, content });
3340
+ }
3341
+ }
3342
+ for (const aspect of Object.values(mapOutput.glossary.aspects)) {
3343
+ for (const f of aspect.files) await collectFile(f);
3344
+ }
3345
+ for (const flow of Object.values(mapOutput.glossary.flows)) {
3346
+ for (const f of flow.files) await collectFile(f);
3347
+ }
3348
+ for (const f of mapOutput.node.files) await collectFile(f);
3349
+ for (const ancestor of mapOutput.hierarchy) {
3350
+ for (const f of ancestor.files ?? []) await collectFile(f);
3351
+ }
3352
+ for (const dep of mapOutput.dependencies) {
3353
+ for (const ancestor of dep.hierarchy) {
3354
+ for (const f of ancestor.files ?? []) await collectFile(f);
3162
3355
  }
3356
+ for (const f of dep.files ?? []) await collectFile(f);
3163
3357
  }
3164
3358
  output += formatFullContent(allFiles);
3165
3359
  }
@@ -3172,14 +3366,6 @@ function registerBuildCommand(program2) {
3172
3366
  });
3173
3367
  }
3174
3368
  async function findFileContent(filePath, graph) {
3175
- async function readYamlFromDisk(relativePath) {
3176
- try {
3177
- const fullPath = path12.join(graph.rootPath, relativePath);
3178
- return (await readFile14(fullPath, "utf-8")).trim();
3179
- } catch {
3180
- return void 0;
3181
- }
3182
- }
3183
3369
  if (filePath.startsWith("model/")) {
3184
3370
  const rest = filePath.slice("model/".length);
3185
3371
  const parts = rest.split("/");
@@ -3187,9 +3373,6 @@ async function findFileContent(filePath, graph) {
3187
3373
  const nodePath = parts.join("/");
3188
3374
  const node = graph.nodes.get(nodePath);
3189
3375
  if (!node) return void 0;
3190
- if (filename === "yg-node.yaml") {
3191
- return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
3192
- }
3193
3376
  const art = node.artifacts.find((a) => a.filename === filename);
3194
3377
  return art?.content;
3195
3378
  }
@@ -3200,9 +3383,6 @@ async function findFileContent(filePath, graph) {
3200
3383
  const filename = parts.slice(1).join("/");
3201
3384
  const aspect = graph.aspects.find((a) => a.id === aspectId);
3202
3385
  if (!aspect) return void 0;
3203
- if (filename === "yg-aspect.yaml") {
3204
- return readYamlFromDisk(filePath);
3205
- }
3206
3386
  const art = aspect.artifacts.find((a) => a.filename === filename);
3207
3387
  return art?.content;
3208
3388
  }
@@ -3213,9 +3393,6 @@ async function findFileContent(filePath, graph) {
3213
3393
  const filename = parts.slice(1).join("/");
3214
3394
  const flow = graph.flows.find((f) => f.path === flowPath);
3215
3395
  if (!flow) return void 0;
3216
- if (filename === "yg-flow.yaml") {
3217
- return readYamlFromDisk(filePath);
3218
- }
3219
3396
  const art = flow.artifacts.find((a) => a.filename === filename);
3220
3397
  return art?.content;
3221
3398
  }
@@ -3271,7 +3448,7 @@ ${errors.length} errors, ${warnings.length} warnings.
3271
3448
  import chalk2 from "chalk";
3272
3449
 
3273
3450
  // src/io/drift-state-store.ts
3274
- import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3451
+ import { readFile as readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3275
3452
  import path13 from "path";
3276
3453
  import { parse as yamlParse } from "yaml";
3277
3454
  var DRIFT_STATE_DIR = ".drift-state";
@@ -3318,7 +3495,7 @@ async function removeEmptyParents(filePath, stopDir) {
3318
3495
  async function readNodeDriftState(yggRoot, nodePath) {
3319
3496
  try {
3320
3497
  const filePath = nodeStatePath(yggRoot, nodePath);
3321
- const content = await readFile15(filePath, "utf-8");
3498
+ const content = await readFile14(filePath, "utf-8");
3322
3499
  const parsed = JSON.parse(content);
3323
3500
  return parsed;
3324
3501
  } catch {
@@ -3354,7 +3531,7 @@ async function readDriftState(yggRoot) {
3354
3531
  return {};
3355
3532
  }
3356
3533
  if (driftStat.isFile()) {
3357
- const content = await readFile15(driftPath, "utf-8");
3534
+ const content = await readFile14(driftPath, "utf-8");
3358
3535
  let raw;
3359
3536
  try {
3360
3537
  raw = JSON.parse(content);
@@ -3386,20 +3563,20 @@ async function readDriftState(yggRoot) {
3386
3563
  }
3387
3564
 
3388
3565
  // src/utils/hash.ts
3389
- import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
3566
+ import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3390
3567
  import path14 from "path";
3391
3568
  import { createHash } from "crypto";
3392
3569
  import { createRequire } from "module";
3393
3570
  var require2 = createRequire(import.meta.url);
3394
3571
  var ignoreFactory = require2("ignore");
3395
3572
  async function hashFile(filePath) {
3396
- const content = await readFile16(filePath);
3573
+ const content = await readFile15(filePath);
3397
3574
  return createHash("sha256").update(content).digest("hex");
3398
3575
  }
3399
3576
  async function loadRootGitignoreStack(projectRoot) {
3400
3577
  if (!projectRoot) return [];
3401
3578
  try {
3402
- const content = await readFile16(path14.join(projectRoot, ".gitignore"), "utf-8");
3579
+ const content = await readFile15(path14.join(projectRoot, ".gitignore"), "utf-8");
3403
3580
  const matcher = ignoreFactory();
3404
3581
  matcher.add(content);
3405
3582
  return [{ basePath: projectRoot, matcher }];
@@ -3474,7 +3651,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3474
3651
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3475
3652
  let stack = options.gitignoreStack ?? [];
3476
3653
  try {
3477
- const localContent = await readFile16(path14.join(directoryPath, ".gitignore"), "utf-8");
3654
+ const localContent = await readFile15(path14.join(directoryPath, ".gitignore"), "utf-8");
3478
3655
  const localMatcher = ignoreFactory();
3479
3656
  localMatcher.add(localContent);
3480
3657
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -3629,7 +3806,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
3629
3806
  }
3630
3807
 
3631
3808
  // src/core/drift-detector.ts
3632
- import { access as access2 } from "fs/promises";
3809
+ import { access as access3 } from "fs/promises";
3633
3810
  import path16 from "path";
3634
3811
  function getChildMappingExclusions(graph, nodePath) {
3635
3812
  const node = graph.nodes.get(nodePath);
@@ -3743,7 +3920,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3743
3920
  for (const mp of mappingPaths) {
3744
3921
  const absPath = path16.join(projectRoot, mp);
3745
3922
  try {
3746
- await access2(absPath);
3923
+ await access3(absPath);
3747
3924
  return false;
3748
3925
  } catch {
3749
3926
  }
@@ -3761,12 +3938,39 @@ async function syncDriftState(graph, nodePath) {
3761
3938
  const storedFileData = existingEntry?.files ? { hashes: existingEntry.files, mtimes: existingEntry.mtimes ?? {} } : void 0;
3762
3939
  const { canonicalHash, fileHashes, fileMtimes } = await hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes);
3763
3940
  const previousHash = existingEntry?.hash;
3941
+ let sourceOnlyChange = false;
3942
+ if (previousHash && previousHash !== canonicalHash && existingEntry?.files) {
3943
+ let hasSourceChange = false;
3944
+ let hasGraphChange = false;
3945
+ const yggPrefix = path16.relative(projectRoot, graph.rootPath).split(path16.sep).join("/");
3946
+ for (const [filePath, hash] of Object.entries(fileHashes)) {
3947
+ const storedHash = existingEntry.files[filePath];
3948
+ if (storedHash && storedHash === hash) continue;
3949
+ if (!storedHash || storedHash !== hash) {
3950
+ if (filePath.startsWith(yggPrefix)) {
3951
+ hasGraphChange = true;
3952
+ } else {
3953
+ hasSourceChange = true;
3954
+ }
3955
+ }
3956
+ }
3957
+ for (const storedPath of Object.keys(existingEntry.files)) {
3958
+ if (!(storedPath in fileHashes)) {
3959
+ if (storedPath.startsWith(yggPrefix)) {
3960
+ hasGraphChange = true;
3961
+ } else {
3962
+ hasSourceChange = true;
3963
+ }
3964
+ }
3965
+ }
3966
+ sourceOnlyChange = hasSourceChange && !hasGraphChange;
3967
+ }
3764
3968
  await writeNodeDriftState(graph.rootPath, nodePath, {
3765
3969
  hash: canonicalHash,
3766
3970
  files: fileHashes,
3767
3971
  mtimes: fileMtimes
3768
3972
  });
3769
- return { previousHash, currentHash: canonicalHash };
3973
+ return { previousHash, currentHash: canonicalHash, sourceOnlyChange };
3770
3974
  }
3771
3975
 
3772
3976
  // src/cli/drift.ts
@@ -3939,13 +4143,20 @@ function registerDriftSyncCommand(program2) {
3939
4143
  }
3940
4144
  continue;
3941
4145
  }
3942
- const { previousHash, currentHash } = await syncDriftState(graph, np);
4146
+ const { previousHash, currentHash, sourceOnlyChange } = await syncDriftState(graph, np);
3943
4147
  process.stdout.write(chalk3.green(`Synchronized: ${np}
3944
4148
  `));
3945
4149
  process.stdout.write(
3946
4150
  ` Hash: ${previousHash ? previousHash.slice(0, 8) : "none"} -> ${currentHash.slice(0, 8)}
3947
4151
  `
3948
4152
  );
4153
+ if (sourceOnlyChange) {
4154
+ process.stderr.write(
4155
+ chalk3.yellow(` \u26A0 W018: Source files changed but graph artifacts are unchanged for '${np}'.
4156
+ `) + chalk3.yellow(` Update artifacts BEFORE syncing \u2014 drift-sync without artifact update hides staleness.
4157
+ `)
4158
+ );
4159
+ }
3949
4160
  }
3950
4161
  if (options.all) {
3951
4162
  const validPaths = new Set(nodesToSync);
@@ -4071,10 +4282,10 @@ function registerTreeCommand(program2) {
4071
4282
  let roots;
4072
4283
  let showProjectName;
4073
4284
  if (options.root?.trim()) {
4074
- const path20 = options.root.trim().replace(/\/$/, "");
4075
- const node = graph.nodes.get(path20);
4285
+ const path19 = options.root.trim().replace(/\/$/, "");
4286
+ const node = graph.nodes.get(path19);
4076
4287
  if (!node) {
4077
- process.stderr.write(`Error: path '${path20}' not found
4288
+ process.stderr.write(`Error: path '${path19}' not found
4078
4289
  `);
4079
4290
  process.exit(1);
4080
4291
  }
@@ -4117,76 +4328,9 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
4117
4328
  }
4118
4329
  }
4119
4330
 
4120
- // src/cli/owner.ts
4121
- import path17 from "path";
4122
- import { access as access3 } from "fs/promises";
4123
- function normalizeForMatch(inputPath) {
4124
- return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
4125
- }
4126
- function findOwner(graph, projectRoot, rawPath) {
4127
- const file = normalizeForMatch(normalizeProjectRelativePath(projectRoot, rawPath));
4128
- let best = null;
4129
- for (const [nodePath, node] of graph.nodes) {
4130
- const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizeForMatch).filter((mappingPath) => mappingPath.length > 0);
4131
- for (const mappingPath of mappingPaths) {
4132
- if (file === mappingPath) {
4133
- return { file, nodePath, mappingPath, direct: true };
4134
- }
4135
- if (file.startsWith(mappingPath + "/")) {
4136
- if (!best || best && mappingPath.length > best.mappingPath.length) {
4137
- best = { nodePath, mappingPath, exact: false };
4138
- }
4139
- }
4140
- }
4141
- }
4142
- return best ? { file, nodePath: best.nodePath, mappingPath: best.mappingPath, direct: false } : { file, nodePath: null };
4143
- }
4144
- function registerOwnerCommand(program2) {
4145
- program2.command("owner").description("Find which graph node owns a source file").requiredOption("--file <path>", "File path (relative to repository root)").action(async (options) => {
4146
- try {
4147
- const cwd = process.cwd();
4148
- const graph = await loadGraph(cwd);
4149
- const repoRoot = projectRootFromGraph(graph.rootPath);
4150
- const rawPath = options.file.trim();
4151
- const absolute = path17.resolve(cwd, rawPath);
4152
- const repoRelative = path17.relative(repoRoot, absolute).split(path17.sep).join("/");
4153
- const result = findOwner(graph, repoRoot, repoRelative);
4154
- if (!result.nodePath) {
4155
- const absPath = path17.resolve(repoRoot, result.file);
4156
- let exists = true;
4157
- try {
4158
- await access3(absPath);
4159
- } catch {
4160
- exists = false;
4161
- }
4162
- if (exists) {
4163
- process.stdout.write(`${result.file} -> no graph coverage
4164
- `);
4165
- } else {
4166
- process.stdout.write(`${result.file} -> no graph coverage (file not found)
4167
- `);
4168
- }
4169
- } else {
4170
- process.stdout.write(`${result.file} -> ${result.nodePath}
4171
- `);
4172
- if (result.direct === false && result.mappingPath) {
4173
- process.stdout.write(
4174
- ` File has no direct mapping; context comes from ancestor directory ${result.mappingPath}. Use: yg build-context --node ${result.nodePath}
4175
- `
4176
- );
4177
- }
4178
- }
4179
- } catch (error) {
4180
- process.stderr.write(`Error: ${error.message}
4181
- `);
4182
- process.exit(1);
4183
- }
4184
- });
4185
- }
4186
-
4187
4331
  // src/core/dependency-resolver.ts
4188
4332
  import { execSync } from "child_process";
4189
- import path18 from "path";
4333
+ import path17 from "path";
4190
4334
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
4191
4335
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
4192
4336
  function filterRelationType(relType, filter) {
@@ -4263,7 +4407,7 @@ function registerDepsCommand(program2) {
4263
4407
  // src/core/graph-from-git.ts
4264
4408
  import { mkdtemp, rm as rm3 } from "fs/promises";
4265
4409
  import { tmpdir } from "os";
4266
- import path19 from "path";
4410
+ import path18 from "path";
4267
4411
  import { execSync as execSync2 } from "child_process";
4268
4412
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
4269
4413
  const yggPath = ".yggdrasil";
@@ -4274,8 +4418,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
4274
4418
  return null;
4275
4419
  }
4276
4420
  try {
4277
- tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
4278
- const archivePath = path19.join(tmpDir, "archive.tar");
4421
+ tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
4422
+ const archivePath = path18.join(tmpDir, "archive.tar");
4279
4423
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
4280
4424
  cwd: projectRoot,
4281
4425
  stdio: "pipe"
@@ -4345,14 +4489,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
4345
4489
  }
4346
4490
  const chains = [];
4347
4491
  for (const node of transitiveOnly) {
4348
- const path20 = [];
4492
+ const path19 = [];
4349
4493
  let current = node;
4350
4494
  while (current) {
4351
- path20.unshift(current);
4495
+ path19.unshift(current);
4352
4496
  current = parent.get(current);
4353
4497
  }
4354
- if (path20.length >= 3) {
4355
- chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
4498
+ if (path19.length >= 3) {
4499
+ chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
4356
4500
  }
4357
4501
  }
4358
4502
  return chains.sort();
@@ -4396,14 +4540,14 @@ function collectIndirectDependents(graph, directlyAffected) {
4396
4540
  }
4397
4541
  for (const [node] of parent) {
4398
4542
  if (directSet.has(node)) continue;
4399
- const path20 = [node];
4543
+ const path19 = [node];
4400
4544
  let current = node;
4401
4545
  while (parent.has(current)) {
4402
4546
  current = parent.get(current);
4403
- path20.push(current);
4547
+ path19.push(current);
4404
4548
  }
4405
- const chain = path20.map((p) => `<- ${p}`).join(" ");
4406
- const depth = path20.length;
4549
+ const chain = path19.map((p) => `<- ${p}`).join(" ");
4550
+ const depth = path19.length;
4407
4551
  const existing = bestChain.get(node);
4408
4552
  if (!existing || depth < existing.depth) {
4409
4553
  bestChain.set(node, { chain, depth });
@@ -4600,23 +4744,39 @@ Total scope: ${sorted.length + indirectPaths.length} nodes
4600
4744
  }
4601
4745
  }
4602
4746
  function registerImpactCommand(program2) {
4603
- program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--method <name>", "Filter impact to dependents consuming a specific method (requires --node)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
4747
+ program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--file <file-path>", "Source file path \u2014 resolves owner node automatically").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--method <name>", "Filter impact to dependents consuming a specific method (requires --node or --file)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
4604
4748
  async (options) => {
4605
4749
  try {
4606
- const modeCount = [options.node, options.aspect, options.flow].filter(Boolean).length;
4750
+ if (options.node && options.file) {
4751
+ process.stderr.write("Error: '--node' and '--file' are mutually exclusive\n");
4752
+ process.exit(1);
4753
+ }
4754
+ const modeCount = [options.node || options.file, options.aspect, options.flow].filter(Boolean).length;
4607
4755
  if (modeCount === 0) {
4608
4756
  process.stderr.write(
4609
- "Error: one of --node, --aspect, or --flow is required\n"
4757
+ "Error: one of --node, --file, --aspect, or --flow is required\n"
4610
4758
  );
4611
4759
  process.exit(1);
4612
4760
  }
4613
4761
  if (modeCount > 1) {
4614
4762
  process.stderr.write(
4615
- "Error: --node, --aspect, and --flow are mutually exclusive\n"
4763
+ "Error: --node/--file, --aspect, and --flow are mutually exclusive\n"
4616
4764
  );
4617
4765
  process.exit(1);
4618
4766
  }
4619
4767
  const graph = await loadGraph(process.cwd());
4768
+ if (options.file) {
4769
+ const repoRoot = projectRootFromGraph(graph.rootPath);
4770
+ const result = findOwner(graph, repoRoot, options.file.trim());
4771
+ if (!result.nodePath) {
4772
+ process.stderr.write(`${result.file} -> no graph coverage
4773
+ `);
4774
+ process.exit(1);
4775
+ }
4776
+ process.stderr.write(`${result.file} -> ${result.nodePath}
4777
+ `);
4778
+ options.node = result.nodePath;
4779
+ }
4620
4780
  if (options.aspect) {
4621
4781
  await handleAspectImpact(graph, options.aspect.trim(), options.simulate);
4622
4782
  return;
@@ -4845,6 +5005,7 @@ function registerFlowsCommand(program2) {
4845
5005
  participants: flow.nodes.length,
4846
5006
  nodes: flow.nodes.sort()
4847
5007
  };
5008
+ if (flow.description) entry.description = flow.description;
4848
5009
  if (flow.aspects && flow.aspects.length > 0) entry.aspects = flow.aspects;
4849
5010
  return entry;
4850
5011
  });