@chrisdudek/yg 1.4.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -4,35 +4,37 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/cli/init.ts
7
- import { mkdir as mkdir2, writeFile as writeFile2, readdir, readFile as readFile2, stat } from "fs/promises";
8
- import path2 from "path";
7
+ import { mkdir as mkdir2, writeFile as writeFile3, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
8
+ import path4 from "path";
9
9
  import { fileURLToPath } from "url";
10
+ import { readFileSync } from "fs";
11
+ import { gt as gt2, valid as valid2 } from "semver";
10
12
 
11
13
  // src/templates/default-config.ts
12
- var DEFAULT_CONFIG = `name: ""
14
+ var DEFAULT_CONFIG = `version: "2.0.0"
13
15
 
14
- stack:
15
- language: ""
16
- runtime: ""
17
-
18
- standards: ""
16
+ name: ""
19
17
 
20
18
  node_types:
21
- - name: module
22
- - name: service
23
- - name: library
24
- - name: infrastructure
19
+ module:
20
+ description: "Business logic unit with clear domain responsibility"
21
+ service:
22
+ description: "Component providing functionality to other nodes"
23
+ library:
24
+ description: "Shared utility code with no domain knowledge"
25
+ infrastructure:
26
+ description: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
25
27
 
26
28
  artifacts:
27
29
  responsibility.md:
28
30
  required: always
29
31
  description: "What this node is responsible for, and what it is not"
30
- structural_context: true
32
+ included_in_relations: true
31
33
  interface.md:
32
34
  required:
33
35
  when: has_incoming_relations
34
36
  description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures"
35
- structural_context: true
37
+ included_in_relations: true
36
38
  internals.md:
37
39
  required: never
38
40
  description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
@@ -52,14 +54,25 @@ import path from "path";
52
54
  // src/templates/rules.ts
53
55
  var CORE_PROTOCOL = `## CORE PROTOCOL
54
56
 
55
- Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the repository and provides deterministic implementation context for every node. This document is your complete operating manual. Follow it strictly.
57
+ <EXTREMELY-IMPORTANT>
58
+ This is your operating manual for working in a Yggdrasil-managed repository. Every rule below is mandatory \u2014 no skill, plan, workflow, or instruction overrides these requirements.
59
+
60
+ BEFORE working with ANY source file, you MUST run \`yg owner\` to check if it is mapped. If mapped, you MUST use the graph before reading source. YOU DO NOT HAVE A CHOICE.
61
+
62
+ This is not negotiable. This is not optional. You cannot rationalize your way out of this.
63
+ </EXTREMELY-IMPORTANT>
64
+
65
+ Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the repository and provides deterministic implementation context for every node.
56
66
 
57
67
  ### Quick Start Protocol
58
68
 
59
69
  \`\`\`
60
70
  BEFORE reading, researching, planning, OR modifying ANY mapped file:
61
71
  1. yg owner --file <path>
62
- 2. yg build-context --node <owner>
72
+ 2. Choose the right graph tool for your task:
73
+ - Understanding how/why it works \u2192 yg build-context --node <owner>
74
+ - Assessing what is affected by a change \u2192 yg impact --node <owner>
75
+ - Planning modifications \u2192 both (build-context first, then impact)
63
76
  The context package is your primary source of ARCHITECTURAL understanding:
64
77
  intent, constraints, relations, rationale. For IMPLEMENTATION precision
65
78
  (exact behavior, error handling, await patterns, edge cases) \u2014 verify
@@ -79,6 +92,8 @@ NEVER: modify code without graph coverage.
79
92
  NEVER: read mapped source files to understand a component without
80
93
  running yg build-context first \u2014 the graph captures intent,
81
94
  constraints, and relations that source files cannot.
95
+ NEVER: assess blast radius of a change without running yg impact first
96
+ \u2014 the graph knows the dependency structure that grep cannot infer.
82
97
  NEVER: invent rationale, business rules, or decisions.
83
98
  NEVER: auto-resolve drift without asking the user.
84
99
  WHEN UNSURE: ask the user. Never guess. Never assume.
@@ -86,7 +101,7 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
86
101
 
87
102
  ### Five Core Rules
88
103
 
89
- 1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and \`yg build-context\`. Always. The context package is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
104
+ 1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and the appropriate graph tool: \`yg build-context\` to understand a component, \`yg impact\` to assess blast radius. The graph is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
90
105
  2. **Code and graph are one.** Code changed \u2192 graph updated in the same response. Graph changed \u2192 source verified in the same response. No exceptions.
91
106
  3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
92
107
  4. **Always capture why \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
@@ -94,15 +109,22 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
94
109
 
95
110
  ### Recognizing Graph-Required Actions
96
111
 
97
- What matters is the ACTION you are performing, not what instructed it. If the action involves understanding mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a workflow step, or your own initiative.
112
+ What matters is the ACTION you are performing, not what instructed it. If the action involves reading, understanding, or modifying mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a brainstorming session, a debugging workflow, or your own initiative. This is not negotiable. You cannot rationalize your way out of this.
98
113
 
99
- **Actions that require \`yg owner\` + \`yg build-context\` first:**
114
+ **Actions that require \`yg owner\` + \`yg build-context\`:**
100
115
 
101
116
  - Reading or exploring source files to understand a component
102
117
  - Proposing approaches, designs, or plans for changing code
103
118
  - Reviewing or debugging code
104
119
  - Any form of reasoning about how mapped code works or should change
105
120
 
121
+ **Actions that require \`yg owner\` + \`yg impact\`:**
122
+
123
+ - Assessing blast radius before changing or removing a component
124
+ - Finding all dependents of a component
125
+ - Planning cross-cutting refactors or feature removals
126
+ - Scoping work that spans multiple nodes
127
+
106
128
  **Actions that do NOT require yg:**
107
129
 
108
130
  - Git operations (log, diff, status, blame)
@@ -110,11 +132,22 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
110
132
  - Running tests, builds, or linters
111
133
  - Working with files that \`yg owner\` reports as unmapped
112
134
 
135
+ **Evasion patterns \u2014 if you think any of these, STOP:**
136
+
137
+ | Thought | Reality |
138
+ |---|---|
139
+ | "The skill/plan says to explore the codebase" | Exploring mapped code = yg owner + graph tool first |
140
+ | "I'm just scoping/searching, not understanding" | Scoping IS a graph action; use yg impact |
141
+ | "The plan step says to read this file" | Reading a mapped file = yg owner first |
142
+ | "I'm brainstorming, not implementing" | Brainstorming about mapped code needs graph context |
143
+ | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
144
+ | "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
145
+
113
146
  ### Failure States
114
147
 
115
148
  You have broken Yggdrasil if you do any of the following:
116
149
 
117
- - \u274C Worked on a mapped file without running \`yg owner\` + \`yg build-context\` first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
150
+ - \u274C Worked on a mapped file without running \`yg owner\` + the appropriate graph tool (\`build-context\` or \`impact\`) first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
118
151
  - \u274C Modified source code without updating graph artifacts in the same response, or vice versa.
119
152
  - \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
120
153
  - \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
@@ -136,7 +169,22 @@ Before preflight:
136
169
 
137
170
  - Verify \`yg\` CLI is available. If not found, inform user and stop.
138
171
  - If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
139
- - If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.`;
172
+ - If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
173
+
174
+ ### Delegating to Subagents
175
+
176
+ When you delegate work to a subagent (any subprocess, tool agent, or spawned assistant), the subagent does NOT inherit your Yggdrasil knowledge. Before any other instruction, the subagent MUST:
177
+
178
+ 1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
179
+ 2. Follow the Quick Start Protocol from that file before touching any mapped code
180
+
181
+ Include this as the FIRST instruction in every subagent prompt:
182
+
183
+ \`\`\`
184
+ BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
185
+ \`\`\`
186
+
187
+ A subagent that skips this step will read code without graph context, miss architectural constraints, and produce changes that break graph-code consistency.`;
140
188
  var OPERATIONS = `## OPERATIONS
141
189
 
142
190
  ### Conversation Lifecycle
@@ -144,9 +192,8 @@ var OPERATIONS = `## OPERATIONS
144
192
  \`\`\`
145
193
  PREFLIGHT (every conversation, before any work):
146
194
  - [ ] 1. yg preflight \u2192 read unified report
147
- - [ ] 2. If journal entries: consolidate to graph, then yg journal-archive
148
- - [ ] 3. If drift: resolve per Drift Resolution, then yg drift-sync per node
149
- - [ ] 4. If validation errors: fix, re-run yg validate
195
+ - [ ] 2. If drift: resolve per Drift Resolution, then yg drift-sync per node
196
+ - [ ] 3. If validation errors: fix, re-run yg validate
150
197
  Exception: read-only requests (explain, analyze) \u2014 skip preflight.
151
198
 
152
199
  UNDERSTANDING mapped code (questions, research, OR planning):
@@ -157,14 +204,14 @@ UNDERSTANDING mapped code (questions, research, OR planning):
157
204
  Raw reads supplement the context package \u2014 they do not replace it.
158
205
 
159
206
  WRAP-UP (user signals "done", "wrap up", "that's enough"):
160
- - [ ] 1. Consolidate journal if used \u2192 yg journal-archive
161
- - [ ] 2. yg drift --drifted-only \u2192 resolve
162
- - [ ] 3. yg validate \u2192 fix errors
163
- - [ ] 4. Report: which nodes and files were changed
207
+ - [ ] 1. yg drift --drifted-only \u2192 resolve
208
+ - [ ] 2. yg validate \u2192 fix errors
209
+ - [ ] 3. Report: which nodes and files were changed
164
210
 
165
211
  BEFORE ENDING ANY RESPONSE (self-audit):
212
+ - [ ] Did I interact with mapped code (read, research, or modify)? If yes \u2192 did I use a graph tool BEFORE reading source?
166
213
  - [ ] Did I modify source code? If yes \u2192 did I update graph artifacts in this same response?
167
- - [ ] If you changed code and did not sync the graph, you have broken the protocol. Do not finish until both are done.
214
+ - [ ] If you broke either rule, you have broken the protocol. Do not finish until both are fixed.
168
215
  \`\`\`
169
216
 
170
217
  ### Modify Source Code
@@ -220,11 +267,11 @@ Per area checklist:
220
267
 
221
268
  - [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
222
269
  - [ ] 2. Determine node granularity \u2014 propose to user if unclear
223
- - [ ] 3. Create node directory, read \`schemas/node.yaml\`, create \`node.yaml\`
224
- - [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
225
- - [ ] 5. Identify relations \u2014 add to \`node.yaml\`
270
+ - [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
271
+ - [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
272
+ - [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
226
273
  - [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
227
- - [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add to \`node.yaml\` \`anchors\` field
274
+ - [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add as \`anchors\` in the aspect entry in \`yg-node.yaml\`
228
275
  - [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
229
276
  - [ ] 8. \`yg validate\` \u2014 fix errors
230
277
  - [ ] 9. \`yg drift-sync --node <path>\`
@@ -274,7 +321,7 @@ When reviewing graph quality (triggered by user or quality improvement):
274
321
  - [ ] 1. \`yg build-context --node <path>\`
275
322
  - [ ] 2. Read mapped source files
276
323
  - [ ] 3. For each claim in graph: verify against source code
277
- - [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`aspect_exceptions\` in \`node.yaml\`
324
+ - [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`exceptions\` to the aspect entry in \`yg-node.yaml\`
278
325
  - [ ] 5. Report inconsistencies
279
326
 
280
327
  **Step 2 \u2014 Completeness** (catches MISSING information):
@@ -297,27 +344,21 @@ var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
297
344
 
298
345
  \`\`\`
299
346
  .yggdrasil/
300
- config.yaml \u2190 vocabulary, stack, node types, artifact rules, required aspects
347
+ yg-config.yaml \u2190 version, vocabulary, node types, artifact rules, required aspects
301
348
  model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
302
349
  aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
303
350
  flows/ \u2190 why and in what process: business processes with node participation
304
351
  schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
305
- .drift-state \u2190 generated by CLI; never edit manually
306
- .journal.yaml \u2190 generated by CLI; never edit manually
352
+ .drift-state/ \u2190 generated by CLI; never edit manually
307
353
  \`\`\`
308
354
 
309
355
  Key facts:
310
356
 
311
357
  - **Hierarchy:** nodes nest in \`model/\`. Children inherit parent context. Do not repeat parent content in children.
312
- - **Aspect id = directory path** under \`aspects/\`. Each aspect has \`aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
358
+ - **Aspect id = directory path** under \`aspects/\`. Each aspect has \`yg-aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
313
359
  - **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
314
360
 
315
- **Node type guidance:**
316
-
317
- - \`module\` \u2014 business logic unit with clear domain responsibility
318
- - \`service\` \u2014 component providing functionality to other nodes
319
- - \`library\` \u2014 shared utility code with no domain knowledge
320
- - \`infrastructure\` \u2014 guards, resolvers, middleware, interceptors, validators that intercept or modify request flow. These affect blast radius of changes but are invisible in call graphs. Map them to make blast radius analysis accurate. Key signal: code that runs WITHOUT being explicitly called by business logic (e.g., NestJS guards, Express middleware, GraphQL resolvers).
361
+ **Node type guidance:** Each type in \`yg-config.yaml node_types\` has a \`description\` that tells you when to use it. Check the project's config for the full list and descriptions. Common types: \`module\` (business logic), \`service\` (providing functionality), \`library\` (shared utilities), \`infrastructure\` (guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius).
321
362
 
322
363
  ### Artifact Structure
323
364
 
@@ -329,26 +370,28 @@ Three artifacts capture node knowledge at three levels:
329
370
 
330
371
  **Enrichment priority (when adding incrementally):** \`interface.md\` first (highest cross-module ROI \u2014 contracts enable other nodes to reason about interactions), then \`responsibility.md\` (identity and boundaries), then \`internals.md\` (depth for complex nodes). A node with only \`interface.md\` provides more cross-module value than one with only \`internals.md\`.
331
372
 
373
+ Projects can define additional artifact types in \`yg-config.yaml\` under \`artifacts\`. Each custom artifact has a \`description\` (tells you what to write), a \`required\` condition (\`always\`, \`never\`, \`when: has_incoming_relations\`, \`when: has_aspect:<id>\`), and an \`included_in_relations\` flag (if true, included in dependency context packages for structural relations). The three standard artifacts are always present in config. Check \`yg-config.yaml\` to see all defined artifacts for the project.
374
+
332
375
  ### Context Assembly
333
376
 
334
- Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global config, hierarchy, own artifacts, aspects, and relational context. It is your architectural map. For implementation-level claims (exact call patterns, error handling, await vs fire-and-forget) \u2014 verify against source code. If the package is insufficient, enrich the graph.
377
+ Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global project identity, hierarchy, own artifacts, aspects, and relational context. It is your architectural map. For implementation-level claims (exact call patterns, error handling, await vs fire-and-forget) \u2014 verify against source code. If the package is insufficient, enrich the graph.
335
378
 
336
379
  ### Information Routing
337
380
 
338
381
  When you encounter information, route it to the correct location:
339
382
 
340
- - **Specific to this node** \u2192 local node artifact (check \`config.yaml artifacts\` for available types)
341
- - **Rule for many nodes** \u2192 aspect (\`aspects/<id>/\` with \`aspect.yaml\` + content \`.md\` files). If applies to ALL nodes of a type \u2192 \`node_types[*].required_aspects\` in \`config.yaml\`
342
- - **Business process** \u2192 flow (\`flows/<name>/\` with \`flow.yaml\` + \`description.md\`). Ask user if process unclear.
383
+ - **Specific to this node** \u2192 local node artifact (check \`yg-config.yaml artifacts\` for available types)
384
+ - **Rule for many nodes** \u2192 aspect (\`aspects/<id>/\` with \`yg-aspect.yaml\` + content \`.md\` files). If applies to ALL nodes of a type \u2192 \`node_types.<type>.required_aspects\` in \`yg-config.yaml\`
385
+ - **Business process** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\` + \`description.md\`). Ask user if process unclear.
343
386
  - **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
344
- - **Technology stack or standard** \u2192 \`config.yaml\` under \`stack\` or \`standards\` (+ \`rationale\` field)
345
- - **Decision (why + why NOT):** one node \u2192 Decisions section of \`internals.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192 \`config.yaml\` rationale field. Always include rejected alternatives \u2014 they are the highest-value graph content. If the rationale is unknown: record the decision with "rationale: unknown" and note what CAN be observed from the code. Never invent a plausible-sounding rationale.
387
+ - **Technology stack or standard** \u2192 node artifact at the appropriate hierarchy level (e.g., root node's \`responsibility.md\` for single-stack repos, or deployment unit node for monorepos)
388
+ - **Decision (why + why NOT):** one node \u2192 Decisions section of \`internals.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192 node artifact at the level where the technology applies. Always include rejected alternatives \u2014 they are the highest-value graph content. If the rationale is unknown: record the decision with "rationale: unknown" and note what CAN be observed from the code. Never invent a plausible-sounding rationale.
346
389
 
347
390
  ### Creating Aspects
348
391
 
349
- - [ ] 1. Read \`schemas/aspect.yaml\`
392
+ - [ ] 1. Read \`schemas/yg-aspect.yaml\`
350
393
  - [ ] 2. Create \`aspects/<id>/\` directory
351
- - [ ] 3. Write \`aspect.yaml\` \u2014 name, optional description, optional implies
394
+ - [ ] 3. Write \`yg-aspect.yaml\` \u2014 name, optional description, optional implies
352
395
  - [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
353
396
  - [ ] 5. \`yg validate\`
354
397
 
@@ -360,23 +403,23 @@ Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No
360
403
  - **Architectural:** Structural patterns with rationale (e.g., dual-rollback on provider failure, idempotency via key generation, fire-and-forget dispatch)
361
404
  - **Concurrency:** Shared concurrency strategies (e.g., pessimistic locking, retry-on-deadlock, optimistic versioning)
362
405
 
363
- When a node follows an aspect's pattern with exceptions, record exceptions in \`node.yaml\` under \`aspect_exceptions\`. Example: aspect says "fire-and-forget" but this node awaits the publish call. The exception appears in the context package next to the aspect content, preventing abstractions from masking implementation details.
406
+ When a node follows an aspect's pattern with exceptions, record them in the \`exceptions\` field of the aspect entry in \`yg-node.yaml\`. Example: aspect says "fire-and-forget" but this node awaits the publish call \u2014 add \`exceptions: ["awaits publish call instead of fire-and-forget because..."]\`. Exceptions appear in the context package next to the aspect content, preventing abstractions from masking implementation details.
364
407
 
365
408
  **Aspect lifecycle warning.** Aspects decay CATASTROPHICALLY \u2014 a pattern either exists or it doesn't. When a pattern changes, ALL aspect claims become wrong at once. This differs from other artifacts: \`interface.md\` and \`responsibility.md\` are most stable (~9-year half-life); \`internals.md\` has moderate stability (~2.5-year half-life); aspects are least stable (~2.4-year half-life, binary decay). After any significant feature addition, review ALL aspects touching the affected area. Don't wait for drift \u2014 aspects can be 100% wrong without any mapped file changing.
366
409
 
367
- **Aspect stability tiers.** If an aspect has a \`stability\` field in \`aspect.yaml\`, use it to calibrate review urgency:
410
+ **Aspect stability tiers.** If an aspect has a \`stability\` field in \`yg-aspect.yaml\`, use it to calibrate review urgency:
368
411
 
369
412
  - \`schema\` \u2014 enforced by data model; review only when data model changes (most stable)
370
413
  - \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
371
414
  - \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
372
415
 
373
- When code anchors (\`anchors\` field in \`node.yaml\`) are present for an aspect, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
416
+ When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are present, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
374
417
 
375
418
  ### Creating Flows
376
419
 
377
- - [ ] 1. Read \`schemas/flow.yaml\`
420
+ - [ ] 1. Read \`schemas/yg-flow.yaml\`
378
421
  - [ ] 2. Create \`flows/<name>/\` directory
379
- - [ ] 3. Write \`flow.yaml\` \u2014 declare participants and flow-level aspects
422
+ - [ ] 3. Write \`yg-flow.yaml\` \u2014 declare participants and flow-level aspects
380
423
  - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
381
424
  - [ ] 5. \`yg validate\`
382
425
 
@@ -387,7 +430,7 @@ Test: "Does this describe what happens in the world, or only in the software?" I
387
430
  ### Operational Rules
388
431
 
389
432
  - **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
390
- - **Read schemas before creating** any \`node.yaml\`, \`aspect.yaml\`, or \`flow.yaml\`.
433
+ - **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
391
434
  - **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
392
435
  - **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
393
436
  - **Completeness test:** Two checks, both required:
@@ -399,7 +442,7 @@ Test: "Does this describe what happens in the world, or only in the software?" I
399
442
  ### CLI Reference
400
443
 
401
444
  \`\`\`
402
- yg preflight [--quick] Unified diagnostic: journal + drift + status + validate.
445
+ yg preflight [--quick] Unified diagnostic: drift + status + validate.
403
446
  yg owner --file <path> Find the node that owns this file.
404
447
  yg build-context --node <path> Assemble context package for this node.
405
448
  yg tree [--root <path>] [--depth N] Print graph structure.
@@ -417,24 +460,20 @@ yg drift [--scope <path>|all] [--drifted-only] [--limit <n>]
417
460
  Detect source and graph drift (bidirectional).
418
461
  yg drift-sync --node <path> [--recursive] | --all
419
462
  Record file hashes as new baseline.
420
- yg journal-read Read pending journal entries.
421
- yg journal-add --note "<content>" [--target <node_path>]
422
- Add a journal entry.
423
- yg journal-archive Archive consolidated journal entries.
424
463
  \`\`\`
425
464
 
426
465
  ### Quick Routing Table
427
466
 
428
467
  | What you have | Where it goes |
429
468
  |---|---|
430
- | Information specific to this node | Local node artifact (check \`config.yaml artifacts\` for types) |
469
+ | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
431
470
  | Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
432
- | Architectural invariant for a node type | Required aspect in \`config.yaml node_types\` |
433
- | Business process participation | Flow (\`flow.yaml participants\`) |
471
+ | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
472
+ | Business process participation | Flow (\`yg-flow.yaml participants\`) |
434
473
  | Process-level requirement | Flow \`aspects\` + aspect directory |
435
474
  | Context shared across a domain | Parent node artifact |
436
- | Technology stack | \`config.yaml stack\` (+ \`rationale\` field) |
437
- | Global coding standards | \`config.yaml standards\` |`;
475
+ | Technology stack | Node artifact at appropriate hierarchy level |
476
+ | Coding standards | Node artifact at appropriate hierarchy level |`;
438
477
  var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
439
478
 
440
479
  // src/templates/platform.ts
@@ -677,16 +716,316 @@ function escapeRegex(s) {
677
716
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
678
717
  }
679
718
 
719
+ // src/core/migrator.ts
720
+ import { readFile as readFile2, access } from "fs/promises";
721
+ import path2 from "path";
722
+ import { parse as parseYaml } from "yaml";
723
+ import { gt, valid, compare } from "semver";
724
+ async function detectVersion(yggRoot) {
725
+ const newConfigPath = path2.join(yggRoot, "yg-config.yaml");
726
+ try {
727
+ const content = await readFile2(newConfigPath, "utf-8");
728
+ const raw = parseYaml(content);
729
+ if (raw && typeof raw === "object" && typeof raw.version === "string") {
730
+ return raw.version.trim();
731
+ }
732
+ return "1.4.3";
733
+ } catch {
734
+ }
735
+ const oldConfigPath = path2.join(yggRoot, "config.yaml");
736
+ try {
737
+ await access(oldConfigPath);
738
+ return "1.4.3";
739
+ } catch {
740
+ return null;
741
+ }
742
+ }
743
+ async function runMigrations(currentVersion, migrations, yggRoot) {
744
+ const cVer = valid(currentVersion);
745
+ if (!cVer) return [];
746
+ const applicable = migrations.filter((m) => {
747
+ const mVer = valid(m.to);
748
+ if (!mVer) return false;
749
+ return gt(mVer, cVer);
750
+ }).sort((a, b) => compare(valid(a.to), valid(b.to)));
751
+ const results = [];
752
+ for (const migration of applicable) {
753
+ const result = await migration.run(yggRoot);
754
+ results.push(result);
755
+ }
756
+ return results;
757
+ }
758
+
759
+ // src/migrations/to-2.0.0.ts
760
+ import { readFile as readFile3, writeFile as writeFile2, rename, readdir, rm, stat } from "fs/promises";
761
+ import path3 from "path";
762
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
763
+ var KNOWN_TYPE_DESCRIPTIONS = {
764
+ module: "Business logic unit with clear domain responsibility",
765
+ service: "Component providing functionality to other nodes",
766
+ library: "Shared utility code with no domain knowledge",
767
+ infrastructure: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
768
+ };
769
+ var STANDARD_ARTIFACTS = {
770
+ "responsibility.md": {
771
+ required: "always",
772
+ description: "What this node is responsible for, and what it is not",
773
+ included_in_relations: true
774
+ },
775
+ "interface.md": {
776
+ required: { when: "has_incoming_relations" },
777
+ description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures",
778
+ included_in_relations: true
779
+ },
780
+ "internals.md": {
781
+ required: "never",
782
+ description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
783
+ }
784
+ };
785
+ async function migrateTo2(yggRoot) {
786
+ const actions = [];
787
+ const warnings = [];
788
+ const oldConfigPath = path3.join(yggRoot, "config.yaml");
789
+ const newConfigPath = path3.join(yggRoot, "yg-config.yaml");
790
+ let configContent;
791
+ const oldConfigExists = await fileExists(oldConfigPath);
792
+ if (oldConfigExists) {
793
+ configContent = await readFile3(oldConfigPath, "utf-8");
794
+ await rename(oldConfigPath, newConfigPath);
795
+ actions.push("Renamed config.yaml \u2192 yg-config.yaml");
796
+ } else {
797
+ configContent = await readFile3(newConfigPath, "utf-8");
798
+ }
799
+ const raw = parseYaml2(configContent);
800
+ const nodeTypesRaw = raw.node_types;
801
+ const nodeTypes = {};
802
+ if (Array.isArray(nodeTypesRaw)) {
803
+ for (const typeName of nodeTypesRaw) {
804
+ if (typeof typeName === "string") {
805
+ const desc = KNOWN_TYPE_DESCRIPTIONS[typeName];
806
+ if (desc) {
807
+ nodeTypes[typeName] = { description: desc };
808
+ } else {
809
+ nodeTypes[typeName] = { description: "TODO: add description" };
810
+ warnings.push(`Unknown node type '${typeName}' \u2014 needs a description`);
811
+ }
812
+ }
813
+ }
814
+ actions.push("Converted node_types from array to object format");
815
+ } else if (nodeTypesRaw && typeof nodeTypesRaw === "object") {
816
+ for (const [name, val] of Object.entries(nodeTypesRaw)) {
817
+ const entry = val;
818
+ const desc = KNOWN_TYPE_DESCRIPTIONS[name] ?? (typeof entry?.description === "string" ? entry.description : "TODO: add description");
819
+ nodeTypes[name] = { description: desc };
820
+ if (entry?.required_aspects) {
821
+ nodeTypes[name].required_aspects = entry.required_aspects;
822
+ }
823
+ if (!KNOWN_TYPE_DESCRIPTIONS[name] && (!entry?.description || entry.description === "TODO: add description")) {
824
+ warnings.push(`Unknown node type '${name}' \u2014 needs a description`);
825
+ }
826
+ }
827
+ }
828
+ if (!nodeTypes.infrastructure) {
829
+ nodeTypes.infrastructure = { description: KNOWN_TYPE_DESCRIPTIONS.infrastructure };
830
+ actions.push("Added infrastructure node type");
831
+ }
832
+ const stackRaw = raw.stack;
833
+ const standardsRaw = raw.standards;
834
+ if (stackRaw || standardsRaw) {
835
+ await migrateStackStandards(yggRoot, stackRaw, standardsRaw, actions);
836
+ }
837
+ const newConfig = {
838
+ version: "2.0.0",
839
+ name: raw.name,
840
+ node_types: nodeTypes,
841
+ artifacts: STANDARD_ARTIFACTS
842
+ };
843
+ if (raw.quality) {
844
+ newConfig.quality = raw.quality;
845
+ }
846
+ await writeFile2(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
847
+ actions.push("Updated config: version, artifacts, removed stack/standards");
848
+ const modelDir = path3.join(yggRoot, "model");
849
+ if (await fileExists(modelDir)) {
850
+ await renameFilesRecursively(modelDir, "node.yaml", "yg-node.yaml", actions);
851
+ await transformNodeFiles(modelDir, actions, warnings);
852
+ }
853
+ const aspectsDir = path3.join(yggRoot, "aspects");
854
+ if (await fileExists(aspectsDir)) {
855
+ await renameFilesRecursively(aspectsDir, "aspect.yaml", "yg-aspect.yaml", actions);
856
+ }
857
+ const flowsDir = path3.join(yggRoot, "flows");
858
+ if (await fileExists(flowsDir)) {
859
+ await renameFilesRecursively(flowsDir, "flow.yaml", "yg-flow.yaml", actions);
860
+ }
861
+ const schemasDir = path3.join(yggRoot, "schemas");
862
+ if (await fileExists(schemasDir)) {
863
+ for (const name of ["config.yaml", "node.yaml", "aspect.yaml", "flow.yaml"]) {
864
+ const oldPath = path3.join(schemasDir, name);
865
+ const newPath = path3.join(schemasDir, `yg-${name}`);
866
+ if (await fileExists(oldPath) && !await fileExists(newPath)) {
867
+ await rename(oldPath, newPath);
868
+ actions.push(`Renamed schemas/${name} \u2192 yg-${name}`);
869
+ }
870
+ }
871
+ }
872
+ const driftStatePath = path3.join(yggRoot, ".drift-state");
873
+ if (await fileExists(driftStatePath)) {
874
+ await rm(driftStatePath);
875
+ actions.push("Deleted .drift-state (requires fresh yg drift-sync --all)");
876
+ }
877
+ return { actions, warnings };
878
+ }
879
+ async function fileExists(p) {
880
+ try {
881
+ await stat(p);
882
+ return true;
883
+ } catch {
884
+ return false;
885
+ }
886
+ }
887
+ async function migrateStackStandards(yggRoot, stack, standards, actions) {
888
+ const modelDir = path3.join(yggRoot, "model");
889
+ if (!await fileExists(modelDir)) return;
890
+ const lines = [];
891
+ if (stack && Object.keys(stack).length > 0) {
892
+ lines.push("## Technology Stack");
893
+ lines.push("");
894
+ for (const [key, value] of Object.entries(stack)) {
895
+ lines.push(`- **${key}:** ${value}`);
896
+ }
897
+ lines.push("");
898
+ }
899
+ if (standards) {
900
+ lines.push("## Standards");
901
+ lines.push("");
902
+ lines.push(standards);
903
+ lines.push("");
904
+ }
905
+ if (lines.length === 0) return;
906
+ const rootNodeYgPath = path3.join(modelDir, "yg-node.yaml");
907
+ const rootNodeOldPath = path3.join(modelDir, "node.yaml");
908
+ const hasRootNode = await fileExists(rootNodeYgPath) || await fileExists(rootNodeOldPath);
909
+ if (!hasRootNode) {
910
+ await writeFile2(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
911
+ await writeFile2(path3.join(modelDir, "responsibility.md"), "TBD\n", "utf-8");
912
+ actions.push("Created root node in model/ for stack/standards migration");
913
+ }
914
+ const internalsPath = path3.join(modelDir, "internals.md");
915
+ const existingInternals = await fileExists(internalsPath) ? await readFile3(internalsPath, "utf-8") : "";
916
+ const MIGRATION_MARKER = "<!-- migrated-stack-standards-v2 -->";
917
+ if (existingInternals.includes(MIGRATION_MARKER)) {
918
+ return;
919
+ }
920
+ const markerLine = MIGRATION_MARKER + "\n";
921
+ const newContent = existingInternals ? existingInternals.trimEnd() + "\n\n" + markerLine + lines.join("\n") : markerLine + lines.join("\n");
922
+ await writeFile2(internalsPath, newContent, "utf-8");
923
+ actions.push("Migrated stack/standards to model/internals.md");
924
+ }
925
+ async function renameFilesRecursively(dir, oldName, newName, actions) {
926
+ let entries;
927
+ try {
928
+ entries = await readdir(dir, { withFileTypes: true });
929
+ } catch {
930
+ return;
931
+ }
932
+ for (const entry of entries) {
933
+ const fullPath = path3.join(dir, entry.name);
934
+ if (entry.isDirectory()) {
935
+ await renameFilesRecursively(fullPath, oldName, newName, actions);
936
+ } else if (entry.name === oldName) {
937
+ const destPath = path3.join(dir, newName);
938
+ if (!await fileExists(destPath)) {
939
+ await rename(fullPath, destPath);
940
+ actions.push(`Renamed ${oldName} \u2192 ${newName} in ${dir}`);
941
+ }
942
+ }
943
+ }
944
+ }
945
+ async function transformNodeFiles(dir, actions, warnings) {
946
+ let entries;
947
+ try {
948
+ entries = await readdir(dir, { withFileTypes: true });
949
+ } catch {
950
+ return;
951
+ }
952
+ for (const entry of entries) {
953
+ const fullPath = path3.join(dir, entry.name);
954
+ if (entry.isDirectory()) {
955
+ await transformNodeFiles(fullPath, actions, warnings);
956
+ } else if (entry.name === "yg-node.yaml") {
957
+ await transformSingleNode(fullPath, actions, warnings);
958
+ }
959
+ }
960
+ }
961
+ async function transformSingleNode(filePath, actions, warnings) {
962
+ const content = await readFile3(filePath, "utf-8");
963
+ const raw = parseYaml2(content);
964
+ if (!raw || typeof raw !== "object") {
965
+ warnings.push(`Skipped ${filePath}: not a valid YAML object`);
966
+ return;
967
+ }
968
+ let changed = false;
969
+ if (Array.isArray(raw.aspects) && raw.aspects.length > 0 && typeof raw.aspects[0] === "string") {
970
+ const aspectExceptions = raw.aspect_exceptions ?? {};
971
+ const anchors = raw.anchors ?? {};
972
+ raw.aspects = raw.aspects.map((id) => {
973
+ const entry = { aspect: id };
974
+ if (aspectExceptions[id]) entry.exceptions = aspectExceptions[id];
975
+ if (anchors[id]) entry.anchors = anchors[id];
976
+ return entry;
977
+ });
978
+ delete raw.aspect_exceptions;
979
+ delete raw.anchors;
980
+ changed = true;
981
+ }
982
+ if (raw.tags !== void 0) {
983
+ delete raw.tags;
984
+ changed = true;
985
+ }
986
+ if (changed) {
987
+ await writeFile2(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
988
+ actions.push(`Transformed ${path3.basename(path3.dirname(filePath))}/yg-node.yaml`);
989
+ }
990
+ }
991
+
992
+ // src/migrations/index.ts
993
+ var MIGRATIONS = [
994
+ {
995
+ to: "2.0.0",
996
+ description: "Rename YAML files to yg-* prefix, restructure config, convert aspects format",
997
+ run: migrateTo2
998
+ }
999
+ ];
1000
+
680
1001
  // src/cli/init.ts
681
1002
  function getGraphSchemasDir() {
682
- const currentDir = path2.dirname(fileURLToPath(import.meta.url));
683
- const packageRoot = path2.join(currentDir, "..");
684
- return path2.join(packageRoot, "graph-schemas");
1003
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
1004
+ const packageRoot = path4.join(currentDir, "..");
1005
+ return path4.join(packageRoot, "graph-schemas");
1006
+ }
1007
+ function getCliVersion() {
1008
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
1009
+ const packageRoot = path4.join(currentDir, "..");
1010
+ const pkg2 = JSON.parse(readFileSync(path4.join(packageRoot, "package.json"), "utf-8"));
1011
+ return pkg2.version;
1012
+ }
1013
+ async function refreshSchemas(yggRoot) {
1014
+ const schemasDir = path4.join(yggRoot, "schemas");
1015
+ await mkdir2(schemasDir, { recursive: true });
1016
+ const graphSchemasDir = getGraphSchemasDir();
1017
+ try {
1018
+ const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
1019
+ const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
1020
+ for (const file of schemaFiles) {
1021
+ const srcPath = path4.join(graphSchemasDir, file);
1022
+ const content = await readFile4(srcPath, "utf-8");
1023
+ await writeFile3(path4.join(schemasDir, file), content, "utf-8");
1024
+ }
1025
+ } catch {
1026
+ }
685
1027
  }
686
- var GITIGNORE_CONTENT = `.journal.yaml
687
- .drift-state
688
- journals-archive/
689
- `;
1028
+ var GITIGNORE_CONTENT = ``;
690
1029
  function registerInitCommand(program2) {
691
1030
  program2.command("init").description("Initialize Yggdrasil graph in current project").option(
692
1031
  "--platform <name>",
@@ -694,10 +1033,10 @@ function registerInitCommand(program2) {
694
1033
  "generic"
695
1034
  ).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
696
1035
  const projectRoot = process.cwd();
697
- const yggRoot = path2.join(projectRoot, ".yggdrasil");
1036
+ const yggRoot = path4.join(projectRoot, ".yggdrasil");
698
1037
  let upgradeMode = false;
699
1038
  try {
700
- const statResult = await stat(yggRoot);
1039
+ const statResult = await stat2(yggRoot);
701
1040
  if (!statResult.isDirectory()) {
702
1041
  process.stderr.write("Error: .yggdrasil exists but is not a directory.\n");
703
1042
  process.exit(1);
@@ -721,25 +1060,58 @@ function registerInitCommand(program2) {
721
1060
  process.exit(1);
722
1061
  }
723
1062
  if (upgradeMode) {
1063
+ const projectVersion = await detectVersion(yggRoot);
1064
+ if (!projectVersion) {
1065
+ process.stderr.write("Error: No Yggdrasil project found. Run `yg init` first.\n");
1066
+ process.exit(1);
1067
+ }
1068
+ const cliVersion = getCliVersion();
1069
+ if (valid2(projectVersion) && valid2(cliVersion) && gt2(projectVersion, cliVersion)) {
1070
+ process.stderr.write(
1071
+ `Warning: Project version (${projectVersion}) is newer than CLI (${cliVersion}). Upgrade your CLI.
1072
+ `
1073
+ );
1074
+ process.exit(1);
1075
+ }
1076
+ if (valid2(projectVersion) && valid2(cliVersion) && gt2(cliVersion, projectVersion)) {
1077
+ process.stdout.write(`Migrating from ${projectVersion} to ${cliVersion}...
1078
+
1079
+ `);
1080
+ const results = await runMigrations(projectVersion, MIGRATIONS, yggRoot);
1081
+ for (const result of results) {
1082
+ for (const action of result.actions) {
1083
+ process.stdout.write(` \u2713 ${action}
1084
+ `);
1085
+ }
1086
+ for (const warning of result.warnings) {
1087
+ process.stdout.write(` \u26A0 ${warning}
1088
+ `);
1089
+ }
1090
+ }
1091
+ if (results.length > 0) {
1092
+ process.stdout.write("\n");
1093
+ }
1094
+ }
1095
+ await refreshSchemas(yggRoot);
724
1096
  const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
725
1097
  process.stdout.write("\u2713 Rules refreshed.\n");
726
- process.stdout.write(` ${path2.relative(projectRoot, rulesPath2)}
1098
+ process.stdout.write(` ${path4.relative(projectRoot, rulesPath2)}
727
1099
  `);
728
1100
  return;
729
1101
  }
