@chrisdudek/yg 2.5.1 → 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 nodes (participant list) 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\`.
@@ -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,6 +1523,7 @@ 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
  }
1526
+ const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
1512
1527
  const nodes = raw.nodes ?? raw.participants;
1513
1528
  if (!Array.isArray(nodes) || nodes.length === 0) {
1514
1529
  throw new Error(
@@ -1533,6 +1548,7 @@ async function parseFlow(flowDir, flowYamlPath) {
1533
1548
  return {
1534
1549
  path: path6.basename(flowDir),
1535
1550
  name: raw.name.trim(),
1551
+ description,
1536
1552
  nodes: nodePaths,
1537
1553
  ...aspects !== void 0 && { aspects },
1538
1554
  artifacts
@@ -1779,6 +1795,7 @@ function estimateTokens(text) {
1779
1795
  // src/core/context-builder.ts
1780
1796
  var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
1781
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"]);
1782
1799
  async function buildContext(graph, nodePath) {
1783
1800
  const node = graph.nodes.get(nodePath);
1784
1801
  if (!node) {
@@ -2137,7 +2154,7 @@ function toContextMapOutput(pkg2, graph) {
2137
2154
  });
2138
2155
  const participatingFlows = collectParticipatingFlows(graph, node);
2139
2156
  const flowRefs = participatingFlows.map((f) => {
2140
- const ref = { path: f.path, name: f.name };
2157
+ const ref = { path: f.path };
2141
2158
  if (f.aspects?.length) ref.aspects = f.aspects;
2142
2159
  return ref;
2143
2160
  });
@@ -2145,7 +2162,7 @@ function toContextMapOutput(pkg2, graph) {
2145
2162
  const hierarchyRefs = ancestors.map((a) => {
2146
2163
  const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
2147
2164
  const expanded = expandAspects(nodeAspectIds, graph.aspects);
2148
- 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}`) };
2149
2166
  });
2150
2167
  const ancestorPaths = new Set(ancestors.map((a) => a.path));
2151
2168
  const depRefs = [];
