@chrisdudek/yg 2.3.3 → 2.4.1

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
@@ -78,11 +78,10 @@ BEFORE reading, researching, planning, OR modifying ANY mapped file:
78
78
  - Understanding how/why it works \u2192 yg build-context --node <owner>
79
79
  - Assessing what is affected by a change \u2192 yg impact --node <owner>
80
80
  - Planning modifications \u2192 both (build-context first, then impact)
81
- The context package is your primary source of ARCHITECTURAL understanding:
82
- intent, constraints, relations, rationale. For IMPLEMENTATION precision
83
- (exact behavior, error handling, await patterns, edge cases) \u2014 verify
84
- against source code. Aspects describe intended patterns; individual
85
- implementations may deviate.
81
+ \`yg build-context --node <path>\`. Read the YAML map for topology,
82
+ then read artifact files listed in the artifacts section. For quick
83
+ orientation, the map alone is sufficient. For implementation, read
84
+ all artifact files before changing code.
86
85
  If the context package seems insufficient \u2014 enrich the graph.
87
86
 
88
87
  AFTER modifying:
@@ -107,7 +106,7 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
107
106
  ### Five Core Rules
108
107
 
109
108
  1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and the appropriate graph tool: \`yg build-context\` to understand a component, \`yg impact\` to assess blast radius. The graph is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
110
- 2. **Code and graph are one.** Code changed \u2192 graph updated in the same response. Graph changed \u2192 source verified in the same response. No exceptions.
109
+ 2. **The graph is the specification; code implements it.** The graph absorbs knowledge from every source \u2014 external docs, conversations, decisions \u2014 and must be self-sufficient. If all other sources disappeared, the graph alone must contain enough to understand the system. Do not leave knowledge in external documents and reference them \u2014 capture the knowledge in graph artifacts. Update graph artifacts immediately after each file change, while context is fresh \u2014 do not batch graph updates to the end of a task. Code and graph move together: code changed \u2192 graph updated before moving to the next file. Graph changed \u2192 source verified in the same response. When planning work \u2014 in any tool, skill, or workflow \u2014 graph updates are part of each step's definition of done, never a separate phase.
111
110
  3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
112
111
  4. **Always capture why \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
113
112
  5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
@@ -154,13 +153,14 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
154
153
  You have broken Yggdrasil if you do any of the following:
155
154
 
156
155
  - \u274C Worked on a mapped file without running \`yg owner\` + the appropriate graph tool (\`build-context\` or \`impact\`) first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
157
- - \u274C Modified source code without updating graph artifacts in the same response, or vice versa.
156
+ - \u274C Modified source code without updating graph artifacts before moving to the next file, or vice versa.
158
157
  - \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
159
158
  - \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
160
159
  - \u274C Ran \`yg drift-sync\` before both graph artifacts and source code are current.
161
160
  - \u274C Placed a cross-cutting requirement in a local artifact instead of an aspect, or used an aspect id with no \`aspects/\` directory.
162
161
  - \u274C Invented a rationale, business rule, or decision \u2014 or recorded a decision without documenting rejected alternatives and rationale (use "rationale: unknown" if unknown).
163
162
  - \u274C Used blackbox coverage for greenfield (new) code.
163
+ - \u274C Deleted or shortened graph artifact content to reduce context package size instead of splitting the node.
164
164
 
165
165
  ### Escape Hatch
166
166
 
@@ -188,9 +188,13 @@ Include this as the FIRST instruction in every subagent prompt:
188
188
 
189
189
  \`\`\`
190
190
  BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
191
+ DELIVERABLES \u2014 all required, incomplete work will be rejected:
192
+ 1. Working source code
193
+ 2. Graph nodes with artifacts for every new/modified source file
194
+ 3. \`yg validate\` passing
191
195
  \`\`\`
192
196
 
193
- A subagent that skips this step will read code without graph context, miss architectural constraints, and produce changes that break graph-code consistency.`;
197
+ A subagent that delivers code without corresponding graph updates has not completed its task. Before accepting subagent output, verify: are there new or modified source files without corresponding graph coverage? If yes, the work is incomplete.`;
194
198
  var OPERATIONS = `## OPERATIONS
195
199
 
196
200
  ### Conversation Lifecycle
@@ -204,7 +208,10 @@ PREFLIGHT (every conversation, before any work):
204
208
 
205
209
  UNDERSTANDING mapped code (questions, research, OR planning):
206
210
  - [ ] 1. yg owner --file <path>
