@chrisdudek/yg 1.4.3 → 2.0.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: ""
13
-
14
- stack:
15
- language: ""
16
- runtime: ""
14
+ var DEFAULT_CONFIG = `version: "2.0.0"
17
15
 
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"
@@ -136,7 +138,22 @@ Before preflight:
136
138
 
137
139
  - Verify \`yg\` CLI is available. If not found, inform user and stop.
138
140
  - 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.`;
141
+ - If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
142
+
143
+ ### Delegating to Subagents
144
+
145
+ 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:
146
+
147
+ 1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
148
+ 2. Follow the Quick Start Protocol from that file before touching any mapped code
149
+
150
+ Include this as the FIRST instruction in every subagent prompt:
151
+
152
+ \`\`\`
153
+ BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
154
+ \`\`\`
155
+
156
+ A subagent that skips this step will read code without graph context, miss architectural constraints, and produce changes that break graph-code consistency.`;
140
157
  var OPERATIONS = `## OPERATIONS
141
158
 
142
159
  ### Conversation Lifecycle
@@ -220,11 +237,11 @@ Per area checklist:
220
237
 
221
238
  - [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
222
239
  - [ ] 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\`
240
+ - [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
241
+ - [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
242
+ - [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
226
243
  - [ ] 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
244
+ - [ ] 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
245
  - [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
229
246
  - [ ] 8. \`yg validate\` \u2014 fix errors
230
247
  - [ ] 9. \`yg drift-sync --node <path>\`
@@ -274,7 +291,7 @@ When reviewing graph quality (triggered by user or quality improvement):
274
291
  - [ ] 1. \`yg build-context --node <path>\`
275
292
  - [ ] 2. Read mapped source files
276
293
  - [ ] 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\`
294
+ - [ ] 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
295
  - [ ] 5. Report inconsistencies
279
296
 
280
297
  **Step 2 \u2014 Completeness** (catches MISSING information):
@@ -297,7 +314,7 @@ var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
297
314
 
298
315
  \`\`\`
299
316
  .yggdrasil/
300
- config.yaml \u2190 vocabulary, stack, node types, artifact rules, required aspects
317
+ yg-config.yaml \u2190 version, vocabulary, node types, artifact rules, required aspects
301
318
  model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
302
319
  aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
303
320
  flows/ \u2190 why and in what process: business processes with node participation
@@ -309,15 +326,10 @@ var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
309
326
  Key facts:
310
327
 
311
328
  - **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.
329
+ - **Aspect id = directory path** under \`aspects/\`. Each aspect has \`yg-aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
313
330
  - **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
314
331
 
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).
332
+ **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
333
 
322
334
  ### Artifact Structure
323
335
 
@@ -329,26 +341,28 @@ Three artifacts capture node knowledge at three levels:
329
341
 
330
342
  **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
343
 
344
+ 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.
345
+
332
346
  ### Context Assembly
333
347
 
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.
348
+ 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
349
 
336
350
  ### Information Routing
337
351
 
338
352
  When you encounter information, route it to the correct location:
339
353
 
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.
354
+ - **Specific to this node** \u2192 local node artifact (check \`yg-config.yaml artifacts\` for available types)
355
+ - **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\`
356
+ - **Business process** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\` + \`description.md\`). Ask user if process unclear.
343
357
  - **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.
358
+ - **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)
359
+ - **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
360
 
347
361
  ### Creating Aspects
348
362
 
349
- - [ ] 1. Read \`schemas/aspect.yaml\`
363
+ - [ ] 1. Read \`schemas/yg-aspect.yaml\`
350
364
  - [ ] 2. Create \`aspects/<id>/\` directory
351
- - [ ] 3. Write \`aspect.yaml\` \u2014 name, optional description, optional implies
365
+ - [ ] 3. Write \`yg-aspect.yaml\` \u2014 name, optional description, optional implies
352
366
  - [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
353
367
  - [ ] 5. \`yg validate\`
354
368
 
@@ -360,23 +374,23 @@ Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No
360
374
  - **Architectural:** Structural patterns with rationale (e.g., dual-rollback on provider failure, idempotency via key generation, fire-and-forget dispatch)
361
375
  - **Concurrency:** Shared concurrency strategies (e.g., pessimistic locking, retry-on-deadlock, optimistic versioning)
362
376
 
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.
377
+ 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
378
 
365
379
  **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
380
 
367
- **Aspect stability tiers.** If an aspect has a \`stability\` field in \`aspect.yaml\`, use it to calibrate review urgency:
381
+ **Aspect stability tiers.** If an aspect has a \`stability\` field in \`yg-aspect.yaml\`, use it to calibrate review urgency:
368
382
 
369
383
  - \`schema\` \u2014 enforced by data model; review only when data model changes (most stable)
370
384
  - \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
371
385
  - \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
372
386
 
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.
387
+ 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
388
 
375
389
  ### Creating Flows
376
390
 
377
- - [ ] 1. Read \`schemas/flow.yaml\`
391
+ - [ ] 1. Read \`schemas/yg-flow.yaml\`
378
392
  - [ ] 2. Create \`flows/<name>/\` directory
379
- - [ ] 3. Write \`flow.yaml\` \u2014 declare participants and flow-level aspects
393
+ - [ ] 3. Write \`yg-flow.yaml\` \u2014 declare participants and flow-level aspects
380
394
  - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
381
395
  - [ ] 5. \`yg validate\`
382
396
 
@@ -387,7 +401,7 @@ Test: "Does this describe what happens in the world, or only in the software?" I
387
401
  ### Operational Rules
388
402
 
389
403
  - **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\`.
404
+ - **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
391
405
  - **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
392
406
  - **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
393
407
  - **Completeness test:** Two checks, both required:
@@ -427,14 +441,14 @@ yg journal-archive Archive consolidated journal entries.
427
441
 
428
442
  | What you have | Where it goes |
429
443
  |---|---|
430
- | Information specific to this node | Local node artifact (check \`config.yaml artifacts\` for types) |
444
+ | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
431
445
  | 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\`) |
446
+ | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
447
+ | Business process participation | Flow (\`yg-flow.yaml participants\`) |
434
448
  | Process-level requirement | Flow \`aspects\` + aspect directory |
435
449
  | Context shared across a domain | Parent node artifact |
436
- | Technology stack | \`config.yaml stack\` (+ \`rationale\` field) |
437
- | Global coding standards | \`config.yaml standards\` |`;
450
+ | Technology stack | Node artifact at appropriate hierarchy level |
451
+ | Coding standards | Node artifact at appropriate hierarchy level |`;
438
452
  var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
439
453
 
440
454
  // src/templates/platform.ts
@@ -677,11 +691,314 @@ function escapeRegex(s) {
677
691
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
678
692
  }
679
693
 
694
+ // src/core/migrator.ts
695
+ import { readFile as readFile2, access } from "fs/promises";
696
+ import path2 from "path";
697
+ import { parse as parseYaml } from "yaml";
698
+ import { gt, valid, compare } from "semver";
699
+ async function detectVersion(yggRoot) {
700
+ const newConfigPath = path2.join(yggRoot, "yg-config.yaml");
701
+ try {
702
+ const content = await readFile2(newConfigPath, "utf-8");
703
+ const raw = parseYaml(content);
704
+ if (raw && typeof raw === "object" && typeof raw.version === "string") {
705
+ return raw.version.trim();
706
+ }
707
+ return "1.4.3";
708
+ } catch {
709
+ }
710
+ const oldConfigPath = path2.join(yggRoot, "config.yaml");
711
+ try {
712
+ await access(oldConfigPath);
713
+ return "1.4.3";
714
+ } catch {
715
+ return null;
716
+ }
717
+ }
718
+ async function runMigrations(currentVersion, migrations, yggRoot) {
719
+ const cVer = valid(currentVersion);
720
+ if (!cVer) return [];
721
+ const applicable = migrations.filter((m) => {
722
+ const mVer = valid(m.to);
723
+ if (!mVer) return false;
724
+ return gt(mVer, cVer);
725
+ }).sort((a, b) => compare(valid(a.to), valid(b.to)));
726
+ const results = [];
727
+ for (const migration of applicable) {
728
+ const result = await migration.run(yggRoot);
729
+ results.push(result);
730
+ }
731
+ return results;
732
+ }
733
+
734
+ // src/migrations/to-2.0.0.ts
735
+ import { readFile as readFile3, writeFile as writeFile2, rename, readdir, rm, stat } from "fs/promises";
736
+ import path3 from "path";
737
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
738
+ var KNOWN_TYPE_DESCRIPTIONS = {
739
+ module: "Business logic unit with clear domain responsibility",
740
+ service: "Component providing functionality to other nodes",
741
+ library: "Shared utility code with no domain knowledge",
742
+ infrastructure: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
743
+ };
744
+ var STANDARD_ARTIFACTS = {
745
+ "responsibility.md": {
746
+ required: "always",
747
+ description: "What this node is responsible for, and what it is not",
748
+ included_in_relations: true
749
+ },
750
+ "interface.md": {
751
+ required: { when: "has_incoming_relations" },
752
+ description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures",
753
+ included_in_relations: true
754
+ },
755
+ "internals.md": {
756
+ required: "never",
757
+ description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
758
+ }
759
+ };
760
+ async function migrateTo2(yggRoot) {
761
+ const actions = [];
762
+ const warnings = [];
763
+ const oldConfigPath = path3.join(yggRoot, "config.yaml");
764
+ const newConfigPath = path3.join(yggRoot, "yg-config.yaml");
765
+ let configContent;
766
+ const oldConfigExists = await fileExists(oldConfigPath);
767
+ if (oldConfigExists) {
768
+ configContent = await readFile3(oldConfigPath, "utf-8");
769
+ await rename(oldConfigPath, newConfigPath);
770
+ actions.push("Renamed config.yaml \u2192 yg-config.yaml");
771
+ } else {
772
+ configContent = await readFile3(newConfigPath, "utf-8");
773
+ }
774
+ const raw = parseYaml2(configContent);
775
+ const nodeTypesRaw = raw.node_types;
776
+ const nodeTypes = {};
777
+ if (Array.isArray(nodeTypesRaw)) {
778
+ for (const typeName of nodeTypesRaw) {
779
+ if (typeof typeName === "string") {
780
+ const desc = KNOWN_TYPE_DESCRIPTIONS[typeName];
781
+ if (desc) {
782
+ nodeTypes[typeName] = { description: desc };
783
+ } else {
784
+ nodeTypes[typeName] = { description: "TODO: add description" };
785
+ warnings.push(`Unknown node type '${typeName}' \u2014 needs a description`);
786
+ }
787
+ }
788
+ }
789
+ actions.push("Converted node_types from array to object format");
790
+ } else if (nodeTypesRaw && typeof nodeTypesRaw === "object") {
791
+ for (const [name, val] of Object.entries(nodeTypesRaw)) {
792
+ const entry = val;
793
+ const desc = KNOWN_TYPE_DESCRIPTIONS[name] ?? (typeof entry?.description === "string" ? entry.description : "TODO: add description");
794
+ nodeTypes[name] = { description: desc };
795
+ if (entry?.required_aspects) {
796
+ nodeTypes[name].required_aspects = entry.required_aspects;
797
+ }
798
+ if (!KNOWN_TYPE_DESCRIPTIONS[name] && (!entry?.description || entry.description === "TODO: add description")) {
799
+ warnings.push(`Unknown node type '${name}' \u2014 needs a description`);
800
+ }
801
+ }
802
+ }
803
+ if (!nodeTypes.infrastructure) {
804
+ nodeTypes.infrastructure = { description: KNOWN_TYPE_DESCRIPTIONS.infrastructure };
805
+ actions.push("Added infrastructure node type");
806
+ }
807
+ const stackRaw = raw.stack;
808
+ const standardsRaw = raw.standards;
809
+ if (stackRaw || standardsRaw) {
810
+ await migrateStackStandards(yggRoot, stackRaw, standardsRaw, actions);
811
+ }
812
+ const newConfig = {
813
+ version: "2.0.0",
814
+ name: raw.name,
815
+ node_types: nodeTypes,
816
+ artifacts: STANDARD_ARTIFACTS
817
+ };
818
+ if (raw.quality) {
819
+ newConfig.quality = raw.quality;
820
+ }
821
+ await writeFile2(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
822
+ actions.push("Updated config: version, artifacts, removed stack/standards");
823
+ const modelDir = path3.join(yggRoot, "model");
824
+ if (await fileExists(modelDir)) {
825
+ await renameFilesRecursively(modelDir, "node.yaml", "yg-node.yaml", actions);
826
+ await transformNodeFiles(modelDir, actions, warnings);
827
+ }
828
+ const aspectsDir = path3.join(yggRoot, "aspects");
829
+ if (await fileExists(aspectsDir)) {
830
+ await renameFilesRecursively(aspectsDir, "aspect.yaml", "yg-aspect.yaml", actions);
831
+ }
832
+ const flowsDir = path3.join(yggRoot, "flows");
833
+ if (await fileExists(flowsDir)) {
834
+ await renameFilesRecursively(flowsDir, "flow.yaml", "yg-flow.yaml", actions);
835
+ }
836
+ const schemasDir = path3.join(yggRoot, "schemas");
837
+ if (await fileExists(schemasDir)) {
838
+ for (const name of ["config.yaml", "node.yaml", "aspect.yaml", "flow.yaml"]) {
839
+ const oldPath = path3.join(schemasDir, name);
840
+ const newPath = path3.join(schemasDir, `yg-${name}`);
841
+ if (await fileExists(oldPath) && !await fileExists(newPath)) {
842
+ await rename(oldPath, newPath);
843
+ actions.push(`Renamed schemas/${name} \u2192 yg-${name}`);
844
+ }
845
+ }
846
+ }
847
+ const driftStatePath = path3.join(yggRoot, ".drift-state");
848
+ if (await fileExists(driftStatePath)) {
849
+ await rm(driftStatePath);
850
+ actions.push("Deleted .drift-state (requires fresh yg drift-sync --all)");
851
+ }
852
+ return { actions, warnings };
853
+ }
854
+ async function fileExists(p) {
855
+ try {
856
+ await stat(p);
857
+ return true;
858
+ } catch {
859
+ return false;
860
+ }
861
+ }
862
+ async function migrateStackStandards(yggRoot, stack, standards, actions) {
863
+ const modelDir = path3.join(yggRoot, "model");
864
+ if (!await fileExists(modelDir)) return;
865
+ const lines = [];
866
+ if (stack && Object.keys(stack).length > 0) {
867
+ lines.push("## Technology Stack");
868
+ lines.push("");
869
+ for (const [key, value] of Object.entries(stack)) {
870
+ lines.push(`- **${key}:** ${value}`);
871
+ }
872
+ lines.push("");
873
+ }
874
+ if (standards) {
875
+ lines.push("## Standards");
876
+ lines.push("");
877
+ lines.push(standards);
878
+ lines.push("");
879
+ }
880
+ if (lines.length === 0) return;
881
+ const rootNodeYgPath = path3.join(modelDir, "yg-node.yaml");
882
+ const rootNodeOldPath = path3.join(modelDir, "node.yaml");
883
+ const hasRootNode = await fileExists(rootNodeYgPath) || await fileExists(rootNodeOldPath);
884
+ if (!hasRootNode) {
885
+ await writeFile2(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
886
+ await writeFile2(path3.join(modelDir, "responsibility.md"), "TBD\n", "utf-8");
887
+ actions.push("Created root node in model/ for stack/standards migration");
888
+ }
889
+ const internalsPath = path3.join(modelDir, "internals.md");
890
+ const existingInternals = await fileExists(internalsPath) ? await readFile3(internalsPath, "utf-8") : "";
891
+ const MIGRATION_MARKER = "<!-- migrated-stack-standards-v2 -->";
892
+ if (existingInternals.includes(MIGRATION_MARKER)) {
893
+ return;
894
+ }
895
+ const markerLine = MIGRATION_MARKER + "\n";
896
+ const newContent = existingInternals ? existingInternals.trimEnd() + "\n\n" + markerLine + lines.join("\n") : markerLine + lines.join("\n");
897
+ await writeFile2(internalsPath, newContent, "utf-8");
898
+ actions.push("Migrated stack/standards to model/internals.md");
899
+ }
900
+ async function renameFilesRecursively(dir, oldName, newName, actions) {
901
+ let entries;
902
+ try {
903
+ entries = await readdir(dir, { withFileTypes: true });
904
+ } catch {
905
+ return;
906
+ }
907
+ for (const entry of entries) {
908
+ const fullPath = path3.join(dir, entry.name);
909
+ if (entry.isDirectory()) {
910
+ await renameFilesRecursively(fullPath, oldName, newName, actions);
911
+ } else if (entry.name === oldName) {
912
+ const destPath = path3.join(dir, newName);
913
+ if (!await fileExists(destPath)) {
914
+ await rename(fullPath, destPath);
915
+ actions.push(`Renamed ${oldName} \u2192 ${newName} in ${dir}`);
916
+ }
917
+ }
918
+ }
919
+ }
920
+ async function transformNodeFiles(dir, actions, warnings) {
921
+ let entries;
922
+ try {
923
+ entries = await readdir(dir, { withFileTypes: true });
924
+ } catch {
925
+ return;
926
+ }
927
+ for (const entry of entries) {
928
+ const fullPath = path3.join(dir, entry.name);
929
+ if (entry.isDirectory()) {
930
+ await transformNodeFiles(fullPath, actions, warnings);
931
+ } else if (entry.name === "yg-node.yaml") {
932
+ await transformSingleNode(fullPath, actions, warnings);
933
+ }
934
+ }
935
+ }
936
+ async function transformSingleNode(filePath, actions, warnings) {
937
+ const content = await readFile3(filePath, "utf-8");
938
+ const raw = parseYaml2(content);
939
+ if (!raw || typeof raw !== "object") {
940
+ warnings.push(`Skipped ${filePath}: not a valid YAML object`);
941
+ return;
942
+ }
943
+ let changed = false;
944
+ if (Array.isArray(raw.aspects) && raw.aspects.length > 0 && typeof raw.aspects[0] === "string") {
945
+ const aspectExceptions = raw.aspect_exceptions ?? {};
946
+ const anchors = raw.anchors ?? {};
947
+ raw.aspects = raw.aspects.map((id) => {
948
+ const entry = { aspect: id };
949
+ if (aspectExceptions[id]) entry.exceptions = aspectExceptions[id];
950
+ if (anchors[id]) entry.anchors = anchors[id];
951
+ return entry;
952
+ });
953
+ delete raw.aspect_exceptions;
954
+ delete raw.anchors;
955
+ changed = true;
956
+ }
957
+ if (raw.tags !== void 0) {
958
+ delete raw.tags;
959
+ changed = true;
960
+ }
961
+ if (changed) {
962
+ await writeFile2(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
963
+ actions.push(`Transformed ${path3.basename(path3.dirname(filePath))}/yg-node.yaml`);
964
+ }
965
+ }
966
+
967
+ // src/migrations/index.ts
968
+ var MIGRATIONS = [
969
+ {
970
+ to: "2.0.0",
971
+ description: "Rename YAML files to yg-* prefix, restructure config, convert aspects format",
972
+ run: migrateTo2
973
+ }
974
+ ];
975
+
680
976
  // src/cli/init.ts
681
977
  function getGraphSchemasDir() {
682
- const currentDir = path2.dirname(fileURLToPath(import.meta.url));
683
- const packageRoot = path2.join(currentDir, "..");
684
- return path2.join(packageRoot, "graph-schemas");
978
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
979
+ const packageRoot = path4.join(currentDir, "..");
980
+ return path4.join(packageRoot, "graph-schemas");
981
+ }
982
+ function getCliVersion() {
983
+ const currentDir = path4.dirname(fileURLToPath(import.meta.url));
984
+ const packageRoot = path4.join(currentDir, "..");
985
+ const pkg2 = JSON.parse(readFileSync(path4.join(packageRoot, "package.json"), "utf-8"));
986
+ return pkg2.version;
987
+ }
988
+ async function refreshSchemas(yggRoot) {
989
+ const schemasDir = path4.join(yggRoot, "schemas");
990
+ await mkdir2(schemasDir, { recursive: true });
991
+ const graphSchemasDir = getGraphSchemasDir();
992
+ try {
993
+ const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
994
+ const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
995
+ for (const file of schemaFiles) {
996
+ const srcPath = path4.join(graphSchemasDir, file);
997
+ const content = await readFile4(srcPath, "utf-8");
998
+ await writeFile3(path4.join(schemasDir, file), content, "utf-8");
999
+ }
1000
+ } catch {
1001
+ }
685
1002
  }
686
1003
  var GITIGNORE_CONTENT = `.journal.yaml