@@ -2157,23 +2174,26 @@ function toContextMapOutput(pkg2, graph) {
2157
2174
  const depHierarchy = depAncestors.map((a) => {
2158
2175
  const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
2159
2176
  const expanded = expandAspects(ids, graph.aspects);
2160
- 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}`) : [] };
2161
2179
  });
2162
2180
  const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
2163
2181
  const ref = {
2164
2182
  path: target.path,
2165
2183
  name: target.meta.name,
2166
2184
  type: target.meta.type,
2185
+ description: target.meta.description,
2167
2186
  relation: relation.type,
2168
2187
  aspects: depEffectiveAspects,
2169
- hierarchy: depHierarchy
2188
+ hierarchy: depHierarchy,
2189
+ files: buildDepNodeFiles(target, config, `model/${target.path}`)
2170
2190
  };
2171
2191
  if (relation.consumes?.length) ref.consumes = relation.consumes;
2172
2192
  if (relation.failure) ref.failure = relation.failure;
2173
2193
  if (relation.event_name) ref["event-name"] = relation.event_name;
2174
2194
  depRefs.push(ref);
2175
2195
  }
2176
- const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2196
+ const glossary = buildGlossary(node, depRefs, graph);
2177
2197
  const breakdown = computeBudgetBreakdown(pkg2, graph);
2178
2198
  const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2179
2199
  const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
@@ -2185,56 +2205,29 @@ function toContextMapOutput(pkg2, graph) {
2185
2205
  path: pkg2.nodePath,
2186
2206
  name: pkg2.nodeName,
2187
2207
  type: node.meta.type,
2208
+ description: node.meta.description,
2188
2209
  mappings: normalizeMappingPaths(node.meta.mapping),
2189
2210
  aspects: nodeAspects,
2190
- flows: flowRefs
2211
+ flows: flowRefs,
2212
+ files: buildNodeFiles(node, config, `model/${pkg2.nodePath}`)
2191
2213
  },
2192
2214
  hierarchy: hierarchyRefs,
2193
2215
  dependencies: depRefs,
2194
- artifacts: registry
2216
+ glossary
2195
2217
  };
2196
2218
  }
2197
- function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2198
- const config = graph.config;
2199
- const configArtifactKeys = new Set(Object.keys(config.artifacts ?? {}));
2200
- const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2201
- 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) {
2202
2229
  const aspects = {};
2203
2230
  const flows = {};
2204
- function addNodeEntry(n, includeYgNodeYaml, filter) {
2205
- if (nodes[n.path]) return;
2206
- const files = [];
2207
- if (includeYgNodeYaml) {
2208
- files.push(`model/${n.path}/yg-node.yaml`);
2209
- }
2210
- for (const filename of filter) {
2211
- if (n.artifacts.some((a) => a.filename === filename)) {
2212
- files.push(`model/${n.path}/${filename}`);
2213
- }
2214
- }
2215
- if (files.length > 0) {
2216
- nodes[n.path] = { files };
2217
- }
2218
- }
2219
- addNodeEntry(node, true, [...configArtifactKeys]);
2220
- for (const ancestor of ancestors) {
2221
- addNodeEntry(ancestor, true, [...configArtifactKeys]);
2222
- }
2223
- const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
2224
- for (const dep of dependencies) {
2225
- const target = graph.nodes.get(dep.path);
2226
- if (target) {
2227
- addNodeEntry(target, false, structuralFilenames);
2228
- }
2229
- for (const ancestor of dep.hierarchy) {
2230
- if (seenDepAncestors.has(ancestor.path)) continue;
2231
- seenDepAncestors.add(ancestor.path);
2232
- const ancestorNode = graph.nodes.get(ancestor.path);
2233
- if (ancestorNode) {
2234
- addNodeEntry(ancestorNode, false, structuralFilenames);
2235
- }
2236
- }
2237
- }
2238
2231
  const allAspectIds = collectEffectiveAspectIds(graph, node.path);
2239
2232
  for (const dep of dependencies) {
2240
2233
  for (const id of dep.aspects) {
@@ -2243,33 +2236,29 @@ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2243
2236
  }
2244
2237
  const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2245
2238
  for (const aspect of resolvedAspects) {
2246
- const files = [];
2247
- files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
2248
- for (const art of aspect.artifacts) {
2249
- files.push(`aspects/${aspect.id}/${art.filename}`);
2250
- }
2239
+ const files = aspect.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `aspects/${aspect.id}/${a.filename}`);
2251
2240
  const entry = {
2252
2241
  name: aspect.name,
2253
2242
  files
2254
2243
  };
2244
+ if (aspect.description) entry.description = aspect.description;
2245
+ if (aspect.stability) entry.stability = aspect.stability;
2255
2246
  if (aspect.implies?.length) entry.implies = aspect.implies;
2256
2247
  aspects[aspect.id] = entry;
2257
2248
  }
2258
2249
  const participatingFlows = collectParticipatingFlows(graph, node);
2259
2250
  for (const flow of participatingFlows) {
2260
- const files = [];
2261
- files.push(`flows/${flow.path}/yg-flow.yaml`);
2262
- for (const art of flow.artifacts) {
2263
- files.push(`flows/${flow.path}/${art.filename}`);
2264
- }
2251
+ const files = flow.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `flows/${flow.path}/${a.filename}`);
2265
2252
  const entry = {
2266
2253
  name: flow.name,
2254
+ participants: flow.nodes,
2267
2255
  files
2268
2256
  };
2257
+ if (flow.description) entry.description = flow.description;
2269
2258
  if (flow.aspects?.length) entry.aspects = flow.aspects;
2270
2259
  flows[flow.path] = entry;
2271
2260
  }
2272
- return { nodes, aspects, flows };
2261
+ return { aspects, flows };
2273
2262
  }
2274
2263
  function collectEffectiveAspectIds(graph, nodePath) {
2275
2264
  const node = graph.nodes.get(nodePath);
@@ -2290,23 +2279,39 @@ function collectEffectiveAspectIds(graph, nodePath) {
2290
2279
  }
2291
2280
 
2292
2281
  // src/formatters/context-text.ts
2293
- import { stringify } from "yaml";
2282
+ import { Document } from "yaml";
2294
2283
  function formatContextYaml(data) {
2295
- const output = {
2296
- meta: {
2297
- "token-count": data.meta.tokenCount,
2298
- "budget-status": data.meta.budgetStatus
2299
- },
2300
- project: data.project,
2301
- node: data.node,
2302
- hierarchy: data.hierarchy.length > 0 ? data.hierarchy : void 0,
2303
- dependencies: data.dependencies.length > 0 ? data.dependencies : void 0,
2304
- 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
2305
2294
  };
2306
- for (const key of Object.keys(output)) {
2307
- 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
+ }
2308
2313
  }
2309
- return stringify(output, { lineWidth: 0 });
2314
+ return doc.toString({ lineWidth: 0 });
2310
2315
  }
2311
2316
  function formatFullContent(files) {
2312
2317
  if (files.length === 0) return "";
@@ -2360,6 +2365,7 @@ async function validate(graph, scope = "all") {
2360
2365
  issues.push(...checkInvalidArtifactConditions(graph));
2361
2366
  issues.push(...await checkContextBudget(graph));
2362
2367
  issues.push(...checkHighFanOut(graph));
2368
+ issues.push(...checkMissingDescriptions(graph));
2363
2369
  }
2364
2370
  issues.push(...checkSchemas(graph));
2365
2371
  issues.push(...checkRelationTargets(graph));
@@ -3092,6 +3098,41 @@ function pct(value, total) {
3092
3098
  if (total === 0) return "0%";
3093
3099
  return `${Math.round(value / total * 100)}%`;
3094
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
+ }
3095
3136
 
3096
3137
  // src/cli/build-context.ts
3097
3138
  function collectRelevantNodePaths(graph, nodePath) {
@@ -3144,23 +3185,32 @@ function registerBuildCommand(program2) {
3144
3185
  const mapOutput = toContextMapOutput(pkg2, graph);
3145
3186
  let output = formatContextYaml(mapOutput);
3146
3187
  if (options.full) {
3147
- const allFiles = [];
3148
- const allEntries = [
3149
- ...Object.values(mapOutput.artifacts.nodes),
3150
- ...Object.values(mapOutput.artifacts.aspects),
3151
- ...Object.values(mapOutput.artifacts.flows)
3152
- ];
3153
3188
  const seen = /* @__PURE__ */ new Set();
3154
- for (const entry of allEntries) {
3155
- for (const filePath of entry.files) {
3156
- if (seen.has(filePath)) continue;
3157
- seen.add(filePath);
3158
- const content = await findFileContent(filePath, graph);
3159
- if (content !== void 0) {
3160
- allFiles.push({ path: filePath, content });
3161
- }
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 });
3162
3196
  }
3163
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);
3211
+ }
3212
+ for (const f of dep.files ?? []) await collectFile(f);
3213
+ }
3164
3214
  output += formatFullContent(allFiles);
3165
3215
  }
3166
3216
  process.stdout.write(output);
@@ -3172,14 +3222,6 @@ function registerBuildCommand(program2) {
3172
3222
  });
3173
3223
  }
3174
3224
  async function findFileContent(filePath, graph) {
3175
- async function readYamlFromDisk(relativePath) {
3176
- try {
3177
- const fullPath = path12.join(graph.rootPath, relativePath);
3178
- return (await readFile14(fullPath, "utf-8")).trim();
3179
- } catch {
3180
- return void 0;
3181
- }
3182
- }
3183
3225
  if (filePath.startsWith("model/")) {
3184
3226
  const rest = filePath.slice("model/".length);
3185
3227
  const parts = rest.split("/");
@@ -3187,9 +3229,6 @@ async function findFileContent(filePath, graph) {
3187
3229
  const nodePath = parts.join("/");
3188
3230
  const node = graph.nodes.get(nodePath);
3189
3231
  if (!node) return void 0;
3190
- if (filename === "yg-node.yaml") {
3191
- return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
3192
- }
3193
3232
  const art = node.artifacts.find((a) => a.filename === filename);
3194
3233
  return art?.content;
3195
3234
  }
@@ -3200,9 +3239,6 @@ async function findFileContent(filePath, graph) {
3200
3239
  const filename = parts.slice(1).join("/");
3201
3240
  const aspect = graph.aspects.find((a) => a.id === aspectId);
3202
3241
  if (!aspect) return void 0;
3203
- if (filename === "yg-aspect.yaml") {
3204
- return readYamlFromDisk(filePath);
3205
- }
3206
3242
  const art = aspect.artifacts.find((a) => a.filename === filename);
3207
3243
  return art?.content;
3208
3244
  }
@@ -3213,9 +3249,6 @@ async function findFileContent(filePath, graph) {
3213
3249
  const filename = parts.slice(1).join("/");
3214
3250
  const flow = graph.flows.find((f) => f.path === flowPath);
3215
3251
  if (!flow) return void 0;
3216
- if (filename === "yg-flow.yaml") {
3217
- return readYamlFromDisk(filePath);
3218
- }
3219
3252
  const art = flow.artifacts.find((a) => a.filename === filename);
3220
3253
  return art?.content;
3221
3254
  }
@@ -3271,12 +3304,12 @@ ${errors.length} errors, ${warnings.length} warnings.
3271
3304
  import chalk2 from "chalk";
3272
3305
 
3273
3306
  // src/io/drift-state-store.ts
3274
- import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3275
- 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";
3276
3309
  import { parse as yamlParse } from "yaml";
3277
3310
  var DRIFT_STATE_DIR = ".drift-state";
3278
3311
  function nodeStatePath(yggRoot, nodePath) {
3279
- return path13.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3312
+ return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3280
3313
  }
3281
3314
  async function scanJsonFiles(dir, baseDir) {
3282
3315
  const results = [];
@@ -3287,12 +3320,12 @@ async function scanJsonFiles(dir, baseDir) {
3287
3320
  return results;
3288
3321
  }
3289
3322
  for (const entry of entries) {
3290
- const fullPath = path13.join(dir, entry.name);
3323
+ const fullPath = path12.join(dir, entry.name);
3291
3324
  if (entry.isDirectory()) {
3292
3325
  const nested = await scanJsonFiles(fullPath, baseDir);
3293
3326
  results.push(...nested);
3294
3327
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
3295
- const relPath = path13.relative(baseDir, fullPath);
3328
+ const relPath = path12.relative(baseDir, fullPath);
3296
3329
  const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
3297
3330
  results.push(nodePath);
3298
3331
  }
@@ -3300,13 +3333,13 @@ async function scanJsonFiles(dir, baseDir) {
3300
3333
  return results;
3301
3334
  }
3302
3335
  async function removeEmptyParents(filePath, stopDir) {
3303
- let dir = path13.dirname(filePath);
3336
+ let dir = path12.dirname(filePath);
3304
3337
  while (dir !== stopDir && dir.startsWith(stopDir)) {
3305
3338
  try {
3306
3339
  const entries = await readdir6(dir);
3307
3340
  if (entries.length === 0) {
3308
3341
  await rm2(dir, { recursive: true });
3309
- dir = path13.dirname(dir);
3342
+ dir = path12.dirname(dir);
3310
3343
  } else {
3311
3344
  break;
3312
3345
  }
@@ -3318,7 +3351,7 @@ async function removeEmptyParents(filePath, stopDir) {
3318
3351
  async function readNodeDriftState(yggRoot, nodePath) {
3319
3352
  try {
3320
3353
  const filePath = nodeStatePath(yggRoot, nodePath);
3321
- const content = await readFile15(filePath, "utf-8");
3354
+ const content = await readFile14(filePath, "utf-8");
3322
3355
  const parsed = JSON.parse(content);
3323
3356
  return parsed;
3324
3357
  } catch {
@@ -3327,12 +3360,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
3327
3360
  }
3328
3361
  async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3329
3362
  const filePath = nodeStatePath(yggRoot, nodePath);
3330
- await mkdir3(path13.dirname(filePath), { recursive: true });
3363
+ await mkdir3(path12.dirname(filePath), { recursive: true });
3331
3364
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3332
3365
  await writeFile5(filePath, content, "utf-8");
3333
3366
  }
3334
3367
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3335
- const driftDir = path13.join(yggRoot, DRIFT_STATE_DIR);
3368
+ const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
3336
3369
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
3337
3370
  const removed = [];
3338
3371
  for (const nodePath of allNodePaths) {
@@ -3346,7 +3379,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
3346
3379
  return removed.sort();
3347
3380
  }
3348
3381
  async function readDriftState(yggRoot) {
3349
- const driftPath = path13.join(yggRoot, DRIFT_STATE_DIR);
3382
+ const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
3350
3383
  let driftStat;
3351
3384
  try {
3352
3385
  driftStat = await stat5(driftPath);
@@ -3354,7 +3387,7 @@ async function readDriftState(yggRoot) {
3354
3387
  return {};
3355
3388
  }
3356
3389
  if (driftStat.isFile()) {
3357
- const content = await readFile15(driftPath, "utf-8");
3390
+ const content = await readFile14(driftPath, "utf-8");
3358
3391
  let raw;
3359
3392
  try {
3360
3393
  raw = JSON.parse(content);
@@ -3386,20 +3419,20 @@ async function readDriftState(yggRoot) {
3386
3419
  }
3387
3420
 
3388
3421
  // src/utils/hash.ts
3389
- import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
3390
- import path14 from "path";
3422
+ import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3423
+ import path13 from "path";
3391
3424
  import { createHash } from "crypto";
3392
3425
  import { createRequire } from "module";
3393
3426
  var require2 = createRequire(import.meta.url);
3394
3427
  var ignoreFactory = require2("ignore");
3395
3428
  async function hashFile(filePath) {
3396
- const content = await readFile16(filePath);
3429
+ const content = await readFile15(filePath);
3397
3430
  return createHash("sha256").update(content).digest("hex");
3398
3431
  }
3399
3432
  async function loadRootGitignoreStack(projectRoot) {
3400
3433
  if (!projectRoot) return [];
3401
3434
  try {
3402
- const content = await readFile16(path14.join(projectRoot, ".gitignore"), "utf-8");
3435
+ const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
3403
3436
  const matcher = ignoreFactory();
3404
3437
  matcher.add(content);
3405
3438
  return [{ basePath: projectRoot, matcher }];
@@ -3409,7 +3442,7 @@ async function loadRootGitignoreStack(projectRoot) {
3409
3442
  }
3410
3443
  function isIgnoredByStack(candidatePath, stack) {
3411
3444
  for (const { basePath, matcher } of stack) {
3412
- const relativePath = path14.relative(basePath, candidatePath);
3445
+ const relativePath = path13.relative(basePath, candidatePath);
3413
3446
  if (relativePath === "" || relativePath.startsWith("..")) continue;
3414
3447
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
3415
3448
  }
@@ -3424,7 +3457,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3424
3457
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
3425
3458
  const allFiles = [];
3426
3459
  for (const tf of trackedFiles) {
3427
- const absPath = path14.join(projectRoot, tf.path);
3460
+ const absPath = path13.join(projectRoot, tf.path);
3428
3461
  try {
3429
3462
  const st = await stat6(absPath);
3430
3463
  if (st.isDirectory()) {
@@ -3434,7 +3467,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3434
3467
  });
3435
3468
  for (const entry of dirEntries) {
3436
3469
  allFiles.push({
3437
- relPath: path14.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3470
+ relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3438
3471
  absPath: entry.absPath,
3439
3472
  mtimeMs: entry.mtimeMs
3440
3473
  });
@@ -3474,7 +3507,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3474
3507
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3475
3508
  let stack = options.gitignoreStack ?? [];
3476
3509
  try {
3477
- const localContent = await readFile16(path14.join(directoryPath, ".gitignore"), "utf-8");
3510
+ const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
3478
3511
  const localMatcher = ignoreFactory();
3479
3512
  localMatcher.add(localContent);
3480
3513
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -3484,7 +3517,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3484
3517
  const dirs = [];
3485
3518
  const files = [];
3486
3519
  for (const entry of entries) {
3487
- const absoluteChildPath = path14.join(directoryPath, entry.name);
3520
+ const absoluteChildPath = path13.join(directoryPath, entry.name);
3488
3521
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
3489
3522
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
3490
3523
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -3497,7 +3530,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3497
3530
  Promise.all(files.map(async (f) => {
3498
3531
  const fileStat = await stat6(f);
3499
3532
  return {
3500
- relPath: path14.relative(rootDirectoryPath, f),
3533
+ relPath: path13.relative(rootDirectoryPath, f),
3501
3534
  absPath: f,
3502
3535
  mtimeMs: fileStat.mtimeMs
3503
3536
  };
@@ -3510,14 +3543,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3510
3543
  }
3511
3544
 
3512
3545
  // src/core/context-files.ts
3513
- import path15 from "path";
3546
+ import path14 from "path";
3514
3547
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3515
3548
  function collectTrackedFiles(node, graph) {
3516
3549
  const seen = /* @__PURE__ */ new Set();
3517
3550
  const result = [];
3518
- const projectRoot = path15.dirname(graph.rootPath);
3519
- const yggPrefix = path15.relative(projectRoot, graph.rootPath);
3520
- 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("/");
3521
3554
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
3522
3555
  function addFile(filePath, category) {
3523
3556
  if (seen.has(filePath)) return;
@@ -3630,7 +3663,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
3630
3663
 
3631
3664
  // src/core/drift-detector.ts
3632
3665
  import { access as access2 } from "fs/promises";
3633
- import path16 from "path";
3666
+ import path15 from "path";
3634
3667
  function getChildMappingExclusions(graph, nodePath) {
3635
3668
  const node = graph.nodes.get(nodePath);
3636
3669
  if (!node) return [];
@@ -3652,7 +3685,7 @@ function getChildMappingExclusions(graph, nodePath) {
3652
3685
  return exclusions;
3653
3686
  }
3654
3687
  async function detectDrift(graph, filterNodePath) {
3655
- const projectRoot = path16.dirname(graph.rootPath);
3688
+ const projectRoot = path15.dirname(graph.rootPath);
3656
3689
  const driftState = await readDriftState(graph.rootPath);
3657
3690
  const entries = [];
3658
3691
  for (const [nodePath, node] of graph.nodes) {
@@ -3734,14 +3767,14 @@ async function detectDrift(graph, filterNodePath) {
3734
3767
  };
3735
3768
  }
3736
3769
  function categorizeFile(filePath, _rootPath, projectRoot) {
3737
- const yggPrefix = path16.relative(projectRoot, _rootPath);
3738
- const normalizedPrefix = yggPrefix.split(path16.sep).join("/");
3770
+ const yggPrefix = path15.relative(projectRoot, _rootPath);
3771
+ const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3739
3772
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3740
3773
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3741
3774
  }
3742
3775
  async function allPathsMissing(projectRoot, mappingPaths) {
3743
3776
  for (const mp of mappingPaths) {
3744
- const absPath = path16.join(projectRoot, mp);
3777
+ const absPath = path15.join(projectRoot, mp);
3745
3778
  try {
3746
3779
  await access2(absPath);
3747
3780
  return false;
@@ -3751,7 +3784,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3751
3784
  return true;
3752
3785
  }
3753
3786
  async function syncDriftState(graph, nodePath) {
3754
- const projectRoot = path16.dirname(graph.rootPath);
3787
+ const projectRoot = path15.dirname(graph.rootPath);
3755
3788
  const node = graph.nodes.get(nodePath);
3756
3789
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3757
3790
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
@@ -4071,10 +4104,10 @@ function registerTreeCommand(program2) {
4071
4104
  let roots;
4072
4105
  let showProjectName;
4073
4106
  if (options.root?.trim()) {
4074
- const path20 = options.root.trim().replace(/\/$/, "");
4075
- const node = graph.nodes.get(path20);
4107
+ const path19 = options.root.trim().replace(/\/$/, "");
4108
+ const node = graph.nodes.get(path19);
4076
4109
  if (!node) {
4077
- process.stderr.write(`Error: path '${path20}' not found
4110
+ process.stderr.write(`Error: path '${path19}' not found
4078
4111
  `);
4079
4112
  process.exit(1);
4080
4113
  }
@@ -4118,7 +4151,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
4118
4151
  }