207
- - [ ] 2. Owner found \u2192 yg build-context --node <path>. Use context package as primary source.
211
+ - [ ] 2. Owner found \u2192 yg build-context --node <path>. Read the YAML map
212
+ for topology, then read artifact files from the artifacts section.
213
+ For quick orientation, the map alone is sufficient. For implementation,
214
+ read all artifact files before changing code.
208
215
  - [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
209
216
  Never use grep or raw file reads as primary understanding when graph coverage exists.
210
217
  Raw reads supplement the context package \u2014 they do not replace it.
@@ -216,7 +223,7 @@ WRAP-UP (user signals "done", "wrap up", "that's enough"):
216
223
 
217
224
  BEFORE ENDING ANY RESPONSE (self-audit):
218
225
  - [ ] Did I interact with mapped code (read, research, or modify)? If yes \u2192 did I use a graph tool BEFORE reading source?
219
- - [ ] Did I modify source code? If yes \u2192 did I update graph artifacts in this same response?
226
+ - [ ] Did I modify source code? If yes \u2192 did I update graph artifacts before moving to the next file?
220
227
  - [ ] If you broke either rule, you have broken the protocol. Do not finish until both are fixed.
221
228
  \`\`\`
222
229
 
@@ -231,7 +238,7 @@ You are not allowed to edit or create source code without establishing graph cov
231
238
  - [ ] 1. Read specification: \`yg build-context --node <node_path>\`
232
239
  - [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
233
240
  - [ ] 3. Modify source code
234
- - [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes
241
+ - [ ] 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)
235
242
  - [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
236
243
  - [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
237
244
 
@@ -342,6 +349,7 @@ When reviewing graph quality (triggered by user or quality improvement):
342
349
  - **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
343
350
  - **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
344
351
  - **Budget exceeded** \u2192 if \`yg build-context\` exits with error (context package exceeds budget), warn user: "This node should be split." Do not proceed with implementation.
352
+ - **Budget warning (W005)** \u2192 if \`yg validate\` shows W005 (context near budget), the node needs splitting. NEVER delete knowledge from artifacts to reduce token count \u2014 knowledge destroyed is irrecoverable. Instead: identify a cohesive subset of the node's responsibilities, propose a split to the user, create child nodes, and redistribute artifacts. The total knowledge must be preserved or increased, never reduced.
345
353
  - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
346
354
  - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
347
355
  var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
@@ -380,7 +388,13 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
380
388
 
381
389
  ### Context Assembly
382
390
 
383
- Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global project identity, hierarchy, own artifacts, aspects, and relational context. It is your architectural map. For implementation-level claims (exact call patterns, error handling, await vs fire-and-forget) \u2014 verify against source code. If the package is insufficient, enrich the graph.
391
+ **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>\`.
392
+
393
+ **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.
394
+
395
+ **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.
396
+
397
+ Artifact paths are stable identifiers within a session. When building context for multiple nodes, skip reading files you have already read \u2014 same path means same content.
384
398
 
385
399
  ### Information Routing
386
400
 
@@ -450,7 +464,8 @@ Test: "Does this describe what happens in the world, or only in the software?" I
450
464
  \`\`\`
451
465
  yg preflight [--quick] Unified diagnostic: drift + status + validate.
452
466
  yg owner --file <path> Find the node that owns this file.
453
- yg build-context --node <path> Assemble context package for this node.
467
+ yg build-context --node <path> Assemble context map with artifact paths (default).
468
+ yg build-context --node <path> --full Same map + file contents appended below separator.
454
469
  yg tree [--root <path>] [--depth N] Print graph structure.
455
470
  yg aspects List aspects with metadata (YAML output).
456
471
  yg flows List flows with metadata (YAML output).
@@ -1156,6 +1171,10 @@ function registerInitCommand(program2) {
1156
1171
  });
1157
1172
  }
1158
1173
 
1174
+ // src/cli/build-context.ts
1175
+ import { readFile as readFile14 } from "fs/promises";
1176
+ import path12 from "path";
1177
+
1159
1178
  // src/core/graph-loader.ts
1160
1179
  import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
1161
1180
  import path9 from "path";
@@ -2026,6 +2045,150 @@ function collectAncestors(node) {
2026
2045
  }
2027
2046
  return ancestors;
2028
2047
  }
2048
+ function toContextMapOutput(pkg2, graph) {
2049
+ const node = graph.nodes.get(pkg2.nodePath);
2050
+ const config = graph.config;
2051
+ const nodeAspects = (node.meta.aspects ?? []).map((entry) => {
2052
+ const ref = { id: entry.aspect };
2053
+ if (entry.anchors?.length) ref.anchors = entry.anchors;
2054
+ if (entry.exceptions?.length) ref.exceptions = entry.exceptions;
2055
+ return ref;
2056
+ });
2057
+ const participatingFlows = collectParticipatingFlows(graph, node);
2058
+ const flowRefs = participatingFlows.map((f) => {
2059
+ const ref = { path: f.path, name: f.name };
2060
+ if (f.aspects?.length) ref.aspects = f.aspects;
2061
+ return ref;
2062
+ });
2063
+ const ancestors = collectAncestors(node);
2064
+ const hierarchyRefs = ancestors.map((a) => {
2065
+ const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
2066
+ const expanded = expandAspects(nodeAspectIds, graph.aspects);
2067
+ return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2068
+ });
2069
+ const ancestorPaths = new Set(ancestors.map((a) => a.path));
2070
+ const depRefs = [];
2071
+ for (const relation of node.meta.relations ?? []) {
2072
+ const target = graph.nodes.get(relation.target);
2073
+ if (!target) continue;
2074
+ if (ancestorPaths.has(relation.target)) continue;
2075
+ const depAncestors = collectAncestors(target);
2076
+ const depHierarchy = depAncestors.map((a) => {
2077
+ const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
2078
+ const expanded = expandAspects(ids, graph.aspects);
2079
+ return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2080
+ });
2081
+ const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
2082
+ const ref = {
2083
+ path: target.path,
2084
+ name: target.meta.name,
2085
+ type: target.meta.type,
2086
+ relation: relation.type,
2087
+ aspects: depEffectiveAspects,
2088
+ hierarchy: depHierarchy
2089
+ };
2090
+ if (relation.consumes?.length) ref.consumes = relation.consumes;
2091
+ if (relation.failure) ref.failure = relation.failure;
2092
+ if (relation.event_name) ref["event-name"] = relation.event_name;
2093
+ depRefs.push(ref);
2094
+ }
2095
+ const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2096
+ const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2097
+ const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
2098
+ const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
2099
+ return {
2100
+ meta: { tokenCount: pkg2.tokenCount, budgetStatus },
2101
+ project: config.name,
2102
+ node: {
2103
+ path: pkg2.nodePath,
2104
+ name: pkg2.nodeName,
2105
+ type: node.meta.type,
2106
+ mappings: normalizeMappingPaths(node.meta.mapping),
2107
+ aspects: nodeAspects,
2108
+ flows: flowRefs
2109
+ },
2110
+ hierarchy: hierarchyRefs,
2111
+ dependencies: depRefs,
2112
+ artifacts: registry
2113
+ };
2114
+ }
2115
+ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2116
+ const config = graph.config;
2117
+ const configArtifactKeys = new Set(Object.keys(config.artifacts ?? {}));
2118
+ const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2119
+ const nodes = {};
2120
+ const aspects = {};
2121
+ const flows = {};
2122
+ function addNodeEntry(n, includeYgNodeYaml, filter) {
2123
+ if (nodes[n.path]) return;
2124
+ const files = [];
2125
+ if (includeYgNodeYaml) {
2126
+ files.push(`model/${n.path}/yg-node.yaml`);
2127
+ }
2128
+ for (const filename of filter) {
2129
+ if (n.artifacts.some((a) => a.filename === filename)) {
2130
+ files.push(`model/${n.path}/${filename}`);
2131
+ }
2132
+ }
2133
+ if (files.length > 0) {
2134
+ nodes[n.path] = { files };
2135
+ }
2136
+ }
2137
+ addNodeEntry(node, true, [...configArtifactKeys]);
2138
+ for (const ancestor of ancestors) {
2139
+ addNodeEntry(ancestor, true, [...configArtifactKeys]);
2140
+ }
2141
+ const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
2142
+ for (const dep of dependencies) {
2143
+ const target = graph.nodes.get(dep.path);
2144
+ if (target) {
2145
+ addNodeEntry(target, false, structuralFilenames);
2146
+ }
2147
+ for (const ancestor of dep.hierarchy) {
2148
+ if (seenDepAncestors.has(ancestor.path)) continue;
2149
+ seenDepAncestors.add(ancestor.path);
2150
+ const ancestorNode = graph.nodes.get(ancestor.path);
2151
+ if (ancestorNode) {
2152
+ addNodeEntry(ancestorNode, false, structuralFilenames);
2153
+ }
2154
+ }
2155
+ }
2156
+ const allAspectIds = collectEffectiveAspectIds(graph, node.path);
2157
+ for (const dep of dependencies) {
2158
+ for (const id of dep.aspects) {
2159
+ allAspectIds.add(id);
2160
+ }
2161
+ }
2162
+ const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2163
+ for (const aspect of resolvedAspects) {
2164
+ const files = [];
2165
+ files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
2166
+ for (const art of aspect.artifacts) {
2167
+ files.push(`aspects/${aspect.id}/${art.filename}`);
2168
+ }
2169
+ const entry = {
2170
+ name: aspect.name,
2171
+ files
2172
+ };
2173
+ if (aspect.implies?.length) entry.implies = aspect.implies;
2174
+ aspects[aspect.id] = entry;
2175
+ }
2176
+ const participatingFlows = collectParticipatingFlows(graph, node);
2177
+ for (const flow of participatingFlows) {
2178
+ const files = [];
2179
+ files.push(`flows/${flow.path}/yg-flow.yaml`);
2180
+ for (const art of flow.artifacts) {
2181
+ files.push(`flows/${flow.path}/${art.filename}`);
2182
+ }
2183
+ const entry = {
2184
+ name: flow.name,
2185
+ files
2186
+ };
2187
+ if (flow.aspects?.length) entry.aspects = flow.aspects;
2188
+ flows[flow.path] = entry;
2189
+ }
2190
+ return { nodes, aspects, flows };
2191
+ }
2029
2192
  function collectEffectiveAspectIds(graph, nodePath) {
2030
2193
  const node = graph.nodes.get(nodePath);
2031
2194
  if (!node) return /* @__PURE__ */ new Set();
@@ -2044,6 +2207,38 @@ function collectEffectiveAspectIds(graph, nodePath) {
2044
2207
  return new Set(expandAspects([...raw], graph.aspects));
2045
2208
  }
2046
2209
 
2210
+ // src/formatters/context-text.ts
2211
+ import { stringify } from "yaml";
2212
+ function formatContextYaml(data) {
2213
+ const output = {
2214
+ meta: {
2215
+ "token-count": data.meta.tokenCount,
2216
+ "budget-status": data.meta.budgetStatus
2217
+ },
2218
+ project: data.project,
2219
+ node: data.node,
2220
+ hierarchy: data.hierarchy.length > 0 ? data.hierarchy : void 0,
2221
+ dependencies: data.dependencies.length > 0 ? data.dependencies : void 0,
2222
+ artifacts: data.artifacts
2223
+ };
2224
+ for (const key of Object.keys(output)) {
2225
+ if (output[key] === void 0) delete output[key];
2226
+ }
2227
+ return stringify(output, { lineWidth: 0 });
2228
+ }
2229
+ function formatFullContent(files) {
2230
+ if (files.length === 0) return "";
2231
+ let out = "---\n\n";
2232
+ for (const file of files) {
2233
+ out += `<${file.path}>
2234
+ ${file.content}
2235
+ </${file.path}>
2236
+
2237
+ `;
2238
+ }
2239
+ return out;
2240
+ }
2241
+
2047
2242
  // src/core/validator.ts
2048
2243
  import { readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
2049
2244
  import path11 from "path";
@@ -2786,7 +2981,7 @@ async function checkContextBudget(graph) {
2786
2981
  severity: "warning",
2787
2982
  code: "W005",
2788
2983
  rule: "budget-warning",
2789
- message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Consider splitting the node or reducing dependencies.`,
2984
+ message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Split the node into smaller units \u2014 do not delete knowledge from artifacts to reduce size.`,
2790
2985
  nodePath
2791
2986
  });
2792
2987
  }
@@ -2796,78 +2991,6 @@ async function checkContextBudget(graph) {
2796
2991
  return issues;
2797
2992
  }
2798
2993
 
2799
- // src/formatters/context-text.ts
2800
- function escapeAttr(val) {
2801
- return val.replace(/"/g, "&quot;");
2802
- }
2803
- function formatLayer(layer) {
2804
- switch (layer.type) {
2805
- case "global":
2806
- return `<global>
2807
- ${layer.content}
2808
- </global>`;
2809
- case "hierarchy": {
2810
- const pathMatch = layer.label.match(/\((.+)\/\)/);
2811
- const pathAttr = pathMatch ? ` path="${escapeAttr(pathMatch[1])}"` : "";
2812
- const aspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
2813
- return `<hierarchy${pathAttr}${aspectsAttr}>
2814
- ${layer.content}
2815
- </hierarchy>`;
2816
- }
2817
- case "own": {
2818
- if (layer.label === "Materialization Target") {
2819
- return `<materialization-target paths="${escapeAttr(layer.content)}" />`;
2820
- }
2821
- const ownAspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
2822
- return `<own-artifacts${ownAspectsAttr}>
2823
- ${layer.content}
2824
- </own-artifacts>`;
2825
- }
2826
- case "aspects": {
2827
- const nameMatch = layer.label.match(/^(.+?) \(aspect: (.+)\)$/);
2828
- const name = nameMatch ? escapeAttr(nameMatch[1]) : "";
2829
- const id = nameMatch ? escapeAttr(nameMatch[2]) : "";
2830
- return `<aspect name="${name}" id="${id}">
2831
- ${layer.content}
2832
- </aspect>`;
2833
- }
2834
- case "relational": {
2835
- const attrs = layer.attrs ?? {};
2836
- const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
2837
- const tagName = attrs.type && ["emits", "listens"].includes(attrs.type) ? "event" : "dependency";
2838
- return `<${tagName}${attrStr}>
2839
- ${layer.content}
2840
- </${tagName}>`;
2841
- }
2842
- case "flows": {
2843
- const flowName = layer.label.replace(/^Flow: /, "").trim();
2844
- const flowAspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
2845
- return `<flow name="${escapeAttr(flowName)}"${flowAspectsAttr}>
2846
- ${layer.content}
2847
- </flow>`;
2848
- }
2849
- default:
2850
- return layer.content;
2851
- }
2852
- }
2853
- function formatContextText(pkg2) {
2854
- const attrs = [
2855
- `node-path="${escapeAttr(pkg2.nodePath)}"`,
2856
- `node-name="${escapeAttr(pkg2.nodeName)}"`,
2857
- `token-count="${pkg2.tokenCount}"`
2858
- ].join(" ");
2859
- let out = `<context-package ${attrs}>
2860
-
2861
- `;
2862
- for (const section of pkg2.sections) {
2863
- for (const layer of section.layers) {
2864
- out += formatLayer(layer) + "\n\n";
2865
- }
2866
- }
2867
- out += "</context-package>";
2868
- return out;
2869
- }
2870
-
2871
2994
  // src/cli/build-context.ts
2872
2995
  function collectRelevantNodePaths(graph, nodePath) {
2873
2996
  const relevant = /* @__PURE__ */ new Set();
@@ -2879,19 +3002,24 @@ function collectRelevantNodePaths(graph, nodePath) {
2879
3002
  }
2880
3003
  for (const rel of node.meta.relations ?? []) {
2881
3004
  relevant.add(rel.target);
3005
+ const target = graph.nodes.get(rel.target);
3006
+ if (target) {
3007
+ for (const ancestor of collectAncestors(target)) {
3008
+ relevant.add(ancestor.path);
3009
+ }
3010
+ }
2882
3011
  }
2883
3012
  return relevant;
2884
3013
  }
2885
3014
  function registerBuildCommand(program2) {
2886
- program2.command("build-context").description("Assemble a context package for one node").requiredOption("--node <node-path>", "Node path relative to .yggdrasil/model/").action(async (options) => {
3015
+ program2.command("build-context").description("Assemble a context package for one node").requiredOption("--node <node-path>", "Node path relative to .yggdrasil/model/").option("--full", "Include artifact file contents in output").action(async (options) => {
2887
3016
  try {
2888
3017
  const graph = await loadGraph(process.cwd());
2889
3018
  const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/$/, "");
2890
3019
  const relevantNodes = collectRelevantNodePaths(graph, nodePath);
2891
3020
  const validationResult = await validate(graph, "all");
2892
3021
  const relevantErrors = validationResult.issues.filter(
2893
- (issue) => issue.severity === "error" && // Global errors (no nodePath) always block — e.g., E012 invalid config
2894
- (!issue.nodePath || relevantNodes.has(issue.nodePath))
3022
+ (issue) => issue.severity === "error" && (!issue.nodePath || relevantNodes.has(issue.nodePath))
2895
3023
  );
2896
3024
  if (relevantErrors.length > 0) {
2897
3025
  const totalErrors = validationResult.issues.filter((i) => i.severity === "error").length;
@@ -2911,19 +3039,29 @@ function registerBuildCommand(program2) {
2911
3039
  process.exit(1);
2912
3040
  }
2913
3041
  const pkg2 = await buildContext(graph, nodePath);
2914
- const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
2915
- const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
2916
- const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
2917
- let output = formatContextText(pkg2);
2918
- output += `Budget status: ${budgetStatus}
2919
- `;
2920
- process.stdout.write(output);
2921
- if (budgetStatus === "error") {
2922
- process.stderr.write(
2923
- `Warning: context package exceeds error budget (${pkg2.tokenCount} >= ${errorThreshold}). Consider splitting the node.
2924
- `
2925
- );
3042
+ const mapOutput = toContextMapOutput(pkg2, graph);
3043
+ let output = formatContextYaml(mapOutput);
3044
+ if (options.full) {
3045
+ const allFiles = [];
3046
+ const allEntries = [
3047
+ ...Object.values(mapOutput.artifacts.nodes),
3048
+ ...Object.values(mapOutput.artifacts.aspects),
3049
+ ...Object.values(mapOutput.artifacts.flows)
3050
+ ];
3051
+ const seen = /* @__PURE__ */ new Set();
3052
+ for (const entry of allEntries) {
3053
+ for (const filePath of entry.files) {
3054
+ if (seen.has(filePath)) continue;
3055
+ seen.add(filePath);
3056
+ const content = await findFileContent(filePath, graph);
3057
+ if (content !== void 0) {
3058
+ allFiles.push({ path: filePath, content });
3059
+ }
3060
+ }
3061
+ }
3062
+ output += formatFullContent(allFiles);
2926
3063
  }
3064
+ process.stdout.write(output);
2927
3065
  } catch (error) {
2928
3066
  process.stderr.write(`Error: ${error.message}
2929
3067
  `);
@@ -2931,6 +3069,56 @@ function registerBuildCommand(program2) {
2931
3069
  }
2932
3070
  });
2933
3071
  }
3072
+ async function findFileContent(filePath, graph) {
3073
+ async function readYamlFromDisk(relativePath) {
3074
+ try {
3075
+ const fullPath = path12.join(graph.rootPath, relativePath);
3076
+ return (await readFile14(fullPath, "utf-8")).trim();
3077
+ } catch {
3078
+ return void 0;
3079
+ }
3080
+ }
3081
+ if (filePath.startsWith("model/")) {
3082
+ const rest = filePath.slice("model/".length);
3083
+ const parts = rest.split("/");
3084
+ const filename = parts.pop();
3085
+ const nodePath = parts.join("/");
3086
+ const node = graph.nodes.get(nodePath);
3087
+ if (!node) return void 0;
3088
+ if (filename === "yg-node.yaml") {
3089
+ return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
3090
+ }
3091
+ const art = node.artifacts.find((a) => a.filename === filename);
3092
+ return art?.content;
3093
+ }
3094
+ if (filePath.startsWith("aspects/")) {
3095
+ const rest = filePath.slice("aspects/".length);
3096
+ const parts = rest.split("/");
3097
+ const aspectId = parts[0];
3098
+ const filename = parts.slice(1).join("/");
3099
+ const aspect = graph.aspects.find((a) => a.id === aspectId);
3100
+ if (!aspect) return void 0;
3101
+ if (filename === "yg-aspect.yaml") {
3102
+ return readYamlFromDisk(filePath);
3103
+ }
3104
+ const art = aspect.artifacts.find((a) => a.filename === filename);
3105
+ return art?.content;
3106
+ }
3107
+ if (filePath.startsWith("flows/")) {
3108
+ const rest = filePath.slice("flows/".length);
3109
+ const parts = rest.split("/");
3110
+ const flowPath = parts[0];
3111
+ const filename = parts.slice(1).join("/");
3112
+ const flow = graph.flows.find((f) => f.path === flowPath);
3113
+ if (!flow) return void 0;
3114
+ if (filename === "yg-flow.yaml") {
3115
+ return readYamlFromDisk(filePath);
3116
+ }
3117
+ const art = flow.artifacts.find((a) => a.filename === filename);
3118
+ return art?.content;
3119
+ }
3120
+ return void 0;
3121
+ }
2934
3122
 
2935
3123
  // src/cli/validate.ts
2936
3124
  import chalk from "chalk";
@@ -2981,12 +3169,12 @@ ${errors.length} errors, ${warnings.length} warnings.
2981
3169
  import chalk2 from "chalk";
2982
3170
 
2983
3171
  // src/io/drift-state-store.ts
2984
- import { readFile as readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
2985
- import path12 from "path";
3172
+ import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3173
+ import path13 from "path";
2986
3174
  import { parse as yamlParse } from "yaml";
2987
3175
  var DRIFT_STATE_DIR = ".drift-state";
2988
3176
  function nodeStatePath(yggRoot, nodePath) {
2989
- return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3177
+ return path13.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
2990
3178
  }
2991
3179
  async function scanJsonFiles(dir, baseDir) {
2992
3180
  const results = [];
@@ -2997,12 +3185,12 @@ async function scanJsonFiles(dir, baseDir) {
2997
3185
  return results;
2998
3186
  }
2999
3187
  for (const entry of entries) {
3000
- const fullPath = path12.join(dir, entry.name);
3188
+ const fullPath = path13.join(dir, entry.name);
3001
3189
  if (entry.isDirectory()) {
3002
3190
  const nested = await scanJsonFiles(fullPath, baseDir);
3003
3191
  results.push(...nested);
3004
3192
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
3005
- const relPath = path12.relative(baseDir, fullPath);
3193
+ const relPath = path13.relative(baseDir, fullPath);
3006
3194
  const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
3007
3195
  results.push(nodePath);
3008
3196
  }
@@ -3010,13 +3198,13 @@ async function scanJsonFiles(dir, baseDir) {
3010
3198
  return results;
3011
3199
  }
3012
3200
  async function removeEmptyParents(filePath, stopDir) {
3013
- let dir = path12.dirname(filePath);
3201
+ let dir = path13.dirname(filePath);
3014
3202
  while (dir !== stopDir && dir.startsWith(stopDir)) {
3015
3203
  try {
3016
3204
  const entries = await readdir6(dir);
3017
3205
  if (entries.length === 0) {
3018
3206
  await rm2(dir, { recursive: true });
3019
- dir = path12.dirname(dir);
3207
+ dir = path13.dirname(dir);
3020
3208
  } else {
3021
3209
  break;
3022
3210
  }
@@ -3028,7 +3216,7 @@ async function removeEmptyParents(filePath, stopDir) {
3028
3216
  async function readNodeDriftState(yggRoot, nodePath) {
3029
3217
  try {
3030
3218
  const filePath = nodeStatePath(yggRoot, nodePath);
3031
- const content = await readFile14(filePath, "utf-8");
3219
+ const content = await readFile15(filePath, "utf-8");
3032
3220
  const parsed = JSON.parse(content);
3033
3221
  return parsed;
3034
3222
  } catch {
@@ -3037,12 +3225,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
3037
3225
  }
3038
3226
  async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3039
3227
  const filePath = nodeStatePath(yggRoot, nodePath);
3040
- await mkdir3(path12.dirname(filePath), { recursive: true });
3228
+ await mkdir3(path13.dirname(filePath), { recursive: true });
3041
3229
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3042
3230
  await writeFile5(filePath, content, "utf-8");
3043
3231
  }
3044
3232
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3045
- const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
3233
+ const driftDir = path13.join(yggRoot, DRIFT_STATE_DIR);
3046
3234
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
3047
3235
  const removed = [];
3048
3236
  for (const nodePath of allNodePaths) {
@@ -3056,7 +3244,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
3056
3244
  return removed.sort();
3057
3245
  }
3058
3246
  async function readDriftState(yggRoot) {
3059
- const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
3247
+ const driftPath = path13.join(yggRoot, DRIFT_STATE_DIR);
3060
3248
  let driftStat;
3061
3249
  try {
3062
3250
  driftStat = await stat5(driftPath);
@@ -3064,7 +3252,7 @@ async function readDriftState(yggRoot) {
3064
3252
  return {};
3065
3253
  }
3066
3254
  if (driftStat.isFile()) {
3067
- const content = await readFile14(driftPath, "utf-8");
3255
+ const content = await readFile15(driftPath, "utf-8");
3068
3256
  let raw;
3069
3257
  try {
3070
3258
  raw = JSON.parse(content);
@@ -3096,20 +3284,20 @@ async function readDriftState(yggRoot) {
3096
3284
  }
3097
3285
 
3098
3286
  // src/utils/hash.ts
3099
- import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3100
- import path13 from "path";
3287
+ import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
3288
+ import path14 from "path";
3101
3289
  import { createHash } from "crypto";
3102
3290
  import { createRequire } from "module";
3103
3291
  var require2 = createRequire(import.meta.url);
3104
3292
  var ignoreFactory = require2("ignore");
3105
3293
  async function hashFile(filePath) {
3106
- const content = await readFile15(filePath);
3294
+ const content = await readFile16(filePath);
3107
3295
  return createHash("sha256").update(content).digest("hex");
3108
3296
  }
3109
3297
  async function loadRootGitignoreStack(projectRoot) {
3110
3298
  if (!projectRoot) return [];
3111
3299
  try {
3112
- const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
3300
+ const content = await readFile16(path14.join(projectRoot, ".gitignore"), "utf-8");
3113
3301
  const matcher = ignoreFactory();
3114
3302
  matcher.add(content);
3115
3303
  return [{ basePath: projectRoot, matcher }];
@@ -3119,7 +3307,7 @@ async function loadRootGitignoreStack(projectRoot) {
3119
3307
  }
3120
3308
  function isIgnoredByStack(candidatePath, stack) {
3121
3309
  for (const { basePath, matcher } of stack) {
3122
- const relativePath = path13.relative(basePath, candidatePath);
3310
+ const relativePath = path14.relative(basePath, candidatePath);
3123
3311
  if (relativePath === "" || relativePath.startsWith("..")) continue;
3124
3312
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
3125
3313
  }
@@ -3134,7 +3322,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3134
3322
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
3135
3323
  const allFiles = [];
3136
3324
  for (const tf of trackedFiles) {
3137
- const absPath = path13.join(projectRoot, tf.path);
3325
+ const absPath = path14.join(projectRoot, tf.path);
3138
3326
  try {
3139
3327
  const st = await stat6(absPath);
3140
3328
  if (st.isDirectory()) {
@@ -3144,7 +3332,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3144
3332
  });
3145
3333
  for (const entry of dirEntries) {
3146
3334
  allFiles.push({
3147
- relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3335
+ relPath: path14.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3148
3336
  absPath: entry.absPath,
3149
3337
  mtimeMs: entry.mtimeMs
3150
3338
  });
@@ -3184,7 +3372,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3184
3372
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3185
3373
  let stack = options.gitignoreStack ?? [];
3186
3374
  try {
3187
- const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
3375
+ const localContent = await readFile16(path14.join(directoryPath, ".gitignore"), "utf-8");
3188
3376
  const localMatcher = ignoreFactory();
3189
3377
  localMatcher.add(localContent);
3190
3378
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -3194,7 +3382,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3194
3382
  const dirs = [];
3195
3383
  const files = [];
3196
3384
  for (const entry of entries) {
3197
- const absoluteChildPath = path13.join(directoryPath, entry.name);
3385
+ const absoluteChildPath = path14.join(directoryPath, entry.name);
3198
3386
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
3199
3387
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
3200
3388
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -3207,7 +3395,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3207
3395
  Promise.all(files.map(async (f) => {
3208
3396
  const fileStat = await stat6(f);
3209
3397
  return {
3210
- relPath: path13.relative(rootDirectoryPath, f),
3398
+ relPath: path14.relative(rootDirectoryPath, f),
3211
3399
  absPath: f,
3212
3400
  mtimeMs: fileStat.mtimeMs
3213
3401
  };
@@ -3220,14 +3408,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3220
3408
  }
3221
3409
 
3222
3410
  // src/core/context-files.ts
3223
- import path14 from "path";
3411
+ import path15 from "path";
3224
3412
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3225
3413
  function collectTrackedFiles(node, graph) {
3226
3414
  const seen = /* @__PURE__ */ new Set();
3227
3415
  const result = [];
3228
- const projectRoot = path14.dirname(graph.rootPath);
3229
- const yggPrefix = path14.relative(projectRoot, graph.rootPath);
3230
- const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
3416
+ const projectRoot = path15.dirname(graph.rootPath);
3417
+ const yggPrefix = path15.relative(projectRoot, graph.rootPath);
3418
+ const yggPrefixNormalized = yggPrefix.split(path15.sep).join("/");
3231
3419
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
3232
3420
  function addFile(filePath, category) {
3233
3421
  if (seen.has(filePath)) return;
@@ -3291,6 +3479,35 @@ function collectTrackedFiles(node, graph) {
3291
3479
  }
3292
3480
  }
3293
3481
  }
3482
+ const depAncestors = collectAncestors(target);
3483
+ for (const ancestor of depAncestors) {
3484
+ const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : [...configArtifactKeys];
3485
+ for (const filename of filterFilenames) {
3486
+ if (ancestor.artifacts.some((a) => a.filename === filename)) {
3487
+ addFile(graphPath("model", ancestor.path, filename), "graph");
3488
+ }
3489
+ }
3490
+ }
3491
+ }
3492
+ for (const relation of node.meta.relations ?? []) {
3493
+ if (relation.type !== "emits" && relation.type !== "listens") continue;
3494
+ const target = graph.nodes.get(relation.target);
3495
+ if (!target) continue;
3496
+ const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
3497
+ const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : [...configArtifactKeys];
3498
+ for (const filename of filterFilenames) {
3499
+ if (target.artifacts.some((a) => a.filename === filename)) {
3500
+ addFile(graphPath("model", target.path, filename), "graph");
3501
+ }
3502
+ }
3503
+ const eventAncestors = collectAncestors(target);
3504
+ for (const ancestor of eventAncestors) {
3505
+ for (const filename of filterFilenames) {
3506
+ if (ancestor.artifacts.some((a) => a.filename === filename)) {
3507
+ addFile(graphPath("model", ancestor.path, filename), "graph");
3508
+ }
3509
+ }
3510
+ }
3294
3511
  }
3295
3512
  for (const flow of participatingFlows) {
3296
3513
  addFile(graphPath("flows", flow.path, "yg-flow.yaml"), "graph");
@@ -3311,7 +3528,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
3311
3528
 
3312
3529
  // src/core/drift-detector.ts
3313
3530
  import { access as access2 } from "fs/promises";
3314
- import path15 from "path";
3531
+ import path16 from "path";
3315
3532
  function getChildMappingExclusions(graph, nodePath) {
3316
3533
  const node = graph.nodes.get(nodePath);
3317
3534
  if (!node) return [];
@@ -3333,7 +3550,7 @@ function getChildMappingExclusions(graph, nodePath) {
3333
3550
  return exclusions;
3334
3551
  }
3335
3552
  async function detectDrift(graph, filterNodePath) {
3336
- const projectRoot = path15.dirname(graph.rootPath);
3553
+ const projectRoot = path16.dirname(graph.rootPath);
3337
3554
  const driftState = await readDriftState(graph.rootPath);
3338
3555
  const entries = [];
3339
3556
  for (const [nodePath, node] of graph.nodes) {
@@ -3415,14 +3632,14 @@ async function detectDrift(graph, filterNodePath) {
3415
3632
  };
3416
3633
  }
3417
3634
  function categorizeFile(filePath, _rootPath, projectRoot) {
3418
- const yggPrefix = path15.relative(projectRoot, _rootPath);
3419
- const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3635
+ const yggPrefix = path16.relative(projectRoot, _rootPath);
3636
+ const normalizedPrefix = yggPrefix.split(path16.sep).join("/");
3420
3637
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3421
3638
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3422
3639
  }
3423
3640
  async function allPathsMissing(projectRoot, mappingPaths) {
3424
3641
  for (const mp of mappingPaths) {
3425
- const absPath = path15.join(projectRoot, mp);
3642
+ const absPath = path16.join(projectRoot, mp);
3426
3643
  try {
3427
3644
  await access2(absPath);
3428
3645
  return false;
@@ -3432,7 +3649,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3432
3649
  return true;
3433
3650
  }
3434
3651
  async function syncDriftState(graph, nodePath) {
3435
- const projectRoot = path15.dirname(graph.rootPath);
3652
+ const projectRoot = path16.dirname(graph.rootPath);
3436
3653
  const node = graph.nodes.get(nodePath);
3437
3654
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3438
3655
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
@@ -3752,10 +3969,10 @@ function registerTreeCommand(program2) {
3752
3969
  let roots;
3753
3970
  let showProjectName;
3754
3971
  if (options.root?.trim()) {
3755
- const path19 = options.root.trim().replace(/\/$/, "");
3756
- const node = graph.nodes.get(path19);
3972
+ const path20 = options.root.trim().replace(/\/$/, "");
3973
+ const node = graph.nodes.get(path20);
3757
3974
  if (!node) {
3758
- process.stderr.write(`Error: path '${path19}' not found
3975
+ process.stderr.write(`Error: path '${path20}' not found
3759
3976
  `);
3760
3977
  process.exit(1);
3761
3978
  }