687
1004
  .drift-state
@@ -694,10 +1011,10 @@ function registerInitCommand(program2) {
694
1011
  "generic"
695
1012
  ).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
696
1013
  const projectRoot = process.cwd();
697
- const yggRoot = path2.join(projectRoot, ".yggdrasil");
1014
+ const yggRoot = path4.join(projectRoot, ".yggdrasil");
698
1015
  let upgradeMode = false;
699
1016
  try {
700
- const statResult = await stat(yggRoot);
1017
+ const statResult = await stat2(yggRoot);
701
1018
  if (!statResult.isDirectory()) {
702
1019
  process.stderr.write("Error: .yggdrasil exists but is not a directory.\n");
703
1020
  process.exit(1);
@@ -721,25 +1038,58 @@ function registerInitCommand(program2) {
721
1038
  process.exit(1);
722
1039
  }
723
1040
  if (upgradeMode) {
1041
+ const projectVersion = await detectVersion(yggRoot);
1042
+ if (!projectVersion) {
1043
+ process.stderr.write("Error: No Yggdrasil project found. Run `yg init` first.\n");
1044
+ process.exit(1);
1045
+ }
1046
+ const cliVersion = getCliVersion();
1047
+ if (valid2(projectVersion) && valid2(cliVersion) && gt2(projectVersion, cliVersion)) {
1048
+ process.stderr.write(
1049
+ `Warning: Project version (${projectVersion}) is newer than CLI (${cliVersion}). Upgrade your CLI.
1050
+ `
1051
+ );
1052
+ process.exit(1);
1053
+ }
1054
+ if (valid2(projectVersion) && valid2(cliVersion) && gt2(cliVersion, projectVersion)) {
1055
+ process.stdout.write(`Migrating from ${projectVersion} to ${cliVersion}...
1056
+
1057
+ `);
1058
+ const results = await runMigrations(projectVersion, MIGRATIONS, yggRoot);
1059
+ for (const result of results) {
1060
+ for (const action of result.actions) {
1061
+ process.stdout.write(` \u2713 ${action}
1062
+ `);
1063
+ }
1064
+ for (const warning of result.warnings) {
1065
+ process.stdout.write(` \u26A0 ${warning}
1066
+ `);
1067
+ }
1068
+ }
1069
+ if (results.length > 0) {
1070
+ process.stdout.write("\n");
1071
+ }
1072
+ }
1073
+ await refreshSchemas(yggRoot);
724
1074
  const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
725
1075
  process.stdout.write("\u2713 Rules refreshed.\n");
726
- process.stdout.write(` ${path2.relative(projectRoot, rulesPath2)}
1076
+ process.stdout.write(` ${path4.relative(projectRoot, rulesPath2)}
727
1077
  `);
728
1078
  return;
729
1079
  }
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");
1080
+ await mkdir2(path4.join(yggRoot, "model"), { recursive: true });
1081
+ await mkdir2(path4.join(yggRoot, "aspects"), { recursive: true });
1082
+ await mkdir2(path4.join(yggRoot, "flows"), { recursive: true });
1083
+ const schemasDir = path4.join(yggRoot, "schemas");
734
1084
  await mkdir2(schemasDir, { recursive: true });
735
1085
  const graphSchemasDir = getGraphSchemasDir();
736
1086
  try {
737
- const entries = await readdir(graphSchemasDir, { withFileTypes: true });
1087
+ const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
738
1088
  const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
739
1089
  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");
1090
+ const srcPath = path4.join(graphSchemasDir, file);
1091
+ const content = await readFile4(srcPath, "utf-8");
1092
+ await writeFile3(path4.join(schemasDir, file), content, "utf-8");
743
1093
  }
744
1094
  } catch (err) {
745
1095
  process.stderr.write(
@@ -747,95 +1097,94 @@ function registerInitCommand(program2) {
747
1097
  `
748
1098
  );
749
1099
  }
750
- await writeFile2(path2.join(yggRoot, "config.yaml"), DEFAULT_CONFIG, "utf-8");
751
- await writeFile2(path2.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
1100
+ await writeFile3(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
1101
+ await writeFile3(path4.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
752
1102
  const rulesPath = await installRulesForPlatform(projectRoot, platform);
753
1103
  process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
754
1104
  process.stdout.write("Created:\n");
755
- process.stdout.write(" .yggdrasil/config.yaml\n");
1105
+ process.stdout.write(" .yggdrasil/yg-config.yaml\n");
756
1106
  process.stdout.write(" .yggdrasil/.gitignore\n");
757
1107
  process.stdout.write(" .yggdrasil/model/\n");
758
1108
  process.stdout.write(" .yggdrasil/aspects/\n");
759
1109
  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)
1110
+ process.stdout.write(" .yggdrasil/schemas/ (yg-config, yg-node, yg-aspect, yg-flow)\n");
1111
+ process.stdout.write(` ${path4.relative(projectRoot, rulesPath)} (rules)
762
1112
 
763
1113
  `);
764
1114
  process.stdout.write("Next steps:\n");
765
- process.stdout.write(" 1. Edit .yggdrasil/config.yaml \u2014 set name, stack, standards\n");
1115
+ process.stdout.write(" 1. Edit .yggdrasil/yg-config.yaml \u2014 set name and configure node types\n");
766
1116
  process.stdout.write(" 2. Create nodes under .yggdrasil/model/\n");
767
1117
  process.stdout.write(" 3. Run: yg validate\n");
768
1118
  });
769
1119
  }
770
1120
 
771
1121
  // src/core/graph-loader.ts
772
- import { readdir as readdir3, readFile as readFile9 } from "fs/promises";
773
- import path7 from "path";
1122
+ import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
1123
+ import path9 from "path";
774
1124
 
775
1125
  // src/io/config-parser.ts
776
- import { readFile as readFile3 } from "fs/promises";
777
- import { parse as parseYaml } from "yaml";
1126
+ import { readFile as readFile5 } from "fs/promises";
1127
+ import { parse as parseYaml3 } from "yaml";
778
1128
  var DEFAULT_QUALITY = {
779
1129
  min_artifact_length: 50,
780
1130
  max_direct_relations: 10,
781
1131
  context_budget: { warning: 1e4, error: 2e4 }
782
1132
  };
783
1133
  async function parseConfig(filePath) {
784
- const content = await readFile3(filePath, "utf-8");
785
- const raw = parseYaml(content);
1134
+ const content = await readFile5(filePath, "utf-8");
1135
+ const raw = parseYaml3(content);
786
1136
  if (!raw || typeof raw !== "object") {
787
- throw new Error(`config.yaml: file is empty or not a valid YAML mapping`);
1137
+ throw new Error(`yg-config.yaml: file is empty or not a valid YAML mapping`);
788
1138
  }
1139
+ const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
789
1140
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
790
- throw new Error(`config.yaml: missing or invalid 'name' field`);
1141
+ throw new Error(`yg-config.yaml: missing or invalid 'name' field`);
791
1142
  }
792
1143
  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`);
1144
+ if (!nodeTypesRaw || typeof nodeTypesRaw !== "object" || Array.isArray(nodeTypesRaw) || Object.keys(nodeTypesRaw).length === 0) {
1145
+ throw new Error(`yg-config.yaml: 'node_types' must be a non-empty object`);
795
1146
  }
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
- };
1147
+ const nodeTypes = {};
1148
+ for (const [typeName, val] of Object.entries(nodeTypesRaw)) {
1149
+ const entry = val;
1150
+ if (!entry || typeof entry !== "object" || typeof entry.description !== "string" || entry.description.trim() === "") {
1151
+ throw new Error(
1152
+ `yg-config.yaml: node_types.${typeName} must have a non-empty 'description' string`
1153
+ );
807
1154
  }
808
- throw new Error(
809
- `config.yaml: node_types entry must be string or { name, required_aspects? }`
810
- );
811
- });
1155
+ const requiredAspects = Array.isArray(entry.required_aspects) ? entry.required_aspects.filter((t) => typeof t === "string") : void 0;
1156
+ nodeTypes[typeName] = {
1157
+ description: entry.description,
1158
+ required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
1159
+ };
1160
+ }
812
1161
  const artifacts = raw.artifacts;
813
1162
  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`);