730
- await mkdir2(path2.join(yggRoot, "model"), { recursive: true });
731
- await mkdir2(path2.join(yggRoot, "aspects"), { recursive: true });
732
- await mkdir2(path2.join(yggRoot, "flows"), { recursive: true });
733
- const schemasDir = path2.join(yggRoot, "schemas");
1102
+ await mkdir2(path4.join(yggRoot, "model"), { recursive: true });
1103
+ await mkdir2(path4.join(yggRoot, "aspects"), { recursive: true });
1104
+ await mkdir2(path4.join(yggRoot, "flows"), { recursive: true });
1105
+ const schemasDir = path4.join(yggRoot, "schemas");
734
1106
  await mkdir2(schemasDir, { recursive: true });
735
1107
  const graphSchemasDir = getGraphSchemasDir();
736
1108
  try {
737
- const entries = await readdir(graphSchemasDir, { withFileTypes: true });
1109
+ const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
738
1110
  const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
739
1111
  for (const file of schemaFiles) {
740
- const srcPath = path2.join(graphSchemasDir, file);
741
- const content = await readFile2(srcPath, "utf-8");
742
- await writeFile2(path2.join(schemasDir, file), content, "utf-8");
1112
+ const srcPath = path4.join(graphSchemasDir, file);
1113
+ const content = await readFile4(srcPath, "utf-8");
1114
+ await writeFile3(path4.join(schemasDir, file), content, "utf-8");
743
1115
  }
744
1116
  } catch (err) {
745
1117
  process.stderr.write(
@@ -747,95 +1119,94 @@ function registerInitCommand(program2) {
747
1119
  `
748
1120
  );
749
1121
  }
750
- await writeFile2(path2.join(yggRoot, "config.yaml"), DEFAULT_CONFIG, "utf-8");
751
- await writeFile2(path2.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
1122
+ await writeFile3(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
1123
+ await writeFile3(path4.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
752
1124
  const rulesPath = await installRulesForPlatform(projectRoot, platform);
753
1125
  process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
754
1126
  process.stdout.write("Created:\n");
755
- process.stdout.write(" .yggdrasil/config.yaml\n");
1127
+ process.stdout.write(" .yggdrasil/yg-config.yaml\n");
756
1128
  process.stdout.write(" .yggdrasil/.gitignore\n");
757
1129
  process.stdout.write(" .yggdrasil/model/\n");
758
1130
  process.stdout.write(" .yggdrasil/aspects/\n");
759
1131
  process.stdout.write(" .yggdrasil/flows/\n");
760
- process.stdout.write(" .yggdrasil/schemas/ (config, node, aspect, flow)\n");
761
- process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
1132
+ process.stdout.write(" .yggdrasil/schemas/ (yg-config, yg-node, yg-aspect, yg-flow)\n");
1133
+ process.stdout.write(` ${path4.relative(projectRoot, rulesPath)} (rules)
762
1134
 
763
1135
  `);
764
1136
  process.stdout.write("Next steps:\n");
765
- process.stdout.write(" 1. Edit .yggdrasil/config.yaml \u2014 set name, stack, standards\n");
1137
+ process.stdout.write(" 1. Edit .yggdrasil/yg-config.yaml \u2014 set name and configure node types\n");
766
1138
  process.stdout.write(" 2. Create nodes under .yggdrasil/model/\n");
767
1139
  process.stdout.write(" 3. Run: yg validate\n");
768
1140
  });
769
1141
  }
770
1142
 
771
1143
  // src/core/graph-loader.ts
772
- import { readdir as readdir3, readFile as readFile9 } from "fs/promises";
773
- import path7 from "path";
1144
+ import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
1145
+ import path9 from "path";
774
1146
 
775
1147
  // src/io/config-parser.ts
776
- import { readFile as readFile3 } from "fs/promises";
777
- import { parse as parseYaml } from "yaml";
1148
+ import { readFile as readFile5 } from "fs/promises";
1149
+ import { parse as parseYaml3 } from "yaml";
778
1150
  var DEFAULT_QUALITY = {
779
1151
  min_artifact_length: 50,
780
1152
  max_direct_relations: 10,
781
1153
  context_budget: { warning: 1e4, error: 2e4 }
782
1154
  };
783
1155
  async function parseConfig(filePath) {
784
- const content = await readFile3(filePath, "utf-8");
785
- const raw = parseYaml(content);
1156
+ const content = await readFile5(filePath, "utf-8");
1157
+ const raw = parseYaml3(content);
786
1158
  if (!raw || typeof raw !== "object") {
787
- throw new Error(`config.yaml: file is empty or not a valid YAML mapping`);
1159
+ throw new Error(`yg-config.yaml: file is empty or not a valid YAML mapping`);
788
1160
  }
1161
+ const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
789
1162
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
790
- throw new Error(`config.yaml: missing or invalid 'name' field`);
1163
+ throw new Error(`yg-config.yaml: missing or invalid 'name' field`);
791
1164
  }
792
1165
  const nodeTypesRaw = raw.node_types;
793
- if (!Array.isArray(nodeTypesRaw) || nodeTypesRaw.length === 0) {
794
- throw new Error(`config.yaml: 'node_types' must be a non-empty array`);
1166
+ if (!nodeTypesRaw || typeof nodeTypesRaw !== "object" || Array.isArray(nodeTypesRaw) || Object.keys(nodeTypesRaw).length === 0) {
1167
+ throw new Error(`yg-config.yaml: 'node_types' must be a non-empty object`);
795
1168
  }
796
- const nodeTypes = nodeTypesRaw.map((item) => {
797
- if (typeof item === "string") {
798
- return { name: item };
799
- }
800
- if (typeof item === "object" && item !== null && "name" in item && typeof item.name === "string") {
801
- const obj = item;
802
- const requiredAspects = Array.isArray(obj.required_aspects) ? obj.required_aspects.filter((t) => typeof t === "string") : Array.isArray(obj.required_tags) ? obj.required_tags.filter((t) => typeof t === "string") : void 0;
803
- return {
804
- name: obj.name,
805
- required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
806
- };
1169
+ const nodeTypes = {};
1170
+ for (const [typeName, val] of Object.entries(nodeTypesRaw)) {
1171
+ const entry = val;
1172
+ if (!entry || typeof entry !== "object" || typeof entry.description !== "string" || entry.description.trim() === "") {
1173
+ throw new Error(
1174
+ `yg-config.yaml: node_types.${typeName} must have a non-empty 'description' string`
1175
+ );
807
1176
  }
808
- throw new Error(
809
- `config.yaml: node_types entry must be string or { name, required_aspects? }`
810
- );
811
- });
1177
+ const requiredAspects = Array.isArray(entry.required_aspects) ? entry.required_aspects.filter((t) => typeof t === "string") : void 0;
1178
+ nodeTypes[typeName] = {
1179
+ description: entry.description,
1180
+ required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
1181
+ };
1182
+ }
812
1183
  const artifacts = raw.artifacts;
813
1184
  if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
814
- throw new Error(`config.yaml: 'artifacts' must be a non-empty object`);
1185
+ throw new Error(`yg-config.yaml: 'artifacts' must be a non-empty object`);
815
1186
  }
816
1187
  const artifactsMap = {};
817
1188
  for (const [key, val] of Object.entries(artifacts)) {
818
- if (key === "node.yaml") {
819
- throw new Error(`config.yaml: artifact name 'node.yaml' is reserved`);
1189
+ if (key === "yg-node.yaml") {
1190
+ throw new Error(`yg-config.yaml: artifact name 'yg-node.yaml' is reserved`);
820
1191
  }
821
1192
  const a = val;
822
1193
  const required = a.required;
823
1194
  if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
824
- throw new Error(`config.yaml: artifact '${key}' has invalid 'required' field`);
1195
+ throw new Error(`yg-config.yaml: artifact '${key}' has invalid 'required' field`);
825
1196
  }
826
1197
  if (typeof required === "object" && required && "when" in required) {
827
1198
  const when = required.when;
828
1199
  const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
829
1200
  if (!validWhen) {
830
1201
  throw new Error(
831
- `config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
1202
+ `yg-config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
832
1203
  );
833
1204
  }
834
1205
  }
835
1206
  artifactsMap[key] = {
836
1207
  required,
837
1208
  description: a.description ?? "",
838
- structural_context: a.structural_context ?? false
1209
+ included_in_relations: a.included_in_relations ?? false
839
1210
  };
840
1211
  }
841
1212
  const qualityRaw = raw.quality;
@@ -849,13 +1220,12 @@ async function parseConfig(filePath) {
849
1220
  } : DEFAULT_QUALITY;
850
1221
  if (quality.context_budget.error < quality.context_budget.warning) {
851
1222
  throw new Error(
852
- `config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
1223
+ `yg-config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
853
1224
  );
854
1225
  }
855
1226
  return {
1227
+ version,
856
1228
  name: raw.name.trim(),
857
- stack: raw.stack ?? {},
858
- standards: typeof raw.standards === "string" ? raw.standards : "",
859
1229
  node_types: nodeTypes,
860
1230
  artifacts: artifactsMap,
861
1231
  quality
@@ -863,8 +1233,8 @@ async function parseConfig(filePath) {
863
1233
  }
864
1234
 
865
1235
  // src/io/node-parser.ts
866
- import { readFile as readFile4 } from "fs/promises";
867
- import { parse as parseYaml2 } from "yaml";
1236
+ import { readFile as readFile6 } from "fs/promises";
1237
+ import { parse as parseYaml4 } from "yaml";
868
1238
  var RELATION_TYPES = [
869
1239
  "uses",
870
1240
  "calls",
@@ -877,120 +1247,103 @@ function isValidRelationType(t) {
877
1247
  return typeof t === "string" && RELATION_TYPES.includes(t);
878
1248
  }
879
1249
  async function parseNodeYaml(filePath) {
880
- const content = await readFile4(filePath, "utf-8");
881
- const raw = parseYaml2(content);
1250
+ const content = await readFile6(filePath, "utf-8");
1251
+ const raw = parseYaml4(content);
882
1252
  if (!raw || typeof raw !== "object") {
883
- throw new Error(`node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
1253
+ throw new Error(`yg-node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
884
1254
  }
885
1255
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
886
- throw new Error(`node.yaml at ${filePath}: missing or empty 'name'`);
1256
+ throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'name'`);
887
1257
  }
888
1258
  if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
889
- throw new Error(`node.yaml at ${filePath}: missing or empty 'type'`);
1259
+ throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
890
1260
  }
891
1261
  const relations = parseRelations(raw.relations, filePath);
892
1262
  const mapping = parseMapping(raw.mapping, filePath);
893
- const aspects = parseStringArray(raw.aspects) ?? parseStringArray(raw.tags);
894
- const aspectExceptions = parseAspectExceptions(raw.aspect_exceptions, aspects, filePath);
895
- const anchors = parseAnchors(raw.anchors, filePath);
1263
+ const aspects = parseAspects(raw.aspects, filePath);
896
1264
  return {
897
1265
  name: raw.name.trim(),
898
1266
  type: raw.type.trim(),
899
1267
  aspects,
900
- aspect_exceptions: aspectExceptions,
901
1268
  blackbox: raw.blackbox ?? false,
902
1269
  relations: relations.length > 0 ? relations : void 0,
903
- mapping,
904
- anchors
1270
+ mapping
905
1271
  };
906
1272
  }
907
- function parseAnchors(raw, filePath) {
908
- if (raw === void 0 || raw === null) return void 0;
909
- if (typeof raw !== "object" || Array.isArray(raw)) {
910
- throw new Error(
911
- `node.yaml at ${filePath}: 'anchors' must be an object mapping aspect ids to arrays of strings`
912
- );
913
- }
914
- const obj = raw;
915
- const entries = Object.entries(obj);
916
- if (entries.length === 0) return void 0;
917
- const result = {};
918
- for (const [key, value] of entries) {
919
- if (!Array.isArray(value) || value.length === 0) {
920
- throw new Error(
921
- `node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
922
- );
923
- }
924
- const strings = value.filter((v) => typeof v === "string");
925
- if (strings.length === 0) {
926
- throw new Error(
927
- `node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
928
- );
929
- }
930
- result[key] = strings;
931
- }
932
- return result;
933
- }
934
- function parseAspectExceptions(raw, aspects, filePath) {
1273
+ function parseAspects(raw, filePath) {
935
1274
  if (raw === void 0 || raw === null) return void 0;
936
1275
  if (!Array.isArray(raw)) {
937
- throw new Error(`node.yaml at ${filePath}: 'aspect_exceptions' must be an array`);
1276
+ throw new Error(`yg-node.yaml at ${filePath}: 'aspects' must be an array`);
938
1277
  }
939
1278
  if (raw.length === 0) return void 0;
940
- const aspectSet = new Set(aspects ?? []);
941
1279
  const result = [];
1280
+ const seenAspects = /* @__PURE__ */ new Set();
942
1281
  for (let i = 0; i < raw.length; i++) {
943
1282
  const item = raw[i];
944
1283
  if (typeof item !== "object" || item === null) {
945
- throw new Error(`node.yaml at ${filePath}: aspect_exceptions[${i}] must be an object`);
1284
+ throw new Error(`yg-node.yaml at ${filePath}: aspects[${i}] must be an object with 'aspect' key`);
946
1285
  }
947
1286
  const obj = item;
948
1287
  if (typeof obj.aspect !== "string" || obj.aspect.trim() === "") {
949
1288
  throw new Error(
950
- `node.yaml at ${filePath}: aspect_exceptions[${i}].aspect must be a non-empty string`
951
- );
952
- }
953
- if (typeof obj.note !== "string" || obj.note.trim() === "") {
954
- throw new Error(
955
- `node.yaml at ${filePath}: aspect_exceptions[${i}].note must be a non-empty string`
1289
+ `yg-node.yaml at ${filePath}: aspects[${i}].aspect must be a non-empty string`
956
1290
  );
957
1291
  }
958
1292
  const aspectId = obj.aspect.trim();
959
- if (!aspectSet.has(aspectId)) {
1293
+ if (seenAspects.has(aspectId)) {
960
1294
  throw new Error(
961
- `node.yaml at ${filePath}: aspect_exceptions[${i}].aspect "${aspectId}" is not in this node's aspects list`
1295
+ `yg-node.yaml at ${filePath}: duplicate aspect '${aspectId}' in aspects list`
962
1296
  );
963
1297
  }
964
- result.push({ aspect: aspectId, note: obj.note.trim() });
1298
+ seenAspects.add(aspectId);
1299
+ const entry = { aspect: aspectId };
1300
+ if (obj.exceptions !== void 0 && obj.exceptions !== null) {
1301
+ if (!Array.isArray(obj.exceptions)) {
1302
+ throw new Error(
1303
+ `yg-node.yaml at ${filePath}: aspects[${i}].exceptions must be an array of strings`
1304
+ );
1305
+ }
1306
+ const exceptions = obj.exceptions.filter((e) => typeof e === "string" && e.trim() !== "");
1307
+ if (exceptions.length > 0) {
1308
+ entry.exceptions = exceptions;
1309
+ }
1310
+ }
1311
+ if (obj.anchors !== void 0 && obj.anchors !== null) {
1312
+ if (!Array.isArray(obj.anchors)) {
1313
+ throw new Error(
1314
+ `yg-node.yaml at ${filePath}: aspects[${i}].anchors must be an array of strings`
1315
+ );
1316
+ }
1317
+ const anchors = obj.anchors.filter((a) => typeof a === "string" && a.trim() !== "");
1318
+ if (anchors.length > 0) {
1319
+ entry.anchors = anchors;
1320
+ }
1321
+ }
1322
+ result.push(entry);
965
1323
  }
966
1324
  return result.length > 0 ? result : void 0;
967
1325
  }
968
- function parseStringArray(val) {
969
- if (!Array.isArray(val)) return void 0;
970
- const arr = val.filter((v) => typeof v === "string");
971
- return arr.length > 0 ? arr : void 0;
972
- }
973
1326
  function parseRelations(raw, filePath) {
974
1327
  if (raw === void 0) return [];
975
1328
  if (!Array.isArray(raw)) {
976
- throw new Error(`node.yaml at ${filePath}: 'relations' must be an array`);
1329
+ throw new Error(`yg-node.yaml at ${filePath}: 'relations' must be an array`);
977
1330
  }
978
1331
  const result = [];
979
1332
  for (let index = 0; index < raw.length; index++) {
980
1333
  const r = raw[index];
981
1334
  if (typeof r !== "object" || r === null) {
982
- throw new Error(`node.yaml at ${filePath}: relations[${index}] must be an object`);
1335
+ throw new Error(`yg-node.yaml at ${filePath}: relations[${index}] must be an object`);
983
1336
  }
984
1337
  const obj = r;
985
1338
  const target = obj.target;
986
1339
  const type = obj.type;
987
1340
  if (typeof target !== "string" || target.trim() === "") {
988
1341
  throw new Error(
989
- `node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
1342
+ `yg-node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
990
1343
  );
991
1344
  }
992
1345
  if (!isValidRelationType(type)) {
993
- throw new Error(`node.yaml at ${filePath}: relations[${index}].type is invalid`);
1346
+ throw new Error(`yg-node.yaml at ${filePath}: relations[${index}].type is invalid`);
994
1347
  }
995
1348
  const rel = {
996
1349
  target: target.trim(),
@@ -1012,10 +1365,10 @@ function parseRelations(raw, filePath) {
1012
1365
  function validateRelativePath(pathValue, filePath, fieldName) {
1013
1366
  const normalized = pathValue.trim();
1014
1367
  if (normalized === "") {
1015
- throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
1368
+ throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
1016
1369
  }
1017
1370
  if (normalized.startsWith("/")) {
1018
- throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
1371
+ throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
1019
1372
  }
1020
1373
  return normalized;
1021
1374
  }
@@ -1025,35 +1378,35 @@ function parseMapping(rawMapping, filePath) {
1025
1378
  if (Array.isArray(obj.paths) && obj.paths.length > 0) {
1026
1379
  const paths = obj.paths.filter((p) => typeof p === "string").map((p) => validateRelativePath(p, filePath, "mapping.paths[]"));
1027
1380
  if (paths.length === 0) {
1028
- throw new Error(`node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
1381
+ throw new Error(`yg-node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
1029
1382
  }
1030
1383
  return { paths };
1031
1384
  }
1032
1385
  if (obj.paths !== void 0 || obj.type !== void 0 || obj.path !== void 0) {
1033
1386
  throw new Error(
1034
- `node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
1387
+ `yg-node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
1035
1388
  );
1036
1389
  }
1037
1390
  return void 0;
1038
1391
  }
1039
1392
 
1040
1393
  // src/io/aspect-parser.ts
1041
- import { readFile as readFile6 } from "fs/promises";
1042
- import { parse as parseYaml3 } from "yaml";
1394
+ import { readFile as readFile8 } from "fs/promises";
1395
+ import { parse as parseYaml5 } from "yaml";
1043
1396
 
1044
1397
  // src/io/artifact-reader.ts
1045
- import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
1046
- import path3 from "path";
1047
- async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles) {
1048
- const entries = await readdir2(dirPath, { withFileTypes: true });
1398
+ import { readFile as readFile7, readdir as readdir3 } from "fs/promises";
1399
+ import path5 from "path";
1400
+ async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFiles) {
1401
+ const entries = await readdir3(dirPath, { withFileTypes: true });
1049
1402
  const artifacts = [];
1050
1403
  const includeSet = includeFiles && includeFiles.length > 0 ? new Set(includeFiles) : null;
1051
1404
  for (const entry of entries) {
1052
1405
  if (!entry.isFile()) continue;
1053
1406
  if (excludeFiles.includes(entry.name)) continue;
1054
1407
  if (includeSet && !includeSet.has(entry.name)) continue;
1055
- const filePath = path3.join(dirPath, entry.name);
1056
- const content = await readFile5(filePath, "utf-8");
1408
+ const filePath = path5.join(dirPath, entry.name);
1409
+ const content = await readFile7(filePath, "utf-8");
1057
1410
  artifacts.push({ filename: entry.name, content });
1058
1411
  }
1059
1412
  artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
@@ -1067,8 +1420,8 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
1067
1420
  if (!idTrimmed) {
1068
1421
  throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
1069
1422
  }
1070
- const content = await readFile6(aspectYamlPath, "utf-8");
1071
- const raw = parseYaml3(content);
1423
+ const content = await readFile8(aspectYamlPath, "utf-8");
1424
+ const raw = parseYaml5(content);
1072
1425
  if (!raw || typeof raw !== "object") {
1073
1426
  throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
1074
1427
  }
@@ -1076,7 +1429,7 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
1076
1429
  throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
1077
1430
  }
1078
1431
  const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1079
- const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
1432
+ const artifacts = await readArtifacts(aspectDir, ["yg-aspect.yaml"]);
1080
1433
  let implies;
1081
1434
  if (raw.implies !== void 0) {
1082
1435
  if (!Array.isArray(raw.implies)) {
@@ -1104,37 +1457,37 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
1104
1457
  }
1105
1458
 
1106
1459
  // src/io/flow-parser.ts
1107
- import { readFile as readFile7 } from "fs/promises";
1108
- import path4 from "path";
1109
- import { parse as parseYaml4 } from "yaml";
1460
+ import { readFile as readFile9 } from "fs/promises";
1461
+ import path6 from "path";
1462
+ import { parse as parseYaml6 } from "yaml";
1110
1463
  async function parseFlow(flowDir, flowYamlPath) {
1111
- const content = await readFile7(flowYamlPath, "utf-8");
1112
- const raw = parseYaml4(content);
1464
+ const content = await readFile9(flowYamlPath, "utf-8");
1465
+ const raw = parseYaml6(content);
1113
1466
  if (!raw || typeof raw !== "object") {
1114
- throw new Error(`flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
1467
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
1115
1468
  }
1116
1469
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
1117
- throw new Error(`flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1470
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1118
1471
  }
1119
1472
  const nodes = raw.nodes;
1120
1473
  if (!Array.isArray(nodes) || nodes.length === 0) {
1121
- throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
1474
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
1122
1475
  }
1123
1476
  const nodePaths = nodes.filter((n) => typeof n === "string");
1124
1477
  if (nodePaths.length === 0) {
1125
- throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
1478
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
1126
1479
  }
1127
1480
  let aspects;
1128
1481
  if (raw.aspects !== void 0) {
1129
1482
  if (!Array.isArray(raw.aspects)) {
1130
- throw new Error(`flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
1483
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
1131
1484
  }
1132
1485
  const aspectTags = raw.aspects.filter((a) => typeof a === "string");
1133
1486
  aspects = aspectTags.length > 0 ? aspectTags : [];
1134
1487
  }
1135
- const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
1488
+ const artifacts = await readArtifacts(flowDir, ["yg-flow.yaml"]);
1136
1489
  return {
1137
- path: path4.basename(flowDir),
1490
+ path: path6.basename(flowDir),
1138
1491
  name: raw.name.trim(),
1139
1492
  nodes: nodePaths,
1140
1493
  ...aspects !== void 0 && { aspects },
@@ -1143,27 +1496,28 @@ async function parseFlow(flowDir, flowYamlPath) {
1143
1496
  }
1144
1497
 
1145
1498
  // src/io/schema-parser.ts
1146
- import { readFile as readFile8 } from "fs/promises";
1147
- import path5 from "path";
1148
- import { parse as parseYaml5 } from "yaml";
1499
+ import { readFile as readFile10 } from "fs/promises";
1500
+ import path7 from "path";
1501
+ import { parse as parseYaml7 } from "yaml";
1149
1502
  async function parseSchema(filePath) {
1150
- const content = await readFile8(filePath, "utf-8");
1151
- parseYaml5(content);
1152
- const schemaType = path5.basename(filePath, path5.extname(filePath));
1503
+ const content = await readFile10(filePath, "utf-8");
1504
+ parseYaml7(content);
1505
+ const rawName = path7.basename(filePath, path7.extname(filePath));
1506
+ const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
1153
1507
  return { schemaType };
1154
1508
  }
1155
1509
 
1156
1510
  // src/utils/paths.ts
1157
- import path6 from "path";
1511
+ import path8 from "path";
1158
1512
  import { fileURLToPath as fileURLToPath2 } from "url";
1159
- import { stat as stat2 } from "fs/promises";
1513
+ import { stat as stat3 } from "fs/promises";
1160
1514
  async function findYggRoot(projectRoot) {
1161
- let current = path6.resolve(projectRoot);
1162
- const root = path6.parse(current).root;
1515
+ let current = path8.resolve(projectRoot);
1516
+ const root = path8.parse(current).root;
1163
1517
  while (true) {
1164
- const yggPath = path6.join(current, ".yggdrasil");
1518
+ const yggPath = path8.join(current, ".yggdrasil");
1165
1519
  try {
1166
- const st = await stat2(yggPath);
1520
+ const st = await stat3(yggPath);
1167
1521
  if (!st.isDirectory()) {
1168
1522
  throw new Error(
1169
1523
  `.yggdrasil exists but is not a directory (${yggPath}). Run 'yg init' in a clean location.`
@@ -1175,7 +1529,7 @@ async function findYggRoot(projectRoot) {
1175
1529
  if (current === root) {
1176
1530
  throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
1177
1531
  }
1178
- current = path6.dirname(current);
1532
+ current = path8.dirname(current);
1179
1533
  continue;
1180
1534
  }
1181
1535
  throw err;
@@ -1191,27 +1545,25 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
1191
1545
  if (normalizedInput.length === 0) {
1192
1546
  throw new Error("Path cannot be empty");
1193
1547
  }
1194
- const absolute = path6.resolve(projectRoot, normalizedInput);
1195
- const relative = path6.relative(projectRoot, absolute);
1196
- const isOutside = relative.startsWith("..") || path6.isAbsolute(relative);
1548
+ const absolute = path8.resolve(projectRoot, normalizedInput);
1549
+ const relative = path8.relative(projectRoot, absolute);
1550
+ const isOutside = relative.startsWith("..") || path8.isAbsolute(relative);
1197
1551
  if (isOutside) {
1198
1552
  throw new Error(`Path is outside project root: ${rawPath}`);
1199
1553
  }
1200
- return relative.split(path6.sep).join("/");
1554
+ return relative.split(path8.sep).join("/");
1201
1555
  }
1202
1556
  function projectRootFromGraph(yggRootPath) {
1203
- return path6.dirname(yggRootPath);
1557
+ return path8.dirname(yggRootPath);
1204
1558
  }
1205
1559
 
1206
1560
  // src/core/graph-loader.ts
1207
1561
  function toModelPath(absolutePath, modelDir) {
1208
- return path7.relative(modelDir, absolutePath).split(path7.sep).join("/");
1562
+ return path9.relative(modelDir, absolutePath).split(path9.sep).join("/");
1209
1563
  }
1210
1564
  var FALLBACK_CONFIG = {
1211
1565
  name: "",
1212
- stack: {},
1213
- standards: "",
1214
- node_types: [],
1566
+ node_types: {},
1215
1567
  artifacts: {}
1216
1568
  };
1217
1569
  async function loadGraph(projectRoot, options = {}) {
@@ -1219,14 +1571,14 @@ async function loadGraph(projectRoot, options = {}) {
1219
1571
  let configError;
1220
1572
  let config = FALLBACK_CONFIG;
1221
1573
  try {
1222
- config = await parseConfig(path7.join(yggRoot, "config.yaml"));
1574
+ config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
1223
1575
  } catch (error) {
1224
1576
  if (!options.tolerateInvalidConfig) {
1225
1577
  throw error;
1226
1578
  }
1227
1579
  configError = error.message;
1228
1580
  }
1229
- const modelDir = path7.join(yggRoot, "model");
1581
+ const modelDir = path9.join(yggRoot, "model");
1230
1582
  const nodes = /* @__PURE__ */ new Map();
1231
1583
  const nodeParseErrors = [];
1232
1584
  const artifactFilenames = Object.keys(config.artifacts ?? {});
@@ -1240,9 +1592,9 @@ async function loadGraph(projectRoot, options = {}) {
1240
1592
  }
1241
1593
  throw err;
1242
1594
  }
1243
- const aspects = await loadAspects(path7.join(yggRoot, "aspects"));
1244
- const flows = await loadFlows(path7.join(yggRoot, "flows"));
1245
- const schemas = await loadSchemas(path7.join(yggRoot, "schemas"));
1595
+ const aspects = await loadAspects(path9.join(yggRoot, "aspects"));
1596
+ const flows = await loadFlows(path9.join(yggRoot, "flows"));
1597
+ const schemas = await loadSchemas(path9.join(yggRoot, "schemas"));
1246
1598
  return {
1247
1599
  config,
1248
1600
  configError,
@@ -1255,18 +1607,18 @@ async function loadGraph(projectRoot, options = {}) {
1255
1607
  };
1256
1608
  }
1257
1609
  async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErrors, artifactFilenames) {
1258
- const entries = await readdir3(dirPath, { withFileTypes: true });
1259
- const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
1610
+ const entries = await readdir4(dirPath, { withFileTypes: true });
1611
+ const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
1260
1612
  if (!hasNodeYaml && dirPath !== modelDir) {
1261
1613
  return;
1262
1614
  }
1263
1615
  if (hasNodeYaml) {
1264
1616
  const graphPath = toModelPath(dirPath, modelDir);
1265
- const nodeYamlPath = path7.join(dirPath, "node.yaml");
1617
+ const nodeYamlPath = path9.join(dirPath, "yg-node.yaml");
1266
1618
  let meta;
1267
1619
  let nodeYamlRaw;
1268
1620
  try {
1269
- nodeYamlRaw = await readFile9(nodeYamlPath, "utf-8");
1621
+ nodeYamlRaw = await readFile11(nodeYamlPath, "utf-8");
1270
1622
  meta = await parseNodeYaml(nodeYamlPath);
1271
1623
  } catch (err) {
1272
1624
  nodeParseErrors.push({
@@ -1275,7 +1627,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1275
1627
  });
1276
1628
  return;
1277
1629
  }
1278
- const artifacts = await readArtifacts(dirPath, ["node.yaml"], artifactFilenames);
1630
+ const artifacts = await readArtifacts(dirPath, ["yg-node.yaml"], artifactFilenames);
1279
1631
  const node = {
1280
1632
  path: graphPath,
1281
1633
  meta,
@@ -1292,7 +1644,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1292
1644
  if (!entry.isDirectory()) continue;
1293
1645
  if (entry.name.startsWith(".")) continue;
1294
1646
  await scanModelDirectory(
1295
- path7.join(dirPath, entry.name),
1647
+ path9.join(dirPath, entry.name),
1296
1648
  modelDir,
1297
1649
  node,
1298
1650
  nodes,
@@ -1305,7 +1657,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1305
1657
  if (!entry.isDirectory()) continue;
1306
1658
  if (entry.name.startsWith(".")) continue;
1307
1659
  await scanModelDirectory(
1308
- path7.join(dirPath, entry.name),
1660
+ path9.join(dirPath, entry.name),
1309
1661
  modelDir,
1310
1662
  null,
1311
1663
  nodes,
@@ -1325,28 +1677,28 @@ async function loadAspects(aspectsDir) {
1325
1677
  }
1326
1678
  }
1327
1679
  async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
1328
- const entries = await readdir3(dirPath, { withFileTypes: true });
1329
- const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "aspect.yaml");
1680
+ const entries = await readdir4(dirPath, { withFileTypes: true });
1681
+ const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
1330
1682
  if (hasAspectYaml) {
1331
- const id = path7.relative(aspectsRoot, dirPath).split(path7.sep).join("/");
1332
- const aspectYamlPath = path7.join(dirPath, "aspect.yaml");
1683
+ const id = path9.relative(aspectsRoot, dirPath).split(path9.sep).join("/");
1684
+ const aspectYamlPath = path9.join(dirPath, "yg-aspect.yaml");
1333
1685
  const aspect = await parseAspect(dirPath, aspectYamlPath, id);
1334
1686
  aspects.push(aspect);
1335
1687
  }
1336
1688
  for (const entry of entries) {
1337
1689
  if (!entry.isDirectory()) continue;
1338
1690
  if (entry.name.startsWith(".")) continue;
1339
- await scanAspectsDirectory(path7.join(dirPath, entry.name), aspectsRoot, aspects);
1691
+ await scanAspectsDirectory(path9.join(dirPath, entry.name), aspectsRoot, aspects);
1340
1692
  }
1341
1693
  }
1342
1694
  async function loadFlows(flowsDir) {
1343
1695
  try {
1344
- const entries = await readdir3(flowsDir, { withFileTypes: true });
1696
+ const entries = await readdir4(flowsDir, { withFileTypes: true });
1345
1697
  const flows = [];
1346
1698
  for (const entry of entries) {
1347
1699
  if (!entry.isDirectory()) continue;
1348
- const flowYamlPath = path7.join(flowsDir, entry.name, "flow.yaml");
1349
- const flow = await parseFlow(path7.join(flowsDir, entry.name), flowYamlPath);
1700
+ const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
1701
+ const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
1350
1702
  flows.push(flow);
1351
1703
  }
1352
1704
  return flows;
@@ -1356,12 +1708,12 @@ async function loadFlows(flowsDir) {
1356
1708
  }
1357
1709
  async function loadSchemas(schemasDir) {
1358
1710
  try {
1359
- const entries = await readdir3(schemasDir, { withFileTypes: true });
1711
+ const entries = await readdir4(schemasDir, { withFileTypes: true });
1360
1712
  const schemas = [];
1361
1713
  for (const entry of entries) {
1362
1714
  if (!entry.isFile()) continue;
1363
1715
  if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
1364
- const s = await parseSchema(path7.join(schemasDir, entry.name));
1716
+ const s = await parseSchema(path9.join(schemasDir, entry.name));
1365
1717
  schemas.push(s);
1366
1718
  }
1367
1719
  return schemas;
@@ -1371,8 +1723,8 @@ async function loadSchemas(schemasDir) {
1371
1723
  }
1372
1724
 
1373
1725
  // src/core/context-builder.ts
1374
- import { readFile as readFile10 } from "fs/promises";
1375
- import path8 from "path";
1726
+ import { readFile as readFile12 } from "fs/promises";
1727
+ import path10 from "path";
1376
1728
 
1377
1729
  // src/utils/tokens.ts
1378
1730
  function estimateTokens(text) {
@@ -1421,8 +1773,9 @@ async function buildContext(graph, nodePath) {
1421
1773
  }
1422
1774
  const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
1423
1775
  for (const aspect of aspectsToInclude) {
1424
- const exception = node.meta.aspect_exceptions?.find((e) => e.aspect === aspect.id);
1425
- layers.push(buildAspectLayer(aspect, exception?.note));
1776
+ const entry = node.meta.aspects?.find((a) => a.aspect === aspect.id);
1777
+ const exceptionNote = entry?.exceptions?.join("; ");
1778
+ layers.push(buildAspectLayer(aspect, exceptionNote));
1426
1779
  }
1427
1780
  const fullText = layers.map((l) => l.content).join("\n\n");
1428
1781
  const tokenCount = estimateTokens(fullText);
@@ -1479,18 +1832,7 @@ function resolveAspects(aspectIds, aspects) {
1479
1832
  return expandedIds.map((id) => idToAspect.get(id)).filter((a) => a !== void 0);
1480
1833
  }
1481
1834
  function buildGlobalLayer(config) {
1482
- let content = `**Project:** ${config.name}
1483
-
1484
- `;
1485
- content += `**Stack:**
1486
- `;
1487
- for (const [key, value] of Object.entries(config.stack)) {
1488
- content += `- ${key}: ${value}
1489
- `;
1490
- }
1491
- content += `
1492
- **Standards:**
1493
- ${config.standards || "(none)"}
1835
+ const content = `**Project:** ${config.name}
1494
1836
  `;
1495
1837
  return { type: "global", label: "Global Context", content };
1496
1838
  }
@@ -1502,7 +1844,7 @@ function buildHierarchyLayer(ancestor, config, graph) {
1502
1844
  const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
1503
1845
  const content = filtered.map((a) => `### ${a.filename}
1504
1846
  ${a.content}`).join("\n\n");
1505
- const nodeAspects = ancestor.meta.aspects ?? [];
1847
+ const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
1506
1848
  const expanded = expandAspects(nodeAspects, graph.aspects);
1507
1849
  const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
1508
1850
  return {
@@ -1515,16 +1857,16 @@ ${a.content}`).join("\n\n");
1515
1857
  async function buildOwnLayer(node, config, graphRootPath, graph) {
1516
1858
  const parts = [];
1517
1859
  if (node.nodeYamlRaw) {
1518
- parts.push(`### node.yaml
1860
+ parts.push(`### yg-node.yaml
1519
1861
  ${node.nodeYamlRaw.trim()}`);
1520
1862
  } else {
1521
- const nodeYamlPath = path8.join(graphRootPath, "model", node.path, "node.yaml");
1863
+ const nodeYamlPath = path10.join(graphRootPath, "model", node.path, "yg-node.yaml");
1522
1864
  try {
1523
- const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
1524
- parts.push(`### node.yaml
1865
+ const nodeYamlContent = await readFile12(nodeYamlPath, "utf-8");
1866
+ parts.push(`### yg-node.yaml
1525
1867
  ${nodeYamlContent.trim()}`);
1526
1868
  } catch {
1527
- parts.push(`### node.yaml
1869
+ parts.push(`### yg-node.yaml
1528
1870
  (not found)`);
1529
1871
  }
1530
1872
  }
@@ -1534,7 +1876,7 @@ ${nodeYamlContent.trim()}`);
1534
1876
  ${a.content}`);
1535
1877
  }
1536
1878
  const content = parts.join("\n\n");
1537
- const nodeAspects = node.meta.aspects ?? [];
1879
+ const nodeAspects = (node.meta.aspects ?? []).map((a) => a.aspect);
1538
1880
  const expanded = expandAspects(nodeAspects, graph.aspects);
1539
1881
  const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
1540
1882
  return {
@@ -1556,7 +1898,7 @@ function buildStructuralRelationLayer(target, relation, config) {
1556
1898
 
1557
1899
  `;
1558
1900
  }
1559
- const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
1901
+ const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
1560
1902
  const structuralArts = structuralArtifactFilenames.map((filename) => {
1561
1903
  const art = target.artifacts.find((a) => a.filename === filename);
1562
1904
  return art ? { filename: art.filename, content: art.content } : null;
@@ -1671,10 +2013,10 @@ function collectAncestors(node) {
1671
2013
  function collectEffectiveAspectIds(graph, nodePath) {
1672
2014
  const node = graph.nodes.get(nodePath);
1673
2015
  if (!node) return /* @__PURE__ */ new Set();
1674
- const raw = new Set(node.meta.aspects ?? []);
2016
+ const raw = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
1675
2017
  let ancestor = node.parent;
1676
2018
  while (ancestor) {
1677
- for (const id of ancestor.meta.aspects ?? []) raw.add(id);
2019
+ for (const entry of ancestor.meta.aspects ?? []) raw.add(entry.aspect);
1678
2020
  ancestor = ancestor.parent;
1679
2021
  }
1680
2022
  const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
@@ -1687,8 +2029,11 @@ function collectEffectiveAspectIds(graph, nodePath) {
1687
2029
  }
1688
2030
 
1689
2031
  // src/core/validator.ts
1690
- import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
1691
- import path9 from "path";
2032
+ import { readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
2033
+ import path11 from "path";
2034
+ function getAspectIds(aspects) {
2035
+ return (aspects ?? []).map((a) => a.aspect);
2036
+ }
1692
2037
  var RESERVED_DIRS = /* @__PURE__ */ new Set();
1693
2038
  async function validate(graph, scope = "all") {
1694
2039
  const issues = [];
@@ -1717,7 +2062,6 @@ async function validate(graph, scope = "all") {
1717
2062
  issues.push(...checkImpliedAspectsExist(graph));
1718
2063
  issues.push(...checkImpliesNoCycles(graph));
1719
2064
  issues.push(...checkRequiredAspectsCoverage(graph));
1720
- issues.push(...checkAspectExceptions(graph));
1721
2065
  issues.push(...await checkAnchorPresence(graph));
1722
2066
  issues.push(...checkRequiredArtifacts(graph));
1723
2067
  issues.push(...checkInvalidArtifactConditions(graph));
@@ -1766,7 +2110,7 @@ async function validate(graph, scope = "all") {
1766
2110
  }
1767
2111
  function checkNodeTypes(graph) {
1768
2112
  const issues = [];
1769
- const allowedTypes = new Set((graph.config.node_types ?? []).map((t) => t.name));
2113
+ const allowedTypes = new Set(Object.keys(graph.config.node_types ?? {}));
1770
2114
  for (const [nodePath, node] of graph.nodes) {
1771
2115
  if (!allowedTypes.has(node.meta.type)) {
1772
2116
  issues.push({
@@ -1833,7 +2177,7 @@ function checkAspectsDefined(graph) {
1833
2177
  const issues = [];
1834
2178
  const validAspectIds = new Set(graph.aspects.map((a) => a.id));
1835
2179
  for (const [nodePath, node] of graph.nodes) {
1836
- for (const aspectId of node.meta.aspects ?? []) {
2180
+ for (const aspectId of getAspectIds(node.meta.aspects)) {
1837
2181
  if (!validAspectIds.has(aspectId)) {
1838
2182
  issues.push({
1839
2183
  severity: "error",
@@ -1937,16 +2281,16 @@ function checkImpliesNoCycles(graph) {
1937
2281
  function checkRequiredAspectsCoverage(graph) {
1938
2282
  const issues = [];
1939
2283
  const typeConfig = new Map(
1940
- (graph.config.node_types ?? []).map((t) => [t.name, t.required_aspects ?? []])
2284
+ Object.entries(graph.config.node_types ?? {}).map(([name, cfg]) => [name, cfg.required_aspects ?? []])
1941
2285
  );
1942
2286
  for (const [nodePath, node] of graph.nodes) {
1943
2287
  if (node.meta.blackbox) continue;
1944
2288
  const requiredAspects = typeConfig.get(node.meta.type);
1945
2289
  if (!requiredAspects || requiredAspects.length === 0) continue;
1946
- const nodeAspects = node.meta.aspects ?? [];
2290
+ const nodeAspectIds = getAspectIds(node.meta.aspects);
1947
2291
  let effectiveAspects;
1948
2292
  try {
1949
- effectiveAspects = resolveAspects(nodeAspects, graph.aspects);
2293
+ effectiveAspects = resolveAspects(nodeAspectIds, graph.aspects);
1950
2294
  } catch {
1951
2295
  continue;
1952
2296
  }
@@ -1965,24 +2309,6 @@ function checkRequiredAspectsCoverage(graph) {
1965
2309
  }
1966
2310
  return issues;
1967
2311
  }
1968
- function checkAspectExceptions(graph) {
1969
- const issues = [];
1970
- for (const [nodePath, node] of graph.nodes) {
1971
- for (const exception of node.meta.aspect_exceptions ?? []) {
1972
- const nodeAspects = node.meta.aspects ?? [];
1973
- if (!nodeAspects.includes(exception.aspect)) {
1974
- issues.push({
1975
- severity: "error",
1976
- code: "E018",
1977
- rule: "invalid-aspect-exception",
1978
- message: `aspect_exceptions references aspect '${exception.aspect}' which is not in this node's aspects list (${nodeAspects.join(", ") || "none"})`,
1979
- nodePath
1980
- });
1981
- }
1982
- }
1983
- }
1984
- return issues;
1985
- }
1986
2312
  function checkNoCycles(graph) {
1987
2313
  const WHITE = 0;
1988
2314
  const GRAY = 1;
@@ -2069,12 +2395,12 @@ function checkMappingOverlap(graph) {
2069
2395
  }
2070
2396
  async function checkMappingPathsExist(graph) {
2071
2397
  const issues = [];
2072
- const projectRoot = path9.dirname(graph.rootPath);
2398
+ const projectRoot = path11.dirname(graph.rootPath);
2073
2399
  const { access: access4 } = await import("fs/promises");
2074
2400
  for (const [nodePath, node] of graph.nodes) {
2075
2401
  const mappingPaths = normalizeMappingPaths(node.meta.mapping);
2076
2402
  for (const mp of mappingPaths) {
2077
- const absPath = path9.join(projectRoot, mp);
2403
+ const absPath = path11.join(projectRoot, mp);
2078
2404
  try {
2079
2405
  await access4(absPath);
2080
2406
  } catch {
@@ -2116,7 +2442,7 @@ function artifactRequiredReason(graph, nodePath, node, required) {
2116
2442
  if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
2117
2443
  const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
2118
2444
  const aspectId = when.slice(prefix.length);
2119
- return (node.meta.aspects ?? []).includes(aspectId) ? `node has aspect '${aspectId}'` : null;
2445
+ return (node.meta.aspects ?? []).some((a) => a.aspect === aspectId) ? `node has aspect '${aspectId}'` : null;
2120
2446
  }
2121
2447
  return null;
2122
2448
  }
@@ -2307,7 +2633,7 @@ function checkSchemas(graph) {
2307
2633
  severity: "warning",
2308
2634
  code: "W010",
2309
2635
  rule: "missing-schema",
2310
- message: `Schema '${required}.yaml' missing from .yggdrasil/schemas/`
2636
+ message: `Schema 'yg-${required}.yaml' missing from .yggdrasil/schemas/`
2311
2637
  });
2312
2638
  }
2313
2639
  }
@@ -2315,11 +2641,11 @@ function checkSchemas(graph) {
2315
2641
  }
2316
2642
  async function checkDirectoriesHaveNodeYaml(graph) {
2317
2643
  const issues = [];
2318
- const modelDir = path9.join(graph.rootPath, "model");
2644
+ const modelDir = path11.join(graph.rootPath, "model");
2319
2645
  async function scanDir(dirPath, segments) {
2320
- const entries = await readdir4(dirPath, { withFileTypes: true });
2321
- const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
2322
- const dirName = path9.basename(dirPath);
2646
+ const entries = await readdir5(dirPath, { withFileTypes: true });
2647
+ const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
2648
+ const dirName = path11.basename(dirPath);
2323
2649
  if (RESERVED_DIRS.has(dirName)) return;
2324
2650
  const hasFiles = entries.some((e) => e.isFile());
2325
2651
  const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
@@ -2330,7 +2656,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2330
2656
  severity: "error",
2331
2657
  code: "E015",
2332
2658
  rule: "missing-node-yaml",
2333
- message: `Directory '${graphPath}' has files but no node.yaml`,
2659
+ message: `Directory '${graphPath}' has files but no yg-node.yaml`,
2334
2660
  nodePath: graphPath
2335
2661
  });
2336
2662
  } else if (hasSubdirs) {
@@ -2338,7 +2664,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2338
2664
  severity: "warning",
2339
2665
  code: "W013",
2340
2666
  rule: "directory-without-node",
2341
- message: `Directory '${graphPath}' has subdirectories but no node.yaml \u2014 consider creating a node`,
2667
+ message: `Directory '${graphPath}' has subdirectories but no yg-node.yaml \u2014 consider creating a node`,
2342
2668
  nodePath: graphPath
2343
2669
  });
2344
2670
  }
@@ -2347,15 +2673,15 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2347
2673
  if (!entry.isDirectory()) continue;
2348
2674
  if (RESERVED_DIRS.has(entry.name)) continue;
2349
2675
  if (entry.name.startsWith(".")) continue;
2350
- await scanDir(path9.join(dirPath, entry.name), [...segments, entry.name]);
2676
+ await scanDir(path11.join(dirPath, entry.name), [...segments, entry.name]);
2351
2677
  }
2352
2678
  }
2353
2679
  try {
2354
- const rootEntries = await readdir4(modelDir, { withFileTypes: true });
2680
+ const rootEntries = await readdir5(modelDir, { withFileTypes: true });
2355
2681
  for (const entry of rootEntries) {
2356
2682
  if (!entry.isDirectory()) continue;
2357
2683
  if (entry.name.startsWith(".")) continue;
2358
- await scanDir(path9.join(modelDir, entry.name), [entry.name]);
2684
+ await scanDir(path11.join(modelDir, entry.name), [entry.name]);
2359
2685
  }
2360
2686
  } catch {
2361
2687
  }
@@ -2365,14 +2691,14 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
2365
2691
  const files = [];
2366
2692
  async function collectFiles(absPath) {
2367
2693
  try {
2368
- const s = await stat3(absPath);
2694
+ const s = await stat4(absPath);
2369
2695
  if (s.isFile()) {
2370
2696
  files.push(absPath);
2371
2697
  } else if (s.isDirectory()) {
2372
- const entries = await readdir4(absPath, { withFileTypes: true });
2698
+ const entries = await readdir5(absPath, { withFileTypes: true });
2373
2699
  for (const entry of entries) {
2374
2700
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2375
- const entryPath = path9.join(absPath, entry.name);
2701
+ const entryPath = path11.join(absPath, entry.name);
2376
2702
  if (entry.isFile()) {
2377
2703
  files.push(entryPath);
2378
2704
  } else if (entry.isDirectory()) {
@@ -2384,28 +2710,16 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
2384
2710
  }
2385
2711
  }
2386
2712
  for (const mp of mappingPaths) {
2387
- await collectFiles(path9.join(projectRoot, mp));
2713
+ await collectFiles(path11.join(projectRoot, mp));
2388
2714
  }
2389
2715
  return files;
2390
2716
  }
2391
2717
  async function checkAnchorPresence(graph) {
2392
2718
  const issues = [];
2393
- const projectRoot = path9.dirname(graph.rootPath);
2719
+ const projectRoot = path11.dirname(graph.rootPath);
2394
2720
  for (const [nodePath, node] of graph.nodes) {
2395
- const anchors = node.meta.anchors;
2396
- if (!anchors) continue;
2397
- const nodeAspects = new Set(node.meta.aspects ?? []);
2398
- for (const aspectId of Object.keys(anchors)) {
2399
- if (!nodeAspects.has(aspectId)) {
2400
- issues.push({
2401
- severity: "error",
2402
- code: "E019",
2403
- rule: "invalid-anchor-ref",
2404
- message: `anchors references aspect '${aspectId}' which is not in this node's aspects list (${[...nodeAspects].join(", ") || "none"})`,
2405
- nodePath
2406
- });
2407
- }
2408
- }
2721
+ const aspectsWithAnchors = (node.meta.aspects ?? []).filter((a) => a.anchors && a.anchors.length > 0);
2722
+ if (aspectsWithAnchors.length === 0) continue;
2409
2723
  const mappingPaths = normalizeMappingPaths(node.meta.mapping);
2410
2724
  if (mappingPaths.length === 0) continue;
2411
2725
  const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
@@ -2413,21 +2727,20 @@ async function checkAnchorPresence(graph) {
2413
2727
  const fileContents = [];
2414
2728
  for (const filePath of sourceFiles) {
2415
2729
  try {
2416
- const content = await readFile11(filePath, "utf-8");
2730
+ const content = await readFile13(filePath, "utf-8");
2417
2731
  fileContents.push(content);
2418
2732
  } catch {
2419
2733
  }
2420
2734
  }
2421
- for (const [aspectId, anchorList] of Object.entries(anchors)) {
2422
- if (!nodeAspects.has(aspectId)) continue;
2423
- for (const anchor of anchorList) {
2735
+ for (const entry of aspectsWithAnchors) {
2736
+ for (const anchor of entry.anchors) {
2424
2737
  const found = fileContents.some((content) => content.includes(anchor));
2425
2738
  if (!found) {
2426
2739
  issues.push({
2427
2740
  severity: "warning",
2428
2741
  code: "W014",
2429
2742
  rule: "anchor-not-found",
2430
- message: `Anchor '${anchor}' for aspect '${aspectId}' not found in mapped source files`,
2743
+ message: `Anchor '${anchor}' for aspect '${entry.aspect}' not found in mapped source files`,
2431
2744
  nodePath
2432
2745
  });
2433
2746
  }
@@ -2652,13 +2965,90 @@ ${errors.length} errors, ${warnings.length} warnings.
2652
2965
  import chalk2 from "chalk";
2653
2966
 
2654
2967
  // src/io/drift-state-store.ts
2655
- import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
2656
- import path10 from "path";
2968
+ import { readFile as readFile14, writeFile as writeFile4, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
2969
+ import path12 from "path";
2657
2970
  import { parse as yamlParse } from "yaml";
2658
- var DRIFT_STATE_FILE = ".drift-state";
2971
+ var DRIFT_STATE_DIR = ".drift-state";
2972
+ function nodeStatePath(yggRoot, nodePath) {
2973
+ return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
2974
+ }
2975
+ async function scanJsonFiles(dir, baseDir) {
2976
+ const results = [];
2977
+ let entries;
2978
+ try {
2979
+ entries = await readdir6(dir, { withFileTypes: true });
2980
+ } catch {
2981
+ return results;
2982
+ }
2983
+ for (const entry of entries) {
2984
+ const fullPath = path12.join(dir, entry.name);
2985
+ if (entry.isDirectory()) {
2986
+ const nested = await scanJsonFiles(fullPath, baseDir);
2987
+ results.push(...nested);
2988
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
2989
+ const relPath = path12.relative(baseDir, fullPath);
2990
+ const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
2991
+ results.push(nodePath);
2992
+ }
2993
+ }
2994
+ return results;
2995
+ }
2996
+ async function removeEmptyParents(filePath, stopDir) {
2997
+ let dir = path12.dirname(filePath);
2998
+ while (dir !== stopDir && dir.startsWith(stopDir)) {
2999
+ try {
3000
+ const entries = await readdir6(dir);
3001
+ if (entries.length === 0) {
3002
+ await rm2(dir, { recursive: true });
3003
+ dir = path12.dirname(dir);
3004
+ } else {
3005
+ break;
3006
+ }
3007
+ } catch {
3008
+ break;
3009
+ }
3010
+ }
3011
+ }
3012
+ async function readNodeDriftState(yggRoot, nodePath) {
3013
+ try {
3014
+ const filePath = nodeStatePath(yggRoot, nodePath);
3015
+ const content = await readFile14(filePath, "utf-8");
3016
+ const parsed = JSON.parse(content);
3017
+ return parsed;
3018
+ } catch {
3019
+ return void 0;
3020
+ }
3021
+ }
3022
+ async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3023
+ const filePath = nodeStatePath(yggRoot, nodePath);
3024
+ await mkdir3(path12.dirname(filePath), { recursive: true });
3025
+ const content = JSON.stringify(nodeState, null, 2) + "\n";
3026
+ await writeFile4(filePath, content, "utf-8");
3027
+ }
3028
+ async function garbageCollectDriftState(yggRoot, validNodePaths) {
3029
+ const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
3030
+ const allNodePaths = await scanJsonFiles(driftDir, driftDir);
3031
+ const removed = [];
3032
+ for (const nodePath of allNodePaths) {
3033
+ if (!validNodePaths.has(nodePath)) {
3034
+ const filePath = nodeStatePath(yggRoot, nodePath);
3035
+ await rm2(filePath);
3036
+ await removeEmptyParents(filePath, driftDir);
3037
+ removed.push(nodePath);
3038
+ }
3039
+ }
3040
+ return removed.sort();
3041
+ }
2659
3042
  async function readDriftState(yggRoot) {
3043
+ const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
3044
+ let driftStat;
2660
3045
  try {
2661
- const content = await readFile12(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
3046
+ driftStat = await stat5(driftPath);
3047
+ } catch {
3048
+ return {};
3049
+ }
3050
+ if (driftStat.isFile()) {
3051
+ const content = await readFile14(driftPath, "utf-8");
2662
3052
  let raw;
2663
3053
  try {
2664
3054
  raw = JSON.parse(content);
@@ -2666,37 +3056,44 @@ async function readDriftState(yggRoot) {
2666
3056
  raw = yamlParse(content);
2667
3057
  }
2668
3058
  if (!raw || typeof raw !== "object") return {};
2669
- const state = {};
3059
+ const state2 = {};
2670
3060
  for (const [key, value] of Object.entries(raw)) {
2671
3061
  if (typeof value === "object" && value !== null && "hash" in value) {
2672
- state[key] = value;
3062
+ state2[key] = value;
2673
3063
  }
2674
3064
  }
2675
- return state;
2676
- } catch {
2677
- return {};
3065
+ await rm2(driftPath);
3066
+ for (const [nodePath, nodeState] of Object.entries(state2)) {
3067
+ await writeNodeDriftState(yggRoot, nodePath, nodeState);
3068
+ }
3069
+ return state2;
2678
3070
  }
2679
- }
2680
- async function writeDriftState(yggRoot, state) {
2681
- const content = JSON.stringify(state);
2682
- await writeFile3(path10.join(yggRoot, DRIFT_STATE_FILE), content, "utf-8");
3071
+ const nodePaths = await scanJsonFiles(driftPath, driftPath);
3072
+ const state = {};
3073
+ for (const nodePath of nodePaths) {
3074
+ const nodeState = await readNodeDriftState(yggRoot, nodePath);
3075
+ if (nodeState) {
3076
+ state[nodePath] = nodeState;
3077
+ }
3078
+ }
3079
+ return state;
2683
3080
  }
2684
3081
 
2685
3082
  // src/utils/hash.ts
2686
- import { readFile as readFile13, readdir as readdir5, stat as stat4 } from "fs/promises";
2687
- import path11 from "path";
3083
+ import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3084
+ import path13 from "path";
2688
3085
  import { createHash } from "crypto";
2689
3086
  import { createRequire } from "module";
2690
3087
  var require2 = createRequire(import.meta.url);
2691
3088
  var ignoreFactory = require2("ignore");
2692
3089
  async function hashFile(filePath) {
2693
- const content = await readFile13(filePath);
3090
+ const content = await readFile15(filePath);
2694
3091
  return createHash("sha256").update(content).digest("hex");
2695
3092
  }
2696
3093
  async function loadRootGitignoreStack(projectRoot) {
2697
3094
  if (!projectRoot) return [];
2698
3095
  try {
2699
- const content = await readFile13(path11.join(projectRoot, ".gitignore"), "utf-8");
3096
+ const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
2700
3097
  const matcher = ignoreFactory();
2701
3098
  matcher.add(content);
2702
3099
  return [{ basePath: projectRoot, matcher }];
@@ -2706,7 +3103,7 @@ async function loadRootGitignoreStack(projectRoot) {
2706
3103
  }
2707
3104
  function isIgnoredByStack(candidatePath, stack) {
2708
3105
  for (const { basePath, matcher } of stack) {
2709
- const relativePath = path11.relative(basePath, candidatePath);
3106
+ const relativePath = path13.relative(basePath, candidatePath);
2710
3107
  if (relativePath === "" || relativePath.startsWith("..")) continue;
2711
3108
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
2712
3109
  }
@@ -2721,9 +3118,9 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
2721
3118
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
2722
3119
  const allFiles = [];
2723
3120
  for (const tf of trackedFiles) {
2724
- const absPath = path11.join(projectRoot, tf.path);
3121
+ const absPath = path13.join(projectRoot, tf.path);
2725
3122
  try {
2726
- const st = await stat4(absPath);
3123
+ const st = await stat6(absPath);
2727
3124
  if (st.isDirectory()) {
2728
3125
  const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
2729
3126
  projectRoot,
@@ -2731,7 +3128,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
2731
3128
  });
2732
3129
  for (const entry of dirEntries) {
2733
3130
  allFiles.push({
2734
- relPath: path11.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3131
+ relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
2735
3132
  absPath: entry.absPath,
2736
3133
  mtimeMs: entry.mtimeMs
2737
3134
  });
@@ -2771,17 +3168,17 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
2771
3168
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
2772
3169
  let stack = options.gitignoreStack ?? [];
2773
3170
  try {
2774
- const localContent = await readFile13(path11.join(directoryPath, ".gitignore"), "utf-8");
3171
+ const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
2775
3172
  const localMatcher = ignoreFactory();
2776
3173
  localMatcher.add(localContent);
2777
3174
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
2778
3175
  } catch {
2779
3176
  }
2780
- const entries = await readdir5(directoryPath, { withFileTypes: true });
3177
+ const entries = await readdir7(directoryPath, { withFileTypes: true });
2781
3178
  const dirs = [];
2782
3179
  const files = [];
2783
3180
  for (const entry of entries) {
2784
- const absoluteChildPath = path11.join(directoryPath, entry.name);
3181
+ const absoluteChildPath = path13.join(directoryPath, entry.name);
2785
3182
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
2786
3183
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
2787
3184
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -2792,9 +3189,9 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
2792
3189
  gitignoreStack: stack
2793
3190
  }))),
2794
3191
  Promise.all(files.map(async (f) => {
2795
- const fileStat = await stat4(f);
3192
+ const fileStat = await stat6(f);
2796
3193
  return {
2797
- relPath: path11.relative(rootDirectoryPath, f),
3194
+ relPath: path13.relative(rootDirectoryPath, f),
2798
3195
  absPath: f,
2799
3196
  mtimeMs: fileStat.mtimeMs
2800
3197
  };
@@ -2807,14 +3204,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
2807
3204
  }
2808
3205
 
2809
3206
  // src/core/context-files.ts
2810
- import path12 from "path";
3207
+ import path14 from "path";
2811
3208
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2812
3209
  function collectTrackedFiles(node, graph) {
2813
3210
  const seen = /* @__PURE__ */ new Set();
2814
3211
  const result = [];
2815
- const projectRoot = path12.dirname(graph.rootPath);
2816
- const yggPrefix = path12.relative(projectRoot, graph.rootPath);
2817
- const yggPrefixNormalized = yggPrefix.split(path12.sep).join("/");
3212
+ const projectRoot = path14.dirname(graph.rootPath);
3213
+ const yggPrefix = path14.relative(projectRoot, graph.rootPath);
3214
+ const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
2818
3215
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
2819
3216
  function addFile(filePath, category) {
2820
3217
  if (seen.has(filePath)) return;
@@ -2825,7 +3222,7 @@ function collectTrackedFiles(node, graph) {
2825
3222
  return [yggPrefixNormalized, ...segments].join("/");
2826
3223
  }
2827
3224
  function addNodeFiles(n) {
2828
- addFile(graphPath("model", n.path, "node.yaml"), "graph");
3225
+ addFile(graphPath("model", n.path, "yg-node.yaml"), "graph");
2829
3226
  for (const art of n.artifacts) {
2830
3227
  if (configArtifactKeys.has(art.filename)) {
2831
3228
  addFile(graphPath("model", n.path, art.filename), "graph");
@@ -2838,12 +3235,12 @@ function collectTrackedFiles(node, graph) {
2838
3235
  addNodeFiles(ancestor);
2839
3236
  }
2840
3237
  const allAspectIds = /* @__PURE__ */ new Set();
2841
- for (const id of node.meta.aspects ?? []) {
2842
- allAspectIds.add(id);
3238
+ for (const entry of node.meta.aspects ?? []) {
3239
+ allAspectIds.add(entry.aspect);
2843
3240
  }
2844
3241
  for (const ancestor of ancestors) {
2845
- for (const id of ancestor.meta.aspects ?? []) {
2846
- allAspectIds.add(id);
3242
+ for (const entry of ancestor.meta.aspects ?? []) {
3243
+ allAspectIds.add(entry.aspect);
2847
3244
  }
2848
3245
  }
2849
3246
  const participatingFlows = collectParticipatingFlows2(graph, node, ancestors);
@@ -2854,7 +3251,7 @@ function collectTrackedFiles(node, graph) {
2854
3251
  }
2855
3252
  const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2856
3253
  for (const aspect of resolvedAspects) {
2857
- addFile(graphPath("aspects", aspect.id, "aspect.yaml"), "graph");
3254
+ addFile(graphPath("aspects", aspect.id, "yg-aspect.yaml"), "graph");
2858
3255
  for (const art of aspect.artifacts) {
2859
3256
  addFile(graphPath("aspects", aspect.id, art.filename), "graph");
2860
3257
  }
@@ -2863,7 +3260,7 @@ function collectTrackedFiles(node, graph) {
2863
3260
  if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
2864
3261
  const target = graph.nodes.get(relation.target);
2865
3262
  if (!target) continue;
2866
- const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
3263
+ const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2867
3264
  const structuralArts = structuralFilenames.filter(
2868
3265
  (filename) => target.artifacts.some((a) => a.filename === filename)
2869
3266
  );
@@ -2880,7 +3277,7 @@ function collectTrackedFiles(node, graph) {
2880
3277
  }
2881
3278
  }
2882
3279
  for (const flow of participatingFlows) {
2883
- addFile(graphPath("flows", flow.path, "flow.yaml"), "graph");
3280
+ addFile(graphPath("flows", flow.path, "yg-flow.yaml"), "graph");
2884
3281
  for (const art of flow.artifacts) {
2885
3282
  addFile(graphPath("flows", flow.path, art.filename), "graph");
2886
3283
  }
@@ -2897,8 +3294,8 @@ function collectParticipatingFlows2(graph, node, ancestors) {
2897
3294
  }
2898
3295
 
2899
3296
  // src/core/drift-detector.ts
2900
- import { access } from "fs/promises";
2901
- import path13 from "path";
3297
+ import { access as access2 } from "fs/promises";
3298
+ import path15 from "path";
2902
3299
  function getChildMappingExclusions(graph, nodePath) {
2903
3300
  const node = graph.nodes.get(nodePath);
2904
3301
  if (!node) return [];
@@ -2920,7 +3317,7 @@ function getChildMappingExclusions(graph, nodePath) {
2920
3317
  return exclusions;
2921
3318
  }
2922
3319
  async function detectDrift(graph, filterNodePath) {
2923
- const projectRoot = path13.dirname(graph.rootPath);
3320
+ const projectRoot = path15.dirname(graph.rootPath);
2924
3321
  const driftState = await readDriftState(graph.rootPath);
2925
3322
  const entries = [];
2926
3323
  for (const [nodePath, node] of graph.nodes) {
@@ -3002,16 +3399,16 @@ async function detectDrift(graph, filterNodePath) {
3002
3399
  };
3003
3400
  }
3004
3401
  function categorizeFile(filePath, _rootPath, projectRoot) {
3005
- const yggPrefix = path13.relative(projectRoot, _rootPath);
3006
- const normalizedPrefix = yggPrefix.split(path13.sep).join("/");
3402
+ const yggPrefix = path15.relative(projectRoot, _rootPath);
3403
+ const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3007
3404
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3008
3405
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3009
3406
  }
3010
3407
  async function allPathsMissing(projectRoot, mappingPaths) {
3011
3408
  for (const mp of mappingPaths) {
3012
- const absPath = path13.join(projectRoot, mp);
3409
+ const absPath = path15.join(projectRoot, mp);
3013
3410
  try {
3014
- await access(absPath);
3411
+ await access2(absPath);
3015
3412
  return false;
3016
3413
  } catch {
3017
3414
  }
@@ -3019,24 +3416,21 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3019
3416
  return true;
3020
3417
  }
3021
3418
  async function syncDriftState(graph, nodePath) {
3022
- const projectRoot = path13.dirname(graph.rootPath);
3419
+ const projectRoot = path15.dirname(graph.rootPath);
3023
3420
  const node = graph.nodes.get(nodePath);
3024
3421
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3025
3422
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
3026
3423
  const trackedFiles = collectTrackedFiles(node, graph);
3027
3424
  const excludePrefixes = getChildMappingExclusions(graph, nodePath);
3028
- const existingState = await readDriftState(graph.rootPath);
3029
- const existingEntry = existingState[nodePath];
3425
+ const existingEntry = await readNodeDriftState(graph.rootPath, nodePath);
3030
3426
  const storedFileData = existingEntry?.files ? { hashes: existingEntry.files, mtimes: existingEntry.mtimes ?? {} } : void 0;
3031
3427
  const { canonicalHash, fileHashes, fileMtimes } = await hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes);
3032
3428
  const previousHash = existingEntry?.hash;
3033
- existingState[nodePath] = { hash: canonicalHash, files: fileHashes, mtimes: fileMtimes };
3034
- for (const key of Object.keys(existingState)) {
3035
- if (!graph.nodes.has(key)) {
3036
- delete existingState[key];
3037
- }
3038
- }
3039
- await writeDriftState(graph.rootPath, existingState);
3429
+ await writeNodeDriftState(graph.rootPath, nodePath, {
3430
+ hash: canonicalHash,
3431
+ files: fileHashes,
3432
+ mtimes: fileMtimes
3433
+ });
3040
3434
  return { previousHash, currentHash: canonicalHash };
3041
3435
  }
3042
3436
 
@@ -3218,6 +3612,14 @@ function registerDriftSyncCommand(program2) {
3218
3612
  `
3219
3613
  );
3220
3614
  }
3615
+ if (options.all) {
3616
+ const validPaths = new Set(nodesToSync);
3617
+ const removed = await garbageCollectDriftState(graph.rootPath, validPaths);
3618
+ for (const r of removed) {
3619
+ process.stdout.write(chalk3.dim(`Removed orphaned drift state: ${r}
3620
+ `));
3621
+ }
3622
+ }
3221
3623
  } catch (error) {
3222
3624
  process.stderr.write(`Error: ${error.message}
3223
3625
  `);
@@ -3334,10 +3736,10 @@ function registerTreeCommand(program2) {
3334
3736
  let roots;
3335
3737
  let showProjectName;
3336
3738
  if (options.root?.trim()) {
3337
- const path18 = options.root.trim().replace(/\/$/, "");
3338
- const node = graph.nodes.get(path18);
3739
+ const path19 = options.root.trim().replace(/\/$/, "");
3740
+ const node = graph.nodes.get(path19);
3339
3741
  if (!node) {
3340
- process.stderr.write(`Error: path '${path18}' not found
3742
+ process.stderr.write(`Error: path '${path19}' not found
3341
3743
  `);
3342
3744
  process.exit(1);
3343
3745
  }
@@ -3381,8 +3783,8 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
3381
3783
  }
3382
3784
 
3383
3785
  // src/cli/owner.ts
3384
- import path14 from "path";
3385
- import { access as access2 } from "fs/promises";
3786
+ import path16 from "path";
3787
+ import { access as access3 } from "fs/promises";
3386
3788
  function normalizeForMatch(inputPath) {
3387
3789
  return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
3388
3790
  }
@@ -3411,14 +3813,14 @@ function registerOwnerCommand(program2) {
3411
3813
  const graph = await loadGraph(cwd);
3412
3814
  const repoRoot = projectRootFromGraph(graph.rootPath);
3413
3815
  const rawPath = options.file.trim();
3414
- const absolute = path14.resolve(cwd, rawPath);
3415
- const repoRelative = path14.relative(repoRoot, absolute).split(path14.sep).join("/");
3816
+ const absolute = path16.resolve(cwd, rawPath);
3817
+ const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
3416
3818
  const result = findOwner(graph, repoRoot, repoRelative);
3417
3819
  if (!result.nodePath) {
3418
- const absPath = path14.resolve(repoRoot, result.file);
3820
+ const absPath = path16.resolve(repoRoot, result.file);
3419
3821
  let exists = true;
3420
3822
  try {
3421
- await access2(absPath);
3823
+ await access3(absPath);
3422
3824
  } catch {
3423
3825
  exists = false;
3424
3826
  }
@@ -3449,7 +3851,7 @@ function registerOwnerCommand(program2) {
3449
3851
 
3450
3852
  // src/core/dependency-resolver.ts
3451
3853
  import { execSync } from "child_process";
3452
- import path15 from "path";
3854
+ import path17 from "path";
3453
3855
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3454
3856
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
3455
3857
  function filterRelationType(relType, filter) {
@@ -3524,9 +3926,9 @@ function registerDepsCommand(program2) {
3524
3926
  }
3525
3927
 
3526
3928
  // src/core/graph-from-git.ts
3527
- import { mkdtemp, rm } from "fs/promises";
3929
+ import { mkdtemp, rm as rm3 } from "fs/promises";
3528
3930
  import { tmpdir } from "os";
3529
- import path16 from "path";
3931
+ import path18 from "path";
3530
3932
  import { execSync as execSync2 } from "child_process";
3531
3933
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3532
3934
  const yggPath = ".yggdrasil";
@@ -3537,8 +3939,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3537
3939
  return null;
3538
3940
  }
3539
3941
  try {
3540
- tmpDir = await mkdtemp(path16.join(tmpdir(), "ygg-git-"));
3541
- const archivePath = path16.join(tmpDir, "archive.tar");
3942
+ tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
3943
+ const archivePath = path18.join(tmpDir, "archive.tar");
3542
3944
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
3543
3945
  cwd: projectRoot,
3544
3946
  stdio: "pipe"
@@ -3550,7 +3952,7 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3550
3952
  return null;
3551
3953
  } finally {
3552
3954
  if (tmpDir) {
3553
- await rm(tmpDir, { recursive: true, force: true });
3955
+ await rm3(tmpDir, { recursive: true, force: true });
3554
3956
  }
3555
3957
  }
3556
3958
  }
@@ -3608,14 +4010,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
3608
4010
  }
3609
4011
  const chains = [];
3610
4012
  for (const node of transitiveOnly) {
3611
- const path18 = [];
4013
+ const path19 = [];
3612
4014
  let current = node;
3613
4015
  while (current) {
3614
- path18.unshift(current);
4016
+ path19.unshift(current);
3615
4017
  current = parent.get(current);
3616
4018
  }
3617
- if (path18.length >= 3) {
3618
- chains.push(path18.slice(1).map((p) => `<- ${p}`).join(" "));
4019
+ if (path19.length >= 3) {
4020
+ chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
3619
4021
  }
3620
4022
  }
3621
4023
  return chains.sort();
@@ -3685,14 +4087,14 @@ async function handleAspectImpact(graph, aspectId, simulate) {
3685
4087
  const effective = collectEffectiveAspectIds(graph, nodePath);
3686
4088
  if (effective.has(aspectId)) {
3687
4089
  const node = graph.nodes.get(nodePath);
3688
- const ownAspects = new Set(node.meta.aspects ?? []);
3689
- if (ownAspects.has(aspectId)) {
4090
+ const ownAspectIds = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
4091
+ if (ownAspectIds.has(aspectId)) {
3690
4092
  affected.push({ path: nodePath, source: "own" });
3691
4093
  } else {
3692
4094
  let fromHierarchy = false;
3693
4095
  let anc = node.parent;
3694
4096
  while (anc) {
3695
- if ((anc.meta.aspects ?? []).includes(aspectId)) {
4097
+ if ((anc.meta.aspects ?? []).some((a) => a.aspect === aspectId)) {
3696
4098
  fromHierarchy = true;
3697
4099
  break;
3698
4100
  }
@@ -4036,131 +4438,12 @@ function registerFlowsCommand(program2) {
4036
4438
  });
4037
4439
  }
4038
4440
 
4039
- // src/io/journal-store.ts
4040
- import { readFile as readFile14, writeFile as writeFile4, mkdir as mkdir3, rename, access as access3 } from "fs/promises";
4041
- import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
4042
- import path17 from "path";
4043
- var JOURNAL_FILE = ".journal.yaml";
4044
- var ARCHIVE_DIR = "journals-archive";
4045
- async function readJournal(yggRoot) {
4046
- const filePath = path17.join(yggRoot, JOURNAL_FILE);
4047
- try {
4048
- const content = await readFile14(filePath, "utf-8");
4049
- const raw = parseYaml6(content);
4050
- const entries = raw.entries ?? [];
4051
- return Array.isArray(entries) ? entries : [];
4052
- } catch {
4053
- return [];
4054
- }
4055
- }
4056
- async function appendJournalEntry(yggRoot, note, target) {
4057
- const entries = await readJournal(yggRoot);
4058
- const at = (/* @__PURE__ */ new Date()).toISOString();
4059
- const entry = target ? { at, target, note } : { at, note };
4060
- entries.push(entry);
4061
- const filePath = path17.join(yggRoot, JOURNAL_FILE);
4062
- const content = stringifyYaml({ entries });
4063
- await writeFile4(filePath, content, "utf-8");
4064
- return entry;
4065
- }
4066
- async function archiveJournal(yggRoot) {
4067
- const journalPath = path17.join(yggRoot, JOURNAL_FILE);
4068
- try {
4069
- await access3(journalPath);
4070
- } catch {
4071
- return null;
4072
- }
4073
- const entries = await readJournal(yggRoot);
4074
- if (entries.length === 0) return null;
4075
- const archiveDir = path17.join(yggRoot, ARCHIVE_DIR);
4076
- await mkdir3(archiveDir, { recursive: true });
4077
- const now = /* @__PURE__ */ new Date();
4078
- 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")}`;
4079
- const archiveName = `.journal.${timestamp}.yaml`;
4080
- const archivePath = path17.join(archiveDir, archiveName);
4081
- await rename(journalPath, archivePath);
4082
- return { archiveName, entryCount: entries.length };
4083
- }
4084
-
4085
- // src/cli/journal-add.ts
4086
- function registerJournalAddCommand(program2) {
4087
- program2.command("journal-add").description("Add a note to the session journal").requiredOption("--note <text>", "Note content").option("--target <node-path>", "Node path this note relates to").action(async (options) => {
4088
- try {
4089
- const projectRoot = process.cwd();
4090
- const yggRoot = await findYggRoot(projectRoot);
4091
- await appendJournalEntry(yggRoot, options.note, options.target);
4092
- const entries = await readJournal(yggRoot);
4093
- process.stdout.write(`Note added to journal (${entries.length} entries total)
4094
- `);
4095
- } catch (error) {
4096
- process.stderr.write(`Error: ${error.message}
4097
- `);
4098
- process.exit(1);
4099
- }
4100
- });
4101
- }
4102
-
4103
- // src/cli/journal-read.ts
4104
- function registerJournalReadCommand(program2) {
4105
- program2.command("journal-read").description("List pending journal entries").action(async () => {
4106
- try {
4107
- const projectRoot = process.cwd();
4108
- const yggRoot = await findYggRoot(projectRoot);
4109
- const entries = await readJournal(yggRoot);
4110
- if (entries.length === 0) {
4111
- process.stdout.write("Session journal: empty (clean state)\n");
4112
- return;
4113
- }
4114
- process.stdout.write(`Session journal (${entries.length} entries):
4115
-
4116
- `);
4117
- for (const e of entries) {
4118
- const date = e.at.slice(0, 19).replace("T", " ");
4119
- const target = e.target ? ` ${e.target}` : "";
4120
- process.stdout.write(`[${date}]${target}
4121
- ${e.note}
4122
-
4123
- `);
4124
- }
4125
- } catch (error) {
4126
- process.stderr.write(`Error: ${error.message}
4127
- `);
4128
- process.exit(1);
4129
- }
4130
- });
4131
- }
4132
-
4133
- // src/cli/journal-archive.ts
4134
- function registerJournalArchiveCommand(program2) {
4135
- program2.command("journal-archive").description("Archive journal after consolidating notes to graph").action(async () => {
4136
- try {
4137
- const projectRoot = process.cwd();
4138
- const yggRoot = await findYggRoot(projectRoot);
4139
- const result = await archiveJournal(yggRoot);
4140
- if (!result) {
4141
- process.stdout.write("No active journal - nothing to archive.\n");
4142
- return;
4143
- }
4144
- process.stdout.write(
4145
- `Archived journal (${result.entryCount} entries) -> journals-archive/${result.archiveName}
4146
- `
4147
- );
4148
- } catch (error) {
4149
- process.stderr.write(`Error: ${error.message}
4150
- `);
4151
- process.exit(1);
4152
- }
4153
- });
4154
- }
4155
-
4156
4441
  // src/cli/preflight.ts
4157
4442
  function registerPreflightCommand(program2) {
4158
- program2.command("preflight").description("Unified diagnostic report: journal, drift, status, validation").option("--quick", "Skip drift detection for faster results").action(async (options) => {
4443
+ program2.command("preflight").description("Unified diagnostic report: drift, status, validation").option("--quick", "Skip drift detection for faster results").action(async (options) => {
4159
4444
  try {
4160
4445
  const cwd = process.cwd();
4161
4446
  const graph = await loadGraph(cwd);
4162
- const yggRoot = await findYggRoot(cwd);
4163
- const journalEntries = await readJournal(yggRoot);
4164
4447
  const driftedEntries = options.quick ? [] : (await detectDrift(graph)).entries.filter((e) => e.status !== "ok");
4165
4448
  const nodeCount = graph.nodes.size;
4166
4449
  const aspectCount = graph.aspects.length;
@@ -4175,16 +4458,6 @@ function registerPreflightCommand(program2) {
4175
4458
  const lines = [];
4176
4459
  lines.push("=== Preflight Report ===");
4177
4460
  lines.push("");
4178
- if (journalEntries.length === 0) {
4179
- lines.push("Journal: clean");
4180
- } else {
4181
- lines.push(`Journal: ${journalEntries.length} pending entries`);
4182
- for (const entry of journalEntries) {
4183
- const target = entry.target ? ` [${entry.target}]` : "";
4184
- lines.push(` - ${entry.note}${target}`);
4185
- }
4186
- }
4187
- lines.push("");
4188
4461
  if (options.quick) {
4189
4462
  lines.push("Drift: skipped (--quick)");
4190
4463
  } else if (driftedEntries.length === 0) {
@@ -4221,7 +4494,7 @@ function registerPreflightCommand(program2) {
4221
4494
  }
4222
4495
  lines.push("");
4223
4496
  process.stdout.write(lines.join("\n"));
4224
- const hasIssues = journalEntries.length > 0 || !options.quick && driftedEntries.length > 0 || errors.length > 0;
4497
+ const hasIssues = !options.quick && driftedEntries.length > 0 || errors.length > 0;
4225
4498
  process.exit(hasIssues ? 1 : 0);
4226
4499
  } catch (error) {
4227
4500
  process.stderr.write(`Error: ${error.message}
@@ -4232,12 +4505,12 @@ function registerPreflightCommand(program2) {
4232
4505
  }
4233
4506
 
4234
4507
  // src/bin.ts
4235
- import { readFileSync } from "fs";
4508
+ import { readFileSync as readFileSync2 } from "fs";
4236
4509
  import { fileURLToPath as fileURLToPath3 } from "url";
4237
4510
  import { dirname, join } from "path";
4238
4511
  var __filename = fileURLToPath3(import.meta.url);
4239
4512
  var __dirname = dirname(__filename);
4240
- var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
4513
+ var pkg = JSON.parse(readFileSync2(join(__dirname, "..", "package.json"), "utf-8"));
4241
4514
  var program = new Command();
4242
4515
  program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
4243
4516
  registerInitCommand(program);
@@ -4252,9 +4525,6 @@ registerDepsCommand(program);
4252
4525
  registerImpactCommand(program);
4253
4526
  registerAspectsCommand(program);
4254
4527
  registerFlowsCommand(program);
4255
- registerJournalAddCommand(program);
4256
- registerJournalReadCommand(program);
4257
- registerJournalArchiveCommand(program);
4258
4528
  registerPreflightCommand(program);
4259
4529
  program.parse();
4260
4530
  //# sourceMappingURL=bin.js.map