@chrisdudek/yg 0.1.0 → 0.2.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
@@ -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
 
@@ -79,6 +87,16 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
79
87
 
80
88
  ---
81
89
 
90
+ ## 0. ATOMIC INVARIANT \u2014 OVERRIDES EVERYTHING
91
+
92
+ **CODE \u2194 GRAPH ARE ONE.**
93
+ - Code changed \u2192 graph artifacts MUST be updated IN THE SAME RESPONSE. No exceptions.
94
+ - Graph changed \u2192 source files MUST be verified/updated IN THE SAME RESPONSE. No exceptions.
95
+ - There is NO "code-only" response. There is NO "graph-only" response (unless explicitly a dry-run plan).
96
+ - If you complete a response and one side is changed but the other is not, you have VIOLATED the core contract of Yggdrasil.
97
+
98
+ ---
99
+
82
100
  ## 1. CORE PRINCIPLES (NON-NEGOTIABLE)
83
101
 
84
102
  1. **Graph First, Always:** Before answering a question, modifying code, or planning a feature, you MUST consult the graph.
@@ -88,6 +106,20 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
88
106
  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
107
  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
108
  7. **Never Touch Operational Metadata:** NEVER manually edit \`.yggdrasil/.drift-state\` or \`.yggdrasil/.journal.yaml\`.
109
+ 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.
110
+
111
+ ---
112
+
113
+ ## 1.5 FAILURE STATES
114
+
115
+ You have fundamentally broken Yggdrasil if you do any of the following:
116
+ - \u274C You modified source code without updating graph artifacts in the SAME response.
117
+ - \u274C You modified graph files without verifying/updating source code alignment in the SAME response.
118
+ - \u274C You resolved a code\u2194graph inconsistency without asking the user first.
119
+ - \u274C You created or edited a graph element without reading its schema in \`.yggdrasil/templates/\`.
120
+ - \u274C You ran \`yg drift-sync\` before updating graph artifacts.
121
+ - \u274C You ran \`yg drift-sync\` after a graph-only change without verifying source files.
122
+ - \u274C You used Blackbox coverage for greenfield/new code.
91
123
 
92
124
  ---
93
125
 
@@ -96,15 +128,15 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
96
128
  You do not need explicit "session" commands. Follow these conversational triggers:
97
129
 
98
130
  ### A. Preflight (First message of the conversation)
99
- Always execute these commands before doing anything else:
131
+ Always execute these commands before doing anything else. *(Exception: If the user's request is clearly read-only, like "explain this", run ONLY step 1).*
100
132
  1. \`yg journal-read\` -> If entries exist, consolidate them into the graph, then \`yg journal-archive\`.
101
133
  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
134
  3. \`yg status\` -> Report graph health.
103
135
  4. \`yg validate\` -> If W008 stale-knowledge appears, update the knowledge artifacts to reflect current node state.
104
136
 
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.**
137
+ ### B. Session Verification (Wrap-up)
138
+ Triggered by phrases like: "we're done", "wrap up", "that's enough", "done".
139
+ **Note: The graph should ALREADY be up to date. If the graph requires massive updates at this stage, YOU HAVE FAILED.**
108
140
  1. If iterative journal mode was used: consolidate notes to the graph, then \`yg journal-archive\`.
109
141
  2. \`yg drift\` -> Check if files changed manually during the conversation.
110
142
  3. \`yg validate\` -> Fix any structural errors.
@@ -112,64 +144,66 @@ Triggered by phrases like: "ko\u0144czymy", "wrap up", "to tyle", "gotowe".
112
144
 
113
145
  ---
114
146
 
115
- ## 3. WORKFLOW: MODIFYING OR CREATING FILES
147
+ ## 3. WORKFLOW: MODIFYING OR CREATING FILES (Code-First)
116
148
 
117
149
  You are NOT ALLOWED to edit or create source code without establishing graph coverage first.
118
150
 
119
151
  **Step 1: Check coverage** -> Run \`yg owner --file <path>\`
120
152
 
121
153
  **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)
154
+ 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
155
 
129
- *If you do not print this checklist and check off step 3, you have failed the core directive of Yggdrasil.*
156
+ - [ ] 1. Read Specification (ran \`yg build-context\`)
157
+ - [ ] 2. Modify Source Code
158
+ - [ ] 3. Sync Graph Artifacts (manually edit the node's artifact files IMMEDIATELY to match new code behavior)
159
+ - [ ] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
130
160
 
131
161
  **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.
162
+ STOP. Do not modify the code. First determine: **Is this greenfield or existing code?**
139
163
 
164
+ * **If GREENFIELD (empty directory, new project):** Do NOT offer blackbox. Create proper nodes (reverse engineering or upfront design) before implementing.
165
+ * **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.
166
+ * **If EXISTING CODE (legacy, third-party):** Present the user with 3 options and wait:
167
+ * **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
168
+ * **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node to establish ownership without deep semantic exploration.
169
+ * **Option 3: Abort/Change Plan:** Do not touch the file.
140
170
 
141
171
  ---
142
172
 
143
- ## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS
144
-
145
- When adding features or changing architecture, update the graph FIRST.
173
+ ## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS (Graph-First)
146
174
 
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.
175
+ When adding features, changing architecture, or doing graph-first design:
150
176
 
151
177
  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
178
  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.
179
+ * Check \`.yggdrasil/config.yaml\` for allowed \`node_types\` and \`tags\`.
180
+ * **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
181
  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.
182
+ 4. **Token Economy & W-codes:**
183
+ * W005/W006: Context package too large. Consider splitting the node.
184
+ * W008: Stale semantic memory. Update knowledge artifacts.
160
185
 
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.
186
+ **Graph Modification Checklist**
187
+ Whenever you change the graph structure or semantics, you MUST output and execute this exact checklist:
188
+
189
+ - [ ] 1. Read schema from \`.yggdrasil/templates/\` (node.yaml, aspect.yaml, flow.yaml, or knowledge.yaml for the element type)
190
+ - [ ] 2. Edit graph files (\`node.yaml\`, artifacts)
191
+ - [ ] 3. Verify corresponding source files exist and their behavior matches updated artifacts
192
+ - [ ] 4. Validate (ran \`yg validate\` \u2014 fix all Errors)
193
+ - [ ] 5. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER steps 2-3 are confirmed)
194
+
195
+ **Journaling (Iterative Mode Scope):**
196
+ * **Default:** Write changes directly to graph files immediately. Do not defer.
197
+ * **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
198
 
165
199
  ---
166
200
 
167
201
  ## 5. PATH CONVENTIONS (CRITICAL)
168
202
 
169
203
  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\`).
204
+ * **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
205
+ * **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
206
+ * **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
173
207
 
174
208
  ---
175
209
 
@@ -177,35 +211,84 @@ To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
177
211
 
178
212
  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
213
 
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.
214
+ * **\`.yggdrasil/config.yaml\`**: Defines \`node_types\`, \`tags\`, \`artifacts\`, \`knowledge_categories\`.
215
+ * **\`.yggdrasil/templates/\`**: Schemas for each graph layer \u2014 \`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`.
216
+ * **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files.
217
+ * **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` and \`.md\` content.
218
+ * **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` and \`.md\` content.
219
+ * **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom. Directory contains \`knowledge.yaml\` and \`.md\` content.
188
220
 
189
221
  ---
190
222
 
191
- ## 7. CLI TOOLS REFERENCE (\`yg\`)
223
+ ## 7. CONTEXT ASSEMBLY & KNOWLEDGE DECONSTRUCTION (HOW TO MAP FILES)
224
+
225
+ 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.
226
+
227
+ 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.
228
+
229
+ ### CRITICAL RULE: CAPTURE INTENT, BUT NEVER INVENT IT
230
+ 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".
231
+
232
+ 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.
233
+ 2. **NEVER Invent the "Why":** Files that imply human judgment (like \`decisions.md\` or \`knowledge/invariants\`) must reflect ACTUAL human choices.
234
+ 3. **NO Hallucinations:** You MUST NEVER infer or hallucinate a rationale, an architectural decision, or a business rule.
235
+ 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.
236
+
237
+ When mapping a file, execute this mental routing:
238
+
239
+ ### Layer 1: Unit Identity (Local Node Artifacts)
240
+ * **What goes here:** Things exclusively true for this specific node.
241
+ * **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\`.
242
+ * **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.
243
+ * **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.
244
+
245
+ **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.
246
+
247
+ ### Layer 2: Surroundings (Relations & Flows)
248
+ * **What goes here:** How this node interacts with others. You must not duplicate external interfaces locally.
249
+ * **Routing:**
250
+ * 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).
251
+ * 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.
252
+
253
+ ### Layer 3: Domain Context (Hierarchy)
254
+ * **What goes here:** Business rules shared by a family of nodes.
255
+ * **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.
256
+
257
+ ### Layer 4: Cross-Cutting Rules (Aspects)
258
+ * **What goes here:** Horizontal requirements like logging, auth, rate-limiting, or specific frameworks.
259
+ * **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.
260
+
261
+ ### Layer 5: Long-Term Memory (Knowledge Elements)
262
+ * **What goes here:** Global architectural decisions, design patterns, and systemic invariants.
263
+ * **Routing:** Read \`config.yaml\` (the \`knowledge_categories\` section) to know what categories exist.
264
+ * If the file implements a standard pattern: Do not describe the pattern locally. Add a \`knowledge\` reference in \`node.yaml\` to the existing pattern.
265
+ * 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.
266
+
267
+ **THE COMPLETENESS CHECK:**
268
+ 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?"*
269
+ - If no -> You missed a local constraint, a relation, or you failed to capture the user's provided rationale.
270
+ - If yes, but the local files are bloated -> You failed to deconstruct knowledge into Tags, Aspects, Flows, and Hierarchy. Fix the routing.
271
+
272
+ ---
273
+
274
+ ## 8. CLI TOOLS REFERENCE (\`yg\`)
192
275
 
193
276
  Always use these exact commands.
194
277
 
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.
278
+ * \`yg owner --file <file_path>\` -> Find owning node.
279
+ * \`yg build-context --node <node_path>\` -> Assemble strict specification.
280
+ * \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
281
+ * \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
282
+ * \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
283
+ * \`yg status\` -> Graph health metrics.
284
+ * \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
285
+ * \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
286
+ * \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
204
287
 
205
288
  *(Iterative mode only)*
206
- * \`yg journal-read\`
207
- * \`yg journal-add --note "<content>" [--target <node_path>]\`
208
- * \`yg journal-archive\`
289
+ * \`yg journal-read\`
290
+ * \`yg journal-add --note "<content>" [--target <node_path>]\`
291
+ * \`yg journal-archive\`
209
292
  `;