1163
+ throw new Error(`yg-config.yaml: 'artifacts' must be a non-empty object`);
815
1164
  }
816
1165
  const artifactsMap = {};
817
1166
  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`);
1167
+ if (key === "yg-node.yaml") {
1168
+ throw new Error(`yg-config.yaml: artifact name 'yg-node.yaml' is reserved`);
820
1169
  }
821
1170
  const a = val;
822
1171
  const required = a.required;
823
1172
  if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
824
- throw new Error(`config.yaml: artifact '${key}' has invalid 'required' field`);
1173
+ throw new Error(`yg-config.yaml: artifact '${key}' has invalid 'required' field`);
825
1174
  }
826
1175
  if (typeof required === "object" && required && "when" in required) {
827
1176
  const when = required.when;
828
1177
  const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
829
1178
  if (!validWhen) {
830
1179
  throw new Error(
831
- `config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
1180
+ `yg-config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
832
1181
  );
833
1182
  }
834
1183
  }
835
1184
  artifactsMap[key] = {
836
1185
  required,
837
1186
  description: a.description ?? "",
838
- structural_context: a.structural_context ?? false
1187
+ included_in_relations: a.included_in_relations ?? false
839
1188
  };
840
1189
  }
841
1190
  const qualityRaw = raw.quality;
@@ -849,13 +1198,12 @@ async function parseConfig(filePath) {
849
1198
  } : DEFAULT_QUALITY;
850
1199
  if (quality.context_budget.error < quality.context_budget.warning) {
851
1200
  throw new Error(
852
- `config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
1201
+ `yg-config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
853
1202
  );
854
1203
  }
855
1204
  return {
1205
+ version,
856
1206
  name: raw.name.trim(),
857
- stack: raw.stack ?? {},
858
- standards: typeof raw.standards === "string" ? raw.standards : "",
859
1207
  node_types: nodeTypes,
860
1208
  artifacts: artifactsMap,
861
1209
  quality
@@ -863,8 +1211,8 @@ async function parseConfig(filePath) {
863
1211
  }
864
1212
 
865
1213
  // src/io/node-parser.ts
866
- import { readFile as readFile4 } from "fs/promises";
867
- import { parse as parseYaml2 } from "yaml";
1214
+ import { readFile as readFile6 } from "fs/promises";
1215
+ import { parse as parseYaml4 } from "yaml";
868
1216
  var RELATION_TYPES = [
869
1217
  "uses",
870
1218
  "calls",
@@ -877,120 +1225,103 @@ function isValidRelationType(t) {
877
1225
  return typeof t === "string" && RELATION_TYPES.includes(t);
878
1226
  }
879
1227
  async function parseNodeYaml(filePath) {
880
- const content = await readFile4(filePath, "utf-8");
881
- const raw = parseYaml2(content);
1228
+ const content = await readFile6(filePath, "utf-8");
1229
+ const raw = parseYaml4(content);
882
1230
  if (!raw || typeof raw !== "object") {
883
- throw new Error(`node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
1231
+ throw new Error(`yg-node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
884
1232
  }
885
1233
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
886
- throw new Error(`node.yaml at ${filePath}: missing or empty 'name'`);
1234
+ throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'name'`);
887
1235
  }
888
1236
  if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
889
- throw new Error(`node.yaml at ${filePath}: missing or empty 'type'`);
1237
+ throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
890
1238
  }
891
1239
  const relations = parseRelations(raw.relations, filePath);
892
1240
  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);
1241
+ const aspects = parseAspects(raw.aspects, filePath);
896
1242
  return {
897
1243
  name: raw.name.trim(),
898
1244
  type: raw.type.trim(),
899
1245
  aspects,
900
- aspect_exceptions: aspectExceptions,
901
1246
  blackbox: raw.blackbox ?? false,
902
1247
  relations: relations.length > 0 ? relations : void 0,
903
- mapping,
904
- anchors
1248
+ mapping
905
1249
  };