@@ -3799,7 +4016,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
3799
4016
  }
3800
4017
 
3801
4018
  // src/cli/owner.ts
3802
- import path16 from "path";
4019
+ import path17 from "path";
3803
4020
  import { access as access3 } from "fs/promises";
3804
4021
  function normalizeForMatch(inputPath) {
3805
4022
  return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -3829,11 +4046,11 @@ function registerOwnerCommand(program2) {
3829
4046
  const graph = await loadGraph(cwd);
3830
4047
  const repoRoot = projectRootFromGraph(graph.rootPath);
3831
4048
  const rawPath = options.file.trim();
3832
- const absolute = path16.resolve(cwd, rawPath);
3833
- const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
4049
+ const absolute = path17.resolve(cwd, rawPath);
4050
+ const repoRelative = path17.relative(repoRoot, absolute).split(path17.sep).join("/");
3834
4051
  const result = findOwner(graph, repoRoot, repoRelative);
3835
4052
  if (!result.nodePath) {
3836
- const absPath = path16.resolve(repoRoot, result.file);
4053
+ const absPath = path17.resolve(repoRoot, result.file);
3837
4054
  let exists = true;
3838
4055
  try {
3839
4056
  await access3(absPath);
@@ -3867,7 +4084,7 @@ function registerOwnerCommand(program2) {
3867
4084
 
3868
4085
  // src/core/dependency-resolver.ts
3869
4086
  import { execSync } from "child_process";
3870
- import path17 from "path";
4087
+ import path18 from "path";
3871
4088
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3872
4089
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
3873
4090
  function filterRelationType(relType, filter) {
@@ -3944,7 +4161,7 @@ function registerDepsCommand(program2) {
3944
4161
  // src/core/graph-from-git.ts
3945
4162
  import { mkdtemp, rm as rm3 } from "fs/promises";
3946
4163
  import { tmpdir } from "os";
3947
- import path18 from "path";
4164
+ import path19 from "path";
3948
4165
  import { execSync as execSync2 } from "child_process";
3949
4166
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3950
4167
  const yggPath = ".yggdrasil";
@@ -3955,8 +4172,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3955
4172
  return null;
3956
4173
  }
3957
4174
  try {
3958
- tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
3959
- const archivePath = path18.join(tmpDir, "archive.tar");
4175
+ tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
4176
+ const archivePath = path19.join(tmpDir, "archive.tar");
3960
4177
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
3961
4178
  cwd: projectRoot,
3962
4179
  stdio: "pipe"
@@ -4026,14 +4243,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
4026
4243
  }
4027
4244
  const chains = [];
4028
4245
  for (const node of transitiveOnly) {
4029
- const path19 = [];
4246
+ const path20 = [];
4030
4247
  let current = node;
4031
4248
  while (current) {
4032
- path19.unshift(current);
4249
+ path20.unshift(current);
4033
4250
  current = parent.get(current);
4034
4251
  }
4035
- if (path19.length >= 3) {
4036
- chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
4252
+ if (path20.length >= 3) {
4253
+ chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
4037
4254
  }
4038
4255
  }
4039
4256
  return chains.sort();
@@ -4050,6 +4267,51 @@ function collectDescendants(graph, nodePath) {
4050
4267
  }
4051
4268
  return result.sort();
4052
4269
  }
4270
+ function collectIndirectDependents(graph, directlyAffected) {
4271
+ const directSet = new Set(directlyAffected);
4272
+ const reverse = /* @__PURE__ */ new Map();
4273
+ for (const [nodePath, node] of graph.nodes) {
4274
+ for (const rel of node.meta.relations ?? []) {
4275
+ if (!STRUCTURAL_TYPES.has(rel.type) && rel.type !== "emits" && rel.type !== "listens") continue;
4276
+ const deps = reverse.get(rel.target) ?? /* @__PURE__ */ new Set();
4277
+ deps.add(nodePath);
4278
+ reverse.set(rel.target, deps);
4279
+ }
4280
+ }
4281
+ const bestChain = /* @__PURE__ */ new Map();
4282
+ for (const affected of directlyAffected) {
4283
+ const parent = /* @__PURE__ */ new Map();
4284
+ const queue = [affected];
4285
+ const visited = /* @__PURE__ */ new Set([affected]);
4286
+ while (queue.length > 0) {
4287
+ const current = queue.shift();
4288
+ for (const next of reverse.get(current) ?? []) {
4289
+ if (visited.has(next)) continue;
4290
+ visited.add(next);
4291
+ parent.set(next, current);
4292
+ queue.push(next);
4293
+ }
4294
+ }
4295
+ for (const [node] of parent) {
4296
+ if (directSet.has(node)) continue;
4297
+ const path20 = [node];
4298
+ let current = node;
4299
+ while (parent.has(current)) {
4300
+ current = parent.get(current);
4301
+ path20.push(current);
4302
+ }
4303
+ const chain = path20.map((p) => `<- ${p}`).join(" ");
4304
+ const depth = path20.length;
4305
+ const existing = bestChain.get(node);
4306
+ if (!existing || depth < existing.depth) {
4307
+ bestChain.set(node, { chain, depth });
4308
+ }
4309
+ }
4310
+ }
4311
+ const indirectPaths = [...bestChain.keys()].sort();
4312
+ const chains = indirectPaths.map((p) => bestChain.get(p).chain);
4313
+ return { indirectPaths, chains };
4314
+ }
4053
4315
  async function runSimulation(graph, nodePaths, targetNodePath) {
4054
4316
  const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
4055
4317
  process.stdout.write("\nChanges in context packages:\n\n");
@@ -4129,19 +4391,32 @@ async function handleAspectImpact(graph, aspectId, simulate) {
4129
4391
  }
4130
4392
  }
4131
4393
  affected.sort((a, b) => a.path.localeCompare(b.path));
4394
+ const { indirectPaths, chains } = collectIndirectDependents(
4395
+ graph,
4396
+ affected.map((a) => a.path)
4397
+ );
4132
4398
  const propagatingFlows = graph.flows.filter((f) => (f.aspects ?? []).includes(aspectId)).map((f) => f.name);
4133
4399
  const impliedBy = graph.aspects.filter((a) => (a.implies ?? []).includes(aspectId)).map((a) => a.id);
4134
4400
  const implies = aspect.implies ?? [];
4135
4401
  process.stdout.write(`Impact of changes in aspect ${aspectId}:
4136
4402
 
4137
4403
  `);
4138
- process.stdout.write(`Affected nodes (${affected.length}):
4404
+ process.stdout.write(`Directly affected (${affected.length}):
4139
4405
  `);
4140
4406
  if (affected.length === 0) {
4141
4407
  process.stdout.write(" (none)\n");
4142
4408
  } else {
4143
4409
  for (const { path: p, source } of affected) {
4144
4410
  process.stdout.write(` ${p} (${source})
4411
+ `);
4412
+ }
4413
+ }
4414
+ if (chains.length > 0) {
4415
+ process.stdout.write(`
4416
+ Indirectly affected (structural dependents):
4417
+ `);
4418
+ for (let i = 0; i < indirectPaths.length; i++) {
4419
+ process.stdout.write(` ${indirectPaths[i]} ${chains[i]}
4145
4420
  `);
4146
4421
  }
4147
4422
  }
@@ -4155,12 +4430,13 @@ Flows propagating this aspect: ${propagatingFlows.length > 0 ? propagatingFlows.
4155
4430
  process.stdout.write(`Implies: ${implies.length > 0 ? implies.join(", ") : "(none)"}
4156
4431
  `);
4157
4432
  process.stdout.write(`
4158
- Total scope: ${affected.length} nodes, ${propagatingFlows.length} flows
4433
+ Total scope: ${affected.length + indirectPaths.length} nodes, ${propagatingFlows.length} flows
4159
4434
  `);
4160
- if (simulate && affected.length > 0) {
4435
+ const combinedPaths = [...affected.map((a) => a.path), ...indirectPaths];
4436
+ if (simulate && combinedPaths.length > 0) {
4161
4437
  await runSimulation(
4162
4438
  graph,
4163
- affected.map((a) => a.path),
4439
+ combinedPaths,
4164
4440
  null
4165
4441
  );
4166
4442
  }
@@ -4183,6 +4459,7 @@ async function handleFlowImpact(graph, flowName, simulate) {
4183
4459
  }
4184
4460
  const sorted = [...participants].sort();
4185
4461
  const flowAspects = flow.aspects ?? [];
4462
+ const { indirectPaths, chains } = collectIndirectDependents(graph, sorted);
4186
4463
  process.stdout.write(`Impact of changes in flow ${flow.name}:
4187
4464
 
4188
4465
  `);
@@ -4194,6 +4471,15 @@ async function handleFlowImpact(graph, flowName, simulate) {
4194
4471
  const isDeclared = flow.nodes.includes(p);
4195
4472
  const suffix = isDeclared ? "" : " (descendant)";
4196
4473
  process.stdout.write(` ${p}${suffix}
4474
+ `);
4475
+ }
4476
+ }
4477
+ if (chains.length > 0) {
4478
+ process.stdout.write(`
4479
+ Indirectly affected (structural dependents):
4480
+ `);
4481
+ for (let i = 0; i < indirectPaths.length; i++) {
4482
+ process.stdout.write(` ${indirectPaths[i]} ${chains[i]}
4197
4483
  `);
4198
4484
  }
4199
4485
  }
@@ -4203,10 +4489,11 @@ Flow aspects: ${flowAspects.length > 0 ? flowAspects.join(", ") : "(none)"}
4203
4489
  `
4204
4490
  );
4205
4491
  process.stdout.write(`
4206
- Total scope: ${sorted.length} nodes
4492
+ Total scope: ${sorted.length + indirectPaths.length} nodes
4207
4493
  `);
4208
- if (simulate && sorted.length > 0) {
4209
- await runSimulation(graph, sorted, null);
4494
+ const combinedPaths = [...sorted, ...indirectPaths];
4495
+ if (simulate && combinedPaths.length > 0) {
4496
+ await runSimulation(graph, combinedPaths, null);
4210
4497
  }
4211
4498
  }
4212
4499
  function registerImpactCommand(program2) {
@@ -4343,6 +4630,27 @@ function registerImpactCommand(program2) {
4343
4630
  `);
4344
4631
  }
4345
4632
  }
4633
+ const alreadyShown = /* @__PURE__ */ new Set([nodePath, ...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path)]);
4634
+ let descIndirectPaths = [];
4635
+ if (descendants.length > 0) {
4636
+ const { indirectPaths: rawIndirect, chains: rawChains } = collectIndirectDependents(graph, descendants);
4637
+ const filteredIndirect = [];
4638
+ const filteredChains = [];
4639
+ for (let i = 0; i < rawIndirect.length; i++) {
4640
+ if (!alreadyShown.has(rawIndirect[i])) {
4641
+ filteredIndirect.push(rawIndirect[i]);
4642
+ filteredChains.push(rawChains[i]);
4643
+ }
4644
+ }
4645
+ descIndirectPaths = filteredIndirect;
4646
+ if (filteredIndirect.length > 0) {
4647
+ process.stdout.write("\nIndirectly affected (structural dependents of descendants):\n");
4648
+ for (let i = 0; i < filteredIndirect.length; i++) {
4649
+ process.stdout.write(` ${filteredIndirect[i]} ${filteredChains[i]}
4650
+ `);
4651
+ }
4652
+ }
4653
+ }
4346
4654
  process.stdout.write(
4347
4655
  `
4348
4656
  Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
@@ -4372,7 +4680,7 @@ Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
4372
4680
  `);
4373
4681
  }
4374
4682
  }
4375
- const allAffected = /* @__PURE__ */ new Set([...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path)]);
4683
+ const allAffected = /* @__PURE__ */ new Set([...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path), ...descIndirectPaths]);
4376
4684
  process.stdout.write(
4377
4685
  `
4378
4686
  Total scope: ${allAffected.size} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects