@chrisdudek/yg 2.5.0 → 2.7.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
@@ -80,7 +80,8 @@ BEFORE reading, researching, planning, OR modifying ANY mapped file:
80
80
  - Assessing what is affected by a change \u2192 yg impact --node <owner>
81
81
  - Planning modifications \u2192 both (build-context first, then impact)
82
82
  \`yg build-context --node <path>\`. Read the YAML map for topology,
83
- then read artifact files listed in the artifacts section. For quick
83
+ starting with the glossary at the top (aspect and flow definitions),
84
+ then read artifact files listed inline on each element. For quick
84
85
  orientation, the map alone is sufficient. For implementation, read
85
86
  all artifact files before changing code.
86
87
  If the context package seems insufficient \u2014 enrich the graph.
@@ -148,6 +149,7 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
148
149
  | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
149
150
  | "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
150
151
  | "I'll grep the codebase to find where to start" | Run \`yg select --task\` first \u2014 it matches your intent against graph artifacts. Then \`yg owner\` on results. |
152
+ | "Drift is blocking repo-check, let me just sync it" | Drift means artifacts are stale. Update artifacts first, then sync. \`drift-sync\` without artifact update = hiding staleness. |
151
153
 
152
154
  ### Failure States
153
155
 
@@ -210,9 +212,10 @@ PREFLIGHT (every conversation, before any work):
210
212
  UNDERSTANDING mapped code (questions, research, OR planning):
211
213
  - [ ] 1. yg owner --file <path>
212
214
  - [ ] 2. Owner found \u2192 yg build-context --node <path>. Read the YAML map
213
- for topology, then read artifact files from the artifacts section.
214
- For quick orientation, the map alone is sufficient. For implementation,
215
- read all artifact files before changing code.
215
+ for topology, starting with the glossary at the top for aspect and
216
+ flow definitions, then read artifact files listed inline on each
217
+ element. For quick orientation, the map alone is sufficient. For
218
+ implementation, read all artifact files before changing code.
216
219
  - [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
217
220
  Never use grep or raw file reads as primary understanding when graph coverage exists.
218
221
  Raw reads supplement the context package \u2014 they do not replace it.
@@ -239,7 +242,7 @@ You are not allowed to edit or create source code without establishing graph cov
239
242
  - [ ] 1. Read specification: \`yg build-context --node <node_path>\`
240
243
  - [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
241
244
  - [ ] 3. Modify source code
242
- - [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change)
245
+ - [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change). If the node's purpose changed, update \`description\` in \`yg-node.yaml\`.
243
246
  - [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
244
247
  - [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
245
248
 
@@ -257,7 +260,7 @@ You are not allowed to edit or create source code without establishing graph cov
257
260
 
258
261
  1. Create aspects first (cross-cutting requirements the new code must satisfy)
259
262
  2. Create flows if the code participates in a business process
260
- 3. Create nodes with full artifacts \u2014 responsibility, interface, internals
263
+ 3. Create nodes with full artifacts \u2014 description in \`yg-node.yaml\`, responsibility, interface, internals
261
264
  4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
262
265
  5. Implement code that satisfies the specification
263
266
  6. The graph specifies WHAT and WHY; the code implements HOW (framework APIs, library choices)
@@ -282,6 +285,7 @@ Per area checklist:
282
285
  - [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
283
286
  - [ ] 2. Determine node granularity \u2014 propose to user if unclear
284
287
  - [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
288
+ - [ ] 3b. Write \`description\` in \`yg-node.yaml\` \u2014 a short summary of what the node does
285
289
  - [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
286
290
  - [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
287
291
  - [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
@@ -352,7 +356,7 @@ When reviewing graph quality (triggered by user or quality improvement):
352
356
  - **Budget warning (W005/W006)** \u2192 informational. \`yg validate\` shows a breakdown (own/hierarchy/aspects/flows/dependencies). Large inherited context means the system is complex \u2014 this is not a problem to fix, it is reality to acknowledge. Do not delete knowledge from artifacts. Do not attempt to "reduce" inherited context.
353
357
  - **Own budget warning (W015)** \u2192 own artifacts are large. Consider splitting this node's responsibilities into child nodes. Redistribute knowledge across children so total knowledge is preserved or increased, never reduced.
354
358
  - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
355
- - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
359
+ - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end. But NEVER run \`yg drift-sync\` to silence a failing drift check \u2014 drift is a signal that artifacts need updating. First update artifacts, then sync.`;
356
360
  var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
357
361
 
358
362
  ### Graph Structure
@@ -389,9 +393,20 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
389
393
 
390
394
  ### Context Assembly
391
395
 