906
1250
  }
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) {
1251
+ function parseAspects(raw, filePath) {
935
1252
  if (raw === void 0 || raw === null) return void 0;
936
1253
  if (!Array.isArray(raw)) {
937
- throw new Error(`node.yaml at ${filePath}: 'aspect_exceptions' must be an array`);
1254
+ throw new Error(`yg-node.yaml at ${filePath}: 'aspects' must be an array`);
938
1255
  }
939
1256
  if (raw.length === 0) return void 0;
940
- const aspectSet = new Set(aspects ?? []);
941
1257
  const result = [];
1258
+ const seenAspects = /* @__PURE__ */ new Set();
942
1259
  for (let i = 0; i < raw.length; i++) {
943
1260
  const item = raw[i];
944
1261
  if (typeof item !== "object" || item === null) {
945
- throw new Error(`node.yaml at ${filePath}: aspect_exceptions[${i}] must be an object`);
1262
+ throw new Error(`yg-node.yaml at ${filePath}: aspects[${i}] must be an object with 'aspect' key`);
946
1263
  }
947
1264
  const obj = item;
948
1265
  if (typeof obj.aspect !== "string" || obj.aspect.trim() === "") {
949
1266
  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`
1267
+ `yg-node.yaml at ${filePath}: aspects[${i}].aspect must be a non-empty string`
956
1268
  );
957
1269
  }
958
1270
  const aspectId = obj.aspect.trim();
959
- if (!aspectSet.has(aspectId)) {
1271
+ if (seenAspects.has(aspectId)) {
960
1272
  throw new Error(
961
- `node.yaml at ${filePath}: aspect_exceptions[${i}].aspect "${aspectId}" is not in this node's aspects list`
1273
+ `yg-node.yaml at ${filePath}: duplicate aspect '${aspectId}' in aspects list`
962
1274
  );
963
1275
  }
964
- result.push({ aspect: aspectId, note: obj.note.trim() });
1276
+ seenAspects.add(aspectId);
1277
+ const entry = { aspect: aspectId };
1278
+ if (obj.exceptions !== void 0 && obj.exceptions !== null) {
1279
+ if (!Array.isArray(obj.exceptions)) {
1280
+ throw new Error(
1281
+ `yg-node.yaml at ${filePath}: aspects[${i}].exceptions must be an array of strings`
1282
+ );
1283
+ }
1284
+ const exceptions = obj.exceptions.filter((e) => typeof e === "string" && e.trim() !== "");
1285
+ if (exceptions.length > 0) {
1286
+ entry.exceptions = exceptions;
1287
+ }
1288
+ }
1289
+ if (obj.anchors !== void 0 && obj.anchors !== null) {
1290
+ if (!Array.isArray(obj.anchors)) {
1291
+ throw new Error(
1292
+ `yg-node.yaml at ${filePath}: aspects[${i}].anchors must be an array of strings`
1293
+ );
1294
+ }
1295
+ const anchors = obj.anchors.filter((a) => typeof a === "string" && a.trim() !== "");
1296
+ if (anchors.length > 0) {
1297
+ entry.anchors = anchors;
1298
+ }
1299
+ }
1300
+ result.push(entry);
965
1301
  }
966
1302
  return result.length > 0 ? result : void 0;
967
1303
  }
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
1304
  function parseRelations(raw, filePath) {
974
1305
  if (raw === void 0) return [];
975
1306
  if (!Array.isArray(raw)) {
976
- throw new Error(`node.yaml at ${filePath}: 'relations' must be an array`);
1307
+ throw new Error(`yg-node.yaml at ${filePath}: 'relations' must be an array`);
977
1308
  }
978
1309
  const result = [];
979
1310
  for (let index = 0; index < raw.length; index++) {
980
1311
  const r = raw[index];
981
1312
  if (typeof r !== "object" || r === null) {
982
- throw new Error(`node.yaml at ${filePath}: relations[${index}] must be an object`);
1313
+ throw new Error(`yg-node.yaml at ${filePath}: relations[${index}] must be an object`);
983
1314
  }
984
1315
  const obj = r;
985
1316
  const target = obj.target;
986
1317
  const type = obj.type;
987
1318
  if (typeof target !== "string" || target.trim() === "") {
988
1319
  throw new Error(
989
- `node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
1320
+ `yg-node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
990
1321
  );
991
1322
  }
992
1323
  if (!isValidRelationType(type)) {
993
- throw new Error(`node.yaml at ${filePath}: relations[${index}].type is invalid`);
1324
+ throw new Error(`yg-node.yaml at ${filePath}: relations[${index}].type is invalid`);
994
1325
  }
995
1326
  const rel = {
996
1327
  target: target.trim(),
@@ -1012,10 +1343,10 @@ function parseRelations(raw, filePath) {
1012
1343
  function validateRelativePath(pathValue, filePath, fieldName) {
1013
1344
  const normalized = pathValue.trim();
1014
1345
  if (normalized === "") {
1015
- throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
1346
+ throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
1016
1347
  }
1017
1348
  if (normalized.startsWith("/")) {
1018
- throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
1349
+ throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
1019
1350
  }
1020
1351
  return normalized;
1021
1352
  }
@@ -1025,35 +1356,35 @@ function parseMapping(rawMapping, filePath) {
1025
1356
  if (Array.isArray(obj.paths) && obj.paths.length > 0) {
1026
1357
  const paths = obj.paths.filter((p) => typeof p === "string").map((p) => validateRelativePath(p, filePath, "mapping.paths[]"));
1027
1358
  if (paths.length === 0) {
1028
- throw new Error(`node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
1359
+ throw new Error(`yg-node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
1029
1360
  }
1030
1361
  return { paths };
1031
1362
  }
1032
1363
  if (obj.paths !== void 0 || obj.type !== void 0 || obj.path !== void 0) {
1033
1364
  throw new Error(
1034
- `node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
1365
+ `yg-node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
1035
1366
  );
1036
1367
  }
1037
1368
  return void 0;
1038
1369
  }
1039
1370
 
1040
1371
  // src/io/aspect-parser.ts
1041
- import { readFile as readFile6 } from "fs/promises";
1042
- import { parse as parseYaml3 } from "yaml";
1372
+ import { readFile as readFile8 } from "fs/promises";
1373
+ import { parse as parseYaml5 } from "yaml";
1043
1374
 
1044
1375
  // 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 });
1376
+ import { readFile as readFile7, readdir as readdir3 } from "fs/promises";
1377
+ import path5 from "path";
1378
+ async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFiles) {
1379
+ const entries = await readdir3(dirPath, { withFileTypes: true });
1049
1380
  const artifacts = [];
1050
1381
  const includeSet = includeFiles && includeFiles.length > 0 ? new Set(includeFiles) : null;
1051
1382
  for (const entry of entries) {
1052
1383
  if (!entry.isFile()) continue;
1053
1384
  if (excludeFiles.includes(entry.name)) continue;
1054
1385
  if (includeSet && !includeSet.has(entry.name)) continue;
1055
- const filePath = path3.join(dirPath, entry.name);
1056
- const content = await readFile5(filePath, "utf-8");
1386
+ const filePath = path5.join(dirPath, entry.name);
1387
+ const content = await readFile7(filePath, "utf-8");
1057
1388
  artifacts.push({ filename: entry.name, content });
1058
1389
  }
1059
1390
  artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
@@ -1067,8 +1398,8 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
1067
1398
  if (!idTrimmed) {
1068
1399
  throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
1069
1400
  }
1070
- const content = await readFile6(aspectYamlPath, "utf-8");
1071
- const raw = parseYaml3(content);
1401
+ const content = await readFile8(aspectYamlPath, "utf-8");
1402
+ const raw = parseYaml5(content);
1072
1403
  if (!raw || typeof raw !== "object") {
1073
1404
  throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
1074
1405
  }
@@ -1076,7 +1407,7 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
1076
1407
  throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
1077
1408
  }
1078
1409
  const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1079
- const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
1410
+ const artifacts = await readArtifacts(aspectDir, ["yg-aspect.yaml"]);
1080
1411
  let implies;
1081
1412
  if (raw.implies !== void 0) {
1082
1413
  if (!Array.isArray(raw.implies)) {
@@ -1104,37 +1435,37 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
1104
1435
  }
1105
1436
 
1106
1437
  // 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";
1438
+ import { readFile as readFile9 } from "fs/promises";
1439
+ import path6 from "path";
1440
+ import { parse as parseYaml6 } from "yaml";
1110
1441
  async function parseFlow(flowDir, flowYamlPath) {
1111
- const content = await readFile7(flowYamlPath, "utf-8");
1112
- const raw = parseYaml4(content);
1442
+ const content = await readFile9(flowYamlPath, "utf-8");
1443
+ const raw = parseYaml6(content);
1113
1444
  if (!raw || typeof raw !== "object") {
1114
- throw new Error(`flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
1445
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
1115
1446
  }
1116
1447
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
1117
- throw new Error(`flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1448
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1118
1449
  }
1119
1450
  const nodes = raw.nodes;
1120
1451
  if (!Array.isArray(nodes) || nodes.length === 0) {
1121
- throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
1452
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
1122
1453
  }
1123
1454
  const nodePaths = nodes.filter((n) => typeof n === "string");
1124
1455
  if (nodePaths.length === 0) {
1125
- throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
1456
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
1126
1457
  }
1127
1458
  let aspects;
1128
1459
  if (raw.aspects !== void 0) {
1129
1460
  if (!Array.isArray(raw.aspects)) {
1130
- throw new Error(`flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
1461
+ throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
1131
1462
  }
1132
1463
  const aspectTags = raw.aspects.filter((a) => typeof a === "string");
1133
1464
  aspects = aspectTags.length > 0 ? aspectTags : [];
1134
1465
  }
1135
- const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
1466
+ const artifacts = await readArtifacts(flowDir, ["yg-flow.yaml"]);
1136
1467
  return {
1137
- path: path4.basename(flowDir),
1468
+ path: path6.basename(flowDir),
1138
1469
  name: raw.name.trim(),
1139
1470
  nodes: nodePaths,
1140
1471
  ...aspects !== void 0 && { aspects },
@@ -1143,27 +1474,28 @@ async function parseFlow(flowDir, flowYamlPath) {
1143
1474
  }
1144
1475
 
1145
1476
  // 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";
1477
+ import { readFile as readFile10 } from "fs/promises";
1478
+ import path7 from "path";
1479
+ import { parse as parseYaml7 } from "yaml";
1149
1480
  async function parseSchema(filePath) {
1150
- const content = await readFile8(filePath, "utf-8");
1151
- parseYaml5(content);
1152
- const schemaType = path5.basename(filePath, path5.extname(filePath));
1481
+ const content = await readFile10(filePath, "utf-8");
1482
+ parseYaml7(content);
1483
+ const rawName = path7.basename(filePath, path7.extname(filePath));
1484
+ const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
1153
1485
  return { schemaType };
1154
1486
  }
1155
1487
 
1156
1488
  // src/utils/paths.ts
1157
- import path6 from "path";
1489
+ import path8 from "path";
1158
1490
  import { fileURLToPath as fileURLToPath2 } from "url";
1159
- import { stat as stat2 } from "fs/promises";
1491
+ import { stat as stat3 } from "fs/promises";
1160
1492
  async function findYggRoot(projectRoot) {
1161
- let current = path6.resolve(projectRoot);
1162
- const root = path6.parse(current).root;
1493
+ let current = path8.resolve(projectRoot);
1494
+ const root = path8.parse(current).root;
1163
1495
  while (true) {
1164
- const yggPath = path6.join(current, ".yggdrasil");
1496
+ const yggPath = path8.join(current, ".yggdrasil");
1165
1497
  try {
1166
- const st = await stat2(yggPath);
1498
+ const st = await stat3(yggPath);
1167
1499
  if (!st.isDirectory()) {
1168
1500
  throw new Error(
1169
1501
  `.yggdrasil exists but is not a directory (${yggPath}). Run 'yg init' in a clean location.`
@@ -1175,7 +1507,7 @@ async function findYggRoot(projectRoot) {
1175
1507
  if (current === root) {
1176
1508
  throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
1177
1509
  }
1178
- current = path6.dirname(current);
1510
+ current = path8.dirname(current);
1179
1511
  continue;
1180
1512
  }
1181
1513
  throw err;
@@ -1191,27 +1523,25 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
1191
1523
  if (normalizedInput.length === 0) {
1192
1524
  throw new Error("Path cannot be empty");
1193
1525
  }
1194
- const absolute = path6.resolve(projectRoot, normalizedInput);
1195
- const relative = path6.relative(projectRoot, absolute);
1196
- const isOutside = relative.startsWith("..") || path6.isAbsolute(relative);
1526
+ const absolute = path8.resolve(projectRoot, normalizedInput);
1527
+ const relative = path8.relative(projectRoot, absolute);
1528
+ const isOutside = relative.startsWith("..") || path8.isAbsolute(relative);
1197
1529
  if (isOutside) {
1198
1530
  throw new Error(`Path is outside project root: ${rawPath}`);
1199
1531
  }
1200
- return relative.split(path6.sep).join("/");
1532
+ return relative.split(path8.sep).join("/");
1201
1533
  }
1202
1534
  function projectRootFromGraph(yggRootPath) {
1203
- return path6.dirname(yggRootPath);
1535
+ return path8.dirname(yggRootPath);
1204
1536
  }
1205
1537
 
1206
1538
  // src/core/graph-loader.ts
1207
1539
  function toModelPath(absolutePath, modelDir) {
1208
- return path7.relative(modelDir, absolutePath).split(path7.sep).join("/");
1540
+ return path9.relative(modelDir, absolutePath).split(path9.sep).join("/");
1209
1541
  }
1210
1542
  var FALLBACK_CONFIG = {
1211
1543
  name: "",
1212
- stack: {},
1213
- standards: "",
1214
- node_types: [],
1544
+ node_types: {},
1215
1545
  artifacts: {}
1216
1546
  };
1217
1547
  async function loadGraph(projectRoot, options = {}) {
@@ -1219,14 +1549,14 @@ async function loadGraph(projectRoot, options = {}) {
1219
1549
  let configError;
1220
1550
  let config = FALLBACK_CONFIG;
1221
1551
  try {
1222
- config = await parseConfig(path7.join(yggRoot, "config.yaml"));
1552
+ config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
1223
1553
  } catch (error) {
1224
1554
  if (!options.tolerateInvalidConfig) {
1225
1555
  throw error;
1226
1556
  }
1227
1557
  configError = error.message;
1228
1558
  }
1229
- const modelDir = path7.join(yggRoot, "model");
1559
+ const modelDir = path9.join(yggRoot, "model");
1230
1560
  const nodes = /* @__PURE__ */ new Map();
1231
1561
  const nodeParseErrors = [];
1232
1562
  const artifactFilenames = Object.keys(config.artifacts ?? {});
@@ -1240,9 +1570,9 @@ async function loadGraph(projectRoot, options = {}) {
1240
1570
  }
1241
1571
  throw err;
1242
1572
  }
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"));
1573
+ const aspects = await loadAspects(path9.join(yggRoot, "aspects"));
1574
+ const flows = await loadFlows(path9.join(yggRoot, "flows"));
1575
+ const schemas = await loadSchemas(path9.join(yggRoot, "schemas"));
1246
1576
  return {
1247
1577
  config,
1248
1578
  configError,
@@ -1255,18 +1585,18 @@ async function loadGraph(projectRoot, options = {}) {
1255
1585
  };
1256
1586
  }
1257
1587
  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");
1588
+ const entries = await readdir4(dirPath, { withFileTypes: true });
1589
+ const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
1260
1590
  if (!hasNodeYaml && dirPath !== modelDir) {
1261
1591
  return;
1262
1592
  }
1263
1593
  if (hasNodeYaml) {
1264
1594
  const graphPath = toModelPath(dirPath, modelDir);
1265
- const nodeYamlPath = path7.join(dirPath, "node.yaml");
1595
+ const nodeYamlPath = path9.join(dirPath, "yg-node.yaml");
1266
1596
  let meta;
1267
1597
  let nodeYamlRaw;
1268
1598
  try {
1269
- nodeYamlRaw = await readFile9(nodeYamlPath, "utf-8");
1599
+ nodeYamlRaw = await readFile11(nodeYamlPath, "utf-8");
1270
1600
  meta = await parseNodeYaml(nodeYamlPath);
1271
1601
  } catch (err) {
1272
1602
  nodeParseErrors.push({
@@ -1275,7 +1605,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1275
1605
  });
1276
1606
  return;
1277
1607
  }
1278
- const artifacts = await readArtifacts(dirPath, ["node.yaml"], artifactFilenames);
1608
+ const artifacts = await readArtifacts(dirPath, ["yg-node.yaml"], artifactFilenames);
1279
1609
  const node = {
1280
1610
  path: graphPath,
1281
1611
  meta,
@@ -1292,7 +1622,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1292
1622
  if (!entry.isDirectory()) continue;
1293
1623
  if (entry.name.startsWith(".")) continue;
1294
1624
  await scanModelDirectory(
1295
- path7.join(dirPath, entry.name),
1625
+ path9.join(dirPath, entry.name),
1296
1626
  modelDir,
1297
1627
  node,
1298
1628
  nodes,
@@ -1305,7 +1635,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
1305
1635
  if (!entry.isDirectory()) continue;
1306
1636
  if (entry.name.startsWith(".")) continue;
1307
1637
  await scanModelDirectory(
1308
- path7.join(dirPath, entry.name),
1638
+ path9.join(dirPath, entry.name),
1309
1639
  modelDir,
1310
1640
  null,
1311
1641
  nodes,
@@ -1325,28 +1655,28 @@ async function loadAspects(aspectsDir) {
1325
1655
  }
1326
1656
  }
1327
1657
  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");
1658
+ const entries = await readdir4(dirPath, { withFileTypes: true });
1659
+ const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
1330
1660
  if (hasAspectYaml) {
1331
- const id = path7.relative(aspectsRoot, dirPath).split(path7.sep).join("/");
1332
- const aspectYamlPath = path7.join(dirPath, "aspect.yaml");
1661
+ const id = path9.relative(aspectsRoot, dirPath).split(path9.sep).join("/");
1662
+ const aspectYamlPath = path9.join(dirPath, "yg-aspect.yaml");
1333
1663
  const aspect = await parseAspect(dirPath, aspectYamlPath, id);
1334
1664
  aspects.push(aspect);
1335
1665
  }
1336
1666
  for (const entry of entries) {
1337
1667
  if (!entry.isDirectory()) continue;
1338
1668
  if (entry.name.startsWith(".")) continue;
1339
- await scanAspectsDirectory(path7.join(dirPath, entry.name), aspectsRoot, aspects);
1669
+ await scanAspectsDirectory(path9.join(dirPath, entry.name), aspectsRoot, aspects);
1340
1670
  }
1341
1671
  }
1342
1672
  async function loadFlows(flowsDir) {
1343
1673
  try {
1344
- const entries = await readdir3(flowsDir, { withFileTypes: true });
1674
+ const entries = await readdir4(flowsDir, { withFileTypes: true });
1345
1675
  const flows = [];
1346
1676
  for (const entry of entries) {
1347
1677
  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);
1678
+ const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
1679
+ const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
1350
1680
  flows.push(flow);
1351
1681
  }
1352
1682
  return flows;
@@ -1356,12 +1686,12 @@ async function loadFlows(flowsDir) {
1356
1686
  }
1357
1687
  async function loadSchemas(schemasDir) {
1358
1688
  try {
1359
- const entries = await readdir3(schemasDir, { withFileTypes: true });
1689
+ const entries = await readdir4(schemasDir, { withFileTypes: true });
1360
1690
  const schemas = [];
1361
1691
  for (const entry of entries) {
1362
1692
  if (!entry.isFile()) continue;
1363
1693
  if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
1364
- const s = await parseSchema(path7.join(schemasDir, entry.name));
1694
+ const s = await parseSchema(path9.join(schemasDir, entry.name));
1365
1695
  schemas.push(s);
1366
1696
  }
1367
1697
  return schemas;
@@ -1371,8 +1701,8 @@ async function loadSchemas(schemasDir) {
1371
1701
  }
1372
1702
 
1373
1703
  // src/core/context-builder.ts
1374
- import { readFile as readFile10 } from "fs/promises";
1375
- import path8 from "path";
1704
+ import { readFile as readFile12 } from "fs/promises";
1705
+ import path10 from "path";
1376
1706
 
1377
1707
  // src/utils/tokens.ts
1378
1708
  function estimateTokens(text) {
@@ -1421,8 +1751,9 @@ async function buildContext(graph, nodePath) {
1421
1751
  }
1422
1752
  const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
1423
1753
  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));
1754
+ const entry = node.meta.aspects?.find((a) => a.aspect === aspect.id);
1755
+ const exceptionNote = entry?.exceptions?.join("; ");
1756
+ layers.push(buildAspectLayer(aspect, exceptionNote));
1426
1757
  }
1427
1758
  const fullText = layers.map((l) => l.content).join("\n\n");
1428
1759
  const tokenCount = estimateTokens(fullText);
@@ -1479,18 +1810,7 @@ function resolveAspects(aspectIds, aspects) {
1479
1810
  return expandedIds.map((id) => idToAspect.get(id)).filter((a) => a !== void 0);
1480
1811
  }
1481
1812
  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)"}
1813
+ const content = `**Project:** ${config.name}
1494
1814
  `;
1495
1815
  return { type: "global", label: "Global Context", content };
1496
1816
  }
@@ -1502,7 +1822,7 @@ function buildHierarchyLayer(ancestor, config, graph) {
1502
1822
  const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
1503
1823
  const content = filtered.map((a) => `### ${a.filename}
1504
1824
  ${a.content}`).join("\n\n");
1505
- const nodeAspects = ancestor.meta.aspects ?? [];
1825
+ const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
1506
1826
  const expanded = expandAspects(nodeAspects, graph.aspects);
1507
1827
  const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
1508
1828
  return {
@@ -1515,16 +1835,16 @@ ${a.content}`).join("\n\n");
1515
1835
  async function buildOwnLayer(node, config, graphRootPath, graph) {
1516
1836
  const parts = [];
1517
1837
  if (node.nodeYamlRaw) {
1518
- parts.push(`### node.yaml
1838
+ parts.push(`### yg-node.yaml
1519
1839
  ${node.nodeYamlRaw.trim()}`);
1520
1840
  } else {
1521
- const nodeYamlPath = path8.join(graphRootPath, "model", node.path, "node.yaml");
1841
+ const nodeYamlPath = path10.join(graphRootPath, "model", node.path, "yg-node.yaml");
1522
1842
  try {
1523
- const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
1524
- parts.push(`### node.yaml
1843
+ const nodeYamlContent = await readFile12(nodeYamlPath, "utf-8");
1844
+ parts.push(`### yg-node.yaml
1525
1845
  ${nodeYamlContent.trim()}`);
1526
1846
  } catch {
1527
- parts.push(`### node.yaml
1847
+ parts.push(`### yg-node.yaml
1528
1848
  (not found)`);
1529
1849
  }
1530
1850
  }
@@ -1534,7 +1854,7 @@ ${nodeYamlContent.trim()}`);
1534
1854
  ${a.content}`);
1535
1855
  }
1536
1856
  const content = parts.join("\n\n");
1537
- const nodeAspects = node.meta.aspects ?? [];
1857
+ const nodeAspects = (node.meta.aspects ?? []).map((a) => a.aspect);
1538
1858
  const expanded = expandAspects(nodeAspects, graph.aspects);
1539
1859
  const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
1540
1860
  return {
@@ -1556,7 +1876,7 @@ function buildStructuralRelationLayer(target, relation, config) {
1556
1876
 
1557
1877
  `;
1558
1878
  }
1559
- const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
1879
+ const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
1560
1880
  const structuralArts = structuralArtifactFilenames.map((filename) => {
1561
1881
  const art = target.artifacts.find((a) => a.filename === filename);
1562
1882
  return art ? { filename: art.filename, content: art.content } : null;
@@ -1671,10 +1991,10 @@ function collectAncestors(node) {
1671
1991
  function collectEffectiveAspectIds(graph, nodePath) {
1672
1992
  const node = graph.nodes.get(nodePath);
1673
1993
  if (!node) return /* @__PURE__ */ new Set();
1674
- const raw = new Set(node.meta.aspects ?? []);
1994
+ const raw = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
1675
1995
  let ancestor = node.parent;
1676
1996
  while (ancestor) {
1677
- for (const id of ancestor.meta.aspects ?? []) raw.add(id);
1997
+ for (const entry of ancestor.meta.aspects ?? []) raw.add(entry.aspect);
1678
1998
  ancestor = ancestor.parent;
1679
1999
  }
1680
2000
  const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
@@ -1687,8 +2007,11 @@ function collectEffectiveAspectIds(graph, nodePath) {
1687
2007
  }
1688
2008
 
1689
2009
  // src/core/validator.ts
1690
- import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
1691
- import path9 from "path";
2010
+ import { readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
2011
+ import path11 from "path";
2012
+ function getAspectIds(aspects) {
2013
+ return (aspects ?? []).map((a) => a.aspect);
2014
+ }
1692
2015
  var RESERVED_DIRS = /* @__PURE__ */ new Set();
1693
2016
  async function validate(graph, scope = "all") {
1694
2017
  const issues = [];
@@ -1717,7 +2040,6 @@ async function validate(graph, scope = "all") {
1717
2040
  issues.push(...checkImpliedAspectsExist(graph));
1718
2041
  issues.push(...checkImpliesNoCycles(graph));
1719
2042
  issues.push(...checkRequiredAspectsCoverage(graph));
1720
- issues.push(...checkAspectExceptions(graph));
1721
2043
  issues.push(...await checkAnchorPresence(graph));
1722
2044
  issues.push(...checkRequiredArtifacts(graph));
1723
2045
  issues.push(...checkInvalidArtifactConditions(graph));
@@ -1766,7 +2088,7 @@ async function validate(graph, scope = "all") {
1766
2088
  }
1767
2089
  function checkNodeTypes(graph) {
1768
2090
  const issues = [];
1769
- const allowedTypes = new Set((graph.config.node_types ?? []).map((t) => t.name));
2091
+ const allowedTypes = new Set(Object.keys(graph.config.node_types ?? {}));
1770
2092
  for (const [nodePath, node] of graph.nodes) {
1771
2093
  if (!allowedTypes.has(node.meta.type)) {
1772
2094
  issues.push({
@@ -1833,7 +2155,7 @@ function checkAspectsDefined(graph) {
1833
2155
  const issues = [];
1834
2156
  const validAspectIds = new Set(graph.aspects.map((a) => a.id));
1835
2157
  for (const [nodePath, node] of graph.nodes) {
1836
- for (const aspectId of node.meta.aspects ?? []) {
2158
+ for (const aspectId of getAspectIds(node.meta.aspects)) {
1837
2159
  if (!validAspectIds.has(aspectId)) {
1838
2160
  issues.push({
1839
2161
  severity: "error",
@@ -1937,16 +2259,16 @@ function checkImpliesNoCycles(graph) {
1937
2259
  function checkRequiredAspectsCoverage(graph) {
1938
2260
  const issues = [];
1939
2261
  const typeConfig = new Map(
1940
- (graph.config.node_types ?? []).map((t) => [t.name, t.required_aspects ?? []])
2262
+ Object.entries(graph.config.node_types ?? {}).map(([name, cfg]) => [name, cfg.required_aspects ?? []])
1941
2263
  );
1942
2264
  for (const [nodePath, node] of graph.nodes) {
1943
2265
  if (node.meta.blackbox) continue;
1944
2266
  const requiredAspects = typeConfig.get(node.meta.type);
1945
2267
  if (!requiredAspects || requiredAspects.length === 0) continue;
1946
- const nodeAspects = node.meta.aspects ?? [];
2268
+ const nodeAspectIds = getAspectIds(node.meta.aspects);
1947
2269
  let effectiveAspects;
1948
2270
  try {
1949
- effectiveAspects = resolveAspects(nodeAspects, graph.aspects);
2271
+ effectiveAspects = resolveAspects(nodeAspectIds, graph.aspects);
1950
2272
  } catch {
1951
2273
  continue;
1952
2274
  }
@@ -1965,24 +2287,6 @@ function checkRequiredAspectsCoverage(graph) {
1965
2287
  }
1966
2288
  return issues;
1967
2289
  }
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
2290
  function checkNoCycles(graph) {
1987
2291
  const WHITE = 0;
1988
2292
  const GRAY = 1;
@@ -2069,14 +2373,14 @@ function checkMappingOverlap(graph) {
2069
2373
  }
2070
2374
  async function checkMappingPathsExist(graph) {
2071
2375
  const issues = [];
2072
- const projectRoot = path9.dirname(graph.rootPath);
2073
- const { access: access4 } = await import("fs/promises");
2376
+ const projectRoot = path11.dirname(graph.rootPath);
2377
+ const { access: access5 } = await import("fs/promises");
2074
2378
  for (const [nodePath, node] of graph.nodes) {
2075
2379
  const mappingPaths = normalizeMappingPaths(node.meta.mapping);
2076
2380
  for (const mp of mappingPaths) {
2077
- const absPath = path9.join(projectRoot, mp);
2381
+ const absPath = path11.join(projectRoot, mp);
2078
2382
  try {
2079
- await access4(absPath);
2383
+ await access5(absPath);
2080
2384
  } catch {
2081
2385
  issues.push({
2082
2386
  severity: "warning",
@@ -2116,7 +2420,7 @@ function artifactRequiredReason(graph, nodePath, node, required) {
2116
2420
  if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
2117
2421
  const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
2118
2422
  const aspectId = when.slice(prefix.length);
2119
- return (node.meta.aspects ?? []).includes(aspectId) ? `node has aspect '${aspectId}'` : null;
2423
+ return (node.meta.aspects ?? []).some((a) => a.aspect === aspectId) ? `node has aspect '${aspectId}'` : null;
2120
2424
  }
2121
2425
  return null;
2122
2426
  }
@@ -2307,7 +2611,7 @@ function checkSchemas(graph) {
2307
2611
  severity: "warning",
2308
2612
  code: "W010",
2309
2613
  rule: "missing-schema",
2310
- message: `Schema '${required}.yaml' missing from .yggdrasil/schemas/`
2614
+ message: `Schema 'yg-${required}.yaml' missing from .yggdrasil/schemas/`
2311
2615
  });
2312
2616
  }
2313
2617
  }
@@ -2315,11 +2619,11 @@ function checkSchemas(graph) {
2315
2619
  }
2316
2620
  async function checkDirectoriesHaveNodeYaml(graph) {
2317
2621
  const issues = [];
2318
- const modelDir = path9.join(graph.rootPath, "model");
2622
+ const modelDir = path11.join(graph.rootPath, "model");
2319
2623
  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);
2624
+ const entries = await readdir5(dirPath, { withFileTypes: true });
2625
+ const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
2626
+ const dirName = path11.basename(dirPath);
2323
2627
  if (RESERVED_DIRS.has(dirName)) return;
2324
2628
  const hasFiles = entries.some((e) => e.isFile());
2325
2629
  const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
@@ -2330,7 +2634,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2330
2634
  severity: "error",
2331
2635
  code: "E015",
2332
2636
  rule: "missing-node-yaml",
2333
- message: `Directory '${graphPath}' has files but no node.yaml`,
2637
+ message: `Directory '${graphPath}' has files but no yg-node.yaml`,
2334
2638
  nodePath: graphPath
2335
2639
  });
2336
2640
  } else if (hasSubdirs) {
@@ -2338,7 +2642,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2338
2642
  severity: "warning",
2339
2643
  code: "W013",
2340
2644
  rule: "directory-without-node",
2341
- message: `Directory '${graphPath}' has subdirectories but no node.yaml \u2014 consider creating a node`,
2645
+ message: `Directory '${graphPath}' has subdirectories but no yg-node.yaml \u2014 consider creating a node`,
2342
2646
  nodePath: graphPath
2343
2647
  });
2344
2648
  }
@@ -2347,15 +2651,15 @@ async function checkDirectoriesHaveNodeYaml(graph) {
2347
2651
  if (!entry.isDirectory()) continue;
2348
2652
  if (RESERVED_DIRS.has(entry.name)) continue;
2349
2653
  if (entry.name.startsWith(".")) continue;
2350
- await scanDir(path9.join(dirPath, entry.name), [...segments, entry.name]);
2654
+ await scanDir(path11.join(dirPath, entry.name), [...segments, entry.name]);
2351
2655
  }
2352
2656
  }
2353
2657
  try {
2354
- const rootEntries = await readdir4(modelDir, { withFileTypes: true });
2658
+ const rootEntries = await readdir5(modelDir, { withFileTypes: true });
2355
2659
  for (const entry of rootEntries) {
2356
2660
  if (!entry.isDirectory()) continue;
2357
2661
  if (entry.name.startsWith(".")) continue;
2358
- await scanDir(path9.join(modelDir, entry.name), [entry.name]);
2662
+ await scanDir(path11.join(modelDir, entry.name), [entry.name]);
2359
2663
  }
2360
2664
  } catch {
2361
2665
  }
@@ -2365,14 +2669,14 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
2365
2669
  const files = [];
2366
2670
  async function collectFiles(absPath) {
2367
2671
  try {
2368
- const s = await stat3(absPath);
2672
+ const s = await stat4(absPath);
2369
2673
  if (s.isFile()) {
2370
2674
  files.push(absPath);
2371
2675
  } else if (s.isDirectory()) {
2372
- const entries = await readdir4(absPath, { withFileTypes: true });
2676
+ const entries = await readdir5(absPath, { withFileTypes: true });
2373
2677
  for (const entry of entries) {
2374
2678
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2375
- const entryPath = path9.join(absPath, entry.name);
2679
+ const entryPath = path11.join(absPath, entry.name);
2376
2680
  if (entry.isFile()) {
2377
2681
  files.push(entryPath);
2378
2682
  } else if (entry.isDirectory()) {
@@ -2384,28 +2688,16 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
2384
2688
  }
2385
2689
  }
2386
2690
  for (const mp of mappingPaths) {
2387
- await collectFiles(path9.join(projectRoot, mp));
2691
+ await collectFiles(path11.join(projectRoot, mp));
2388
2692
  }
2389
2693
  return files;
2390
2694
  }
2391
2695
  async function checkAnchorPresence(graph) {
2392
2696
  const issues = [];
2393
- const projectRoot = path9.dirname(graph.rootPath);
2697
+ const projectRoot = path11.dirname(graph.rootPath);
2394
2698
  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
- }
2699
+ const aspectsWithAnchors = (node.meta.aspects ?? []).filter((a) => a.anchors && a.anchors.length > 0);
2700
+ if (aspectsWithAnchors.length === 0) continue;
2409
2701
  const mappingPaths = normalizeMappingPaths(node.meta.mapping);
2410
2702
  if (mappingPaths.length === 0) continue;
2411
2703
  const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
@@ -2413,21 +2705,20 @@ async function checkAnchorPresence(graph) {
2413
2705
  const fileContents = [];
2414
2706
  for (const filePath of sourceFiles) {
2415
2707
  try {
2416
- const content = await readFile11(filePath, "utf-8");
2708
+ const content = await readFile13(filePath, "utf-8");
2417
2709
  fileContents.push(content);
2418
2710
  } catch {
2419
2711
  }
2420
2712
  }
2421
- for (const [aspectId, anchorList] of Object.entries(anchors)) {
2422
- if (!nodeAspects.has(aspectId)) continue;
2423
- for (const anchor of anchorList) {
2713
+ for (const entry of aspectsWithAnchors) {
2714
+ for (const anchor of entry.anchors) {
2424
2715
  const found = fileContents.some((content) => content.includes(anchor));
2425
2716
  if (!found) {
2426
2717
  issues.push({
2427
2718
  severity: "warning",
2428
2719
  code: "W014",
2429
2720
  rule: "anchor-not-found",
2430
- message: `Anchor '${anchor}' for aspect '${aspectId}' not found in mapped source files`,
2721
+ message: `Anchor '${anchor}' for aspect '${entry.aspect}' not found in mapped source files`,
2431
2722
  nodePath
2432
2723
  });
2433
2724
  }
@@ -2652,13 +2943,13 @@ ${errors.length} errors, ${warnings.length} warnings.
2652
2943
  import chalk2 from "chalk";
2653
2944
 
2654
2945
  // src/io/drift-state-store.ts
2655
- import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
2656
- import path10 from "path";
2946
+ import { readFile as readFile14, writeFile as writeFile4 } from "fs/promises";
2947
+ import path12 from "path";
2657
2948
  import { parse as yamlParse } from "yaml";
2658
2949
  var DRIFT_STATE_FILE = ".drift-state";
2659
2950
  async function readDriftState(yggRoot) {
2660
2951
  try {
2661
- const content = await readFile12(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
2952
+ const content = await readFile14(path12.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
2662
2953
  let raw;
2663
2954
  try {
2664
2955
  raw = JSON.parse(content);
@@ -2679,24 +2970,24 @@ async function readDriftState(yggRoot) {
2679
2970
  }
2680
2971
  async function writeDriftState(yggRoot, state) {
2681
2972
  const content = JSON.stringify(state);
2682
- await writeFile3(path10.join(yggRoot, DRIFT_STATE_FILE), content, "utf-8");
2973
+ await writeFile4(path12.join(yggRoot, DRIFT_STATE_FILE), content, "utf-8");
2683
2974
  }
2684
2975
 
2685
2976
  // src/utils/hash.ts
2686
- import { readFile as readFile13, readdir as readdir5, stat as stat4 } from "fs/promises";
2687
- import path11 from "path";
2977
+ import { readFile as readFile15, readdir as readdir6, stat as stat5 } from "fs/promises";
2978
+ import path13 from "path";
2688
2979
  import { createHash } from "crypto";
2689
2980
  import { createRequire } from "module";
2690
2981
  var require2 = createRequire(import.meta.url);
2691
2982
  var ignoreFactory = require2("ignore");
2692
2983
  async function hashFile(filePath) {
2693
- const content = await readFile13(filePath);
2984
+ const content = await readFile15(filePath);
2694
2985
  return createHash("sha256").update(content).digest("hex");
2695
2986
  }
2696
2987
  async function loadRootGitignoreStack(projectRoot) {
2697
2988
  if (!projectRoot) return [];
2698
2989
  try {
2699
- const content = await readFile13(path11.join(projectRoot, ".gitignore"), "utf-8");
2990
+ const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
2700
2991
  const matcher = ignoreFactory();
2701
2992
  matcher.add(content);
2702
2993
  return [{ basePath: projectRoot, matcher }];
@@ -2706,7 +2997,7 @@ async function loadRootGitignoreStack(projectRoot) {
2706
2997
  }
2707
2998
  function isIgnoredByStack(candidatePath, stack) {
2708
2999
  for (const { basePath, matcher } of stack) {
2709
- const relativePath = path11.relative(basePath, candidatePath);
3000
+ const relativePath = path13.relative(basePath, candidatePath);
2710
3001
  if (relativePath === "" || relativePath.startsWith("..")) continue;
2711
3002
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
2712
3003
  }
@@ -2721,9 +3012,9 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
2721
3012
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
2722
3013
  const allFiles = [];
2723
3014
  for (const tf of trackedFiles) {
2724
- const absPath = path11.join(projectRoot, tf.path);
3015
+ const absPath = path13.join(projectRoot, tf.path);
2725
3016
  try {
2726
- const st = await stat4(absPath);
3017
+ const st = await stat5(absPath);
2727
3018
  if (st.isDirectory()) {
2728
3019
  const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
2729
3020
  projectRoot,
@@ -2731,7 +3022,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
2731
3022
  });
2732
3023
  for (const entry of dirEntries) {
2733
3024
  allFiles.push({
2734
- relPath: path11.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3025
+ relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
2735
3026
  absPath: entry.absPath,
2736
3027
  mtimeMs: entry.mtimeMs
2737
3028
  });
@@ -2771,17 +3062,17 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
2771
3062
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
2772
3063
  let stack = options.gitignoreStack ?? [];
2773
3064
  try {
2774
- const localContent = await readFile13(path11.join(directoryPath, ".gitignore"), "utf-8");
3065
+ const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
2775
3066
  const localMatcher = ignoreFactory();
2776
3067
  localMatcher.add(localContent);
2777
3068
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
2778
3069
  } catch {
2779
3070
  }
2780
- const entries = await readdir5(directoryPath, { withFileTypes: true });
3071
+ const entries = await readdir6(directoryPath, { withFileTypes: true });
2781
3072
  const dirs = [];
2782
3073
  const files = [];
2783
3074
  for (const entry of entries) {
2784
- const absoluteChildPath = path11.join(directoryPath, entry.name);
3075
+ const absoluteChildPath = path13.join(directoryPath, entry.name);
2785
3076
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
2786
3077
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
2787
3078
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -2792,9 +3083,9 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
2792
3083
  gitignoreStack: stack
2793
3084
  }))),
2794
3085
  Promise.all(files.map(async (f) => {
2795
- const fileStat = await stat4(f);
3086
+ const fileStat = await stat5(f);
2796
3087
  return {
2797
- relPath: path11.relative(rootDirectoryPath, f),
3088
+ relPath: path13.relative(rootDirectoryPath, f),
2798
3089
  absPath: f,
2799
3090
  mtimeMs: fileStat.mtimeMs
2800
3091
  };
@@ -2807,14 +3098,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
2807
3098
  }
2808
3099
 
2809
3100
  // src/core/context-files.ts
2810
- import path12 from "path";
3101
+ import path14 from "path";
2811
3102
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
2812
3103
  function collectTrackedFiles(node, graph) {
2813
3104
  const seen = /* @__PURE__ */ new Set();
2814
3105
  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("/");
3106
+ const projectRoot = path14.dirname(graph.rootPath);
3107
+ const yggPrefix = path14.relative(projectRoot, graph.rootPath);
3108
+ const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
2818
3109
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
2819
3110
  function addFile(filePath, category) {
2820
3111
  if (seen.has(filePath)) return;
@@ -2825,7 +3116,7 @@ function collectTrackedFiles(node, graph) {
2825
3116
  return [yggPrefixNormalized, ...segments].join("/");
2826
3117
  }
2827
3118
  function addNodeFiles(n) {
2828
- addFile(graphPath("model", n.path, "node.yaml"), "graph");
3119
+ addFile(graphPath("model", n.path, "yg-node.yaml"), "graph");
2829
3120
  for (const art of n.artifacts) {
2830
3121
  if (configArtifactKeys.has(art.filename)) {
2831
3122
  addFile(graphPath("model", n.path, art.filename), "graph");
@@ -2838,12 +3129,12 @@ function collectTrackedFiles(node, graph) {
2838
3129
  addNodeFiles(ancestor);
2839
3130
  }
2840
3131
  const allAspectIds = /* @__PURE__ */ new Set();
2841
- for (const id of node.meta.aspects ?? []) {
2842
- allAspectIds.add(id);
3132
+ for (const entry of node.meta.aspects ?? []) {
3133
+ allAspectIds.add(entry.aspect);
2843
3134
  }
2844
3135
  for (const ancestor of ancestors) {
2845
- for (const id of ancestor.meta.aspects ?? []) {
2846
- allAspectIds.add(id);
3136
+ for (const entry of ancestor.meta.aspects ?? []) {
3137
+ allAspectIds.add(entry.aspect);
2847
3138
  }
2848
3139
  }
2849
3140
  const participatingFlows = collectParticipatingFlows2(graph, node, ancestors);
@@ -2854,7 +3145,7 @@ function collectTrackedFiles(node, graph) {
2854
3145
  }
2855
3146
  const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2856
3147
  for (const aspect of resolvedAspects) {
2857
- addFile(graphPath("aspects", aspect.id, "aspect.yaml"), "graph");
3148
+ addFile(graphPath("aspects", aspect.id, "yg-aspect.yaml"), "graph");
2858
3149
  for (const art of aspect.artifacts) {
2859
3150
  addFile(graphPath("aspects", aspect.id, art.filename), "graph");
2860
3151
  }
@@ -2863,7 +3154,7 @@ function collectTrackedFiles(node, graph) {
2863
3154
  if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
2864
3155
  const target = graph.nodes.get(relation.target);
2865
3156
  if (!target) continue;
2866
- const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
3157
+ const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2867
3158
  const structuralArts = structuralFilenames.filter(
2868
3159
  (filename) => target.artifacts.some((a) => a.filename === filename)
2869
3160
  );
@@ -2880,7 +3171,7 @@ function collectTrackedFiles(node, graph) {
2880
3171
  }
2881
3172
  }
2882
3173
  for (const flow of participatingFlows) {
2883
- addFile(graphPath("flows", flow.path, "flow.yaml"), "graph");
3174
+ addFile(graphPath("flows", flow.path, "yg-flow.yaml"), "graph");
2884
3175
  for (const art of flow.artifacts) {
2885
3176
  addFile(graphPath("flows", flow.path, art.filename), "graph");
2886
3177
  }
@@ -2897,8 +3188,8 @@ function collectParticipatingFlows2(graph, node, ancestors) {
2897
3188
  }
2898
3189
 
2899
3190
  // src/core/drift-detector.ts
2900
- import { access } from "fs/promises";
2901
- import path13 from "path";
3191
+ import { access as access2 } from "fs/promises";
3192
+ import path15 from "path";
2902
3193
  function getChildMappingExclusions(graph, nodePath) {
2903
3194
  const node = graph.nodes.get(nodePath);
2904
3195
  if (!node) return [];
@@ -2920,7 +3211,7 @@ function getChildMappingExclusions(graph, nodePath) {
2920
3211
  return exclusions;
2921
3212
  }
2922
3213
  async function detectDrift(graph, filterNodePath) {
2923
- const projectRoot = path13.dirname(graph.rootPath);
3214
+ const projectRoot = path15.dirname(graph.rootPath);
2924
3215
  const driftState = await readDriftState(graph.rootPath);
2925
3216
  const entries = [];
2926
3217
  for (const [nodePath, node] of graph.nodes) {
@@ -3002,16 +3293,16 @@ async function detectDrift(graph, filterNodePath) {
3002
3293
  };
3003
3294
  }
3004
3295
  function categorizeFile(filePath, _rootPath, projectRoot) {
3005
- const yggPrefix = path13.relative(projectRoot, _rootPath);
3006
- const normalizedPrefix = yggPrefix.split(path13.sep).join("/");
3296
+ const yggPrefix = path15.relative(projectRoot, _rootPath);
3297
+ const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3007
3298
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3008
3299
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3009
3300
  }
3010
3301
  async function allPathsMissing(projectRoot, mappingPaths) {
3011
3302
  for (const mp of mappingPaths) {
3012
- const absPath = path13.join(projectRoot, mp);
3303
+ const absPath = path15.join(projectRoot, mp);
3013
3304
  try {
3014
- await access(absPath);
3305
+ await access2(absPath);
3015
3306
  return false;
3016
3307
  } catch {
3017
3308
  }
@@ -3019,7 +3310,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3019
3310
  return true;
3020
3311
  }
3021
3312
  async function syncDriftState(graph, nodePath) {
3022
- const projectRoot = path13.dirname(graph.rootPath);
3313
+ const projectRoot = path15.dirname(graph.rootPath);
3023
3314
  const node = graph.nodes.get(nodePath);
3024
3315
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3025
3316
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
@@ -3334,10 +3625,10 @@ function registerTreeCommand(program2) {
3334
3625
  let roots;
3335
3626
  let showProjectName;
3336
3627
  if (options.root?.trim()) {
3337
- const path18 = options.root.trim().replace(/\/$/, "");
3338
- const node = graph.nodes.get(path18);
3628
+ const path20 = options.root.trim().replace(/\/$/, "");
3629
+ const node = graph.nodes.get(path20);
3339
3630
  if (!node) {
3340
- process.stderr.write(`Error: path '${path18}' not found
3631
+ process.stderr.write(`Error: path '${path20}' not found
3341
3632
  `);
3342
3633
  process.exit(1);
3343
3634
  }
@@ -3381,8 +3672,8 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
3381
3672
  }
3382
3673
 
3383
3674
  // src/cli/owner.ts
3384
- import path14 from "path";
3385
- import { access as access2 } from "fs/promises";
3675
+ import path16 from "path";
3676
+ import { access as access3 } from "fs/promises";
3386
3677
  function normalizeForMatch(inputPath) {
3387
3678
  return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
3388
3679
  }
@@ -3411,14 +3702,14 @@ function registerOwnerCommand(program2) {
3411
3702
  const graph = await loadGraph(cwd);
3412
3703
  const repoRoot = projectRootFromGraph(graph.rootPath);
3413
3704
  const rawPath = options.file.trim();
3414
- const absolute = path14.resolve(cwd, rawPath);
3415
- const repoRelative = path14.relative(repoRoot, absolute).split(path14.sep).join("/");
3705
+ const absolute = path16.resolve(cwd, rawPath);
3706
+ const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
3416
3707
  const result = findOwner(graph, repoRoot, repoRelative);
3417
3708
  if (!result.nodePath) {
3418
- const absPath = path14.resolve(repoRoot, result.file);
3709
+ const absPath = path16.resolve(repoRoot, result.file);
3419
3710
  let exists = true;
3420
3711
  try {
3421
- await access2(absPath);
3712
+ await access3(absPath);
3422
3713
  } catch {
3423
3714
  exists = false;
3424
3715
  }
@@ -3449,7 +3740,7 @@ function registerOwnerCommand(program2) {
3449
3740
 
3450
3741
  // src/core/dependency-resolver.ts
3451
3742
  import { execSync } from "child_process";
3452
- import path15 from "path";
3743
+ import path17 from "path";
3453
3744
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3454
3745
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
3455
3746
  function filterRelationType(relType, filter) {
@@ -3524,9 +3815,9 @@ function registerDepsCommand(program2) {
3524
3815
  }
3525
3816
 
3526
3817
  // src/core/graph-from-git.ts
3527
- import { mkdtemp, rm } from "fs/promises";
3818
+ import { mkdtemp, rm as rm2 } from "fs/promises";
3528
3819
  import { tmpdir } from "os";
3529
- import path16 from "path";
3820
+ import path18 from "path";
3530
3821
  import { execSync as execSync2 } from "child_process";
3531
3822
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3532
3823
  const yggPath = ".yggdrasil";
@@ -3537,8 +3828,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3537
3828
  return null;
3538
3829
  }
3539
3830
  try {
3540
- tmpDir = await mkdtemp(path16.join(tmpdir(), "ygg-git-"));
3541
- const archivePath = path16.join(tmpDir, "archive.tar");
3831
+ tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
3832
+ const archivePath = path18.join(tmpDir, "archive.tar");
3542
3833
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
3543
3834
  cwd: projectRoot,
3544
3835
  stdio: "pipe"
@@ -3550,7 +3841,7 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3550
3841
  return null;
3551
3842
  } finally {
3552
3843
  if (tmpDir) {
3553
- await rm(tmpDir, { recursive: true, force: true });
3844
+ await rm2(tmpDir, { recursive: true, force: true });
3554
3845
  }
3555
3846
  }
3556
3847
  }
@@ -3608,14 +3899,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
3608
3899
  }
3609
3900
  const chains = [];
3610
3901
  for (const node of transitiveOnly) {
3611
- const path18 = [];
3902
+ const path20 = [];
3612
3903
  let current = node;
3613
3904
  while (current) {
3614
- path18.unshift(current);
3905
+ path20.unshift(current);
3615
3906
  current = parent.get(current);
3616
3907
  }
3617
- if (path18.length >= 3) {
3618
- chains.push(path18.slice(1).map((p) => `<- ${p}`).join(" "));
3908
+ if (path20.length >= 3) {
3909
+ chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
3619
3910
  }
3620
3911
  }
3621
3912
  return chains.sort();
@@ -3685,14 +3976,14 @@ async function handleAspectImpact(graph, aspectId, simulate) {
3685
3976
  const effective = collectEffectiveAspectIds(graph, nodePath);
3686
3977
  if (effective.has(aspectId)) {
3687
3978
  const node = graph.nodes.get(nodePath);
3688
- const ownAspects = new Set(node.meta.aspects ?? []);
3689
- if (ownAspects.has(aspectId)) {
3979
+ const ownAspectIds = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
3980
+ if (ownAspectIds.has(aspectId)) {
3690
3981
  affected.push({ path: nodePath, source: "own" });
3691
3982
  } else {
3692
3983
  let fromHierarchy = false;
3693
3984
  let anc = node.parent;
3694
3985
  while (anc) {
3695
- if ((anc.meta.aspects ?? []).includes(aspectId)) {
3986
+ if ((anc.meta.aspects ?? []).some((a) => a.aspect === aspectId)) {
3696
3987
  fromHierarchy = true;
3697
3988
  break;
3698
3989
  }
@@ -4037,16 +4328,16 @@ function registerFlowsCommand(program2) {
4037
4328
  }
4038
4329
 
4039
4330
  // 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";
4331
+ import { readFile as readFile16, writeFile as writeFile5, mkdir as mkdir3, rename as rename2, access as access4 } from "fs/promises";
4332
+ import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
4333
+ import path19 from "path";
4043
4334
  var JOURNAL_FILE = ".journal.yaml";
4044
4335
  var ARCHIVE_DIR = "journals-archive";
4045
4336
  async function readJournal(yggRoot) {
4046
- const filePath = path17.join(yggRoot, JOURNAL_FILE);
4337
+ const filePath = path19.join(yggRoot, JOURNAL_FILE);
4047
4338
  try {
4048
- const content = await readFile14(filePath, "utf-8");
4049
- const raw = parseYaml6(content);
4339
+ const content = await readFile16(filePath, "utf-8");
4340
+ const raw = parseYaml8(content);
4050
4341
  const entries = raw.entries ?? [];
4051
4342
  return Array.isArray(entries) ? entries : [];
4052
4343
  } catch {
@@ -4058,27 +4349,27 @@ async function appendJournalEntry(yggRoot, note, target) {
4058
4349
  const at = (/* @__PURE__ */ new Date()).toISOString();
4059
4350
  const entry = target ? { at, target, note } : { at, note };
4060
4351
  entries.push(entry);
4061
- const filePath = path17.join(yggRoot, JOURNAL_FILE);
4062
- const content = stringifyYaml({ entries });
4063
- await writeFile4(filePath, content, "utf-8");
4352
+ const filePath = path19.join(yggRoot, JOURNAL_FILE);
4353
+ const content = stringifyYaml2({ entries });
4354
+ await writeFile5(filePath, content, "utf-8");
4064
4355
  return entry;
4065
4356
  }
4066
4357
  async function archiveJournal(yggRoot) {
4067
- const journalPath = path17.join(yggRoot, JOURNAL_FILE);
4358
+ const journalPath = path19.join(yggRoot, JOURNAL_FILE);
4068
4359
  try {
4069
- await access3(journalPath);
4360
+ await access4(journalPath);
4070
4361
  } catch {
4071
4362
  return null;
4072
4363
  }
4073
4364
  const entries = await readJournal(yggRoot);
4074
4365
  if (entries.length === 0) return null;
4075
- const archiveDir = path17.join(yggRoot, ARCHIVE_DIR);
4366
+ const archiveDir = path19.join(yggRoot, ARCHIVE_DIR);
4076
4367
  await mkdir3(archiveDir, { recursive: true });
4077
4368
  const now = /* @__PURE__ */ new Date();
4078
4369
  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
4370
  const archiveName = `.journal.${timestamp}.yaml`;
4080
- const archivePath = path17.join(archiveDir, archiveName);
4081
- await rename(journalPath, archivePath);
4371
+ const archivePath = path19.join(archiveDir, archiveName);
4372
+ await rename2(journalPath, archivePath);
4082
4373
  return { archiveName, entryCount: entries.length };
4083
4374
  }
4084
4375
 
@@ -4232,12 +4523,12 @@ function registerPreflightCommand(program2) {
4232
4523
  }
4233
4524
 
4234
4525
  // src/bin.ts
4235
- import { readFileSync } from "fs";
4526
+ import { readFileSync as readFileSync2 } from "fs";
4236
4527
  import { fileURLToPath as fileURLToPath3 } from "url";
4237
4528
  import { dirname, join } from "path";
4238
4529
  var __filename = fileURLToPath3(import.meta.url);
4239
4530
  var __dirname = dirname(__filename);
4240
- var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
4531
+ var pkg = JSON.parse(readFileSync2(join(__dirname, "..", "package.json"), "utf-8"));
4241
4532
  var program = new Command();
4242
4533
  program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
4243
4534
  registerInitCommand(program);