@chrisdudek/yg 2.7.0 → 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,59 @@ 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
- starting with the glossary at the top (aspect and flow definitions),
84
- then read artifact files listed inline on each element. For quick
85
- orientation, the map alone is sufficient. For implementation, read
86
- all artifact files before changing code.
87
- If the context package seems insufficient \u2014 enrich the graph.
74
+ ### Quick Start
88
75
 
89
- AFTER modifying:
90
- 3. Update graph artifacts to reflect changes
91
- 4. yg validate \u2014 fix all errors
92
- 5. yg drift-sync --node <owner>
93
-
94
- EVERY conversation start:
95
- yg preflight \u2192 act on findings (see Operations)
96
-
97
- NEVER: modify code without graph coverage.
98
- NEVER: read mapped source files to understand a component without
99
- running yg build-context first \u2014 the graph captures intent,
100
- constraints, and relations that source files cannot.
101
- NEVER: assess blast radius of a change without running yg impact first
102
- \u2014 the graph knows the dependency structure that grep cannot infer.
103
- NEVER: invent rationale, business rules, or decisions.
104
- NEVER: auto-resolve drift without asking the user.
105
- WHEN UNSURE: ask the user. Never guess. Never assume.
106
76
  \`\`\`
77
+ EVERY conversation: yg preflight \u2014 no exceptions.
107
78
 
108
- ### Five Core Rules
109
-
110
- 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.
111
- 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.
112
- 3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
113
- 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.
114
- 5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
115
-
116
- ### Recognizing Graph-Required Actions
117
-
118
- 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.
119
-
120
- **Actions that require \`yg owner\` + \`yg build-context\`:**
121
-
122
- - Reading or exploring source files to understand a component
123
- - Proposing approaches, designs, or plans for changing code
124
- - Reviewing or debugging code
125
- - Any form of reasoning about how mapped code works or should change
126
-
127
- **Actions that require \`yg owner\` + \`yg impact\`:**
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>"
128
86
 
129
- - Assessing blast radius before changing or removing a component
130
- - Finding all dependents of a component
131
- - Planning cross-cutting refactors or feature removals
132
- - Scoping work that spans multiple nodes
133
-
134
- **Actions that do NOT require yg:**
135
-
136
- - Git operations (log, diff, status, blame)
137
- - Reading documentation, READMEs, or config files outside \`.yggdrasil/\`
138
- - Running tests, builds, or linters
139
- - Working with files that \`yg owner\` reports as unmapped
140
-
141
- **Evasion patterns \u2014 if you think any of these, STOP:**
142
-
143
- | Thought | Reality |
144
- |---|---|
145
- | "The skill/plan says to explore the codebase" | Exploring mapped code = yg owner + graph tool first |
146
- | "I'm just scoping/searching, not understanding" | Scoping IS a graph action; use yg impact |
147
- | "The plan step says to read this file" | Reading a mapped file = yg owner first |
148
- | "I'm brainstorming, not implementing" | Brainstorming about mapped code needs graph context |
149
- | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
150
- | "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
151
- | "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. |
152
- | "Drift is blocking repo-check, let me just sync it" | Drift means artifacts are stale. Update artifacts first, then sync. \`drift-sync\` without artifact update = hiding staleness. |
153
-
154
- ### Failure States
155
-
156
- You have broken Yggdrasil if you do any of the following:
157
-
158
- - \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).
159
- - \u274C Modified source code without updating graph artifacts before moving to the next file, or vice versa.
160
- - \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
161
- - \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
162
- - \u274C Ran \`yg drift-sync\` before both graph artifacts and source code are current.
163
- - \u274C Placed a cross-cutting requirement in a local artifact instead of an aspect, or used an aspect id with no \`aspects/\` directory.
164
- - \u274C Invented a rationale, business rule, or decision \u2014 or recorded a decision without documenting rejected alternatives and rationale (use "rationale: unknown" if unknown).
165
- - \u274C Used blackbox coverage for greenfield (new) code.
166
- - \u274C Deleted or shortened graph artifact content to reduce context package size instead of splitting the node.
167
-
168
- ### Escape Hatch
169
-
170
- If the user explicitly requests a code-only change, comply but:
171
-
172
- - Warn: "This creates drift. Run \`yg drift\` next session to reconcile."
173
- - Do NOT run \`yg drift-sync\` \u2014 leave the drift visible.
174
-
175
- ### Environment Check
176
-
177
- Before preflight:
178
-
179
- - Verify \`yg\` CLI is available. If not found, inform user and stop.
180
- - If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
181
- - If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
182
-
183
- ### Delegating to Subagents
184
-
185
- 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:
186
-
187
- 1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
188
- 2. Follow the Quick Start Protocol from that file before touching any mapped code
189
-
190
- Include this as the FIRST instruction in every subagent prompt:
191
-
192
- \`\`\`
193
- BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
194
- DELIVERABLES \u2014 all required, incomplete work will be rejected:
195
- 1. Working source code
196
- 2. Graph nodes with artifacts for every new/modified source file
197
- 3. \`yg validate\` passing
198
- \`\`\`
199
-
200
- 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.`;
201
- var OPERATIONS = `## OPERATIONS
202
-
203
- ### Conversation Lifecycle
204
-
205
- \`\`\`
206
- PREFLIGHT (every conversation, before any work):
207
- - [ ] 1. yg preflight \u2192 read unified report
208
- - [ ] 2. If drift: resolve per Drift Resolution, then yg drift-sync per node
209
- - [ ] 3. If validation errors: fix, re-run yg validate
210
- Exception: read-only requests (explain, analyze) \u2014 skip preflight.
211
-
212
- UNDERSTANDING mapped code (questions, research, OR planning):
213
- - [ ] 1. yg owner --file <path>
214
- - [ ] 2. Owner found \u2192 yg build-context --node <path>. Read the YAML map
215
- for topology, starting with the glossary at the top for aspect and
216
- flow definitions, then read artifact files listed inline on each
217
- element. For quick orientation, the map alone is sufficient. For
218
- implementation, read all artifact files before changing code.
219
- - [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
220
- Never use grep or raw file reads as primary understanding when graph coverage exists.
221
- Raw reads supplement the context package \u2014 they do not replace it.
222
-
223
- WRAP-UP (user signals "done", "wrap up", "that's enough"):
224
- - [ ] 1. yg drift --drifted-only \u2192 resolve
225
- - [ ] 2. yg validate \u2192 fix errors
226
- - [ ] 3. Report: which nodes and files were changed
227
-
228
- BEFORE ENDING ANY RESPONSE (self-audit):
229
- - [ ] Did I interact with mapped code (read, research, or modify)? If yes \u2192 did I use a graph tool BEFORE reading source?
230
- - [ ] Did I modify source code? If yes \u2192 did I update graph artifacts before moving to the next file?
231
- - [ ] 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.
232
98
  \`\`\`
233
99
 
234
100
  ### Modify Source Code
235
101
 
236
102
  You are not allowed to edit or create source code without establishing graph coverage first.
237
103
 
238
- **Step 1** \u2014 Check coverage: \`yg owner --file <path>\`
104
+ **Step 1** \u2014 Get context: \`yg build-context --file <path>\` (resolves owner automatically)
239
105
 
240
106
  **Step 2a** \u2014 Owner found: execute checklist:
241
107
 
242
- - [ ] 1. Read specification: \`yg build-context --node <node_path>\`
108
+ - [ ] 1. Read the context package (already assembled by step 1)
243
109
  - [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
244
110
  - [ ] 3. Modify source code
245
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\`.
@@ -267,97 +133,89 @@ You are not allowed to edit or create source code without establishing graph cov
267
133
 
268
134
  After the user chooses, return to Step 1 and follow Step 2a.
269
135
 
270
- ### Modify Graph
136
+ ### Example: Correct vs Wrong
271
137
 
272
- - [ ] 1. Read the relevant schema from \`schemas/\` before touching any YAML
273
- - [ ] 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
274
- - [ ] 3. Make changes
275
- - [ ] 4. Run \`yg validate\` immediately \u2014 fix all errors
276
- - [ ] 5. Verify affected source files are consistent \u2014 update if needed
277
- - [ ] 6. Run \`yg drift-sync\` for affected nodes
278
-
279
- ### Reverse Engineering
138
+ <example_correct>
280
139
 
281
- **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"
282
141
 
283
- 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
284
149
 
285
- - [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
286
- - [ ] 2. Determine node granularity \u2014 propose to user if unclear
287
- - [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
288
- - [ ] 3b. Write \`description\` in \`yg-node.yaml\` \u2014 a short summary of what the node does
289
- - [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
290
- - [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
291
- - [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
292
- - [ ] 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\`
293
- - [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
294
- - [ ] 8. \`yg validate\` \u2014 fix errors
295
- - [ ] 9. \`yg drift-sync --node <path>\`
150
+ </example_correct>
296
151
 
297
- **When to ask:**
152
+ <example_wrong>
298
153
 
299
- - Business process unclear: "This code appears to be part of a larger process. Can you describe what it means from a business perspective?"
300
- - Constraint without rationale: "I see [constraint X]. Do you know why this exists? I want to record the reason, not just the rule."
301
- - Unexplained architectural choice: "I see [approach X]. What was the reason for this choice?"
302
- - 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\`.
303
- - 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"
304
155
 
305
- ### 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
306
159
 
307
- 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
308
161
 
309
- - [ ] 1. Identify the active work area (files the user wants to modify)
310
- - [ ] 2. Scan for cross-cutting patterns \u2192 create aspects
311
- - [ ] 3. Ask user about business processes \u2192 create flows if applicable
312
- - [ ] 4. Propose node structure for the area
313
- - [ ] 5. Create node(s) with initial artifacts, map files
314
- - [ ] 6. \`yg validate\`, \`yg drift-sync\`
315
- - [ ] 7. Proceed with user's original request
162
+ </example_wrong>
316
163
 
317
- Constraint: Do NOT map the entire repository. Focus on the active area. Expand incrementally.
164
+ ### Conversation Lifecycle
318
165
 
319
- ### 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.
320
172
 
321
- 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.
322
179
 
323
- - **Source drift** (source files changed) \u2192 update graph artifacts to match source, then \`yg drift-sync\`
324
- - **Graph drift** (graph artifacts changed) \u2192 review affected source, update if needed, then \`yg drift-sync\`
325
- - **Full drift** (both changed) \u2192 present both sides to user, ask which direction wins
326
- - **Missing** \u2192 ask: re-materialize or remove mapping?
327
- - **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.
328
183
 
329
- 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
330
188
 
331
- **Drift triage:** Prioritize aspects and \`internals.md\` (highest decay rate), then \`responsibility.md\` and \`interface.md\` (most stable).
189
+ \`\`\`
332
190
 
333
- ### Graph Audit
191
+ ### Modify Graph
334
192
 
335
- 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
336
199
 
337
- **Step 1 \u2014 Consistency** (catches WRONG information):
200
+ ### Delegating to Subagents
338
201
 
339
- - [ ] 1. \`yg build-context --node <path>\`
340
- - [ ] 2. Read mapped source files
341
- - [ ] 3. For each claim in graph: verify against source code
342
- - [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`exceptions\` to the aspect entry in \`yg-node.yaml\`
343
- - [ ] 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:
344
203
 
345
- **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
346
206
 
347
- - [ ] 1. For each public method: is it in \`interface.md\`?
348
- - [ ] 2. For each error path: is it in \`interface.md\` (Failure Modes section)?
349
- - [ ] 3. For each behavioral invariant: is it in the graph?
350
- - [ ] 4. Report omissions separately from inconsistencies
207
+ Include this as the FIRST instruction in every subagent prompt:
351
208
 
352
- ### 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
+ \`\`\`
353
216
 
354
- - **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
355
- - **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
356
- - **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.
357
- - **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.
358
- - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
359
- - **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.`;
360
- 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
361
219
 