210
293
 
211
294
  // src/templates/platform.ts
@@ -532,7 +615,7 @@ function registerInitCommand(program2) {
532
615
  process.stdout.write(" .yggdrasil/flows/\n");
533
616
  process.stdout.write(" .yggdrasil/knowledge/ (decisions, patterns, invariants)\n");
534
617
  process.stdout.write(
535
- " .yggdrasil/templates/ (module, service, library, node, aspect, flow, knowledge)\n"
618
+ " .yggdrasil/templates/ (node, aspect, flow, knowledge)\n"
536
619
  );
537
620
  process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
538
621
 
@@ -546,7 +629,7 @@ function registerInitCommand(program2) {
546
629
 
547
630
  // src/core/graph-loader.ts
548
631
  import { readdir as readdir3 } from "fs/promises";
549
- import path5 from "path";
632
+ import path6 from "path";
550
633
 
551
634
  // src/io/config-parser.ts
552
635
  import { readFile as readFile3 } from "fs/promises";
@@ -554,7 +637,7 @@ import { parse as parseYaml } from "yaml";
554
637
  var DEFAULT_QUALITY = {
555
638
  min_artifact_length: 50,
556
639
  max_direct_relations: 10,
557
- context_budget: { warning: 5e3, error: 1e4 },
640
+ context_budget: { warning: 1e4, error: 2e4 },
558
641
  knowledge_staleness_days: 90
559
642
  };
560
643
  async function parseConfig(filePath) {
@@ -869,30 +952,24 @@ function parseScope(raw, filePath) {
869
952
 
870
953
  // src/io/template-parser.ts
871
954
  import { readFile as readFile9 } from "fs/promises";
955
+ import path4 from "path";
872
956
  import { parse as parseYaml6 } from "yaml";
873
- async function parseTemplate(filePath) {
957
+ async function parseSchema(filePath) {
874
958
  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
- };
959
+ parseYaml6(content);
960
+ const schemaType = path4.basename(filePath, path4.extname(filePath));
961
+ return { schemaType };
885
962
  }
886
963
 
887
964
  // src/utils/paths.ts
888
- import path4 from "path";
965
+ import path5 from "path";
889
966
  import { fileURLToPath as fileURLToPath2 } from "url";
890
967
  import { stat as stat2 } from "fs/promises";
891
968
  async function findYggRoot(projectRoot) {
892
- let current = path4.resolve(projectRoot);
893
- const root = path4.parse(current).root;
969
+ let current = path5.resolve(projectRoot);
970
+ const root = path5.parse(current).root;
894
971
  while (true) {
895
- const yggPath = path4.join(current, ".yggdrasil");
972
+ const yggPath = path5.join(current, ".yggdrasil");
896
973
  try {
897
974
  const st = await stat2(yggPath);
898
975
  if (!st.isDirectory()) {
@@ -906,7 +983,7 @@ async function findYggRoot(projectRoot) {
906
983
  if (current === root) {
907
984
  throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
908
985
  }
909
- current = path4.dirname(current);
986
+ current = path5.dirname(current);
910
987
  continue;
911
988
  }
912
989
  throw err;
@@ -922,18 +999,18 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
922
999
  if (normalizedInput.length === 0) {
923
1000
  throw new Error("Path cannot be empty");
924
1001
  }
925
- const absolute = path4.resolve(projectRoot, normalizedInput);
926
- const relative = path4.relative(projectRoot, absolute);
927
- const isOutside = relative.startsWith("..") || path4.isAbsolute(relative);
1002
+ const absolute = path5.resolve(projectRoot, normalizedInput);
1003
+ const relative = path5.relative(projectRoot, absolute);
1004
+ const isOutside = relative.startsWith("..") || path5.isAbsolute(relative);
928
1005
  if (isOutside) {
929
1006
  throw new Error(`Path is outside project root: ${rawPath}`);
930
1007
  }
931
- return relative.split(path4.sep).join("/");
1008
+ return relative.split(path5.sep).join("/");
932
1009
  }
933
1010
 
934
1011
  // src/core/graph-loader.ts
935
1012
  function toModelPath(absolutePath, modelDir) {
936
- return path5.relative(modelDir, absolutePath).split(path5.sep).join("/");
1013
+ return path6.relative(modelDir, absolutePath).split(path6.sep).join("/");
937
1014
  }
938
1015
  var FALLBACK_CONFIG = {
939
1016
  name: "",
@@ -949,14 +1026,14 @@ async function loadGraph(projectRoot, options = {}) {
949
1026
  let configError;
950
1027
  let config = FALLBACK_CONFIG;
951
1028
  try {
952
- config = await parseConfig(path5.join(yggRoot, "config.yaml"));
1029
+ config = await parseConfig(path6.join(yggRoot, "config.yaml"));
953
1030
  } catch (error) {
954
1031
  if (!options.tolerateInvalidConfig) {
955
1032
  throw error;
956
1033
  }
957
1034
  configError = error.message;
958
1035
  }
959
- const modelDir = path5.join(yggRoot, "model");
1036
+ const modelDir = path6.join(yggRoot, "model");
960
1037
  const nodes = /* @__PURE__ */ new Map();
961
1038
  const nodeParseErrors = [];
962
1039
  const artifactFilenames = Object.keys(config.artifacts ?? {});
@@ -970,13 +1047,13 @@ async function loadGraph(projectRoot, options = {}) {
970
1047
  }
971
1048
  throw err;
972
1049
  }
973
- const aspects = await loadAspects(path5.join(yggRoot, "aspects"));
974
- const flows = await loadFlows(path5.join(yggRoot, "flows"));
1050
+ const aspects = await loadAspects(path6.join(yggRoot, "aspects"));
1051
+ const flows = await loadFlows(path6.join(yggRoot, "flows"));
975
1052
  const knowledge = await loadKnowledge(
976
- path5.join(yggRoot, "knowledge"),
1053
+ path6.join(yggRoot, "knowledge"),
977
1054
  config.knowledge_categories
978
1055
  );
979
- const templates = await loadTemplates(path5.join(yggRoot, "templates"));
1056
+ const schemas = await loadSchemas(path6.join(yggRoot, "templates"));
980
1057
  return {
981
1058
  config,
982
1059
  configError,
@@ -985,7 +1062,7 @@ async function loadGraph(projectRoot, options = {}) {
985
1062
  aspects,
986
1063
  flows,
987
1064
  knowledge,
988
- templates,
1065
+ schemas,
989
1066
  rootPath: yggRoot
990
1067
  };
991
1068
  }
@@ -999,7 +1076,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
999
1076
  const graphPath = toModelPath(dirPath, modelDir);
1000
1077
  let meta;
1001
1078
  try {
1002
- meta = await parseNodeYaml(path5.join(dirPath, "node.yaml"));
1079
+ meta = await parseNodeYaml(path6.join(dirPath, "node.yaml"));
1003
1080
  } catch (err) {
1004
1081
  nodeParseErrors.push({
1005
1082
  nodePath: graphPath,
@@ -1023,7 +1100,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1023
1100
  if (!entry.isDirectory()) continue;
1024
1101
  if (entry.name.startsWith(".")) continue;
1025
1102
  await scanModelDirectory(
1026
- path5.join(dirPath, entry.name),
1103
+ path6.join(dirPath, entry.name),
1027
1104
  modelDir,
1028
1105
  node,
1029
1106
  nodes,
@@ -1036,7 +1113,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1036
1113
  if (!entry.isDirectory()) continue;
1037
1114
  if (entry.name.startsWith(".")) continue;
1038
1115
  await scanModelDirectory(
1039
- path5.join(dirPath, entry.name),
1116
+ path6.join(dirPath, entry.name),
1040
1117
  modelDir,
1041
1118
  null,
1042
1119
  nodes,
@@ -1052,8 +1129,8 @@ async function loadAspects(aspectsDir) {
1052
1129
  const aspects = [];
1053
1130
  for (const entry of entries) {
1054
1131
  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);
1132
+ const aspectYamlPath = path6.join(aspectsDir, entry.name, "aspect.yaml");
1133
+ const aspect = await parseAspect(path6.join(aspectsDir, entry.name), aspectYamlPath);
1057
1134
  aspects.push(aspect);
1058
1135
  }
1059
1136
  return aspects;
@@ -1067,8 +1144,8 @@ async function loadFlows(flowsDir) {
1067
1144
  const flows = [];
1068
1145
  for (const entry of entries) {
1069
1146
  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);
1147
+ const flowYamlPath = path6.join(flowsDir, entry.name, "flow.yaml");
1148
+ const flow = await parseFlow(path6.join(flowsDir, entry.name), flowYamlPath);
1072
1149
  flows.push(flow);
1073
1150
  }
1074
1151
  return flows;
@@ -1084,12 +1161,12 @@ async function loadKnowledge(knowledgeDir, categories) {
1084
1161
  for (const catEntry of catEntries) {
1085
1162
  if (!catEntry.isDirectory()) continue;
1086
1163
  if (!categorySet.has(catEntry.name)) continue;
1087
- const catPath = path5.join(knowledgeDir, catEntry.name);
1164
+ const catPath = path6.join(knowledgeDir, catEntry.name);
1088
1165
  const itemEntries = await readdir3(catPath, { withFileTypes: true });
1089
1166
  for (const itemEntry of itemEntries) {
1090
1167
  if (!itemEntry.isDirectory()) continue;
1091
- const itemDir = path5.join(catPath, itemEntry.name);
1092
- const knowledgeYamlPath = path5.join(itemDir, "knowledge.yaml");
1168
+ const itemDir = path6.join(catPath, itemEntry.name);
1169
+ const knowledgeYamlPath = path6.join(itemDir, "knowledge.yaml");
1093
1170
  const relativePath = `${catEntry.name}/${itemEntry.name}`;
1094
1171
  const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
1095
1172
  items.push(item);
@@ -1099,17 +1176,17 @@ async function loadKnowledge(knowledgeDir, categories) {
1099
1176
  }
1100
1177
  return items;
1101
1178
  }
1102
- async function loadTemplates(templatesDir) {
1179
+ async function loadSchemas(templatesDir) {
1103
1180
  try {
1104
1181
  const entries = await readdir3(templatesDir, { withFileTypes: true });
1105
- const templates = [];
1182
+ const schemas = [];
1106
1183
  for (const entry of entries) {
1107
1184
  if (!entry.isFile()) continue;
1108
1185
  if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
1109
- const t = await parseTemplate(path5.join(templatesDir, entry.name));
1110
- templates.push(t);
1186
+ const s = await parseSchema(path6.join(templatesDir, entry.name));
1187
+ schemas.push(s);
1111
1188
  }
1112
- return templates;
1189
+ return schemas;
1113
1190
  } catch {
1114
1191
  return [];
1115
1192
  }
@@ -1117,7 +1194,7 @@ async function loadTemplates(templatesDir) {
1117
1194
 
1118
1195
  // src/core/context-builder.ts
1119
1196
  import { readFile as readFile10 } from "fs/promises";
1120
- import path6 from "path";
1197
+ import path7 from "path";
1121
1198
 
1122
1199
  // src/utils/tokens.ts
1123
1200
  function estimateTokens(text) {
@@ -1270,7 +1347,7 @@ ${a.content}`).join("\n\n");
1270
1347
  }
1271
1348
  async function buildOwnLayer(node, config, graphRootPath) {
1272
1349
  const parts = [];
1273
- const nodeYamlPath = path6.join(graphRootPath, "model", node.path, "node.yaml");
1350
+ const nodeYamlPath = path7.join(graphRootPath, "model", node.path, "node.yaml");
1274
1351
  try {
1275
1352
  const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
1276
1353
  parts.push(`### node.yaml
@@ -1387,13 +1464,13 @@ function collectAncestors(node) {
1387
1464
 
1388
1465
  // src/core/validator.ts
1389
1466
  import { readdir as readdir4 } from "fs/promises";
1390
- import path8 from "path";
1467
+ import path9 from "path";
1391
1468
 
1392
1469
  // src/utils/git.ts
1393
1470
  import { execSync } from "child_process";
1394
- import path7 from "path";
1471
+ import path8 from "path";
1395
1472
  function getLastCommitTimestamp(projectRoot, relativePath) {
1396
- const normalized = path7.normalize(relativePath).replace(/\\/g, "/");
1473
+ const normalized = path8.normalize(relativePath).replace(/\\/g, "/");
1397
1474
  try {
1398
1475
  const out = execSync(`git log -1 --format=%ct -- "${normalized}"`, {
1399
1476
  cwd: projectRoot,
@@ -1441,8 +1518,8 @@ async function validate(graph, scope = "all") {
1441
1518
  issues.push(...await checkContextBudget(graph));
1442
1519
  issues.push(...checkHighFanOut(graph));
1443
1520
  issues.push(...await checkStaleKnowledge(graph));
1444
- issues.push(...checkTemplates(graph));
1445
1521
  }
1522
+ issues.push(...checkSchemas(graph));
1446
1523
  issues.push(...checkRelationTargets(graph));
1447
1524
  issues.push(...checkNoCycles(graph));
1448
1525
  issues.push(...checkMappingOverlap(graph));
@@ -1815,7 +1892,7 @@ function checkScopeTagsDefined(graph) {
1815
1892
  async function checkUnknownKnowledgeCategories(graph) {
1816
1893
  const issues = [];
1817
1894
  const categorySet = new Set((graph.config.knowledge_categories ?? []).map((c) => c.name));
1818
- const knowledgeDir = path8.join(graph.rootPath, "knowledge");
1895
+ const knowledgeDir = path9.join(graph.rootPath, "knowledge");
1819
1896
  const existingDirs = /* @__PURE__ */ new Set();
1820
1897
  try {
1821
1898
  const entries = await readdir4(knowledgeDir, { withFileTypes: true });
@@ -1946,7 +2023,7 @@ async function checkMissingPatternExamples(graph) {
1946
2023
  const issues = [];
1947
2024
  const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
1948
2025
  if (!hasPatterns) return issues;
1949
- const patternsDir = path8.join(graph.rootPath, "knowledge", "patterns");
2026
+ const patternsDir = path9.join(graph.rootPath, "knowledge", "patterns");
1950
2027
  try {
1951
2028
  const entries = await readdir4(patternsDir, { withFileTypes: true });
1952
2029
  const exampleExtensions = /* @__PURE__ */ new Set([
@@ -1962,10 +2039,10 @@ async function checkMissingPatternExamples(graph) {
1962
2039
  ]);
1963
2040
  for (const e of entries) {
1964
2041
  if (!e.isDirectory()) continue;
1965
- const itemDir = path8.join(patternsDir, e.name);
2042
+ const itemDir = path9.join(patternsDir, e.name);
1966
2043
  const itemEntries = await readdir4(itemDir, { withFileTypes: true });
1967
2044
  const hasExample = itemEntries.some(
1968
- (f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path8.extname(f.name).toLowerCase()))
2045
+ (f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path9.extname(f.name).toLowerCase()))
1969
2046
  );
1970
2047
  if (!hasExample) {
1971
2048
  issues.push({
@@ -2016,8 +2093,8 @@ function getNodesInScope(k, graph) {
2016
2093
  async function checkStaleKnowledge(graph) {
2017
2094
  const issues = [];
2018
2095
  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";
2096
+ const projectRoot = path9.dirname(graph.rootPath);
2097
+ const yggRel = path9.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
2021
2098
  for (const k of graph.knowledge) {
2022
2099
  const scopeNodes = getNodesInScope(k, graph);
2023
2100
  if (scopeNodes.length === 0) continue;
@@ -2096,51 +2173,29 @@ function checkUnpairedEvents(graph) {
2096
2173
  }
2097
2174
  return issues;
2098
2175
  }
2099
- function checkTemplates(graph) {
2176
+ var REQUIRED_SCHEMAS = ["node", "aspect", "flow", "knowledge"];
2177
+ function checkSchemas(graph) {
2100
2178
  const issues = [];
2101
- const allowedTypes = new Set(graph.config.node_types ?? []);
2102
- const allowedArtifacts = new Set(Object.keys(graph.config.artifacts ?? {}));
2103
- const typeToTemplate = /* @__PURE__ */ new Map();
2104
- for (const template of graph.templates) {
2105
- if (!allowedTypes.has(template.nodeType)) {
2106
- issues.push({
2107
- severity: "error",
2108
- code: "E002",
2109
- rule: "unknown-node-type",
2110
- message: `Template for '${template.nodeType}' references node_type not in config.node_types (${[...allowedTypes].join(", ")})`
2111
- });
2112
- }
2113
- for (const artifact of template.suggestedArtifacts ?? []) {
2114
- if (!allowedArtifacts.has(artifact)) {
2115
- issues.push({
2116
- severity: "warning",
2117
- code: "W001",
2118
- rule: "missing-artifact",
2119
- message: `Template for '${template.nodeType}' suggests artifact '${artifact}' not defined in config.artifacts`
2120
- });
2121
- }
2122
- }
2123
- const existing = typeToTemplate.get(template.nodeType);
2124
- if (existing) {
2179
+ const present = new Set(graph.schemas.map((s) => s.schemaType));
2180
+ for (const required of REQUIRED_SCHEMAS) {
2181
+ if (!present.has(required)) {
2125
2182
  issues.push({
2126
- severity: "error",
2127
- code: "E016",
2128
- rule: "duplicate-template",
2129
- message: `Multiple templates for node_type '${template.nodeType}'`
2183
+ severity: "warning",
2184
+ code: "W010",
2185
+ rule: "missing-schema",
2186
+ message: `Schema '${required}.yaml' missing from .yggdrasil/templates/`
2130
2187
  });
2131
- } else {
2132
- typeToTemplate.set(template.nodeType, template.nodeType);
2133
2188
  }
2134
2189
  }
2135
2190
  return issues;
2136
2191
  }
2137
2192
  async function checkDirectoriesHaveNodeYaml(graph) {
2138
2193
  const issues = [];
2139
- const modelDir = path8.join(graph.rootPath, "model");
2194
+ const modelDir = path9.join(graph.rootPath, "model");
2140
2195
  async function scanDir(dirPath, segments) {
2141
2196
  const entries = await readdir4(dirPath, { withFileTypes: true });
2142
2197
  const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
2143
- const dirName = path8.basename(dirPath);
2198
+ const dirName = path9.basename(dirPath);
2144
2199
  if (RESERVED_DIRS.has(dirName)) return;
2145
2200
  const hasContent = entries.some((e) => e.isFile()) || entries.some((e) => e.isDirectory());
2146
2201
  const graphPath = segments.join("/");
@@ -2157,7 +2212,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2157
2212
  if (!entry.isDirectory()) continue;
2158
2213
  if (RESERVED_DIRS.has(entry.name)) continue;
2159
2214
  if (entry.name.startsWith(".")) continue;
2160
- await scanDir(path8.join(dirPath, entry.name), [...segments, entry.name]);
2215
+ await scanDir(path9.join(dirPath, entry.name), [...segments, entry.name]);
2161
2216
  }
2162
2217
  }
2163
2218
  try {
@@ -2165,7 +2220,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2165
2220
  for (const entry of rootEntries) {
2166
2221
  if (!entry.isDirectory()) continue;
2167
2222
  if (entry.name.startsWith(".")) continue;
2168
- await scanDir(path8.join(modelDir, entry.name), [entry.name]);
2223
+ await scanDir(path9.join(modelDir, entry.name), [entry.name]);
2169
2224
  }
2170
2225
  } catch {
2171
2226
  }
@@ -2330,7 +2385,7 @@ import chalk2 from "chalk";
2330
2385
  // src/io/drift-state-store.ts
2331
2386
  import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
2332
2387
  import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
2333
- import path9 from "path";
2388
+ import path10 from "path";
2334
2389
  var DRIFT_STATE_FILE = ".drift-state";
2335
2390
  function getCanonicalHash(entry) {
2336
2391
  return typeof entry === "string" ? entry : entry.hash;
@@ -2339,7 +2394,7 @@ function getFileHashes(entry) {
2339
2394
  return typeof entry === "object" ? entry.files : void 0;
2340
2395
  }
2341
2396
  async function readDriftState(yggRoot) {
2342
- const filePath = path9.join(yggRoot, DRIFT_STATE_FILE);
2397
+ const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
2343
2398
  try {
2344
2399
  const content = await readFile11(filePath, "utf-8");
2345
2400
  const raw = parseYaml7(content);
@@ -2360,14 +2415,14 @@ async function readDriftState(yggRoot) {
2360
2415
  }
2361
2416
  }
2362
2417
  async function writeDriftState(yggRoot, state) {
2363
- const filePath = path9.join(yggRoot, DRIFT_STATE_FILE);
2418
+ const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
2364
2419
  const content = stringifyYaml(state);
2365
2420
  await writeFile3(filePath, content, "utf-8");
2366
2421
  }
2367
2422
 
2368
2423
  // src/utils/hash.ts
2369
2424
  import { readFile as readFile12, readdir as readdir5, stat as stat3 } from "fs/promises";
2370
- import path10 from "path";
2425
+ import path11 from "path";
2371
2426
  import { createHash } from "crypto";
2372
2427
  import { createRequire } from "module";
2373
2428
  var require2 = createRequire(import.meta.url);
@@ -2377,7 +2432,7 @@ async function hashFile(filePath) {
2377
2432
  return createHash("sha256").update(content).digest("hex");
2378
2433
  }
2379
2434
  async function hashPath(targetPath, options = {}) {
2380
- const projectRoot = options.projectRoot ? path10.resolve(options.projectRoot) : void 0;
2435
+ const projectRoot = options.projectRoot ? path11.resolve(options.projectRoot) : void 0;
2381
2436
  const gitignoreMatcher = await loadGitignoreMatcher(projectRoot);
2382
2437
  const targetStat = await stat3(targetPath);
2383
2438
  if (targetStat.isFile()) {
@@ -2400,7 +2455,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
2400
2455
  const entries = await readdir5(directoryPath, { withFileTypes: true });
2401
2456
  const result = [];
2402
2457
  for (const entry of entries) {
2403
- const absoluteChildPath = path10.join(directoryPath, entry.name);
2458
+ const absoluteChildPath = path11.join(directoryPath, entry.name);
2404
2459
  if (isIgnoredPath(absoluteChildPath, options.projectRoot, options.gitignoreMatcher)) {
2405
2460
  continue;
2406
2461
  }
@@ -2412,7 +2467,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
2412
2467
  );
2413
2468
  for (const nestedEntry of nested) {
2414
2469
  result.push({
2415
- path: path10.relative(rootDirectoryPath, path10.join(absoluteChildPath, nestedEntry.path)),
2470
+ path: path11.relative(rootDirectoryPath, path11.join(absoluteChildPath, nestedEntry.path)),
2416
2471
  hash: nestedEntry.hash
2417
2472
  });
2418
2473
  }
@@ -2422,7 +2477,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
2422
2477
  continue;
2423
2478
  }
2424
2479
  result.push({
2425
- path: path10.relative(rootDirectoryPath, absoluteChildPath),
2480
+ path: path11.relative(rootDirectoryPath, absoluteChildPath),
2426
2481
  hash: await hashFile(absoluteChildPath)
2427
2482
  });
2428
2483
  }
@@ -2433,7 +2488,7 @@ async function loadGitignoreMatcher(projectRoot) {
2433
2488
  return void 0;
2434
2489
  }
2435
2490
  try {
2436
- const gitignorePath = path10.join(projectRoot, ".gitignore");
2491
+ const gitignorePath = path11.join(projectRoot, ".gitignore");
2437
2492
  const gitignoreContent = await readFile12(gitignorePath, "utf-8");
2438
2493
  const matcher = ignoreFactory();
2439
2494
  matcher.add(gitignoreContent);
@@ -2446,7 +2501,7 @@ function isIgnoredPath(candidatePath, projectRoot, matcher) {
2446
2501
  if (!projectRoot || !matcher) {
2447
2502
  return false;
2448
2503
  }
2449
- const relativePath = path10.relative(projectRoot, candidatePath);
2504
+ const relativePath = path11.relative(projectRoot, candidatePath);
2450
2505
  if (relativePath === "" || relativePath.startsWith("..")) {
2451
2506
  return false;
2452
2507
  }
@@ -2456,13 +2511,13 @@ function hashString(content) {
2456
2511
  return createHash("sha256").update(content).digest("hex");
2457
2512
  }
2458
2513
  async function perFileHashes(projectRoot, mapping) {
2459
- const root = path10.resolve(projectRoot);
2514
+ const root = path11.resolve(projectRoot);
2460
2515
  const paths = mapping.paths ?? [];
2461
2516
  if (paths.length === 0) return [];
2462
2517
  const result = [];
2463
2518
  const gitignoreMatcher = await loadGitignoreMatcher(root);
2464
2519
  for (const p of paths) {
2465
- const absPath = path10.join(root, p);
2520
+ const absPath = path11.join(root, p);
2466
2521
  const st = await stat3(absPath);
2467
2522
  if (st.isFile()) {
2468
2523
  result.push({ path: p, hash: await hashFile(absPath) });
@@ -2473,7 +2528,7 @@ async function perFileHashes(projectRoot, mapping) {
2473
2528
  });
2474
2529
  for (const h of hashes) {
2475
2530
  result.push({
2476
- path: path10.join(p, h.path).split(path10.sep).join("/"),
2531
+ path: path11.join(p, h.path).split(path11.sep).join("/"),
2477
2532
  hash: h.hash
2478
2533
  });
2479
2534
  }
@@ -2482,12 +2537,12 @@ async function perFileHashes(projectRoot, mapping) {
2482
2537
  return result;
2483
2538
  }
2484
2539
  async function hashForMapping(projectRoot, mapping) {
2485
- const root = path10.resolve(projectRoot);
2540
+ const root = path11.resolve(projectRoot);
2486
2541
  const paths = mapping.paths ?? [];
2487
2542
  if (paths.length === 0) throw new Error("Invalid mapping for hash: no paths");
2488
2543
  const pairs = [];
2489
2544
  for (const p of paths) {
2490
- const absPath = path10.join(root, p);
2545
+ const absPath = path11.join(root, p);
2491
2546
  const st = await stat3(absPath);
2492
2547
  if (st.isFile()) {
2493
2548
  pairs.push({ path: p, hash: await hashFile(absPath) });
@@ -2502,9 +2557,9 @@ async function hashForMapping(projectRoot, mapping) {
2502
2557
 
2503
2558
  // src/core/drift-detector.ts
2504
2559
  import { access } from "fs/promises";
2505
- import path11 from "path";
2560
+ import path12 from "path";
2506
2561
  async function detectDrift(graph, filterNodePath) {
2507
- const projectRoot = path11.dirname(graph.rootPath);
2562
+ const projectRoot = path12.dirname(graph.rootPath);
2508
2563
  const driftState = await readDriftState(graph.rootPath);
2509
2564
  const entries = [];
2510
2565
  for (const [nodePath, node] of graph.nodes) {
@@ -2578,7 +2633,7 @@ async function diagnoseChangedFiles(projectRoot, mapping, storedFileHashes) {
2578
2633
  }
2579
2634
  async function allPathsMissing(projectRoot, mappingPaths) {
2580
2635
  for (const mp of mappingPaths) {
2581
- const absPath = path11.join(projectRoot, mp);
2636
+ const absPath = path12.join(projectRoot, mp);
2582
2637
  try {
2583
2638
  await access(absPath);
2584
2639
  return false;
@@ -2588,7 +2643,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
2588
2643
  return true;
2589
2644
  }
2590
2645
  async function syncDriftState(graph, nodePath) {
2591
- const projectRoot = path11.dirname(graph.rootPath);
2646
+ const projectRoot = path12.dirname(graph.rootPath);
2592
2647
  const node = graph.nodes.get(nodePath);
2593
2648
  if (!node) throw new Error(`Node not found: ${nodePath}`);
2594
2649
  const mapping = node.meta.mapping;
@@ -2760,10 +2815,10 @@ function registerTreeCommand(program2) {
2760
2815
  let roots;
2761
2816
  let showProjectName;
2762
2817
  if (options.root?.trim()) {
2763
- const path15 = options.root.trim().replace(/\/$/, "");
2764
- const node = graph.nodes.get(path15);
2818
+ const path16 = options.root.trim().replace(/\/$/, "");
2819
+ const node = graph.nodes.get(path16);
2765
2820
  if (!node) {
2766
- process.stderr.write(`Error: path '${path15}' not found
2821
+ process.stderr.write(`Error: path '${path16}' not found
2767
2822
  `);
2768
2823
  process.exit(1);
2769
2824
  }
@@ -2851,7 +2906,7 @@ function registerOwnerCommand(program2) {
2851
2906
 
2852
2907
  // src/core/dependency-resolver.ts
2853
2908
  import { execSync as execSync2 } from "child_process";
2854
- import path12 from "path";
2909
+ import path13 from "path";
2855
2910
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2856
2911
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
2857
2912
  function filterRelationType(relType, filter) {
@@ -2928,7 +2983,7 @@ function registerDepsCommand(program2) {
2928
2983
  // src/core/graph-from-git.ts
2929
2984
  import { mkdtemp, rm } from "fs/promises";
2930
2985
  import { tmpdir } from "os";
2931
- import path13 from "path";
2986
+ import path14 from "path";
2932
2987
  import { execSync as execSync3 } from "child_process";
2933
2988
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
2934
2989
  const yggPath = ".yggdrasil";
@@ -2939,8 +2994,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
2939
2994
  return null;
2940
2995
  }
2941
2996
  try {
2942
- tmpDir = await mkdtemp(path13.join(tmpdir(), "ygg-git-"));
2943
- const archivePath = path13.join(tmpDir, "archive.tar");
2997
+ tmpDir = await mkdtemp(path14.join(tmpdir(), "ygg-git-"));
2998
+ const archivePath = path14.join(tmpDir, "archive.tar");
2944
2999
  execSync3(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
2945
3000
  cwd: projectRoot,
2946
3001
  stdio: "pipe"
@@ -3010,14 +3065,14 @@ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
3010
3065
  }
3011
3066
  const chains = [];
3012
3067
  for (const node of transitiveOnly) {
3013
- const path15 = [];
3068
+ const path16 = [];
3014
3069
  let current = node;
3015
3070
  while (current) {
3016
- path15.unshift(current);
3071
+ path16.unshift(current);
3017
3072
  current = parent.get(current);
3018
3073
  }
3019
- if (path15.length >= 2) {
3020
- chains.push(path15.map((p) => `<- ${p}`).join(" "));
3074
+ if (path16.length >= 2) {
3075
+ chains.push(path16.map((p) => `<- ${p}`).join(" "));
3021
3076
  }
3022
3077
  }
3023
3078
  return chains.sort();
@@ -3069,7 +3124,7 @@ function registerImpactCommand(program2) {
3069
3124
  }
3070
3125
  }
3071
3126
  }
3072
- const budget = graph.config.quality?.context_budget ?? { warning: 5e3, error: 1e4 };
3127
+ const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
3073
3128
  process.stdout.write(`Impact of changes in ${nodePath}:
3074
3129
 
3075
3130
  `);
@@ -3160,11 +3215,11 @@ ${changedLine}${budgetLine}${driftLine}
3160
3215
  // src/io/journal-store.ts
3161
3216
  import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access2 } from "fs/promises";
3162
3217
  import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
3163
- import path14 from "path";
3218
+ import path15 from "path";
3164
3219
  var JOURNAL_FILE = ".journal.yaml";
3165
3220
  var ARCHIVE_DIR = "journals-archive";
3166
3221
  async function readJournal(yggRoot) {
3167
- const filePath = path14.join(yggRoot, JOURNAL_FILE);
3222
+ const filePath = path15.join(yggRoot, JOURNAL_FILE);
3168
3223
  try {
3169
3224
  const content = await readFile13(filePath, "utf-8");
3170
3225
  const raw = parseYaml8(content);
@@ -3179,13 +3234,13 @@ async function appendJournalEntry(yggRoot, note, target) {
3179
3234
  const at = (/* @__PURE__ */ new Date()).toISOString();
3180
3235
  const entry = target ? { at, target, note } : { at, note };
3181
3236
  entries.push(entry);
3182
- const filePath = path14.join(yggRoot, JOURNAL_FILE);
3237
+ const filePath = path15.join(yggRoot, JOURNAL_FILE);
3183
3238
  const content = stringifyYaml2({ entries });
3184
3239
  await writeFile4(filePath, content, "utf-8");
3185
3240
  return entry;
3186
3241
  }
3187
3242
  async function archiveJournal(yggRoot) {
3188
- const journalPath = path14.join(yggRoot, JOURNAL_FILE);
3243
+ const journalPath = path15.join(yggRoot, JOURNAL_FILE);
3189
3244
  try {
3190
3245
  await access2(journalPath);
3191
3246
  } catch {
@@ -3193,12 +3248,12 @@ async function archiveJournal(yggRoot) {
3193
3248
  }
3194
3249
  const entries = await readJournal(yggRoot);
3195
3250
  if (entries.length === 0) return null;
3196
- const archiveDir = path14.join(yggRoot, ARCHIVE_DIR);
3251
+ const archiveDir = path15.join(yggRoot, ARCHIVE_DIR);
3197
3252
  await mkdir3(archiveDir, { recursive: true });
3198
3253
  const now = /* @__PURE__ */ new Date();
3199
3254
  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
3255
  const archiveName = `.journal.${timestamp}.yaml`;
3201
- const archivePath = path14.join(archiveDir, archiveName);
3256
+ const archivePath = path15.join(archiveDir, archiveName);
3202
3257
  await rename(journalPath, archivePath);
3203
3258
  return { archiveName, entryCount: entries.length };
3204
3259
  }