@chrisdudek/yg 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -28,25 +28,33 @@ artifacts:
28
28
  responsibility.md:
29
29
  required: always
30
30
  description: "What this node is responsible for, and what it is not"
31
+ structural_context: true
31
32
  interface.md:
32
33
  required:
33
34
  when: has_incoming_relations
34
35
  description: "Public API \u2014 methods, parameters, return types, contracts"
35
36
  structural_context: true
37
+ logic.md:
38
+ required: never
39
+ description: "Algorithmic flow, control flow, branching logic, decision trees \u2014 the 'how' of execution"
36
40
  constraints.md:
37
41
  required: never
38
42
  description: "Validation rules, business rules, invariants"
43
+ structural_context: true
39
44
  errors.md:
40
45
  required:
41
46
  when: has_incoming_relations
42
- description: "Error conditions, codes, recovery behavior"
47
+ description: "Failure modes, edge cases, error conditions, recovery behavior"
43
48
  structural_context: true
49
+ model.md:
50
+ required: never
51
+ description: "Data structures, schemas, entities, type definitions \u2014 the shape of data this node owns or manages"
44
52
  state.md:
45
53
  required: never
46
54
  description: "State machines, lifecycle, transitions"
47
55
  decisions.md:
48
56
  required: never
49
- description: "Local design decisions and rationale"
57
+ description: "Local design decisions and rationale \u2014 choices specific to this node, not system-wide"
50
58
 
51
59
  knowledge_categories:
52
60
  - name: decisions
@@ -60,8 +68,8 @@ quality:
60
68
  min_artifact_length: 50
61
69
  max_direct_relations: 10
62
70
  context_budget:
63
- warning: 5000
64
- error: 10000
71
+ warning: 10000
72
+ error: 20000
65
73
  knowledge_staleness_days: 90
66
74
  `;
67
75
 
@@ -75,10 +83,22 @@ var AGENT_RULES_CONTENT = `# Yggdrasil - System Semantic Memory (Operating Manua
75
83
  You are working in a repository managed by Yggdrasil.