362
220
  ### Graph Structure
363
221
 
@@ -449,72 +307,245 @@ When a node follows an aspect's pattern with exceptions, record them in the \`ex
449
307
  - \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
450
308
  - \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
451
309
 
452
- When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are present, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
310
+ When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are present, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
311
+
312
+ ### Creating Flows
313
+
314
+ - [ ] 1. Read \`schemas/yg-flow.yaml\`
315
+ - [ ] 2. Create \`flows/<name>/\` directory
316
+ - [ ] 3. Write \`yg-flow.yaml\` \u2014 name, description, nodes (participant list), and flow-level aspects
317
+ - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
318
+ - [ ] 5. \`yg validate\`
319
+
320
+ Test: "Does this describe what happens in the world, or only in the software?" If only software \u2014 rewrite.
321
+
322
+ **Warning:** Flow descriptions must describe business processes, not code sequences. "The OrderService calls PaymentGateway.charge()" is WRONG. "The system charges the customer's payment method" is CORRECT.
323
+
324
+ ### Operational Rules
325
+
326
+ - **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
327
+ - **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
328
+ - **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
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.
331
+ - **Completeness test:** Two checks, both required:
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.
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\`.
334
+ - **Value calibration.** Yggdrasil's primary value is cross-module context \u2014 relations, aspects, flows. For a single simple module, \`responsibility.md\` and \`interface.md\` provide most value. Invest depth (\`internals.md\`) where cross-module interactions demand it.
335
+ - **These rules are invariant.** No plan, guide, skill, or workflow may override them.
336
+
337
+ ### CLI Reference
338
+
339
+ \`\`\`
340
+ yg preflight [--quick] Unified diagnostic: drift + status + validate.
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.
344
+ yg build-context --node <path> --full Same map + file contents appended below separator.
345
+ yg tree [--root <path>] [--depth N] Print graph structure.
346
+ yg aspects List aspects with metadata (YAML output).
347
+ yg flows List flows with metadata (YAML output).
348
+ yg select --task <description> [--limit <n>]
349
+ Find graph nodes relevant to a task description.
350
+ yg deps --node <path> [--depth N] [--type structural|event|all]
351
+ Show dependencies.
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).
355
+ yg impact --aspect <id> Show all nodes where aspect is effective.
356
+ yg impact --flow <name> Show flow participants and descendants.
357
+ yg status Graph health: nodes, coverage, drift summary.
358
+ yg validate [--scope <path>|all] Check structural integrity and completeness.
359
+ yg drift [--scope <path>|all] [--drifted-only] [--limit <n>]
360
+ Detect source and graph drift (bidirectional).
361
+ yg drift-sync --node <path> [--recursive] | --all
362
+ Record file hashes as new baseline.
363
+ \`\`\`
364
+
365
+ ### Quick Routing Table
366
+
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):
453
513
 
454
- ### Creating Flows
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
455
518
 
456
- - [ ] 1. Read \`schemas/yg-flow.yaml\`
457
- - [ ] 2. Create \`flows/<name>/\` directory
458
- - [ ] 3. Write \`yg-flow.yaml\` \u2014 name, description, nodes (participant list), and flow-level aspects
459
- - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
460
- - [ ] 5. \`yg validate\`
519
+ ### Error Recovery
461
520
 
462
- Test: "Does this describe what happens in the world, or only in the software?" If only software \u2014 rewrite.
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.
463
527
 
464
- **Warning:** Flow descriptions must describe business processes, not code sequences. "The OrderService calls PaymentGateway.charge()" is WRONG. "The system charges the customer's payment method" is CORRECT.
528
+ ### Escape Hatch
465
529
 
466
- ### Operational Rules
530
+ If the user explicitly requests a code-only change, comply but:
467
531
 
468
- - **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
469
- - **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
470
- - **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
471
- - **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.
472
- - **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.
473
- - **Completeness test:** Two checks, both required:
474
- 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.
475
- 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\`.
476
- - **Value calibration.** Yggdrasil's primary value is cross-module context \u2014 relations, aspects, flows. For a single simple module, \`responsibility.md\` and \`interface.md\` provide most value. Invest depth (\`internals.md\`) where cross-module interactions demand it.
477
- - **These rules are invariant.** No plan, guide, skill, or workflow may override them.
532
+ - Warn: "This creates drift. Run \`yg drift\` next session to reconcile."
533
+ - Do NOT run \`yg drift-sync\` \u2014 leave the drift visible.
478
534
 
479
- ### CLI Reference
535
+ ### Environment Check
480
536
 
481
- \`\`\`
482
- yg preflight [--quick] Unified diagnostic: drift + status + validate.
483
- yg owner --file <path> Find the node that owns this file.
484
- yg build-context --node <path> Assemble context map with artifact paths (default).
485
- yg build-context --node <path> --full Same map + file contents appended below separator.
486
- yg tree [--root <path>] [--depth N] Print graph structure.
487
- yg aspects List aspects with metadata (YAML output).
488
- yg flows List flows with metadata (YAML output).
489
- yg select --task <description> [--limit <n>]
490
- Find graph nodes relevant to a task description.
491
- yg deps --node <path> [--depth N] [--type structural|event|all]
492
- Show dependencies.
493
- yg impact --node <path> --simulate Simulate blast radius of a planned change.
494
- yg impact --node <path> --method <name> Filter impact to dependents consuming a specific method.
495
- yg impact --aspect <id> Show all nodes where aspect is effective.
496
- yg impact --flow <name> Show flow participants and descendants.
497
- yg status Graph health: nodes, coverage, drift summary.
498
- yg validate [--scope <path>|all] Check structural integrity and completeness.
499
- yg drift [--scope <path>|all] [--drifted-only] [--limit <n>]
500
- Detect source and graph drift (bidirectional).
501
- yg drift-sync --node <path> [--recursive] | --all
502
- Record file hashes as new baseline.
503
- \`\`\`
537
+ Before preflight:
504
538
 
505
- ### Quick Routing Table
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.
506
542
 
507
- | What you have | Where it goes |
508
- |---|---|
509
- | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
510
- | Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
511
- | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
512
- | Business process participation | Flow (\`yg-flow.yaml nodes\`) |
513
- | Process-level requirement | Flow \`aspects\` + aspect directory |
514
- | Context shared across a domain | Parent node artifact |
515
- | Technology stack | Node artifact at appropriate hierarchy level |
516
- | Coding standards | Node artifact at appropriate hierarchy level |`;
517
- var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
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";
518
549
 