392
- **Reading context:** \`yg build-context --node <path>\` returns a YAML map with the node's topology (hierarchy, dependencies, aspects, flows) and an \`artifacts\` section listing files to read. All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
396
+ **Reading context:** \`yg build-context --node <path>\` returns a YAML map structured as follows:
393
397
 
394
- **Default mode (paths-only):** Use for all graph operations. Read the YAML map first to understand topology. Then read artifact files from the \`artifacts\` section using the Read tool. For quick orientation (scoping, blast radius assessment), the map alone is sufficient. For implementation or modification, read all artifact files before changing code.
398
+ - **\`glossary\`** (top) \u2014 definitions for every aspect and flow referenced in the map, each with \`files\` listing their artifact paths. Read this first to understand IDs used throughout.
399
+ - **\`node\`** \u2014 the target node with inline \`files\` (its artifact paths). No \`yg-node.yaml\` in file lists.
400
+ - **\`hierarchy\`** \u2014 ancestor and sibling nodes, each with inline \`files\`.
401
+ - **\`dependencies\`** \u2014 dependency nodes, each with inline \`files\`.
402
+ - **\`meta\`** (bottom) \u2014 context assembly metadata.
403
+ - YAML comments before each section guide reading order.
404
+
405
+ All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
406
+
407
+ **Default mode (paths-only):** Use for all graph operations. Read the YAML map first \u2014 start with the \`glossary\` to understand aspects and flows, then the \`node\` section for the target. Read artifact files inline on each element using the Read tool. For quick orientation (scoping, blast radius assessment), the map alone is sufficient. For implementation or modification, read all artifact files before changing code.
408
+
409
+ The glossary at the top defines all aspects and flows \u2014 read it first to understand IDs used throughout.
395
410
 
396
411
  **Full mode (\`--full\`):** Use only when you cannot read files individually \u2014 e.g., when pasting context into a prompt, sharing with a user, or when you have no Read tool available.
397
412
 
@@ -440,7 +455,7 @@ When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are prese
440
455
 
441
456
  - [ ] 1. Read \`schemas/yg-flow.yaml\`
442
457
  - [ ] 2. Create \`flows/<name>/\` directory
443
- - [ ] 3. Write \`yg-flow.yaml\` \u2014 declare participants and flow-level aspects
458
+ - [ ] 3. Write \`yg-flow.yaml\` \u2014 name, description, nodes (participant list), and flow-level aspects
444
459
  - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
445
460
  - [ ] 5. \`yg validate\`
446
461
 
@@ -453,7 +468,8 @@ Test: "Does this describe what happens in the world, or only in the software?" I
453
468
  - **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
454
469
  - **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
455
470
  - **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
456
- - **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
471
+ - **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task. \`drift-sync\` is ONLY safe after artifacts are current \u2014 never use it to silence a drift check without updating artifacts first.
472
+ - **Description maintenance.** Every \`yg-node.yaml\`, \`yg-aspect.yaml\`, and \`yg-flow.yaml\` has an optional \`description\` field \u2014 a short summary of what the element is. Write it when creating new elements. Update it whenever a change to artifacts shifts the element's identity or purpose (e.g., responsibility split, scope change). Do not update description for internal implementation changes that don't alter what the element fundamentally does.
457
473
  - **Completeness test:** Two checks, both required:
458
474
  1. **Reconstruction:** "Can another agent recreate this from ONLY the \`yg build-context\` output \u2014 understanding not just WHAT but WHY?" Test: rejected alternatives, correct algorithm, design arguments.
459
475
  2. **Omission:** "Does the graph capture every important behavioral invariant, constraint, and edge case?" Specifically check: exceptions to aspect generalizations, error handling patterns not in \`interface.md\`, concurrency behaviors not in \`internals.md\`.
@@ -493,7 +509,7 @@ yg drift-sync --node <path> [--recursive] | --all
493
509
  | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
494
510
  | Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
495
511
  | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
496
- | Business process participation | Flow (\`yg-flow.yaml participants\`) |
512
+ | Business process participation | Flow (\`yg-flow.yaml nodes\`) |
497
513
  | Process-level requirement | Flow \`aspects\` + aspect directory |
498
514
  | Context shared across a domain | Parent node artifact |
499
515
  | Technology stack | Node artifact at appropriate hierarchy level |
@@ -1172,10 +1188,6 @@ function registerInitCommand(program2) {
1172
1188
  });
1173
1189
  }
1174
1190
 
1175
- // src/cli/build-context.ts
1176
- import { readFile as readFile14 } from "fs/promises";
1177
- import path12 from "path";
1178
-
1179
1191
  // src/core/graph-loader.ts
1180
1192
  import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
1181
1193
  import path9 from "path";
@@ -1298,12 +1310,14 @@ async function parseNodeYaml(filePath) {
1298
1310
  if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
1299
1311
  throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
1300
1312
  }
1313
+ const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1301
1314
  const relations = parseRelations(raw.relations, filePath);
1302
1315
  const mapping = parseMapping(raw.mapping, filePath);
1303
1316
  const aspects = parseAspects(raw.aspects, filePath);
1304
1317
  return {
1305
1318
  name: raw.name.trim(),
1306
1319
  type: raw.type.trim(),
1320
+ description,
1307
1321
  aspects,
1308
1322
  blackbox: raw.blackbox ?? false,
1309
1323
  relations: relations.length > 0 ? relations : void 0,
@@ -1509,13 +1523,18 @@ async function parseFlow(flowDir, flowYamlPath) {
1509
1523
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
1510
1524
  throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1511
1525
  }
1512
- const nodes = raw.nodes;
1526
+ const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1527
+ const nodes = raw.nodes ?? raw.participants;
1513
1528
  if (!Array.isArray(nodes) || nodes.length === 0) {
1514
- throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
1529
+ throw new Error(
1530
+ `yg-flow.yaml at ${flowYamlPath}: 'nodes' (or 'participants') must be a non-empty array`
1531
+ );
1515
1532
  }
1516
1533
  const nodePaths = nodes.filter((n) => typeof n === "string");
1517
1534
  if (nodePaths.length === 0) {
1518
- throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
1535
+ throw new Error(
1536
+ `yg-flow.yaml at ${flowYamlPath}: 'nodes' (or 'participants') must contain string node paths`
1537
+ );
1519
1538
  }
1520
1539
  let aspects;
1521
1540
  if (raw.aspects !== void 0) {
@@ -1529,6 +1548,7 @@ async function parseFlow(flowDir, flowYamlPath) {
1529
1548
  return {
1530
1549
  path: path6.basename(flowDir),
1531
1550
  name: raw.name.trim(),
1551
+ description,
1532
1552
  nodes: nodePaths,
1533
1553
  ...aspects !== void 0 && { aspects },
1534
1554
  artifacts
@@ -1732,19 +1752,20 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
1732
1752
  }
1733
1753
  }
1734
1754
  async function loadFlows(flowsDir) {
1755
+ let entries;
1735
1756
  try {
1736
- const entries = await readdir4(flowsDir, { withFileTypes: true });
1737
- const flows = [];
1738
- for (const entry of entries) {
1739
- if (!entry.isDirectory()) continue;
1740
- const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
1741
- const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
1742
- flows.push(flow);
1743
- }
1744
- return flows;
1757
+ entries = await readdir4(flowsDir, { withFileTypes: true });
1745
1758
  } catch {
1746
1759
  return [];
1747
1760
  }
1761
+ const flows = [];
1762
+ for (const entry of entries) {
1763
+ if (!entry.isDirectory()) continue;
1764
+ const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
1765
+ const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
1766
+ flows.push(flow);
1767
+ }
1768
+ return flows;
1748
1769
  }
1749
1770
  async function loadSchemas(schemasDir) {
1750
1771
  try {
@@ -1774,6 +1795,7 @@ function estimateTokens(text) {
1774
1795
  // src/core/context-builder.ts
1775
1796
  var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
1776
1797
  var EVENT_RELATION_TYPES = /* @__PURE__ */ new Set(["emits", "listens"]);
1798
+ var YG_YAML_FILES = /* @__PURE__ */ new Set(["yg-node.yaml", "yg-aspect.yaml", "yg-flow.yaml"]);
1777
1799
  async function buildContext(graph, nodePath) {
1778
1800
  const node = graph.nodes.get(nodePath);
1779
1801
  if (!node) {
@@ -2132,7 +2154,7 @@ function toContextMapOutput(pkg2, graph) {
2132
2154
  });
2133
2155
  const participatingFlows = collectParticipatingFlows(graph, node);
2134
2156
  const flowRefs = participatingFlows.map((f) => {
2135
- const ref = { path: f.path, name: f.name };
2157
+ const ref = { path: f.path };
2136
2158
  if (f.aspects?.length) ref.aspects = f.aspects;
2137
2159
  return ref;
2138
2160
  });
@@ -2140,7 +2162,7 @@ function toContextMapOutput(pkg2, graph) {
2140
2162
  const hierarchyRefs = ancestors.map((a) => {
2141
2163
  const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
2142
2164
  const expanded = expandAspects(nodeAspectIds, graph.aspects);
2143
- return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2165
+ return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: buildNodeFiles(a, config, `model/${a.path}`) };
2144
2166
  });
2145
2167
  const ancestorPaths = new Set(ancestors.map((a) => a.path));
2146
2168
  const depRefs = [];
@@ -2152,23 +2174,26 @@ function toContextMapOutput(pkg2, graph) {
2152
2174
  const depHierarchy = depAncestors.map((a) => {
2153
2175
  const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
2154
2176
  const expanded = expandAspects(ids, graph.aspects);
2155
- return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2177
+ const ancestorNode = graph.nodes.get(a.path);
2178
+ return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: ancestorNode ? buildDepNodeFiles(ancestorNode, config, `model/${a.path}`) : [] };
2156
2179
  });
2157
2180
  const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
2158
2181
  const ref = {
2159
2182
  path: target.path,
2160
2183
  name: target.meta.name,
2161
2184
  type: target.meta.type,
2185
+ description: target.meta.description,
2162
2186
  relation: relation.type,
2163
2187
  aspects: depEffectiveAspects,
2164
- hierarchy: depHierarchy
2188
+ hierarchy: depHierarchy,
2189
+ files: buildDepNodeFiles(target, config, `model/${target.path}`)
2165
2190
  };
2166
2191
  if (relation.consumes?.length) ref.consumes = relation.consumes;
2167
2192
  if (relation.failure) ref.failure = relation.failure;
2168
2193
  if (relation.event_name) ref["event-name"] = relation.event_name;
2169
2194
  depRefs.push(ref);
2170
2195
  }
2171
- const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2196
+ const glossary = buildGlossary(node, depRefs, graph);
2172
2197
  const breakdown = computeBudgetBreakdown(pkg2, graph);
2173
2198
  const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2174
2199
  const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
@@ -2180,56 +2205,29 @@ function toContextMapOutput(pkg2, graph) {
2180
2205
  path: pkg2.nodePath,
2181
2206
  name: pkg2.nodeName,
2182
2207
  type: node.meta.type,
2208
+ description: node.meta.description,
2183
2209
  mappings: normalizeMappingPaths(node.meta.mapping),
2184
2210
  aspects: nodeAspects,
2185
- flows: flowRefs
2211
+ flows: flowRefs,
2212
+ files: buildNodeFiles(node, config, `model/${pkg2.nodePath}`)
2186
2213
  },
2187
2214
  hierarchy: hierarchyRefs,
2188
2215
  dependencies: depRefs,
2189
- artifacts: registry
2216
+ glossary
2190
2217
  };
2191
2218
  }
2192
- function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2193
- const config = graph.config;
2194
- const configArtifactKeys = new Set(Object.keys(config.artifacts ?? {}));
2195
- const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2196
- const nodes = {};
2219
+ function buildNodeFiles(node, config, prefix) {
2220
+ const configKeys = Object.keys(config.artifacts ?? {});
2221
+ return configKeys.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
2222
+ }
2223
+ function buildDepNodeFiles(node, config, prefix) {
2224
+ const structural = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([f]) => f);
2225
+ const filenames = structural.length > 0 ? structural : Object.keys(config.artifacts ?? {});
2226
+ return filenames.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
2227
+ }
2228
+ function buildGlossary(node, dependencies, graph) {
2197
2229
  const aspects = {};
2198
2230
  const flows = {};
2199
- function addNodeEntry(n, includeYgNodeYaml, filter) {
2200
- if (nodes[n.path]) return;
2201
- const files = [];
2202
- if (includeYgNodeYaml) {
2203
- files.push(`model/${n.path}/yg-node.yaml`);
2204
- }
2205
- for (const filename of filter) {
2206
- if (n.artifacts.some((a) => a.filename === filename)) {
2207
- files.push(`model/${n.path}/${filename}`);
2208
- }
2209
- }
2210
- if (files.length > 0) {
2211
- nodes[n.path] = { files };
2212
- }
2213
- }
2214
- addNodeEntry(node, true, [...configArtifactKeys]);
2215
- for (const ancestor of ancestors) {
2216
- addNodeEntry(ancestor, true, [...configArtifactKeys]);
2217
- }
2218
- const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
2219
- for (const dep of dependencies) {
2220
- const target = graph.nodes.get(dep.path);
2221
- if (target) {
2222
- addNodeEntry(target, false, structuralFilenames);
2223
- }
2224
- for (const ancestor of dep.hierarchy) {
2225
- if (seenDepAncestors.has(ancestor.path)) continue;
2226
- seenDepAncestors.add(ancestor.path);
2227
- const ancestorNode = graph.nodes.get(ancestor.path);
2228
- if (ancestorNode) {
2229
- addNodeEntry(ancestorNode, false, structuralFilenames);
2230
- }
2231
- }
2232
- }
2233
2231
  const allAspectIds = collectEffectiveAspectIds(graph, node.path);
2234
2232
  for (const dep of dependencies) {
2235
2233
  for (const id of dep.aspects) {
@@ -2238,33 +2236,29 @@ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2238
2236
  }
2239
2237
  const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2240
2238
  for (const aspect of resolvedAspects) {
2241
- const files = [];
2242
- files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
2243
- for (const art of aspect.artifacts) {
2244
- files.push(`aspects/${aspect.id}/${art.filename}`);
2245
- }
2239
+ const files = aspect.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `aspects/${aspect.id}/${a.filename}`);
2246
2240
  const entry = {
2247
2241
  name: aspect.name,
2248
2242
  files
2249
2243
  };
2244
+ if (aspect.description) entry.description = aspect.description;
2245
+ if (aspect.stability) entry.stability = aspect.stability;
2250
2246
  if (aspect.implies?.length) entry.implies = aspect.implies;
2251
2247
  aspects[aspect.id] = entry;
2252
2248
  }
2253
2249
  const participatingFlows = collectParticipatingFlows(graph, node);
2254
2250
  for (const flow of participatingFlows) {
2255
- const files = [];
2256
- files.push(`flows/${flow.path}/yg-flow.yaml`);
2257
- for (const art of flow.artifacts) {
2258
- files.push(`flows/${flow.path}/${art.filename}`);
2259
- }
2251
+ const files = flow.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `flows/${flow.path}/${a.filename}`);
2260
2252
  const entry = {
2261
2253
  name: flow.name,
2254
+ participants: flow.nodes,
2262
2255
  files
2263
2256
  };
2257
+ if (flow.description) entry.description = flow.description;
2264
2258
  if (flow.aspects?.length) entry.aspects = flow.aspects;
2265
2259
  flows[flow.path] = entry;
2266
2260
  }
2267
- return { nodes, aspects, flows };
2261
+ return { aspects, flows };
2268
2262
  }
2269
2263
  function collectEffectiveAspectIds(graph, nodePath) {
2270
2264
  const node = graph.nodes.get(nodePath);
@@ -2285,23 +2279,39 @@ function collectEffectiveAspectIds(graph, nodePath) {
2285
2279
  }
2286
2280
 
2287
2281
  // src/formatters/context-text.ts
2288
- import { stringify } from "yaml";
2282
+ import { Document } from "yaml";
2289
2283
  function formatContextYaml(data) {
2290
- const output = {
2291
- meta: {
2292
- "token-count": data.meta.tokenCount,
2293
- "budget-status": data.meta.budgetStatus
2294
- },
2295
- project: data.project,
2296
- node: data.node,
2297
- hierarchy: data.hierarchy.length > 0 ? data.hierarchy : void 0,
2298
- dependencies: data.dependencies.length > 0 ? data.dependencies : void 0,
2299
- artifacts: data.artifacts
2284
+ const output = {};
2285
+ output.project = data.project;
2286
+ output.glossary = data.glossary;
2287
+ output.node = data.node;
2288
+ if (data.hierarchy.length > 0) output.hierarchy = data.hierarchy;
2289
+ if (data.dependencies.length > 0) output.dependencies = data.dependencies;
2290
+ output.meta = {
2291
+ "token-count": data.meta.tokenCount,
2292
+ "budget-status": data.meta.budgetStatus,
2293
+ breakdown: data.meta.breakdown
2300
2294
  };
2301
- for (const key of Object.keys(output)) {
2302
- if (output[key] === void 0) delete output[key];
2295
+ const doc = new Document(output, { aliasDuplicateObjects: false });
2296
+ const map = doc.contents;
2297
+ for (const pair of map.items) {
2298
+ const key = String(pair.key);
2299
+ switch (key) {
2300
+ case "glossary":
2301
+ pair.key.commentBefore = " Glossary: definitions of all aspects and flows referenced in this context.\n Read this first \u2014 IDs below (in node, hierarchy, dependencies) refer to entries here.";
2302
+ break;
2303
+ case "node":
2304
+ pair.key.commentBefore = " Target node: the component you are working on.";
2305
+ break;
2306
+ case "hierarchy":
2307
+ pair.key.commentBefore = " Hierarchy: ancestor modules from root to parent. Context is inherited top-down.";
2308
+ break;
2309
+ case "dependencies":
2310
+ pair.key.commentBefore = " Dependencies: components this node directly depends on.";
2311
+ break;
2312
+ }
2303
2313
  }
2304
- return stringify(output, { lineWidth: 0 });
2314
+ return doc.toString({ lineWidth: 0 });
2305
2315
  }
2306
2316
  function formatFullContent(files) {
2307
2317
  if (files.length === 0) return "";
@@ -2355,6 +2365,7 @@ async function validate(graph, scope = "all") {
2355
2365
  issues.push(...checkInvalidArtifactConditions(graph));
2356
2366
  issues.push(...await checkContextBudget(graph));
2357
2367
  issues.push(...checkHighFanOut(graph));
2368
+ issues.push(...checkMissingDescriptions(graph));
2358
2369
  }
2359
2370
  issues.push(...checkSchemas(graph));
2360
2371
  issues.push(...checkRelationTargets(graph));
@@ -3087,6 +3098,41 @@ function pct(value, total) {
3087
3098
  if (total === 0) return "0%";
3088
3099
  return `${Math.round(value / total * 100)}%`;
3089
3100
  }
3101
+ function checkMissingDescriptions(graph) {
3102
+ const issues = [];
3103
+ for (const [nodePath, node] of graph.nodes) {
3104
+ if (!node.meta.description?.trim()) {
3105
+ issues.push({
3106
+ severity: "warning",
3107
+ code: "W016",
3108
+ rule: "missing-description",
3109
+ message: `Node has no description`,
3110
+ nodePath
3111
+ });
3112
+ }
3113
+ }
3114
+ for (const aspect of graph.aspects) {
3115
+ if (!aspect.description?.trim()) {
3116
+ issues.push({
3117
+ severity: "warning",
3118
+ code: "W016",
3119
+ rule: "missing-description",
3120
+ message: `Aspect '${aspect.id}' has no description`
3121
+ });
3122
+ }
3123
+ }
3124
+ for (const flow of graph.flows) {
3125
+ if (!flow.description?.trim()) {
3126
+ issues.push({
3127
+ severity: "warning",
3128
+ code: "W016",
3129
+ rule: "missing-description",
3130
+ message: `Flow '${flow.name}' has no description`
3131
+ });
3132
+ }
3133
+ }
3134
+ return issues;
3135
+ }
3090
3136
 
3091
3137
  // src/cli/build-context.ts
3092
3138
  function collectRelevantNodePaths(graph, nodePath) {
@@ -3139,22 +3185,31 @@ function registerBuildCommand(program2) {
3139
3185
  const mapOutput = toContextMapOutput(pkg2, graph);
3140
3186
  let output = formatContextYaml(mapOutput);
3141
3187
  if (options.full) {
3142
- const allFiles = [];
3143
- const allEntries = [
3144
- ...Object.values(mapOutput.artifacts.nodes),
3145
- ...Object.values(mapOutput.artifacts.aspects),
3146
- ...Object.values(mapOutput.artifacts.flows)
3147
- ];
3148
3188
  const seen = /* @__PURE__ */ new Set();
3149
- for (const entry of allEntries) {
3150
- for (const filePath of entry.files) {
3151
- if (seen.has(filePath)) continue;
3152
- seen.add(filePath);
3153
- const content = await findFileContent(filePath, graph);
3154
- if (content !== void 0) {
3155
- allFiles.push({ path: filePath, content });
3156
- }
3189
+ const allFiles = [];
3190
+ async function collectFile(filePath) {
3191
+ if (seen.has(filePath)) return;
3192
+ seen.add(filePath);
3193
+ const content = await findFileContent(filePath, graph);
3194
+ if (content !== void 0) {
3195
+ allFiles.push({ path: filePath, content });
3196
+ }
3197
+ }
3198
+ for (const aspect of Object.values(mapOutput.glossary.aspects)) {
3199
+ for (const f of aspect.files) await collectFile(f);
3200
+ }
3201
+ for (const flow of Object.values(mapOutput.glossary.flows)) {
3202
+ for (const f of flow.files) await collectFile(f);
3203
+ }
3204
+ for (const f of mapOutput.node.files) await collectFile(f);
3205
+ for (const ancestor of mapOutput.hierarchy) {
3206
+ for (const f of ancestor.files ?? []) await collectFile(f);
3207
+ }
3208
+ for (const dep of mapOutput.dependencies) {
3209
+ for (const ancestor of dep.hierarchy) {
3210
+ for (const f of ancestor.files ?? []) await collectFile(f);
3157
3211
  }
3212
+ for (const f of dep.files ?? []) await collectFile(f);
3158
3213
  }
3159
3214
  output += formatFullContent(allFiles);
3160
3215
  }
@@ -3167,14 +3222,6 @@ function registerBuildCommand(program2) {
3167
3222
  });
3168
3223
  }
3169
3224
  async function findFileContent(filePath, graph) {
3170
- async function readYamlFromDisk(relativePath) {
3171
- try {
3172
- const fullPath = path12.join(graph.rootPath, relativePath);
3173
- return (await readFile14(fullPath, "utf-8")).trim();
3174
- } catch {
3175
- return void 0;
3176
- }
3177
- }
3178
3225
  if (filePath.startsWith("model/")) {
3179
3226
  const rest = filePath.slice("model/".length);
3180
3227
  const parts = rest.split("/");
@@ -3182,9 +3229,6 @@ async function findFileContent(filePath, graph) {
3182
3229
  const nodePath = parts.join("/");
3183
3230
  const node = graph.nodes.get(nodePath);
3184
3231
  if (!node) return void 0;
3185
- if (filename === "yg-node.yaml") {
3186
- return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
3187
- }
3188
3232
  const art = node.artifacts.find((a) => a.filename === filename);
3189
3233
  return art?.content;
3190
3234
  }
@@ -3195,9 +3239,6 @@ async function findFileContent(filePath, graph) {
3195
3239
  const filename = parts.slice(1).join("/");
3196
3240
  const aspect = graph.aspects.find((a) => a.id === aspectId);
3197
3241
  if (!aspect) return void 0;
3198
- if (filename === "yg-aspect.yaml") {
3199
- return readYamlFromDisk(filePath);
3200
- }
3201
3242
  const art = aspect.artifacts.find((a) => a.filename === filename);
3202
3243
  return art?.content;
3203
3244
  }
@@ -3208,9 +3249,6 @@ async function findFileContent(filePath, graph) {
3208
3249
  const filename = parts.slice(1).join("/");
3209
3250
  const flow = graph.flows.find((f) => f.path === flowPath);
3210
3251
  if (!flow) return void 0;
3211
- if (filename === "yg-flow.yaml") {
3212
- return readYamlFromDisk(filePath);
3213
- }
3214
3252
  const art = flow.artifacts.find((a) => a.filename === filename);
3215
3253
  return art?.content;
3216
3254
  }
@@ -3266,12 +3304,12 @@ ${errors.length} errors, ${warnings.length} warnings.
3266
3304
  import chalk2 from "chalk";
3267
3305
 
3268
3306
  // src/io/drift-state-store.ts
3269
- import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3270
- import path13 from "path";
3307
+ import { readFile as readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3308
+ import path12 from "path";
3271
3309
  import { parse as yamlParse } from "yaml";
3272
3310
  var DRIFT_STATE_DIR = ".drift-state";
3273
3311
  function nodeStatePath(yggRoot, nodePath) {
3274
- return path13.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3312
+ return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3275
3313
  }
3276
3314
  async function scanJsonFiles(dir, baseDir) {
3277
3315
  const results = [];
@@ -3282,12 +3320,12 @@ async function scanJsonFiles(dir, baseDir) {
3282
3320
  return results;
3283
3321
  }
3284
3322
  for (const entry of entries) {
3285
- const fullPath = path13.join(dir, entry.name);
3323
+ const fullPath = path12.join(dir, entry.name);
3286
3324
  if (entry.isDirectory()) {
3287
3325
  const nested = await scanJsonFiles(fullPath, baseDir);
3288
3326
  results.push(...nested);
3289
3327
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
3290
- const relPath = path13.relative(baseDir, fullPath);
3328
+ const relPath = path12.relative(baseDir, fullPath);
3291
3329
  const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
3292
3330
  results.push(nodePath);
3293
3331
  }
@@ -3295,13 +3333,13 @@ async function scanJsonFiles(dir, baseDir) {
3295
3333
  return results;
3296
3334
  }
3297
3335
  async function removeEmptyParents(filePath, stopDir) {
3298
- let dir = path13.dirname(filePath);
3336
+ let dir = path12.dirname(filePath);
3299
3337
  while (dir !== stopDir && dir.startsWith(stopDir)) {
3300
3338
  try {
3301
3339
  const entries = await readdir6(dir);
3302
3340
  if (entries.length === 0) {
3303
3341
  await rm2(dir, { recursive: true });
3304
- dir = path13.dirname(dir);
3342
+ dir = path12.dirname(dir);
3305
3343
  } else {
3306
3344
  break;
3307
3345
  }
@@ -3313,7 +3351,7 @@ async function removeEmptyParents(filePath, stopDir) {
3313
3351
  async function readNodeDriftState(yggRoot, nodePath) {
3314
3352
  try {
3315
3353
  const filePath = nodeStatePath(yggRoot, nodePath);
3316
- const content = await readFile15(filePath, "utf-8");
3354
+ const content = await readFile14(filePath, "utf-8");
3317
3355
  const parsed = JSON.parse(content);
3318
3356
  return parsed;
3319
3357
  } catch {
@@ -3322,12 +3360,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
3322
3360
  }
3323
3361
  async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3324
3362
  const filePath = nodeStatePath(yggRoot, nodePath);
3325
- await mkdir3(path13.dirname(filePath), { recursive: true });
3363
+ await mkdir3(path12.dirname(filePath), { recursive: true });
3326
3364
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3327
3365
  await writeFile5(filePath, content, "utf-8");
3328
3366
  }
3329
3367
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3330
- const driftDir = path13.join(yggRoot, DRIFT_STATE_DIR);
3368
+ const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
3331
3369
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
3332
3370
  const removed = [];
3333
3371
  for (const nodePath of allNodePaths) {
@@ -3341,7 +3379,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
3341
3379
  return removed.sort();
3342
3380
  }
3343
3381
  async function readDriftState(yggRoot) {
3344
- const driftPath = path13.join(yggRoot, DRIFT_STATE_DIR);
3382
+ const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
3345
3383
  let driftStat;
3346
3384
  try {
3347
3385
  driftStat = await stat5(driftPath);
@@ -3349,7 +3387,7 @@ async function readDriftState(yggRoot) {
3349
3387
  return {};
3350
3388
  }
3351
3389
  if (driftStat.isFile()) {
3352
- const content = await readFile15(driftPath, "utf-8");
3390
+ const content = await readFile14(driftPath, "utf-8");
3353
3391
  let raw;
3354
3392
  try {
3355
3393
  raw = JSON.parse(content);
@@ -3381,20 +3419,20 @@ async function readDriftState(yggRoot) {
3381
3419
  }
3382
3420
 
3383
3421
  // src/utils/hash.ts
3384
- import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
3385
- import path14 from "path";
3422
+ import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3423
+ import path13 from "path";
3386
3424
  import { createHash } from "crypto";
3387
3425
  import { createRequire } from "module";
3388
3426
  var require2 = createRequire(import.meta.url);
3389
3427
  var ignoreFactory = require2("ignore");
3390
3428
  async function hashFile(filePath) {
3391
- const content = await readFile16(filePath);
3429
+ const content = await readFile15(filePath);
3392
3430
  return createHash("sha256").update(content).digest("hex");
3393
3431
  }
3394
3432
  async function loadRootGitignoreStack(projectRoot) {
3395
3433
  if (!projectRoot) return [];
3396
3434
  try {
3397
- const content = await readFile16(path14.join(projectRoot, ".gitignore"), "utf-8");
3435
+ const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
3398
3436
  const matcher = ignoreFactory();
3399
3437
  matcher.add(content);
3400
3438
  return [{ basePath: projectRoot, matcher }];
@@ -3404,7 +3442,7 @@ async function loadRootGitignoreStack(projectRoot) {
3404
3442
  }
3405
3443
  function isIgnoredByStack(candidatePath, stack) {
3406
3444
  for (const { basePath, matcher } of stack) {
3407
- const relativePath = path14.relative(basePath, candidatePath);
3445
+ const relativePath = path13.relative(basePath, candidatePath);
3408
3446
  if (relativePath === "" || relativePath.startsWith("..")) continue;
3409
3447
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
3410
3448
  }
@@ -3419,7 +3457,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3419
3457
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
3420
3458
  const allFiles = [];
3421
3459
  for (const tf of trackedFiles) {
3422
- const absPath = path14.join(projectRoot, tf.path);
3460
+ const absPath = path13.join(projectRoot, tf.path);
3423
3461
  try {
3424
3462
  const st = await stat6(absPath);
3425
3463
  if (st.isDirectory()) {
@@ -3429,7 +3467,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3429
3467
  });
3430
3468
  for (const entry of dirEntries) {
3431
3469
  allFiles.push({
3432
- relPath: path14.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3470
+ relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3433
3471
  absPath: entry.absPath,
3434
3472
  mtimeMs: entry.mtimeMs
3435
3473
  });
@@ -3469,7 +3507,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3469
3507
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3470
3508
  let stack = options.gitignoreStack ?? [];
3471
3509
  try {
3472
- const localContent = await readFile16(path14.join(directoryPath, ".gitignore"), "utf-8");
3510
+ const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
3473
3511
  const localMatcher = ignoreFactory();
3474
3512
  localMatcher.add(localContent);
3475
3513
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -3479,7 +3517,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3479
3517
  const dirs = [];
3480
3518
  const files = [];
3481
3519
  for (const entry of entries) {
3482
- const absoluteChildPath = path14.join(directoryPath, entry.name);
3520
+ const absoluteChildPath = path13.join(directoryPath, entry.name);
3483
3521
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
3484
3522
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
3485
3523
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -3492,7 +3530,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3492
3530
  Promise.all(files.map(async (f) => {
3493
3531
  const fileStat = await stat6(f);
3494
3532
  return {
3495
- relPath: path14.relative(rootDirectoryPath, f),
3533
+ relPath: path13.relative(rootDirectoryPath, f),
3496
3534
  absPath: f,
3497
3535
  mtimeMs: fileStat.mtimeMs
3498
3536
  };
@@ -3505,14 +3543,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3505
3543
  }
3506
3544
 
3507
3545
  // src/core/context-files.ts
3508
- import path15 from "path";
3546
+ import path14 from "path";
3509
3547
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3510
3548
  function collectTrackedFiles(node, graph) {
3511
3549
  const seen = /* @__PURE__ */ new Set();
3512
3550
  const result = [];
3513
- const projectRoot = path15.dirname(graph.rootPath);
3514
- const yggPrefix = path15.relative(projectRoot, graph.rootPath);
3515
- const yggPrefixNormalized = yggPrefix.split(path15.sep).join("/");
3551
+ const projectRoot = path14.dirname(graph.rootPath);
3552
+ const yggPrefix = path14.relative(projectRoot, graph.rootPath);
3553
+ const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
3516
3554
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
3517
3555
  function addFile(filePath, category) {
3518
3556
  if (seen.has(filePath)) return;
@@ -3625,7 +3663,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
3625
3663
 
3626
3664
  // src/core/drift-detector.ts
3627
3665
  import { access as access2 } from "fs/promises";
3628
- import path16 from "path";
3666
+ import path15 from "path";
3629
3667
  function getChildMappingExclusions(graph, nodePath) {
3630
3668
  const node = graph.nodes.get(nodePath);
3631
3669
  if (!node) return [];
@@ -3647,7 +3685,7 @@ function getChildMappingExclusions(graph, nodePath) {
3647
3685
  return exclusions;
3648
3686
  }
3649
3687
  async function detectDrift(graph, filterNodePath) {
3650
- const projectRoot = path16.dirname(graph.rootPath);
3688
+ const projectRoot = path15.dirname(graph.rootPath);
3651
3689
  const driftState = await readDriftState(graph.rootPath);
3652
3690
  const entries = [];
3653
3691
  for (const [nodePath, node] of graph.nodes) {
@@ -3729,14 +3767,14 @@ async function detectDrift(graph, filterNodePath) {
3729
3767
  };
3730
3768
  }
3731
3769
  function categorizeFile(filePath, _rootPath, projectRoot) {
3732
- const yggPrefix = path16.relative(projectRoot, _rootPath);
3733
- const normalizedPrefix = yggPrefix.split(path16.sep).join("/");
3770
+ const yggPrefix = path15.relative(projectRoot, _rootPath);
3771
+ const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3734
3772
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3735
3773
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3736
3774
  }
3737
3775
  async function allPathsMissing(projectRoot, mappingPaths) {
3738
3776
  for (const mp of mappingPaths) {
3739
- const absPath = path16.join(projectRoot, mp);
3777
+ const absPath = path15.join(projectRoot, mp);
3740
3778
  try {
3741
3779
  await access2(absPath);
3742
3780
  return false;
@@ -3746,7 +3784,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3746
3784
  return true;
3747
3785
  }
3748
3786
  async function syncDriftState(graph, nodePath) {
3749
- const projectRoot = path16.dirname(graph.rootPath);
3787
+ const projectRoot = path15.dirname(graph.rootPath);
3750
3788
  const node = graph.nodes.get(nodePath);
3751
3789
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3752
3790
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
@@ -4066,10 +4104,10 @@ function registerTreeCommand(program2) {
4066
4104
  let roots;
4067
4105
  let showProjectName;
4068
4106
  if (options.root?.trim()) {
4069
- const path20 = options.root.trim().replace(/\/$/, "");
4070
- const node = graph.nodes.get(path20);
4107
+ const path19 = options.root.trim().replace(/\/$/, "");
4108
+ const node = graph.nodes.get(path19);
4071
4109
  if (!node) {
4072
- process.stderr.write(`Error: path '${path20}' not found
4110
+ process.stderr.write(`Error: path '${path19}' not found
4073
4111
  `);