76
84
  Yggdrasil is a persistent, structured semantic memory graph stored in \`.yggdrasil/\`. It maps the repository, dictates system rules, and assembles implementation contexts.
77
85
 
86
+ **Why:** AI agents degrade with project size \u2014 too little context breaks contracts, too much context drowns in noise. The graph is persistent semantic memory: agents need exactly the right tokens for the task, not the whole repo. Small, precise context beats massive noisy. A repository with Yggdrasil is self-aware.
87
+
78
88
  THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strictly.
79
89
 
80
90
  ---
81
91
 
92
+ ## 0. ATOMIC INVARIANT \u2014 OVERRIDES EVERYTHING
93
+
94
+ **CODE \u2194 GRAPH ARE ONE.**
95
+ - Code changed \u2192 graph artifacts MUST be updated IN THE SAME RESPONSE. No exceptions.
96
+ - Graph changed \u2192 source files MUST be verified/updated IN THE SAME RESPONSE. No exceptions.
97
+ - There is NO "code-only" response. There is NO "graph-only" response (unless explicitly a dry-run plan).
98
+ - If you complete a response and one side is changed but the other is not, you have VIOLATED the core contract of Yggdrasil.
99
+
100
+ ---
101
+
82
102
  ## 1. CORE PRINCIPLES (NON-NEGOTIABLE)
83
103
 
84
104
  1. **Graph First, Always:** Before answering a question, modifying code, or planning a feature, you MUST consult the graph.
@@ -88,6 +108,21 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
88
108
  5. **Tools Read, You Write:** The \`yg\` CLI tools only read, validate, and manage metadata. YOU must create and edit graph directories, \`.yaml\` files, and \`.md\` artifacts manually.
89
109
  6. **English Only for Artifacts:** All graph artifact files (filenames from \`config.artifacts\`, in the same directory as \`node.yaml\`) MUST be written in English. Conversation can be in the user's language.
90
110
  7. **Never Touch Operational Metadata:** NEVER manually edit \`.yggdrasil/.drift-state\` or \`.yggdrasil/.journal.yaml\`.
111
+ 8. **Ask, Never Infer:** If graph and code diverge in a way with multiple valid resolutions, or if a required decision is ambiguous \u2014 STOP. State the ambiguity. List interpretations. Ask the user to decide. Never silently choose. Never patch without confirmation. When you stop, always explain the context and available options clearly so the user can make an informed choice.
112
+
113
+ ---
114
+
115
+ ## 1.5 FAILURE STATES
116
+
117
+ You have fundamentally broken Yggdrasil if you do any of the following:
118
+ - \u274C You modified source code without having run \`yg owner --file <path>\` for each modified file in the SAME response.
119
+ - \u274C You modified source code without updating graph artifacts in the SAME response.
120
+ - \u274C You modified graph files without verifying/updating source code alignment in the SAME response.
121
+ - \u274C You resolved a code\u2194graph inconsistency without asking the user first.
122
+ - \u274C You created or edited a graph element without reading its schema in \`.yggdrasil/templates/\`.
123
+ - \u274C You ran \`yg drift-sync\` before updating graph artifacts.
124
+ - \u274C You ran \`yg drift-sync\` after a graph-only change without verifying source files.
125
+ - \u274C You used Blackbox coverage for greenfield/new code.
91
126
 
92
127
  ---
93
128
 
@@ -96,15 +131,15 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
96
131
  You do not need explicit "session" commands. Follow these conversational triggers:
97
132
 
98
133
  ### A. Preflight (First message of the conversation)
99
- Always execute these commands before doing anything else:
134
+ Always execute these commands before doing anything else. *(Exception: If the user's request is clearly read-only, run ONLY step 1).* **Read-only** means the user asks only for explanation, clarification, analysis, or evaluation \u2014 no code or graph modification will occur. Examples: "explain this", "evaluate X", "what does Y do?", "analyze Z". If unsure, run full preflight.
100
135
  1. \`yg journal-read\` -> If entries exist, consolidate them into the graph, then \`yg journal-archive\`.
101
136
  2. \`yg drift\` -> If divergence is detected, present states (\`ok\`, \`drift\`, \`missing\`, \`unmaterialized\`). Ask the user: Absorb (update graph) or Reject (re-materialize code from graph)?
102
137
  3. \`yg status\` -> Report graph health.
103
138
  4. \`yg validate\` -> If W008 stale-knowledge appears, update the knowledge artifacts to reflect current node state.
104
139
 
105
- ### B. Wrap-up (User signals closing the topic)
106
- Triggered by phrases like: "ko\u0144czymy", "wrap up", "to tyle", "gotowe".
107
- **Note: The graph should ALREADY be up to date. Do not wait for wrap-up to update graph artifacts.**
140
+ ### B. Session Verification (Wrap-up)
141
+ Triggered by phrases like: "we're done", "wrap up", "that's enough", "done".
142
+ **Note: The graph should ALREADY be up to date. If the graph requires massive updates at this stage, YOU HAVE FAILED.**
108
143
  1. If iterative journal mode was used: consolidate notes to the graph, then \`yg journal-archive\`.
109
144
  2. \`yg drift\` -> Check if files changed manually during the conversation.
110
145
  3. \`yg validate\` -> Fix any structural errors.
@@ -112,64 +147,68 @@ Triggered by phrases like: "ko\u0144czymy", "wrap up", "to tyle", "gotowe".
112
147
 
113
148
  ---
114
149
 
115
- ## 3. WORKFLOW: MODIFYING OR CREATING FILES
150
+ ## 3. WORKFLOW: MODIFYING OR CREATING FILES (Code-First)
116
151
 
117
152
  You are NOT ALLOWED to edit or create source code without establishing graph coverage first.
118
153
 
154
+ **Gate:** Before using any tool that modifies files, you MUST have run \`yg owner --file <path>\` for each file you intend to modify. If you have not \u2014 run it first, then proceed. No exceptions. Gate applies to **source files** (files outside \`.yggdrasil/\`). For graph files (\`.yggdrasil/model/\`, \`.yggdrasil/aspects/\`, etc.), follow the Graph Modification Checklist in section 4 instead.
155
+
119
156
  **Step 1: Check coverage** -> Run \`yg owner --file <path>\`
120
157
 
121
158
  **Step 2: If Owner FOUND (The Execution Checklist)**
122
- Whenever you write or edit source code, you MUST output this exact checklist in your response to the user, and execute each step BEFORE finishing your turn. This forces you to remember the graph:
123
-
124
- - [x] 1. Read Specification (ran \`yg build-context\`)
125
- - [x] 2. Modify Source Code
126
- - [x] 3. Sync Graph Artifacts (manually edit the node's artifact files \u2014 filenames from \`config.artifacts\` \u2014 IMMEDIATELY to match new code behavior)
127
- - [x] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
159
+ Whenever you write or edit source code, you MUST output this exact checklist in your response to the user, and execute each step BEFORE finishing your turn:
128
160
 
129
- *If you do not print this checklist and check off step 3, you have failed the core directive of Yggdrasil.*
161
+ - [ ] 1. Read Specification (ran \`yg build-context\`)
162
+ - [ ] 2. Modify Source Code
163
+ - [ ] 3. Sync Graph Artifacts (manually edit the node's artifact files IMMEDIATELY to match new code behavior)
164
+ - [ ] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
130
165
 
131
166
  **Step 3: If Owner NOT FOUND (Uncovered Area)**
132
- STOP. Do not modify the code. First determine: **Is this greenfield (empty or new code to be created)?**
133
-
134
- * **If GREENFIELD (empty directory, new project, code not yet written):** Do NOT offer blackbox. Use Option 1 only \u2014 create proper nodes (reverse engineering or upfront design) before implementing. Blackbox is forbidden for new code.
135
- * **If EXISTING CODE (legacy, third-party, shipped-but-unmapped):** Present the user with 3 options and wait for their decision:
136
- * **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
137
- * **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node at a user-chosen granularity to establish ownership without deep semantic exploration.
138
- * **Option 3: Abort/Change Plan:** Do not touch the file.
167
+ STOP. Do not modify the code. First determine: **Is this greenfield or existing code?**
139
168
 
169
+ * **If GREENFIELD (empty directory, new project):** Do NOT offer blackbox. Create proper nodes (reverse engineering or upfront design) before implementing.
170
+ * **If PARTIALLY MAPPED (file is unmapped, but lives inside a mapped module):** Stop and ask the user if this file should be added to the existing node or if a new node is required.
171
+ * **If EXISTING CODE (legacy, third-party):** Present the user with 3 options and wait:
172
+ * **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
173
+ * **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node to establish ownership without deep semantic exploration.
174
+ * **Option 3: Abort/Change Plan:** Do not touch the file.
140
175
 
141
176
  ---
142
177
 
143
- ## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS
144
-
145
- When adding features or changing architecture, update the graph FIRST.
178
+ ## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS (Graph-First)
146
179
 
147
- **DO NOT DEFER GRAPH UPDATES:**
148
- * **DO NOT wait for the user to confirm if a change is "final".** The graph must evolve continuously with your code edits.
149
- * **Default Behavior:** If iterative journal mode is OFF, you MUST write structural and semantic changes directly to the graph files (\`node.yaml\`, artifacts or other files like aspects or flows, etc.) IMMEDIATELY. Suppress your innate safety bias to wait for permission.
180
+ When adding features, changing architecture, or doing graph-first design:
150
181
 
151
182
  1. **Check Blast Radius:** Before modifying a node that others depend on, run \`yg impact --node <node_path> --simulate\`. Report the impact to the user.
152
183
  2. **Read Config & Templates:**
153
- * Check \`.yggdrasil/config.yaml\` for allowed \`node_types\` and \`tags\`.
154
- * **CRITICAL:** ALWAYS read the required schema files in \`.yggdrasil/templates/\` (e.g., \`node.yaml\`, \`service.yaml\`) to know the exact fields and structure before creating or editing any graph file.
184
+ * Check \`.yggdrasil/config.yaml\` for allowed \`node_types\` and \`tags\`.
185
+ * **CRITICAL:** ALWAYS read the schema in \`.yggdrasil/templates/\` for the element type (node.yaml, aspect.yaml, flow.yaml, knowledge.yaml) before creating or editing it.
155
186
  3. **Validate & Fix:** Run \`yg validate\`. You must fix all E-codes (Errors).
156
- 4. **Token Economy & W-codes (Warnings):**
157
- * If you see \`W005 budget-warning\` or \`W006 budget-error\`, the context package is too large. You MUST consider splitting the node or reducing dependencies.
158
- * If you see \`W008 stale-knowledge\`, the semantic memory is outdated compared to the code. Update the knowledge artifacts.
159
- * **Smallest Viable Scope:** Prefer \`scope: nodes\` over \`scope: tags\`. Prefer tags over \`scope: global\`. Global scope costs token budget in EVERY context package.
187
+ 4. **Token Economy & W-codes:**
188
+ * W005/W006: Context package too large. Consider splitting the node.
189
+ * W008: Stale semantic memory. Update knowledge artifacts.
190
+
191
+ **Graph Modification Checklist**
192
+ Whenever you change the graph structure or semantics, you MUST output and execute this exact checklist:
193
+
194
+ - [ ] 1. Read schema from \`.yggdrasil/templates/\` (node.yaml, aspect.yaml, flow.yaml, or knowledge.yaml for the element type)
195
+ - [ ] 2. Edit graph files (\`node.yaml\`, artifacts)
196
+ - [ ] 3. Verify corresponding source files exist and their behavior matches updated artifacts
197
+ - [ ] 4. Validate (ran \`yg validate\` \u2014 fix all Errors)
198
+ - [ ] 5. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER steps 2-3 are confirmed)
160
199
 
161
- **Journaling (Iterative Mode):**
162
- * **Default:** Write changes directly to graph files immediately.
163
- * **Opt-in:** ONLY if the user says "tryb iteracyjny" or "u\u017Cyj journala", use \`yg journal-add --note "..."\` to buffer intent during fast ping-pong changes.
200
+ **Journaling (Iterative Mode Scope):**
201
+ * **Default:** Write changes directly to graph files immediately. Do not defer.
202
+ * **Opt-in:** ONLY if the user says "use iterative mode" or "use journal". Once activated, it remains active for the ENTIRE conversation until wrap-up. Use \`yg journal-add --note "..."\` to buffer intent.
164
203
 
165
204
  ---
166
205
 
167
206
  ## 5. PATH CONVENTIONS (CRITICAL)
168
207
 
169
208
  To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
170
- * **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
171
- * **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
172
- * **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
209
+ * **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
210
+ * **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
211
+ * **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
173
212
 
174
213
  ---
175
214
 
@@ -177,35 +216,84 @@ To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
177
216
 
178
217
  The graph lives entirely under \`.yggdrasil/\`. You NEVER guess structure. You MUST ALWAYS read the corresponding schema reference in \`.yggdrasil/templates/\` before creating or editing any graph file.
179
218
 
180
- * **\`.yggdrasil/config.yaml\`**: The ONLY config file. Defines \`node_types\`, \`tags\`, \`artifacts\`, \`knowledge_categories\`, and quality thresholds. Read this before any graph work.
181
- * **\`.yggdrasil/templates/\`**: The SINGLE place for all templates and schemas.
182
- * Contains node-type templates (e.g., \`service.yaml\`, \`module.yaml\`) with suggested artifacts and guidance.
183
- * Contains schema references (\`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`) showing exact file structures.
184
- * **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files (filenames from \`config.artifacts\`; required ones depend on config).
185
- * **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` (binds via \`tag: <name>\`) and \`.md\` content.
186
- * **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` (lists \`nodes: [paths]\` and \`knowledge: [paths]\`) and \`.md\` content.
187
- * **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom (\`decisions/\`, \`patterns/\`, \`invariants/\`). Directory contains \`knowledge.yaml\` and \`.md\` content.
219
+ * **\`.yggdrasil/config.yaml\`**: Defines \`node_types\`, \`tags\`, \`artifacts\`, \`knowledge_categories\`.
220
+ * **\`.yggdrasil/templates/\`**: Schemas for each graph layer \u2014 \`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`.
221
+ * **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files.
222
+ * **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` and \`.md\` content.
223
+ * **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` and \`.md\` content.
224
+ * **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom. Directory contains \`knowledge.yaml\` and \`.md\` content.
188
225
 
189
226
  ---
190
227
 
191
- ## 7. CLI TOOLS REFERENCE (\`yg\`)
228
+ ## 7. CONTEXT ASSEMBLY & KNOWLEDGE DECONSTRUCTION (HOW TO MAP FILES)
229
+
230
+ Your ultimate goal when describing a file or node is **Context Reproducibility**. A future agent reading ONLY the output of \`yg build-context\` for this node must be able to perfectly reconstruct the source code's behavior, constraints, environment, and purpose.
231
+
232
+ However, you must NOT dump all knowledge into a single file. Yggdrasil's context package is **multi-layered** and hierarchically assembled. When you map existing code or design new code, you must deconstruct the knowledge and place it at the correct abstraction layer so the engine can mechanically reassemble it.
233
+
234
+ ### CRITICAL RULE: CAPTURE INTENT, BUT NEVER INVENT IT
235
+ The graph is not just a structural map; it is the semantic meaning of the system. Code explains "what" and "how". The graph MUST explain "WHY".
236
+
237
+ 1. **ALWAYS Capture the User's "Why":** If the user explains the business reason, intent, or rationale behind a request (e.g., "We need to do X because Y"), you MUST permanently record this reasoning in the relevant graph artifacts (e.g., \`responsibility.md\`, \`constraints.md\`, \`decisions.md\`). Do not let the conversation context evaporate.
238
+ 2. **NEVER Invent the "Why":** Files that imply human judgment (like \`decisions.md\` or \`knowledge/invariants\`) must reflect ACTUAL human choices.
239
+ 3. **NO Hallucinations:** You MUST NEVER infer or hallucinate a rationale, an architectural decision, or a business rule.
240
+ 4. **Ask if Missing:** If the user requests a significant architectural or business logic change but does not provide the rationale, you MUST ask them "Why are we making this change?" before documenting the decision in the graph.
241
+
242
+ When mapping a file, execute this mental routing:
243
+
244
+ ### Layer 1: Unit Identity (Local Node Artifacts)
245
+ * **What goes here:** Things exclusively true for this specific node.
246
+ * **Routing:** **DO NOT ASSUME FILE NAMES.** You MUST read \`.yggdrasil/config.yaml\` (the \`artifacts\` section) to see the exact allowed filenames for the current project and their requirement conditions (e.g., \`required: always\` vs \`when: has_incoming_relations\`). Write local node knowledge ONLY into these configured files next to \`node.yaml\`.
247
+ * **logic.md:** Algorithmic flow, control flow, branching logic, decision trees \u2014 the "how" of execution. Create when the node has non-trivial logic that cannot be inferred from interface alone. Extract from source; do not invent.
248
+ * **model.md:** Data structures, schemas, entities, type definitions \u2014 the shape of data this node owns. Create when the node manages non-trivial data structures. Extract from source types/interfaces; do not invent fields.
249
+
250
+ **Subagents:** When mapping a node (reverse engineering or graph-first), if the source code contains non-trivial logic (branching, control flow, algorithms, workflows), create \`logic.md\` for that node. Document: control flow, branching conditions, decision trees, sequencing. Follow Layer 1 routing. Do not invent \u2014 extract only what is implemented.
251
+
252
+ ### Layer 2: Surroundings (Relations & Flows)
253
+ * **What goes here:** How this node interacts with others. You must not duplicate external interfaces locally.
254
+ * **Routing:**
255
+ * If it calls another module: Add an outgoing structural \`relation\` in \`node.yaml\`. (The engine will automatically fetch the target's structural-context artifacts: responsibility, interface, constraints, errors).
256
+ * If it participates in an end-to-end process: Do not explain the whole process locally. Ensure the node is listed in \`.yggdrasil/flows/<flow_name>/flow.yaml\`. The engine will attach the flow knowledge automatically.
257
+
258
+ ### Layer 3: Domain Context (Hierarchy)
259
+ * **What goes here:** Business rules shared by a family of nodes.
260
+ * **Routing:** Do not repeat module-wide rules in every child node. Place the child node directory *inside* a parent Module Node directory. Write the shared rules in the parent's configured artifacts. The engine inherently passes parent context to children.
261
+
262
+ ### Layer 4: Cross-Cutting Rules (Aspects)
263
+ * **What goes here:** Horizontal requirements like logging, auth, rate-limiting, or specific frameworks.
264
+ * **Routing:** Do NOT write generic rules like "This node must log all errors" in local artifacts. Instead, read \`config.yaml\` for available \`tags\`. Add the relevant tag (e.g., \`requires-audit\`) to \`node.yaml\`. The engine will automatically attach the aspect knowledge.
265
+
266
+ ### Layer 5: Long-Term Memory (Knowledge Elements)
267
+ * **What goes here:** Global architectural decisions, design patterns, and systemic invariants.
268
+ * **Routing:** Read \`config.yaml\` (the \`knowledge_categories\` section) to know what categories exist.
269
+ * If the file implements a standard pattern: Do not describe the pattern locally. Add a \`knowledge\` reference in \`node.yaml\` to the existing pattern.
270
+ * If the file reveals an undocumented global invariant or decision: Ask the user to confirm it. If confirmed, create it under \`.yggdrasil/knowledge/<category>/\` so all future nodes inherit it.
271
+
272
+ **THE COMPLETENESS CHECK:**
273
+ Before finishing a mapping, ask yourself: *"If I delete the source file and give another agent ONLY the output of \`yg build-context\`, can they recreate it perfectly based on the configured artifacts, AND will they understand EXACTLY WHY this code exists and why it was designed this way?"*
274
+ - If no -> You missed a local constraint, a relation, or you failed to capture the user's provided rationale.
275
+ - If yes, but the local files are bloated -> You failed to deconstruct knowledge into Tags, Aspects, Flows, and Hierarchy. Fix the routing.
276
+
277
+ ---
278
+
279
+ ## 8. CLI TOOLS REFERENCE (\`yg\`)
192
280
 
193
281
  Always use these exact commands.
194
282
 
195
- * \`yg owner --file <file_path>\` -> Find owning node.
196
- * \`yg build-context --node <node_path>\` -> Assemble strict specification.
197
- * \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
198
- * \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
199
- * \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
200
- * \`yg status\` -> Graph health metrics.
201
- * \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
202
- * \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
203
- * \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
283
+ * \`yg owner --file <file_path>\` -> Find owning node.
284
+ * \`yg build-context --node <node_path>\` -> Assemble strict specification.
285
+ * \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
286
+ * \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
287
+ * \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
288
+ * \`yg status\` -> Graph health metrics.
289
+ * \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
290
+ * \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
291
+ * \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
204
292
 
205
293
  *(Iterative mode only)*
206
- * \`yg journal-read\`
207
- * \`yg journal-add --note "<content>" [--target <node_path>]\`
208
- * \`yg journal-archive\`
294
+ * \`yg journal-read\`
295
+ * \`yg journal-add --note "<content>" [--target <node_path>]\`
296
+ * \`yg journal-archive\`
209
297
  `;
210
298
 
211
299
  // src/templates/platform.ts
@@ -532,7 +620,7 @@ function registerInitCommand(program2) {
532
620
  process.stdout.write(" .yggdrasil/flows/\n");
533
621
  process.stdout.write(" .yggdrasil/knowledge/ (decisions, patterns, invariants)\n");
534
622
  process.stdout.write(
535
- " .yggdrasil/templates/ (module, service, library, node, aspect, flow, knowledge)\n"
623
+ " .yggdrasil/templates/ (node, aspect, flow, knowledge)\n"
536
624
  );
537
625
  process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
538
626
 
@@ -546,7 +634,7 @@ function registerInitCommand(program2) {
546
634
 
547
635
  // src/core/graph-loader.ts
548
636
  import { readdir as readdir3 } from "fs/promises";
549
- import path5 from "path";
637
+ import path6 from "path";
550
638
 
551
639
  // src/io/config-parser.ts
552
640
  import { readFile as readFile3 } from "fs/promises";
@@ -554,7 +642,7 @@ import { parse as parseYaml } from "yaml";
554
642
  var DEFAULT_QUALITY = {
555
643
  min_artifact_length: 50,
556
644
  max_direct_relations: 10,
557
- context_budget: { warning: 5e3, error: 1e4 },
645
+ context_budget: { warning: 1e4, error: 2e4 },
558
646
  knowledge_staleness_days: 90
559
647
  };
560
648
  async function parseConfig(filePath) {
@@ -869,30 +957,24 @@ function parseScope(raw, filePath) {
869
957
 
870
958
  // src/io/template-parser.ts
871
959
  import { readFile as readFile9 } from "fs/promises";
960
+ import path4 from "path";
872
961
  import { parse as parseYaml6 } from "yaml";
873
- async function parseTemplate(filePath) {
962
+ async function parseSchema(filePath) {
874
963
  const content = await readFile9(filePath, "utf-8");
875
- const raw = parseYaml6(content);
876
- if (!raw.node_type || typeof raw.node_type !== "string" || raw.node_type.trim() === "") {
877
- throw new Error(`template at ${filePath}: missing or empty 'node_type'`);
878
- }
879
- const suggestedArtifacts = Array.isArray(raw.suggested_artifacts) ? raw.suggested_artifacts.filter((a) => typeof a === "string") : void 0;
880
- return {
881
- nodeType: raw.node_type.trim(),
882
- suggestedArtifacts: suggestedArtifacts && suggestedArtifacts.length > 0 ? suggestedArtifacts : void 0,
883
- guidance: typeof raw.guidance === "string" ? raw.guidance : void 0
884
- };
964
+ parseYaml6(content);
965
+ const schemaType = path4.basename(filePath, path4.extname(filePath));
966
+ return { schemaType };
885
967
  }
886
968
 
887
969
  // src/utils/paths.ts
888
- import path4 from "path";
970
+ import path5 from "path";
889
971
  import { fileURLToPath as fileURLToPath2 } from "url";
890
972
  import { stat as stat2 } from "fs/promises";
891
973
  async function findYggRoot(projectRoot) {
892
- let current = path4.resolve(projectRoot);
893
- const root = path4.parse(current).root;
974
+ let current = path5.resolve(projectRoot);
975
+ const root = path5.parse(current).root;
894
976
  while (true) {
895
- const yggPath = path4.join(current, ".yggdrasil");
977
+ const yggPath = path5.join(current, ".yggdrasil");
896
978
  try {
897
979
  const st = await stat2(yggPath);
898
980
  if (!st.isDirectory()) {
@@ -906,7 +988,7 @@ async function findYggRoot(projectRoot) {
906
988
  if (current === root) {
907
989
  throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
908
990
  }
909
- current = path4.dirname(current);
991
+ current = path5.dirname(current);
910
992
  continue;
911
993
  }
912
994
  throw err;
@@ -922,18 +1004,18 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
922
1004
  if (normalizedInput.length === 0) {
923
1005
  throw new Error("Path cannot be empty");
924
1006
  }
925
- const absolute = path4.resolve(projectRoot, normalizedInput);
926
- const relative = path4.relative(projectRoot, absolute);
927
- const isOutside = relative.startsWith("..") || path4.isAbsolute(relative);
1007
+ const absolute = path5.resolve(projectRoot, normalizedInput);
1008
+ const relative = path5.relative(projectRoot, absolute);
1009
+ const isOutside = relative.startsWith("..") || path5.isAbsolute(relative);
928
1010
  if (isOutside) {
929
1011
  throw new Error(`Path is outside project root: ${rawPath}`);
930
1012
  }
931
- return relative.split(path4.sep).join("/");
1013
+ return relative.split(path5.sep).join("/");
932
1014
  }
933
1015
 
934
1016
  // src/core/graph-loader.ts
935
1017
  function toModelPath(absolutePath, modelDir) {
936
- return path5.relative(modelDir, absolutePath).split(path5.sep).join("/");
1018
+ return path6.relative(modelDir, absolutePath).split(path6.sep).join("/");
937
1019
  }
938
1020
  var FALLBACK_CONFIG = {
939
1021
  name: "",
@@ -949,14 +1031,14 @@ async function loadGraph(projectRoot, options = {}) {
949
1031
  let configError;
950
1032
  let config = FALLBACK_CONFIG;
951
1033
  try {
952
- config = await parseConfig(path5.join(yggRoot, "config.yaml"));
1034
+ config = await parseConfig(path6.join(yggRoot, "config.yaml"));
953
1035
  } catch (error) {
954
1036
  if (!options.tolerateInvalidConfig) {
955
1037
  throw error;
956
1038
  }
957
1039
  configError = error.message;
958
1040
  }
959
- const modelDir = path5.join(yggRoot, "model");
1041
+ const modelDir = path6.join(yggRoot, "model");
960
1042
  const nodes = /* @__PURE__ */ new Map();
961
1043
  const nodeParseErrors = [];
962
1044
  const artifactFilenames = Object.keys(config.artifacts ?? {});
@@ -970,13 +1052,13 @@ async function loadGraph(projectRoot, options = {}) {
970
1052
  }
971
1053
  throw err;
972
1054
  }
973
- const aspects = await loadAspects(path5.join(yggRoot, "aspects"));
974
- const flows = await loadFlows(path5.join(yggRoot, "flows"));
1055
+ const aspects = await loadAspects(path6.join(yggRoot, "aspects"));
1056
+ const flows = await loadFlows(path6.join(yggRoot, "flows"));
975
1057
  const knowledge = await loadKnowledge(
976
- path5.join(yggRoot, "knowledge"),
1058
+ path6.join(yggRoot, "knowledge"),
977
1059
  config.knowledge_categories
978
1060
  );
979
- const templates = await loadTemplates(path5.join(yggRoot, "templates"));
1061
+ const schemas = await loadSchemas(path6.join(yggRoot, "templates"));
980
1062
  return {
981
1063
  config,
982
1064
  configError,
@@ -985,7 +1067,7 @@ async function loadGraph(projectRoot, options = {}) {
985
1067
  aspects,
986
1068
  flows,
987
1069
  knowledge,
988
- templates,
1070
+ schemas,
989
1071
  rootPath: yggRoot
990
1072
  };
991
1073
  }
@@ -999,7 +1081,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
999
1081
  const graphPath = toModelPath(dirPath, modelDir);
1000
1082
  let meta;
1001
1083
  try {
1002
- meta = await parseNodeYaml(path5.join(dirPath, "node.yaml"));
1084
+ meta = await parseNodeYaml(path6.join(dirPath, "node.yaml"));
1003
1085
  } catch (err) {
1004
1086
  nodeParseErrors.push({
1005
1087
  nodePath: graphPath,
@@ -1023,7 +1105,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1023
1105
  if (!entry.isDirectory()) continue;
1024
1106
  if (entry.name.startsWith(".")) continue;
1025
1107
  await scanModelDirectory(
1026
- path5.join(dirPath, entry.name),
1108
+ path6.join(dirPath, entry.name),
1027
1109
  modelDir,
1028
1110
  node,
1029
1111
  nodes,
@@ -1036,7 +1118,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1036
1118
  if (!entry.isDirectory()) continue;
1037
1119
  if (entry.name.startsWith(".")) continue;
1038
1120
  await scanModelDirectory(
1039
- path5.join(dirPath, entry.name),
1121
+ path6.join(dirPath, entry.name),
1040
1122
  modelDir,
1041
1123
  null,
1042
1124
  nodes,
@@ -1052,8 +1134,8 @@ async function loadAspects(aspectsDir) {
1052
1134
  const aspects = [];
1053
1135
  for (const entry of entries) {
1054
1136
  if (!entry.isDirectory()) continue;
1055
- const aspectYamlPath = path5.join(aspectsDir, entry.name, "aspect.yaml");
1056
- const aspect = await parseAspect(path5.join(aspectsDir, entry.name), aspectYamlPath);
1137
+ const aspectYamlPath = path6.join(aspectsDir, entry.name, "aspect.yaml");
1138
+ const aspect = await parseAspect(path6.join(aspectsDir, entry.name), aspectYamlPath);
1057
1139
  aspects.push(aspect);
1058
1140
  }
1059
1141
  return aspects;
@@ -1067,8 +1149,8 @@ async function loadFlows(flowsDir) {
1067
1149
  const flows = [];
1068
1150
  for (const entry of entries) {
1069
1151
  if (!entry.isDirectory()) continue;
1070
- const flowYamlPath = path5.join(flowsDir, entry.name, "flow.yaml");
1071
- const flow = await parseFlow(path5.join(flowsDir, entry.name), flowYamlPath);
1152
+ const flowYamlPath = path6.join(flowsDir, entry.name, "flow.yaml");
1153
+ const flow = await parseFlow(path6.join(flowsDir, entry.name), flowYamlPath);
1072
1154
  flows.push(flow);
1073
1155
  }
1074
1156
  return flows;
@@ -1084,12 +1166,12 @@ async function loadKnowledge(knowledgeDir, categories) {
1084
1166
  for (const catEntry of catEntries) {
1085
1167
  if (!catEntry.isDirectory()) continue;
1086
1168
  if (!categorySet.has(catEntry.name)) continue;
1087
- const catPath = path5.join(knowledgeDir, catEntry.name);
1169
+ const catPath = path6.join(knowledgeDir, catEntry.name);
1088
1170
  const itemEntries = await readdir3(catPath, { withFileTypes: true });
1089
1171
  for (const itemEntry of itemEntries) {
1090
1172
  if (!itemEntry.isDirectory()) continue;
1091
- const itemDir = path5.join(catPath, itemEntry.name);
1092
- const knowledgeYamlPath = path5.join(itemDir, "knowledge.yaml");
1173
+ const itemDir = path6.join(catPath, itemEntry.name);
1174
+ const knowledgeYamlPath = path6.join(itemDir, "knowledge.yaml");
1093
1175
  const relativePath = `${catEntry.name}/${itemEntry.name}`;
1094
1176
  const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
1095
1177
  items.push(item);
@@ -1099,17 +1181,17 @@ async function loadKnowledge(knowledgeDir, categories) {
1099
1181
  }
1100
1182
  return items;
1101
1183
  }
1102
- async function loadTemplates(templatesDir) {
1184
+ async function loadSchemas(templatesDir) {
1103
1185
  try {
1104
1186
  const entries = await readdir3(templatesDir, { withFileTypes: true });
1105
- const templates = [];
1187
+ const schemas = [];
1106
1188
  for (const entry of entries) {
1107
1189
  if (!entry.isFile()) continue;
1108
1190
  if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
1109
- const t = await parseTemplate(path5.join(templatesDir, entry.name));
1110
- templates.push(t);
1191
+ const s = await parseSchema(path6.join(templatesDir, entry.name));
1192
+ schemas.push(s);
1111
1193
  }
1112
- return templates;
1194
+ return schemas;
1113
1195
  } catch {
1114
1196
  return [];
1115
1197
  }
@@ -1117,7 +1199,7 @@ async function loadTemplates(templatesDir) {
1117
1199
 
1118
1200
  // src/core/context-builder.ts
1119
1201
  import { readFile as readFile10 } from "fs/promises";
1120
- import path6 from "path";
1202
+ import path7 from "path";
1121
1203
 
1122
1204
  // src/utils/tokens.ts
1123
1205
  function estimateTokens(text) {
@@ -1270,7 +1352,7 @@ ${a.content}`).join("\n\n");
1270
1352
  }
1271
1353
  async function buildOwnLayer(node, config, graphRootPath) {
1272
1354
  const parts = [];
1273
- const nodeYamlPath = path6.join(graphRootPath, "model", node.path, "node.yaml");
1355
+ const nodeYamlPath = path7.join(graphRootPath, "model", node.path, "node.yaml");
1274
1356
  try {
1275
1357
  const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
1276
1358
  parts.push(`### node.yaml
@@ -1387,13 +1469,13 @@ function collectAncestors(node) {
1387
1469
 
1388
1470
  // src/core/validator.ts
1389
1471
  import { readdir as readdir4 } from "fs/promises";
1390
- import path8 from "path";
1472
+ import path9 from "path";
1391
1473
 
1392
1474
  // src/utils/git.ts
1393
1475
  import { execSync } from "child_process";
1394
- import path7 from "path";
1476
+ import path8 from "path";
1395
1477
  function getLastCommitTimestamp(projectRoot, relativePath) {
1396
- const normalized = path7.normalize(relativePath).replace(/\\/g, "/");
1478
+ const normalized = path8.normalize(relativePath).replace(/\\/g, "/");
1397
1479
  try {
1398
1480
  const out = execSync(`git log -1 --format=%ct -- "${normalized}"`, {
1399
1481
  cwd: projectRoot,
@@ -1441,8 +1523,8 @@ async function validate(graph, scope = "all") {
1441
1523
  issues.push(...await checkContextBudget(graph));
1442
1524
  issues.push(...checkHighFanOut(graph));
1443
1525
  issues.push(...await checkStaleKnowledge(graph));
1444
- issues.push(...checkTemplates(graph));
1445
1526
  }
1527
+ issues.push(...checkSchemas(graph));
1446
1528
  issues.push(...checkRelationTargets(graph));
1447
1529
  issues.push(...checkNoCycles(graph));
1448
1530
  issues.push(...checkMappingOverlap(graph));
@@ -1815,7 +1897,7 @@ function checkScopeTagsDefined(graph) {
1815
1897
  async function checkUnknownKnowledgeCategories(graph) {
1816
1898
  const issues = [];
1817
1899
  const categorySet = new Set((graph.config.knowledge_categories ?? []).map((c) => c.name));
1818
- const knowledgeDir = path8.join(graph.rootPath, "knowledge");
1900
+ const knowledgeDir = path9.join(graph.rootPath, "knowledge");
1819
1901
  const existingDirs = /* @__PURE__ */ new Set();
1820
1902
  try {
1821
1903
  const entries = await readdir4(knowledgeDir, { withFileTypes: true });
@@ -1946,7 +2028,7 @@ async function checkMissingPatternExamples(graph) {
1946
2028
  const issues = [];
1947
2029
  const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
1948
2030
  if (!hasPatterns) return issues;
1949
- const patternsDir = path8.join(graph.rootPath, "knowledge", "patterns");
2031
+ const patternsDir = path9.join(graph.rootPath, "knowledge", "patterns");
1950
2032
  try {
1951
2033
  const entries = await readdir4(patternsDir, { withFileTypes: true });
1952
2034
  const exampleExtensions = /* @__PURE__ */ new Set([
@@ -1962,10 +2044,10 @@ async function checkMissingPatternExamples(graph) {
1962
2044
  ]);
1963
2045
  for (const e of entries) {
1964
2046
  if (!e.isDirectory()) continue;
1965
- const itemDir = path8.join(patternsDir, e.name);
2047
+ const itemDir = path9.join(patternsDir, e.name);
1966
2048
  const itemEntries = await readdir4(itemDir, { withFileTypes: true });
1967
2049
  const hasExample = itemEntries.some(
1968
- (f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path8.extname(f.name).toLowerCase()))
2050
+ (f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path9.extname(f.name).toLowerCase()))
1969
2051
  );
1970
2052
  if (!hasExample) {
1971
2053
  issues.push({
@@ -2016,8 +2098,8 @@ function getNodesInScope(k, graph) {
2016
2098
  async function checkStaleKnowledge(graph) {
2017
2099
  const issues = [];
2018
2100
  const stalenessDays = graph.config.quality?.knowledge_staleness_days ?? 90;
2019
- const projectRoot = path8.dirname(graph.rootPath);
2020
- const yggRel = path8.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
2101
+ const projectRoot = path9.dirname(graph.rootPath);
2102
+ const yggRel = path9.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
2021
2103
  for (const k of graph.knowledge) {
2022
2104
  const scopeNodes = getNodesInScope(k, graph);
2023
2105
  if (scopeNodes.length === 0) continue;
@@ -2096,51 +2178,29 @@ function checkUnpairedEvents(graph) {
2096
2178
  }
2097
2179
  return issues;
2098
2180
  }
2099
- function checkTemplates(graph) {
2181
+ var REQUIRED_SCHEMAS = ["node", "aspect", "flow", "knowledge"];
2182
+ function checkSchemas(graph) {
2100
2183
  const issues = [];
2101
- const allowedTypes = new Set(graph.config.node_types ?? []);
2102
- const allowedArtifacts = new Set(Object.keys(graph.config.artifacts ?? {}));
2103
- const typeToTemplate = /* @__PURE__ */ new Map();
2104
- for (const template of graph.templates) {
2105
- if (!allowedTypes.has(template.nodeType)) {
2184
+ const present = new Set(graph.schemas.map((s) => s.schemaType));
2185
+ for (const required of REQUIRED_SCHEMAS) {
2186
+ if (!present.has(required)) {
2106
2187
  issues.push({
2107
- severity: "error",
2108
- code: "E002",
2109
- rule: "unknown-node-type",
2110
- message: `Template for '${template.nodeType}' references node_type not in config.node_types (${[...allowedTypes].join(", ")})`
2111
- });
2112
- }
2113
- for (const artifact of template.suggestedArtifacts ?? []) {
2114
- if (!allowedArtifacts.has(artifact)) {
2115
- issues.push({
2116
- severity: "warning",
2117
- code: "W001",
2118
- rule: "missing-artifact",
2119
- message: `Template for '${template.nodeType}' suggests artifact '${artifact}' not defined in config.artifacts`
2120
- });
2121
- }
2122
- }
2123
- const existing = typeToTemplate.get(template.nodeType);
2124
- if (existing) {
2125
- issues.push({
2126
- severity: "error",
2127
- code: "E016",
2128
- rule: "duplicate-template",
2129
- message: `Multiple templates for node_type '${template.nodeType}'`
2188
+ severity: "warning",
2189
+ code: "W010",
2190
+ rule: "missing-schema",
2191
+ message: `Schema '${required}.yaml' missing from .yggdrasil/templates/`
2130
2192
  });
2131
- } else {
2132
- typeToTemplate.set(template.nodeType, template.nodeType);
2133
2193
  }
2134
2194
  }
2135
2195
  return issues;
2136
2196
  }
2137
2197
  async function checkDirectoriesHaveNodeYaml(graph) {
2138
2198
  const issues = [];
2139
- const modelDir = path8.join(graph.rootPath, "model");
2199
+ const modelDir = path9.join(graph.rootPath, "model");
2140
2200
  async function scanDir(dirPath, segments) {
2141
2201
  const entries = await readdir4(dirPath, { withFileTypes: true });
2142
2202
  const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
2143
- const dirName = path8.basename(dirPath);
2203
+ const dirName = path9.basename(dirPath);
2144
2204
  if (RESERVED_DIRS.has(dirName)) return;
2145
2205
  const hasContent = entries.some((e) => e.isFile()) || entries.some((e) => e.isDirectory());
2146
2206
  const graphPath = segments.join("/");
@@ -2157,7 +2217,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2157
2217
  if (!entry.isDirectory()) continue;
2158
2218
  if (RESERVED_DIRS.has(entry.name)) continue;
2159
2219
  if (entry.name.startsWith(".")) continue;
2160
- await scanDir(path8.join(dirPath, entry.name), [...segments, entry.name]);
2220
+ await scanDir(path9.join(dirPath, entry.name), [...segments, entry.name]);
2161
2221
  }
2162
2222
  }
2163
2223
  try {
@@ -2165,7 +2225,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2165
2225
  for (const entry of rootEntries) {
2166
2226
  if (!entry.isDirectory()) continue;
2167
2227
  if (entry.name.startsWith(".")) continue;
2168
- await scanDir(path8.join(modelDir, entry.name), [entry.name]);
2228
+ await scanDir(path9.join(modelDir, entry.name), [entry.name]);
2169
2229
  }
2170
2230
  } catch {
2171
2231
  }
@@ -2330,7 +2390,7 @@ import chalk2 from "chalk";
2330
2390
  // src/io/drift-state-store.ts
2331
2391
  import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
2332
2392
  import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
2333
- import path9 from "path";
2393
+ import path10 from "path";
2334
2394
  var DRIFT_STATE_FILE = ".drift-state";
2335
2395
  function getCanonicalHash(entry) {
2336
2396
  return typeof entry === "string" ? entry : entry.hash;
@@ -2339,7 +2399,7 @@ function getFileHashes(entry) {
2339
2399
  return typeof entry === "object" ? entry.files : void 0;
2340
2400
  }
2341
2401
  async function readDriftState(yggRoot) {
2342
- const filePath = path9.join(yggRoot, DRIFT_STATE_FILE);
2402
+ const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
2343
2403
  try {
2344
2404
  const content = await readFile11(filePath, "utf-8");
2345
2405
  const raw = parseYaml7(content);
@@ -2360,14 +2420,14 @@ async function readDriftState(yggRoot) {
2360
2420
  }
2361
2421
  }
2362
2422
  async function writeDriftState(yggRoot, state) {
2363
- const filePath = path9.join(yggRoot, DRIFT_STATE_FILE);
2423
+ const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
2364
2424
  const content = stringifyYaml(state);
2365
2425
  await writeFile3(filePath, content, "utf-8");
2366
2426
  }
2367
2427
 
2368
2428
  // src/utils/hash.ts
2369
2429
  import { readFile as readFile12, readdir as readdir5, stat as stat3 } from "fs/promises";
2370
- import path10 from "path";
2430
+ import path11 from "path";
2371
2431
  import { createHash } from "crypto";
2372
2432
  import { createRequire } from "module";
2373
2433
  var require2 = createRequire(import.meta.url);
@@ -2377,7 +2437,7 @@ async function hashFile(filePath) {
2377
2437
  return createHash("sha256").update(content).digest("hex");
2378
2438
  }
2379
2439
  async function hashPath(targetPath, options = {}) {
2380
- const projectRoot = options.projectRoot ? path10.resolve(options.projectRoot) : void 0;
2440
+ const projectRoot = options.projectRoot ? path11.resolve(options.projectRoot) : void 0;
2381
2441
  const gitignoreMatcher = await loadGitignoreMatcher(projectRoot);
2382
2442
  const targetStat = await stat3(targetPath);
2383
2443
  if (targetStat.isFile()) {
@@ -2400,7 +2460,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
2400
2460
  const entries = await readdir5(directoryPath, { withFileTypes: true });
2401
2461
  const result = [];
2402
2462
  for (const entry of entries) {
2403
- const absoluteChildPath = path10.join(directoryPath, entry.name);
2463
+ const absoluteChildPath = path11.join(directoryPath, entry.name);
2404
2464
  if (isIgnoredPath(absoluteChildPath, options.projectRoot, options.gitignoreMatcher)) {
2405
2465
  continue;
2406
2466
  }
@@ -2412,7 +2472,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
2412
2472
  );
2413
2473
  for (const nestedEntry of nested) {
2414
2474
  result.push({
2415
- path: path10.relative(rootDirectoryPath, path10.join(absoluteChildPath, nestedEntry.path)),
2475
+ path: path11.relative(rootDirectoryPath, path11.join(absoluteChildPath, nestedEntry.path)),
2416
2476
  hash: nestedEntry.hash
2417
2477
  });
2418
2478
  }
@@ -2422,7 +2482,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
2422
2482
  continue;
2423
2483
  }
2424
2484
  result.push({
2425
- path: path10.relative(rootDirectoryPath, absoluteChildPath),
2485
+ path: path11.relative(rootDirectoryPath, absoluteChildPath),
2426
2486
  hash: await hashFile(absoluteChildPath)
2427
2487
  });
2428
2488
  }
@@ -2433,7 +2493,7 @@ async function loadGitignoreMatcher(projectRoot) {
2433
2493
  return void 0;
2434
2494
  }
2435
2495
  try {
2436
- const gitignorePath = path10.join(projectRoot, ".gitignore");
2496
+ const gitignorePath = path11.join(projectRoot, ".gitignore");
2437
2497
  const gitignoreContent = await readFile12(gitignorePath, "utf-8");
2438
2498
  const matcher = ignoreFactory();
2439
2499
  matcher.add(gitignoreContent);
@@ -2446,7 +2506,7 @@ function isIgnoredPath(candidatePath, projectRoot, matcher) {
2446
2506
  if (!projectRoot || !matcher) {
2447
2507
  return false;
2448
2508
  }
2449
- const relativePath = path10.relative(projectRoot, candidatePath);
2509
+ const relativePath = path11.relative(projectRoot, candidatePath);
2450
2510
  if (relativePath === "" || relativePath.startsWith("..")) {
2451
2511
  return false;
2452
2512
  }
@@ -2456,13 +2516,13 @@ function hashString(content) {
2456
2516
  return createHash("sha256").update(content).digest("hex");
2457
2517
  }
2458
2518
  async function perFileHashes(projectRoot, mapping) {
2459
- const root = path10.resolve(projectRoot);
2519
+ const root = path11.resolve(projectRoot);
2460
2520
  const paths = mapping.paths ?? [];
2461
2521
  if (paths.length === 0) return [];
2462
2522
  const result = [];
2463
2523
  const gitignoreMatcher = await loadGitignoreMatcher(root);
2464
2524
  for (const p of paths) {
2465
- const absPath = path10.join(root, p);
2525
+ const absPath = path11.join(root, p);
2466
2526
  const st = await stat3(absPath);
2467
2527
  if (st.isFile()) {
2468
2528
  result.push({ path: p, hash: await hashFile(absPath) });
@@ -2473,7 +2533,7 @@ async function perFileHashes(projectRoot, mapping) {
2473
2533
  });
2474
2534
  for (const h of hashes) {
2475
2535
  result.push({
2476
- path: path10.join(p, h.path).split(path10.sep).join("/"),
2536
+ path: path11.join(p, h.path).split(path11.sep).join("/"),
2477
2537
  hash: h.hash
2478
2538
  });
2479
2539
  }
@@ -2482,12 +2542,12 @@ async function perFileHashes(projectRoot, mapping) {
2482
2542
  return result;
2483
2543
  }
2484
2544
  async function hashForMapping(projectRoot, mapping) {
2485
- const root = path10.resolve(projectRoot);
2545
+ const root = path11.resolve(projectRoot);
2486
2546
  const paths = mapping.paths ?? [];
2487
2547
  if (paths.length === 0) throw new Error("Invalid mapping for hash: no paths");
2488
2548
  const pairs = [];
2489
2549
  for (const p of paths) {
2490
- const absPath = path10.join(root, p);
2550
+ const absPath = path11.join(root, p);
2491
2551
  const st = await stat3(absPath);
2492
2552
  if (st.isFile()) {
2493
2553
  pairs.push({ path: p, hash: await hashFile(absPath) });
@@ -2502,9 +2562,9 @@ async function hashForMapping(projectRoot, mapping) {
2502
2562
 
2503
2563
  // src/core/drift-detector.ts
2504
2564
  import { access } from "fs/promises";
2505
- import path11 from "path";
2565
+ import path12 from "path";
2506
2566
  async function detectDrift(graph, filterNodePath) {
2507
- const projectRoot = path11.dirname(graph.rootPath);
2567
+ const projectRoot = path12.dirname(graph.rootPath);
2508
2568
  const driftState = await readDriftState(graph.rootPath);
2509
2569
  const entries = [];
2510
2570
  for (const [nodePath, node] of graph.nodes) {
@@ -2578,7 +2638,7 @@ async function diagnoseChangedFiles(projectRoot, mapping, storedFileHashes) {
2578
2638
  }
2579
2639
  async function allPathsMissing(projectRoot, mappingPaths) {
2580
2640
  for (const mp of mappingPaths) {
2581
- const absPath = path11.join(projectRoot, mp);
2641
+ const absPath = path12.join(projectRoot, mp);
2582
2642
  try {
2583
2643
  await access(absPath);
2584
2644
  return false;
@@ -2588,7 +2648,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
2588
2648
  return true;
2589
2649
  }
2590
2650
  async function syncDriftState(graph, nodePath) {
2591
- const projectRoot = path11.dirname(graph.rootPath);
2651
+ const projectRoot = path12.dirname(graph.rootPath);
2592
2652
  const node = graph.nodes.get(nodePath);
2593
2653
  if (!node) throw new Error(`Node not found: ${nodePath}`);
2594
2654
  const mapping = node.meta.mapping;
@@ -2760,10 +2820,10 @@ function registerTreeCommand(program2) {
2760
2820
  let roots;
2761
2821
  let showProjectName;
2762
2822
  if (options.root?.trim()) {
2763
- const path15 = options.root.trim().replace(/\/$/, "");
2764
- const node = graph.nodes.get(path15);
2823
+ const path16 = options.root.trim().replace(/\/$/, "");
2824
+ const node = graph.nodes.get(path16);
2765
2825
  if (!node) {
2766
- process.stderr.write(`Error: path '${path15}' not found
2826
+ process.stderr.write(`Error: path '${path16}' not found
2767
2827
  `);
2768
2828
  process.exit(1);
2769
2829
  }
@@ -2851,7 +2911,7 @@ function registerOwnerCommand(program2) {
2851
2911
 
2852
2912
  // src/core/dependency-resolver.ts
2853
2913
  import { execSync as execSync2 } from "child_process";
2854
- import path12 from "path";
2914
+ import path13 from "path";
2855
2915
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2856
2916
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
2857
2917
  function filterRelationType(relType, filter) {
@@ -2928,7 +2988,7 @@ function registerDepsCommand(program2) {
2928
2988
  // src/core/graph-from-git.ts
2929
2989
  import { mkdtemp, rm } from "fs/promises";
2930
2990
  import { tmpdir } from "os";
2931
- import path13 from "path";
2991
+ import path14 from "path";
2932
2992
  import { execSync as execSync3 } from "child_process";
2933
2993
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
2934
2994
  const yggPath = ".yggdrasil";
@@ -2939,8 +2999,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
2939
2999
  return null;
2940
3000
  }
2941
3001
  try {
2942
- tmpDir = await mkdtemp(path13.join(tmpdir(), "ygg-git-"));
2943
- const archivePath = path13.join(tmpDir, "archive.tar");
3002
+ tmpDir = await mkdtemp(path14.join(tmpdir(), "ygg-git-"));
3003
+ const archivePath = path14.join(tmpDir, "archive.tar");
2944
3004
  execSync3(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
2945
3005
  cwd: projectRoot,
2946
3006
  stdio: "pipe"
@@ -3010,14 +3070,14 @@ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
3010
3070
  }
3011
3071
  const chains = [];
3012
3072
  for (const node of transitiveOnly) {
3013
- const path15 = [];
3073
+ const path16 = [];
3014
3074
  let current = node;
3015
3075
  while (current) {
3016
- path15.unshift(current);
3076
+ path16.unshift(current);
3017
3077
  current = parent.get(current);
3018
3078
  }
3019
- if (path15.length >= 2) {
3020
- chains.push(path15.map((p) => `<- ${p}`).join(" "));
3079
+ if (path16.length >= 2) {
3080
+ chains.push(path16.map((p) => `<- ${p}`).join(" "));
3021
3081
  }
3022
3082
  }
3023
3083
  return chains.sort();
@@ -3069,7 +3129,7 @@ function registerImpactCommand(program2) {
3069
3129
  }
3070
3130
  }
3071
3131
  }
3072
- const budget = graph.config.quality?.context_budget ?? { warning: 5e3, error: 1e4 };
3132
+ const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
3073
3133
  process.stdout.write(`Impact of changes in ${nodePath}:
3074
3134
 
3075
3135
  `);
@@ -3160,11 +3220,11 @@ ${changedLine}${budgetLine}${driftLine}
3160
3220
  // src/io/journal-store.ts
3161
3221
  import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access2 } from "fs/promises";
3162
3222
  import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
3163
- import path14 from "path";
3223
+ import path15 from "path";
3164
3224
  var JOURNAL_FILE = ".journal.yaml";
3165
3225
  var ARCHIVE_DIR = "journals-archive";
3166
3226
  async function readJournal(yggRoot) {
3167
- const filePath = path14.join(yggRoot, JOURNAL_FILE);
3227
+ const filePath = path15.join(yggRoot, JOURNAL_FILE);
3168
3228
  try {
3169
3229
  const content = await readFile13(filePath, "utf-8");
3170
3230
  const raw = parseYaml8(content);
@@ -3179,13 +3239,13 @@ async function appendJournalEntry(yggRoot, note, target) {
3179
3239
  const at = (/* @__PURE__ */ new Date()).toISOString();
3180
3240
  const entry = target ? { at, target, note } : { at, note };
3181
3241
  entries.push(entry);
3182
- const filePath = path14.join(yggRoot, JOURNAL_FILE);
3242
+ const filePath = path15.join(yggRoot, JOURNAL_FILE);
3183
3243
  const content = stringifyYaml2({ entries });
3184
3244
  await writeFile4(filePath, content, "utf-8");
3185
3245
  return entry;
3186
3246
  }
3187
3247
  async function archiveJournal(yggRoot) {
3188
- const journalPath = path14.join(yggRoot, JOURNAL_FILE);
3248
+ const journalPath = path15.join(yggRoot, JOURNAL_FILE);
3189
3249
  try {
3190
3250
  await access2(journalPath);
3191
3251
  } catch {
@@ -3193,12 +3253,12 @@ async function archiveJournal(yggRoot) {
3193
3253
  }
3194
3254
  const entries = await readJournal(yggRoot);
3195
3255
  if (entries.length === 0) return null;
3196
- const archiveDir = path14.join(yggRoot, ARCHIVE_DIR);
3256
+ const archiveDir = path15.join(yggRoot, ARCHIVE_DIR);
3197
3257
  await mkdir3(archiveDir, { recursive: true });
3198
3258
  const now = /* @__PURE__ */ new Date();
3199
3259
  const timestamp = `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}-${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`;
3200
3260
  const archiveName = `.journal.${timestamp}.yaml`;
3201
- const archivePath = path14.join(archiveDir, archiveName);
3261
+ const archivePath = path15.join(archiveDir, archiveName);
3202
3262
  await rename(journalPath, archivePath);
3203
3263
  return { archiveName, entryCount: entries.length };
3204
3264
  }