519
550
  // src/templates/platform.ts
520
551
  var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
@@ -2376,6 +2407,7 @@ async function validate(graph, scope = "all") {
2376
2407
  issues.push(...checkFlowAspectIds(graph));
2377
2408
  issues.push(...await checkDirectoriesHaveNodeYaml(graph));
2378
2409
  issues.push(...await checkShallowArtifacts(graph));
2410
+ issues.push(...await checkWideNodes(graph));
2379
2411
  issues.push(...checkUnpairedEvents(graph));
2380
2412
  let filtered = issues;
2381
2413
  let nodesScanned = graph.nodes.size;
@@ -2857,6 +2889,29 @@ async function checkShallowArtifacts(graph) {
2857
2889
  }
2858
2890
  return issues;
2859
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
+ }
2860
2915
  function checkHighFanOut(graph) {
2861
2916
  const issues = [];
2862
2917
  const maxRel = graph.config.quality?.max_direct_relations ?? 10;
@@ -3134,6 +3189,73 @@ function checkMissingDescriptions(graph) {
3134
3189
  return issues;
3135
3190
  }
3136
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
+ }
3258
+
3137
3259
  // src/cli/build-context.ts
3138
3260
  function collectRelevantNodePaths(graph, nodePath) {
3139
3261
  const relevant = /* @__PURE__ */ new Set();
@@ -3155,10 +3277,32 @@ function collectRelevantNodePaths(graph, nodePath) {
3155
3277
  return relevant;
3156
3278
  }
3157
3279
  function registerBuildCommand(program2) {
3158
- 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) => {
3159
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
+ }
3160
3290
  const graph = await loadGraph(process.cwd());
3161
- 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
+ }
3162
3306
  const relevantNodes = collectRelevantNodePaths(graph, nodePath);