4074
4112
  process.exit(1);
4075
4113
  }
@@ -4113,7 +4151,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
4113
4151
  }
4114
4152
 
4115
4153
  // src/cli/owner.ts
4116
- import path17 from "path";
4154
+ import path16 from "path";
4117
4155
  import { access as access3 } from "fs/promises";
4118
4156
  function normalizeForMatch(inputPath) {
4119
4157
  return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -4143,11 +4181,11 @@ function registerOwnerCommand(program2) {
4143
4181
  const graph = await loadGraph(cwd);
4144
4182
  const repoRoot = projectRootFromGraph(graph.rootPath);
4145
4183
  const rawPath = options.file.trim();
4146
- const absolute = path17.resolve(cwd, rawPath);
4147
- const repoRelative = path17.relative(repoRoot, absolute).split(path17.sep).join("/");
4184
+ const absolute = path16.resolve(cwd, rawPath);
4185
+ const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
4148
4186
  const result = findOwner(graph, repoRoot, repoRelative);
4149
4187
  if (!result.nodePath) {
4150
- const absPath = path17.resolve(repoRoot, result.file);
4188
+ const absPath = path16.resolve(repoRoot, result.file);
4151
4189
  let exists = true;
4152
4190
  try {
4153
4191
  await access3(absPath);
@@ -4181,7 +4219,7 @@ function registerOwnerCommand(program2) {
4181
4219
 
4182
4220
  // src/core/dependency-resolver.ts
4183
4221
  import { execSync } from "child_process";
4184
- import path18 from "path";
4222
+ import path17 from "path";
4185
4223
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
4186
4224
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
4187
4225
  function filterRelationType(relType, filter) {
@@ -4258,7 +4296,7 @@ function registerDepsCommand(program2) {
4258
4296
  // src/core/graph-from-git.ts
4259
4297
  import { mkdtemp, rm as rm3 } from "fs/promises";
4260
4298
  import { tmpdir } from "os";
4261
- import path19 from "path";
4299
+ import path18 from "path";
4262
4300
  import { execSync as execSync2 } from "child_process";
4263
4301
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
4264
4302
  const yggPath = ".yggdrasil";
@@ -4269,8 +4307,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
4269
4307
  return null;
4270
4308
  }
4271
4309
  try {
4272
- tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
4273
- const archivePath = path19.join(tmpDir, "archive.tar");
4310
+ tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
4311
+ const archivePath = path18.join(tmpDir, "archive.tar");
4274
4312
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
4275
4313
  cwd: projectRoot,
4276
4314
  stdio: "pipe"
@@ -4340,14 +4378,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
4340
4378
  }
4341
4379
  const chains = [];
4342
4380
  for (const node of transitiveOnly) {
4343
- const path20 = [];
4381
+ const path19 = [];
4344
4382
  let current = node;
4345
4383
  while (current) {
4346
- path20.unshift(current);
4384
+ path19.unshift(current);
4347
4385
  current = parent.get(current);
4348
4386
  }
4349
- if (path20.length >= 3) {
4350
- chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
4387
+ if (path19.length >= 3) {
4388
+ chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
4351
4389
  }
4352
4390
  }
4353
4391
  return chains.sort();
@@ -4391,14 +4429,14 @@ function collectIndirectDependents(graph, directlyAffected) {
4391
4429
  }
4392
4430
  for (const [node] of parent) {
4393
4431
  if (directSet.has(node)) continue;
4394
- const path20 = [node];
4432
+ const path19 = [node];
4395
4433
  let current = node;
4396
4434
  while (parent.has(current)) {
4397
4435
  current = parent.get(current);
4398
- path20.push(current);
4436
+ path19.push(current);
4399
4437
  }
4400
- const chain = path20.map((p) => `<- ${p}`).join(" ");
4401
- const depth = path20.length;
4438
+ const chain = path19.map((p) => `<- ${p}`).join(" ");
4439
+ const depth = path19.length;
4402
4440
  const existing = bestChain.get(node);
4403
4441
  if (!existing || depth < existing.depth) {
4404
4442
  bestChain.set(node, { chain, depth });
@@ -4840,6 +4878,7 @@ function registerFlowsCommand(program2) {
4840
4878
  participants: flow.nodes.length,
4841
4879
  nodes: flow.nodes.sort()
4842
4880
  };
4881
+ if (flow.description) entry.description = flow.description;
4843
4882
  if (flow.aspects && flow.aspects.length > 0) entry.aspects = flow.aspects;
4844
4883
  return entry;
4845
4884
  });