4119
4152
 
4120
4153
  // src/cli/owner.ts
4121
- import path17 from "path";
4154
+ import path16 from "path";
4122
4155
  import { access as access3 } from "fs/promises";
4123
4156
  function normalizeForMatch(inputPath) {
4124
4157
  return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -4148,11 +4181,11 @@ function registerOwnerCommand(program2) {
4148
4181
  const graph = await loadGraph(cwd);
4149
4182
  const repoRoot = projectRootFromGraph(graph.rootPath);
4150
4183
  const rawPath = options.file.trim();
4151
- const absolute = path17.resolve(cwd, rawPath);
4152
- 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("/");
4153
4186
  const result = findOwner(graph, repoRoot, repoRelative);
4154
4187
  if (!result.nodePath) {
4155
- const absPath = path17.resolve(repoRoot, result.file);
4188
+ const absPath = path16.resolve(repoRoot, result.file);
4156
4189
  let exists = true;
4157
4190
  try {
4158
4191
  await access3(absPath);
@@ -4186,7 +4219,7 @@ function registerOwnerCommand(program2) {
4186
4219
 
4187
4220
  // src/core/dependency-resolver.ts
4188
4221
  import { execSync } from "child_process";
4189
- import path18 from "path";
4222
+ import path17 from "path";
4190
4223
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
4191
4224
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
4192
4225
  function filterRelationType(relType, filter) {
@@ -4263,7 +4296,7 @@ function registerDepsCommand(program2) {
4263
4296
  // src/core/graph-from-git.ts
4264
4297
  import { mkdtemp, rm as rm3 } from "fs/promises";
4265
4298
  import { tmpdir } from "os";
4266
- import path19 from "path";
4299
+ import path18 from "path";
4267
4300
  import { execSync as execSync2 } from "child_process";
4268
4301
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
4269
4302
  const yggPath = ".yggdrasil";
@@ -4274,8 +4307,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
4274
4307
  return null;
4275
4308
  }
4276
4309
  try {
4277
- tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
4278
- const archivePath = path19.join(tmpDir, "archive.tar");
4310
+ tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
4311
+ const archivePath = path18.join(tmpDir, "archive.tar");
4279
4312
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
4280
4313
  cwd: projectRoot,
4281
4314
  stdio: "pipe"
@@ -4345,14 +4378,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
4345
4378
  }
4346
4379
  const chains = [];
4347
4380
  for (const node of transitiveOnly) {
4348
- const path20 = [];
4381
+ const path19 = [];
4349
4382
  let current = node;
4350
4383
  while (current) {
4351
- path20.unshift(current);
4384
+ path19.unshift(current);
4352
4385
  current = parent.get(current);
4353
4386
  }
4354
- if (path20.length >= 3) {
4355
- chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
4387
+ if (path19.length >= 3) {
4388
+ chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
4356
4389
  }
4357
4390
  }
4358
4391
  return chains.sort();
@@ -4396,14 +4429,14 @@ function collectIndirectDependents(graph, directlyAffected) {
4396
4429
  }
4397
4430
  for (const [node] of parent) {
4398
4431
  if (directSet.has(node)) continue;
4399
- const path20 = [node];
4432
+ const path19 = [node];
4400
4433
  let current = node;
4401
4434
  while (parent.has(current)) {
4402
4435
  current = parent.get(current);
4403
- path20.push(current);
4436
+ path19.push(current);
4404
4437
  }
4405
- const chain = path20.map((p) => `<- ${p}`).join(" ");
4406
- const depth = path20.length;
4438
+ const chain = path19.map((p) => `<- ${p}`).join(" ");
4439
+ const depth = path19.length;
4407
4440
  const existing = bestChain.get(node);
4408
4441
  if (!existing || depth < existing.depth) {
4409
4442
  bestChain.set(node, { chain, depth });
@@ -4845,6 +4878,7 @@ function registerFlowsCommand(program2) {
4845
4878
  participants: flow.nodes.length,
4846
4879
  nodes: flow.nodes.sort()
4847
4880
  };
4881
+ if (flow.description) entry.description = flow.description;
4848
4882
  if (flow.aspects && flow.aspects.length > 0) entry.aspects = flow.aspects;
4849
4883
  return entry;
4850
4884
  });