3163
3307
  const validationResult = await validate(graph, "all");
3164
3308
  const relevantErrors = validationResult.issues.filter(
@@ -3305,11 +3449,11 @@ import chalk2 from "chalk";
3305
3449
 
3306
3450
  // src/io/drift-state-store.ts
3307
3451
  import { readFile as readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3308
- import path12 from "path";
3452
+ import path13 from "path";
3309
3453
  import { parse as yamlParse } from "yaml";
3310
3454
  var DRIFT_STATE_DIR = ".drift-state";
3311
3455
  function nodeStatePath(yggRoot, nodePath) {
3312
- return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3456
+ return path13.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3313
3457
  }
3314
3458
  async function scanJsonFiles(dir, baseDir) {
3315
3459
  const results = [];
@@ -3320,12 +3464,12 @@ async function scanJsonFiles(dir, baseDir) {
3320
3464
  return results;
3321
3465
  }
3322
3466
  for (const entry of entries) {
3323
- const fullPath = path12.join(dir, entry.name);
3467
+ const fullPath = path13.join(dir, entry.name);
3324
3468
  if (entry.isDirectory()) {
3325
3469
  const nested = await scanJsonFiles(fullPath, baseDir);
3326
3470
  results.push(...nested);
3327
3471
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
3328
- const relPath = path12.relative(baseDir, fullPath);
3472
+ const relPath = path13.relative(baseDir, fullPath);
3329
3473
  const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
3330
3474
  results.push(nodePath);
3331
3475
  }
@@ -3333,13 +3477,13 @@ async function scanJsonFiles(dir, baseDir) {
3333
3477
  return results;
3334
3478
  }
3335
3479
  async function removeEmptyParents(filePath, stopDir) {
3336
- let dir = path12.dirname(filePath);
3480
+ let dir = path13.dirname(filePath);
3337
3481
  while (dir !== stopDir && dir.startsWith(stopDir)) {
3338
3482
  try {
3339
3483
  const entries = await readdir6(dir);
3340
3484
  if (entries.length === 0) {
3341
3485
  await rm2(dir, { recursive: true });
3342
- dir = path12.dirname(dir);
3486
+ dir = path13.dirname(dir);
3343
3487
  } else {
3344
3488
  break;
3345
3489
  }
@@ -3360,12 +3504,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
3360
3504
  }
3361
3505
  async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3362
3506
  const filePath = nodeStatePath(yggRoot, nodePath);
3363
- await mkdir3(path12.dirname(filePath), { recursive: true });
3507
+ await mkdir3(path13.dirname(filePath), { recursive: true });
3364
3508
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3365
3509
  await writeFile5(filePath, content, "utf-8");
3366
3510
  }
3367
3511
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3368
- const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
3512
+ const driftDir = path13.join(yggRoot, DRIFT_STATE_DIR);
3369
3513
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
3370
3514
  const removed = [];
3371
3515
  for (const nodePath of allNodePaths) {
@@ -3379,7 +3523,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
3379
3523
  return removed.sort();
3380
3524
  }
3381
3525
  async function readDriftState(yggRoot) {
3382
- const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
3526
+ const driftPath = path13.join(yggRoot, DRIFT_STATE_DIR);
3383
3527
  let driftStat;
3384
3528
  try {
3385
3529
  driftStat = await stat5(driftPath);
@@ -3420,7 +3564,7 @@ async function readDriftState(yggRoot) {
3420
3564
 
3421
3565
  // src/utils/hash.ts
3422
3566
  import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3423
- import path13 from "path";
3567
+ import path14 from "path";
3424
3568
  import { createHash } from "crypto";
3425
3569
  import { createRequire } from "module";
3426
3570
  var require2 = createRequire(import.meta.url);
@@ -3432,7 +3576,7 @@ async function hashFile(filePath) {
3432
3576
  async function loadRootGitignoreStack(projectRoot) {
3433
3577
  if (!projectRoot) return [];
3434
3578
  try {
3435
- const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
3579
+ const content = await readFile15(path14.join(projectRoot, ".gitignore"), "utf-8");
3436
3580
  const matcher = ignoreFactory();
3437
3581
  matcher.add(content);
3438
3582
  return [{ basePath: projectRoot, matcher }];
@@ -3442,7 +3586,7 @@ async function loadRootGitignoreStack(projectRoot) {
3442
3586
  }
3443
3587
  function isIgnoredByStack(candidatePath, stack) {
3444
3588
  for (const { basePath, matcher } of stack) {
3445
- const relativePath = path13.relative(basePath, candidatePath);
3589
+ const relativePath = path14.relative(basePath, candidatePath);
3446
3590
  if (relativePath === "" || relativePath.startsWith("..")) continue;
3447
3591
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
3448
3592
  }
@@ -3457,7 +3601,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3457
3601
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
3458
3602
  const allFiles = [];
3459
3603
  for (const tf of trackedFiles) {
3460
- const absPath = path13.join(projectRoot, tf.path);
3604
+ const absPath = path14.join(projectRoot, tf.path);
3461
3605
  try {
3462
3606
  const st = await stat6(absPath);
3463
3607
  if (st.isDirectory()) {
@@ -3467,7 +3611,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3467
3611
  });
3468
3612
  for (const entry of dirEntries) {
3469
3613
  allFiles.push({
3470
- relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3614
+ relPath: path14.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3471
3615
  absPath: entry.absPath,
3472
3616
  mtimeMs: entry.mtimeMs
3473
3617
  });
@@ -3507,7 +3651,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3507
3651
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3508
3652
  let stack = options.gitignoreStack ?? [];
3509
3653
  try {
3510
- const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
3654
+ const localContent = await readFile15(path14.join(directoryPath, ".gitignore"), "utf-8");
3511
3655
  const localMatcher = ignoreFactory();
3512
3656
  localMatcher.add(localContent);
3513
3657
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -3517,7 +3661,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3517
3661
  const dirs = [];
3518
3662
  const files = [];
3519
3663
  for (const entry of entries) {
3520
- const absoluteChildPath = path13.join(directoryPath, entry.name);
3664
+ const absoluteChildPath = path14.join(directoryPath, entry.name);
3521
3665
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
3522
3666
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
3523
3667
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -3530,7 +3674,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3530
3674
  Promise.all(files.map(async (f) => {
3531
3675
  const fileStat = await stat6(f);
3532
3676
  return {
3533
- relPath: path13.relative(rootDirectoryPath, f),
3677
+ relPath: path14.relative(rootDirectoryPath, f),
3534
3678
  absPath: f,
3535
3679
  mtimeMs: fileStat.mtimeMs
3536
3680
  };
@@ -3543,14 +3687,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3543
3687
  }
3544
3688
 
3545
3689
  // src/core/context-files.ts
3546
- import path14 from "path";
3690
+ import path15 from "path";
3547
3691
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3548
3692
  function collectTrackedFiles(node, graph) {
3549
3693
  const seen = /* @__PURE__ */ new Set();
3550
3694
  const result = [];
3551
- const projectRoot = path14.dirname(graph.rootPath);
3552
- const yggPrefix = path14.relative(projectRoot, graph.rootPath);
3553
- const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
3695
+ const projectRoot = path15.dirname(graph.rootPath);
3696
+ const yggPrefix = path15.relative(projectRoot, graph.rootPath);
3697
+ const yggPrefixNormalized = yggPrefix.split(path15.sep).join("/");
3554
3698
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
3555
3699
  function addFile(filePath, category) {
3556
3700
  if (seen.has(filePath)) return;
@@ -3662,8 +3806,8 @@ function collectParticipatingFlows2(graph, node, ancestors) {
3662
3806
  }
3663
3807
 
3664
3808
  // src/core/drift-detector.ts
3665
- import { access as access2 } from "fs/promises";
3666
- import path15 from "path";
3809
+ import { access as access3 } from "fs/promises";
3810
+ import path16 from "path";
3667
3811
  function getChildMappingExclusions(graph, nodePath) {
3668
3812
  const node = graph.nodes.get(nodePath);
3669
3813
  if (!node) return [];
@@ -3685,7 +3829,7 @@ function getChildMappingExclusions(graph, nodePath) {
3685
3829
  return exclusions;
3686
3830
  }
3687
3831
  async function detectDrift(graph, filterNodePath) {
3688
- const projectRoot = path15.dirname(graph.rootPath);
3832
+ const projectRoot = path16.dirname(graph.rootPath);
3689
3833
  const driftState = await readDriftState(graph.rootPath);
3690
3834
  const entries = [];
3691
3835
  for (const [nodePath, node] of graph.nodes) {
@@ -3767,16 +3911,16 @@ async function detectDrift(graph, filterNodePath) {
3767
3911
  };
3768
3912
  }
3769
3913
  function categorizeFile(filePath, _rootPath, projectRoot) {
3770
- const yggPrefix = path15.relative(projectRoot, _rootPath);
3771
- const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3914
+ const yggPrefix = path16.relative(projectRoot, _rootPath);
3915
+ const normalizedPrefix = yggPrefix.split(path16.sep).join("/");
3772
3916
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3773
3917
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3774
3918
  }
3775
3919
  async function allPathsMissing(projectRoot, mappingPaths) {
3776
3920
  for (const mp of mappingPaths) {
3777
- const absPath = path15.join(projectRoot, mp);
3921
+ const absPath = path16.join(projectRoot, mp);
3778
3922
  try {
3779
- await access2(absPath);
3923
+ await access3(absPath);
3780
3924
  return false;
3781
3925
  } catch {
3782
3926
  }
@@ -3784,7 +3928,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3784
3928
  return true;
3785
3929
  }
3786
3930
  async function syncDriftState(graph, nodePath) {
3787
- const projectRoot = path15.dirname(graph.rootPath);
3931
+ const projectRoot = path16.dirname(graph.rootPath);
3788
3932
  const node = graph.nodes.get(nodePath);
3789
3933
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3790
3934
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
@@ -3794,12 +3938,39 @@ async function syncDriftState(graph, nodePath) {
3794
3938
  const storedFileData = existingEntry?.files ? { hashes: existingEntry.files, mtimes: existingEntry.mtimes ?? {} } : void 0;
3795
3939
  const { canonicalHash, fileHashes, fileMtimes } = await hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes);
3796
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
+ }
3797
3968
  await writeNodeDriftState(graph.rootPath, nodePath, {
3798
3969
  hash: canonicalHash,
3799
3970
  files: fileHashes,
3800
3971
  mtimes: fileMtimes
3801
3972
  });
3802
- return { previousHash, currentHash: canonicalHash };
3973
+ return { previousHash, currentHash: canonicalHash, sourceOnlyChange };
3803
3974
  }
3804
3975
 
3805
3976
  // src/cli/drift.ts
@@ -3972,13 +4143,20 @@ function registerDriftSyncCommand(program2) {
3972
4143
  }
3973
4144
  continue;
3974
4145
  }
3975
- const { previousHash, currentHash } = await syncDriftState(graph, np);
4146
+ const { previousHash, currentHash, sourceOnlyChange } = await syncDriftState(graph, np);
3976
4147
  process.stdout.write(chalk3.green(`Synchronized: ${np}
3977
4148
  `));
3978
4149
  process.stdout.write(
3979
4150
  ` Hash: ${previousHash ? previousHash.slice(0, 8) : "none"} -> ${currentHash.slice(0, 8)}
3980
4151
  `
3981
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
+ }
3982
4160
  }
3983
4161
  if (options.all) {
3984
4162
  const validPaths = new Set(nodesToSync);
@@ -4150,73 +4328,6 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
4150
4328
  }
4151
4329
  }
4152
4330
 
4153
- // src/cli/owner.ts
4154
- import path16 from "path";
4155
- import { access as access3 } from "fs/promises";
4156
- function normalizeForMatch(inputPath) {
4157
- return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
4158
- }
4159
- function findOwner(graph, projectRoot, rawPath) {
4160
- const file = normalizeForMatch(normalizeProjectRelativePath(projectRoot, rawPath));
4161
- let best = null;
4162
- for (const [nodePath, node] of graph.nodes) {
4163
- const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizeForMatch).filter((mappingPath) => mappingPath.length > 0);
4164
- for (const mappingPath of mappingPaths) {
4165
- if (file === mappingPath) {
4166
- return { file, nodePath, mappingPath, direct: true };
4167
- }
4168
- if (file.startsWith(mappingPath + "/")) {
4169
- if (!best || best && mappingPath.length > best.mappingPath.length) {
4170
- best = { nodePath, mappingPath, exact: false };
4171
- }
4172
- }
4173
- }
4174
- }
4175
- return best ? { file, nodePath: best.nodePath, mappingPath: best.mappingPath, direct: false } : { file, nodePath: null };
4176
- }
4177
- function registerOwnerCommand(program2) {
4178
- program2.command("owner").description("Find which graph node owns a source file").requiredOption("--file <path>", "File path (relative to repository root)").action(async (options) => {
4179
- try {
4180
- const cwd = process.cwd();
4181
- const graph = await loadGraph(cwd);
4182
- const repoRoot = projectRootFromGraph(graph.rootPath);
4183
- const rawPath = options.file.trim();
4184
- const absolute = path16.resolve(cwd, rawPath);
4185
- const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
4186
- const result = findOwner(graph, repoRoot, repoRelative);
4187
- if (!result.nodePath) {
4188
- const absPath = path16.resolve(repoRoot, result.file);
4189
- let exists = true;
4190
- try {
4191
- await access3(absPath);
4192
- } catch {
4193
- exists = false;
4194
- }
4195
- if (exists) {
4196
- process.stdout.write(`${result.file} -> no graph coverage
4197
- `);
4198
- } else {
4199
- process.stdout.write(`${result.file} -> no graph coverage (file not found)
4200
- `);
4201
- }
4202
- } else {
4203
- process.stdout.write(`${result.file} -> ${result.nodePath}
4204
- `);
4205
- if (result.direct === false && result.mappingPath) {
4206
- process.stdout.write(
4207
- ` File has no direct mapping; context comes from ancestor directory ${result.mappingPath}. Use: yg build-context --node ${result.nodePath}
4208
- `
4209
- );
4210
- }
4211
- }
4212
- } catch (error) {
4213
- process.stderr.write(`Error: ${error.message}
4214
- `);
4215
- process.exit(1);
4216
- }
4217
- });
4218
- }
4219
-
4220
4331
  // src/core/dependency-resolver.ts
4221
4332
  import { execSync } from "child_process";
4222
4333
  import path17 from "path";
@@ -4633,23 +4744,39 @@ Total scope: ${sorted.length + indirectPaths.length} nodes
4633
4744
  }
4634
4745
  }
4635
4746
  function registerImpactCommand(program2) {
4636
- 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(
4637
4748
  async (options) => {
4638
4749
  try {
4639
- 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;
4640
4755
  if (modeCount === 0) {
4641
4756
  process.stderr.write(
4642
- "Error: one of --node, --aspect, or --flow is required\n"
4757
+ "Error: one of --node, --file, --aspect, or --flow is required\n"
4643
4758
  );
4644
4759
  process.exit(1);
4645
4760
  }
4646
4761
  if (modeCount > 1) {
4647
4762
  process.stderr.write(
4648
- "Error: --node, --aspect, and --flow are mutually exclusive\n"
4763
+ "Error: --node/--file, --aspect, and --flow are mutually exclusive\n"
4649
4764
  );
4650
4765
  process.exit(1);
4651
4766
  }
4652
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
+ }
4653
4780
  if (options.aspect) {
4654
4781
  await handleAspectImpact(graph, options.aspect.trim(), options.simulate);
4655
4782
  return;