@chrisdudek/yg 2.3.3 → 2.4.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
@@ -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,7 +153,7 @@ 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.
@@ -188,9 +187,13 @@ Include this as the FIRST instruction in every subagent prompt:
188
187
 
189
188
  \`\`\`
190
189
  BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
190
+ DELIVERABLES \u2014 all required, incomplete work will be rejected:
191
+ 1. Working source code
192
+ 2. Graph nodes with artifacts for every new/modified source file
193
+ 3. \`yg validate\` passing
191
194
  \`\`\`
192
195
 
193
- A subagent that skips this step will read code without graph context, miss architectural constraints, and produce changes that break graph-code consistency.`;
196
+ 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
197
  var OPERATIONS = `## OPERATIONS
195
198
 
196
199
  ### Conversation Lifecycle
@@ -204,7 +207,10 @@ PREFLIGHT (every conversation, before any work):
204
207
 
205
208
  UNDERSTANDING mapped code (questions, research, OR planning):
206
209
  - [ ] 1. yg owner --file <path>
207
- - [ ] 2. Owner found \u2192 yg build-context --node <path>. Use context package as primary source.
210
+ - [ ] 2. Owner found \u2192 yg build-context --node <path>. Read the YAML map
211
+ for topology, then read artifact files from the artifacts section.
212
+ For quick orientation, the map alone is sufficient. For implementation,
213
+ read all artifact files before changing code.
208
214
  - [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
209
215
  Never use grep or raw file reads as primary understanding when graph coverage exists.
210
216
  Raw reads supplement the context package \u2014 they do not replace it.
@@ -216,7 +222,7 @@ WRAP-UP (user signals "done", "wrap up", "that's enough"):
216
222
 
217
223
  BEFORE ENDING ANY RESPONSE (self-audit):
218
224
  - [ ] 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?
225
+ - [ ] Did I modify source code? If yes \u2192 did I update graph artifacts before moving to the next file?
220
226
  - [ ] If you broke either rule, you have broken the protocol. Do not finish until both are fixed.
221
227
  \`\`\`
222
228
 
@@ -231,7 +237,7 @@ You are not allowed to edit or create source code without establishing graph cov
231
237
  - [ ] 1. Read specification: \`yg build-context --node <node_path>\`
232
238
  - [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
233
239
  - [ ] 3. Modify source code
234
- - [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes
240
+ - [ ] 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
241
  - [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
236
242
  - [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
237
243
 
@@ -380,7 +386,13 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
380
386
 
381
387
  ### Context Assembly
382
388
 
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.
389
+ **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>\`.
390
+
391
+ **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.
392
+
393
+ **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.
394
+
395
+ 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
396
 
385
397
  ### Information Routing
386
398
 
@@ -450,7 +462,8 @@ Test: "Does this describe what happens in the world, or only in the software?" I
450
462
  \`\`\`
451
463
  yg preflight [--quick] Unified diagnostic: drift + status + validate.
452
464
  yg owner --file <path> Find the node that owns this file.
453
- yg build-context --node <path> Assemble context package for this node.
465
+ yg build-context --node <path> Assemble context map with artifact paths (default).
466
+ yg build-context --node <path> --full Same map + file contents appended below separator.
454
467
  yg tree [--root <path>] [--depth N] Print graph structure.
455
468
  yg aspects List aspects with metadata (YAML output).
456
469
  yg flows List flows with metadata (YAML output).
@@ -1156,6 +1169,10 @@ function registerInitCommand(program2) {
1156
1169
  });
1157
1170
  }
1158
1171
 
1172
+ // src/cli/build-context.ts
1173
+ import { readFile as readFile14 } from "fs/promises";
1174
+ import path12 from "path";
1175
+
1159
1176
  // src/core/graph-loader.ts
1160
1177
  import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
1161
1178
  import path9 from "path";
@@ -2026,6 +2043,150 @@ function collectAncestors(node) {
2026
2043
  }
2027
2044
  return ancestors;
2028
2045
  }
2046
+ function toContextMapOutput(pkg2, graph) {
2047
+ const node = graph.nodes.get(pkg2.nodePath);
2048
+ const config = graph.config;
2049
+ const nodeAspects = (node.meta.aspects ?? []).map((entry) => {
2050
+ const ref = { id: entry.aspect };
2051
+ if (entry.anchors?.length) ref.anchors = entry.anchors;
2052
+ if (entry.exceptions?.length) ref.exceptions = entry.exceptions;
2053
+ return ref;
2054
+ });
2055
+ const participatingFlows = collectParticipatingFlows(graph, node);
2056
+ const flowRefs = participatingFlows.map((f) => {
2057
+ const ref = { path: f.path, name: f.name };
2058
+ if (f.aspects?.length) ref.aspects = f.aspects;
2059
+ return ref;
2060
+ });
2061
+ const ancestors = collectAncestors(node);
2062
+ const hierarchyRefs = ancestors.map((a) => {
2063
+ const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
2064
+ const expanded = expandAspects(nodeAspectIds, graph.aspects);
2065
+ return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2066
+ });
2067
+ const ancestorPaths = new Set(ancestors.map((a) => a.path));
2068
+ const depRefs = [];
2069
+ for (const relation of node.meta.relations ?? []) {
2070
+ const target = graph.nodes.get(relation.target);
2071
+ if (!target) continue;
2072
+ if (ancestorPaths.has(relation.target)) continue;
2073
+ const depAncestors = collectAncestors(target);
2074
+ const depHierarchy = depAncestors.map((a) => {
2075
+ const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
2076
+ const expanded = expandAspects(ids, graph.aspects);
2077
+ return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
2078
+ });
2079
+ const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
2080
+ const ref = {
2081
+ path: target.path,
2082
+ name: target.meta.name,
2083
+ type: target.meta.type,
2084
+ relation: relation.type,
2085
+ aspects: depEffectiveAspects,
2086
+ hierarchy: depHierarchy
2087
+ };
2088
+ if (relation.consumes?.length) ref.consumes = relation.consumes;
2089
+ if (relation.failure) ref.failure = relation.failure;
2090
+ if (relation.event_name) ref["event-name"] = relation.event_name;
2091
+ depRefs.push(ref);
2092
+ }
2093
+ const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2094
+ const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2095
+ const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
2096
+ const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
2097
+ return {
2098
+ meta: { tokenCount: pkg2.tokenCount, budgetStatus },
2099
+ project: config.name,
2100
+ node: {
2101
+ path: pkg2.nodePath,
2102
+ name: pkg2.nodeName,
2103
+ type: node.meta.type,
2104
+ mappings: normalizeMappingPaths(node.meta.mapping),
2105
+ aspects: nodeAspects,
2106
+ flows: flowRefs
2107
+ },
2108
+ hierarchy: hierarchyRefs,
2109
+ dependencies: depRefs,
2110
+ artifacts: registry
2111
+ };
2112
+ }
2113
+ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
2114
+ const config = graph.config;
2115
+ const configArtifactKeys = new Set(Object.keys(config.artifacts ?? {}));
2116
+ const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2117
+ const nodes = {};
2118
+ const aspects = {};
2119
+ const flows = {};
2120
+ function addNodeEntry(n, includeYgNodeYaml, filter) {
2121
+ if (nodes[n.path]) return;
2122
+ const files = [];
2123
+ if (includeYgNodeYaml) {
2124
+ files.push(`model/${n.path}/yg-node.yaml`);
2125
+ }
2126
+ for (const filename of filter) {
2127
+ if (n.artifacts.some((a) => a.filename === filename)) {
2128
+ files.push(`model/${n.path}/${filename}`);
2129
+ }
2130
+ }
2131
+ if (files.length > 0) {
2132
+ nodes[n.path] = { files };
2133
+ }
2134
+ }
2135
+ addNodeEntry(node, true, [...configArtifactKeys]);
2136
+ for (const ancestor of ancestors) {
2137
+ addNodeEntry(ancestor, true, [...configArtifactKeys]);
2138
+ }
2139
+ const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
2140
+ for (const dep of dependencies) {
2141
+ const target = graph.nodes.get(dep.path);
2142
+ if (target) {
2143
+ addNodeEntry(target, false, structuralFilenames);
2144
+ }
2145
+ for (const ancestor of dep.hierarchy) {
2146
+ if (seenDepAncestors.has(ancestor.path)) continue;
2147
+ seenDepAncestors.add(ancestor.path);
2148
+ const ancestorNode = graph.nodes.get(ancestor.path);
2149
+ if (ancestorNode) {
2150
+ addNodeEntry(ancestorNode, false, structuralFilenames);
2151
+ }
2152
+ }
2153
+ }
2154
+ const allAspectIds = collectEffectiveAspectIds(graph, node.path);
2155
+ for (const dep of dependencies) {
2156
+ for (const id of dep.aspects) {
2157
+ allAspectIds.add(id);
2158
+ }
2159
+ }
2160
+ const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
2161
+ for (const aspect of resolvedAspects) {
2162
+ const files = [];
2163
+ files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
2164
+ for (const art of aspect.artifacts) {
2165
+ files.push(`aspects/${aspect.id}/${art.filename}`);
2166
+ }
2167
+ const entry = {
2168
+ name: aspect.name,
2169
+ files
2170
+ };
2171
+ if (aspect.implies?.length) entry.implies = aspect.implies;
2172
+ aspects[aspect.id] = entry;
2173
+ }
2174
+ const participatingFlows = collectParticipatingFlows(graph, node);
2175
+ for (const flow of participatingFlows) {
2176
+ const files = [];
2177
+ files.push(`flows/${flow.path}/yg-flow.yaml`);
2178
+ for (const art of flow.artifacts) {
2179
+ files.push(`flows/${flow.path}/${art.filename}`);
2180
+ }
2181
+ const entry = {
2182
+ name: flow.name,
2183
+ files
2184
+ };
2185
+ if (flow.aspects?.length) entry.aspects = flow.aspects;
2186
+ flows[flow.path] = entry;
2187
+ }
2188
+ return { nodes, aspects, flows };
2189
+ }
2029
2190
  function collectEffectiveAspectIds(graph, nodePath) {
2030
2191
  const node = graph.nodes.get(nodePath);
2031
2192
  if (!node) return /* @__PURE__ */ new Set();
@@ -2044,6 +2205,38 @@ function collectEffectiveAspectIds(graph, nodePath) {
2044
2205
  return new Set(expandAspects([...raw], graph.aspects));
2045
2206
  }
2046
2207
 
2208
+ // src/formatters/context-text.ts
2209
+ import { stringify } from "yaml";
2210
+ function formatContextYaml(data) {
2211
+ const output = {
2212
+ meta: {
2213
+ "token-count": data.meta.tokenCount,
2214
+ "budget-status": data.meta.budgetStatus
2215
+ },
2216
+ project: data.project,
2217
+ node: data.node,
2218
+ hierarchy: data.hierarchy.length > 0 ? data.hierarchy : void 0,
2219
+ dependencies: data.dependencies.length > 0 ? data.dependencies : void 0,
2220
+ artifacts: data.artifacts
2221
+ };
2222
+ for (const key of Object.keys(output)) {
2223
+ if (output[key] === void 0) delete output[key];
2224
+ }
2225
+ return stringify(output, { lineWidth: 0 });
2226
+ }
2227
+ function formatFullContent(files) {
2228
+ if (files.length === 0) return "";
2229
+ let out = "---\n\n";
2230
+ for (const file of files) {
2231
+ out += `<${file.path}>
2232
+ ${file.content}
2233
+ </${file.path}>
2234
+
2235
+ `;
2236
+ }
2237
+ return out;
2238
+ }
2239
+
2047
2240
  // src/core/validator.ts
2048
2241
  import { readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
2049
2242
  import path11 from "path";
@@ -2796,78 +2989,6 @@ async function checkContextBudget(graph) {
2796
2989
  return issues;
2797
2990
  }
2798
2991
 
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
2992
  // src/cli/build-context.ts
2872
2993
  function collectRelevantNodePaths(graph, nodePath) {
2873
2994
  const relevant = /* @__PURE__ */ new Set();
@@ -2879,19 +3000,24 @@ function collectRelevantNodePaths(graph, nodePath) {
2879
3000
  }
2880
3001
  for (const rel of node.meta.relations ?? []) {
2881
3002
  relevant.add(rel.target);
3003
+ const target = graph.nodes.get(rel.target);
3004
+ if (target) {
3005
+ for (const ancestor of collectAncestors(target)) {
3006
+ relevant.add(ancestor.path);
3007
+ }
3008
+ }
2882
3009
  }
2883
3010
  return relevant;
2884
3011
  }
2885
3012
  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) => {
3013
+ 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
3014
  try {
2888
3015
  const graph = await loadGraph(process.cwd());
2889
3016
  const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/$/, "");
2890
3017
  const relevantNodes = collectRelevantNodePaths(graph, nodePath);
2891
3018
  const validationResult = await validate(graph, "all");
2892
3019
  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))
3020
+ (issue) => issue.severity === "error" && (!issue.nodePath || relevantNodes.has(issue.nodePath))
2895
3021
  );
2896
3022
  if (relevantErrors.length > 0) {
2897
3023
  const totalErrors = validationResult.issues.filter((i) => i.severity === "error").length;
@@ -2911,19 +3037,29 @@ function registerBuildCommand(program2) {
2911
3037
  process.exit(1);
2912
3038
  }
2913
3039
  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
- );
3040
+ const mapOutput = toContextMapOutput(pkg2, graph);
3041
+ let output = formatContextYaml(mapOutput);
3042
+ if (options.full) {
3043
+ const allFiles = [];
3044
+ const allEntries = [
3045
+ ...Object.values(mapOutput.artifacts.nodes),
3046
+ ...Object.values(mapOutput.artifacts.aspects),
3047
+ ...Object.values(mapOutput.artifacts.flows)
3048
+ ];
3049
+ const seen = /* @__PURE__ */ new Set();
3050
+ for (const entry of allEntries) {
3051
+ for (const filePath of entry.files) {
3052
+ if (seen.has(filePath)) continue;
3053
+ seen.add(filePath);
3054
+ const content = await findFileContent(filePath, graph);
3055
+ if (content !== void 0) {
3056
+ allFiles.push({ path: filePath, content });
3057
+ }
3058
+ }
3059
+ }
3060
+ output += formatFullContent(allFiles);
2926
3061
  }
3062
+ process.stdout.write(output);
2927
3063
  } catch (error) {
2928
3064
  process.stderr.write(`Error: ${error.message}
2929
3065
  `);
@@ -2931,6 +3067,56 @@ function registerBuildCommand(program2) {
2931
3067
  }
2932
3068
  });
2933
3069
  }
3070
+ async function findFileContent(filePath, graph) {
3071
+ async function readYamlFromDisk(relativePath) {
3072
+ try {
3073
+ const fullPath = path12.join(graph.rootPath, relativePath);
3074
+ return (await readFile14(fullPath, "utf-8")).trim();
3075
+ } catch {
3076
+ return void 0;
3077
+ }
3078
+ }
3079
+ if (filePath.startsWith("model/")) {
3080
+ const rest = filePath.slice("model/".length);
3081
+ const parts = rest.split("/");
3082
+ const filename = parts.pop();
3083
+ const nodePath = parts.join("/");
3084
+ const node = graph.nodes.get(nodePath);
3085
+ if (!node) return void 0;
3086
+ if (filename === "yg-node.yaml") {
3087
+ return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
3088
+ }
3089
+ const art = node.artifacts.find((a) => a.filename === filename);
3090
+ return art?.content;
3091
+ }
3092
+ if (filePath.startsWith("aspects/")) {
3093
+ const rest = filePath.slice("aspects/".length);
3094
+ const parts = rest.split("/");
3095
+ const aspectId = parts[0];
3096
+ const filename = parts.slice(1).join("/");
3097
+ const aspect = graph.aspects.find((a) => a.id === aspectId);
3098
+ if (!aspect) return void 0;
3099
+ if (filename === "yg-aspect.yaml") {
3100
+ return readYamlFromDisk(filePath);
3101
+ }
3102
+ const art = aspect.artifacts.find((a) => a.filename === filename);
3103
+ return art?.content;
3104
+ }
3105
+ if (filePath.startsWith("flows/")) {
3106
+ const rest = filePath.slice("flows/".length);
3107
+ const parts = rest.split("/");
3108
+ const flowPath = parts[0];
3109
+ const filename = parts.slice(1).join("/");
3110
+ const flow = graph.flows.find((f) => f.path === flowPath);
3111
+ if (!flow) return void 0;
3112
+ if (filename === "yg-flow.yaml") {
3113
+ return readYamlFromDisk(filePath);
3114
+ }
3115
+ const art = flow.artifacts.find((a) => a.filename === filename);
3116
+ return art?.content;
3117
+ }
3118
+ return void 0;
3119
+ }
2934
3120
 
2935
3121
  // src/cli/validate.ts
2936
3122
  import chalk from "chalk";
@@ -2981,12 +3167,12 @@ ${errors.length} errors, ${warnings.length} warnings.
2981
3167
  import chalk2 from "chalk";
2982
3168
 
2983
3169
  // 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";
3170
+ import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
3171
+ import path13 from "path";
2986
3172
  import { parse as yamlParse } from "yaml";
2987
3173
  var DRIFT_STATE_DIR = ".drift-state";
2988
3174
  function nodeStatePath(yggRoot, nodePath) {
2989
- return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
3175
+ return path13.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
2990
3176
  }
2991
3177
  async function scanJsonFiles(dir, baseDir) {
2992
3178
  const results = [];
@@ -2997,12 +3183,12 @@ async function scanJsonFiles(dir, baseDir) {
2997
3183
  return results;
2998
3184
  }
2999
3185
  for (const entry of entries) {
3000
- const fullPath = path12.join(dir, entry.name);
3186
+ const fullPath = path13.join(dir, entry.name);
3001
3187
  if (entry.isDirectory()) {
3002
3188
  const nested = await scanJsonFiles(fullPath, baseDir);
3003
3189
  results.push(...nested);
3004
3190
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
3005
- const relPath = path12.relative(baseDir, fullPath);
3191
+ const relPath = path13.relative(baseDir, fullPath);
3006
3192
  const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
3007
3193
  results.push(nodePath);
3008
3194
  }
@@ -3010,13 +3196,13 @@ async function scanJsonFiles(dir, baseDir) {
3010
3196
  return results;
3011
3197
  }
3012
3198
  async function removeEmptyParents(filePath, stopDir) {
3013
- let dir = path12.dirname(filePath);
3199
+ let dir = path13.dirname(filePath);
3014
3200
  while (dir !== stopDir && dir.startsWith(stopDir)) {
3015
3201
  try {
3016
3202
  const entries = await readdir6(dir);
3017
3203
  if (entries.length === 0) {
3018
3204
  await rm2(dir, { recursive: true });
3019
- dir = path12.dirname(dir);
3205
+ dir = path13.dirname(dir);
3020
3206
  } else {
3021
3207
  break;
3022
3208
  }
@@ -3028,7 +3214,7 @@ async function removeEmptyParents(filePath, stopDir) {
3028
3214
  async function readNodeDriftState(yggRoot, nodePath) {
3029
3215
  try {
3030
3216
  const filePath = nodeStatePath(yggRoot, nodePath);
3031
- const content = await readFile14(filePath, "utf-8");
3217
+ const content = await readFile15(filePath, "utf-8");
3032
3218
  const parsed = JSON.parse(content);
3033
3219
  return parsed;
3034
3220
  } catch {
@@ -3037,12 +3223,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
3037
3223
  }
3038
3224
  async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3039
3225
  const filePath = nodeStatePath(yggRoot, nodePath);
3040
- await mkdir3(path12.dirname(filePath), { recursive: true });
3226
+ await mkdir3(path13.dirname(filePath), { recursive: true });
3041
3227
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3042
3228
  await writeFile5(filePath, content, "utf-8");
3043
3229
  }
3044
3230
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3045
- const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
3231
+ const driftDir = path13.join(yggRoot, DRIFT_STATE_DIR);
3046
3232
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
3047
3233
  const removed = [];
3048
3234
  for (const nodePath of allNodePaths) {
@@ -3056,7 +3242,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
3056
3242
  return removed.sort();
3057
3243
  }
3058
3244
  async function readDriftState(yggRoot) {
3059
- const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
3245
+ const driftPath = path13.join(yggRoot, DRIFT_STATE_DIR);
3060
3246
  let driftStat;
3061
3247
  try {
3062
3248
  driftStat = await stat5(driftPath);
@@ -3064,7 +3250,7 @@ async function readDriftState(yggRoot) {
3064
3250
  return {};
3065
3251
  }
3066
3252
  if (driftStat.isFile()) {
3067
- const content = await readFile14(driftPath, "utf-8");
3253
+ const content = await readFile15(driftPath, "utf-8");
3068
3254
  let raw;
3069
3255
  try {
3070
3256
  raw = JSON.parse(content);
@@ -3096,20 +3282,20 @@ async function readDriftState(yggRoot) {
3096
3282
  }
3097
3283
 
3098
3284
  // src/utils/hash.ts
3099
- import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
3100
- import path13 from "path";
3285
+ import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
3286
+ import path14 from "path";
3101
3287
  import { createHash } from "crypto";
3102
3288
  import { createRequire } from "module";
3103
3289
  var require2 = createRequire(import.meta.url);
3104
3290
  var ignoreFactory = require2("ignore");
3105
3291
  async function hashFile(filePath) {
3106
- const content = await readFile15(filePath);
3292
+ const content = await readFile16(filePath);
3107
3293
  return createHash("sha256").update(content).digest("hex");
3108
3294
  }
3109
3295
  async function loadRootGitignoreStack(projectRoot) {
3110
3296
  if (!projectRoot) return [];
3111
3297
  try {
3112
- const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
3298
+ const content = await readFile16(path14.join(projectRoot, ".gitignore"), "utf-8");
3113
3299
  const matcher = ignoreFactory();
3114
3300
  matcher.add(content);
3115
3301
  return [{ basePath: projectRoot, matcher }];
@@ -3119,7 +3305,7 @@ async function loadRootGitignoreStack(projectRoot) {
3119
3305
  }
3120
3306
  function isIgnoredByStack(candidatePath, stack) {
3121
3307
  for (const { basePath, matcher } of stack) {
3122
- const relativePath = path13.relative(basePath, candidatePath);
3308
+ const relativePath = path14.relative(basePath, candidatePath);
3123
3309
  if (relativePath === "" || relativePath.startsWith("..")) continue;
3124
3310
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
3125
3311
  }
@@ -3134,7 +3320,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3134
3320
  const gitignoreStack = await loadRootGitignoreStack(projectRoot);
3135
3321
  const allFiles = [];
3136
3322
  for (const tf of trackedFiles) {
3137
- const absPath = path13.join(projectRoot, tf.path);
3323
+ const absPath = path14.join(projectRoot, tf.path);
3138
3324
  try {
3139
3325
  const st = await stat6(absPath);
3140
3326
  if (st.isDirectory()) {
@@ -3144,7 +3330,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3144
3330
  });
3145
3331
  for (const entry of dirEntries) {
3146
3332
  allFiles.push({
3147
- relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3333
+ relPath: path14.join(tf.path, entry.relPath).replace(/\\/g, "/"),
3148
3334
  absPath: entry.absPath,
3149
3335
  mtimeMs: entry.mtimeMs
3150
3336
  });
@@ -3184,7 +3370,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
3184
3370
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3185
3371
  let stack = options.gitignoreStack ?? [];
3186
3372
  try {
3187
- const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
3373
+ const localContent = await readFile16(path14.join(directoryPath, ".gitignore"), "utf-8");
3188
3374
  const localMatcher = ignoreFactory();
3189
3375
  localMatcher.add(localContent);
3190
3376
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -3194,7 +3380,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3194
3380
  const dirs = [];
3195
3381
  const files = [];
3196
3382
  for (const entry of entries) {
3197
- const absoluteChildPath = path13.join(directoryPath, entry.name);
3383
+ const absoluteChildPath = path14.join(directoryPath, entry.name);
3198
3384
  if (isIgnoredByStack(absoluteChildPath, stack)) continue;
3199
3385
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
3200
3386
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -3207,7 +3393,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3207
3393
  Promise.all(files.map(async (f) => {
3208
3394
  const fileStat = await stat6(f);
3209
3395
  return {
3210
- relPath: path13.relative(rootDirectoryPath, f),
3396
+ relPath: path14.relative(rootDirectoryPath, f),
3211
3397
  absPath: f,
3212
3398
  mtimeMs: fileStat.mtimeMs
3213
3399
  };
@@ -3220,14 +3406,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
3220
3406
  }
3221
3407
 
3222
3408
  // src/core/context-files.ts
3223
- import path14 from "path";
3409
+ import path15 from "path";
3224
3410
  var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3225
3411
  function collectTrackedFiles(node, graph) {
3226
3412
  const seen = /* @__PURE__ */ new Set();
3227
3413
  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("/");
3414
+ const projectRoot = path15.dirname(graph.rootPath);
3415
+ const yggPrefix = path15.relative(projectRoot, graph.rootPath);
3416
+ const yggPrefixNormalized = yggPrefix.split(path15.sep).join("/");
3231
3417
  const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
3232
3418
  function addFile(filePath, category) {
3233
3419
  if (seen.has(filePath)) return;
@@ -3291,6 +3477,35 @@ function collectTrackedFiles(node, graph) {
3291
3477
  }
3292
3478
  }
3293
3479
  }
3480
+ const depAncestors = collectAncestors(target);
3481
+ for (const ancestor of depAncestors) {
3482
+ const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : [...configArtifactKeys];
3483
+ for (const filename of filterFilenames) {
3484
+ if (ancestor.artifacts.some((a) => a.filename === filename)) {
3485
+ addFile(graphPath("model", ancestor.path, filename), "graph");
3486
+ }
3487
+ }
3488
+ }
3489
+ }
3490
+ for (const relation of node.meta.relations ?? []) {
3491
+ if (relation.type !== "emits" && relation.type !== "listens") continue;
3492
+ const target = graph.nodes.get(relation.target);
3493
+ if (!target) continue;
3494
+ const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
3495
+ const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : [...configArtifactKeys];
3496
+ for (const filename of filterFilenames) {
3497
+ if (target.artifacts.some((a) => a.filename === filename)) {
3498
+ addFile(graphPath("model", target.path, filename), "graph");
3499
+ }
3500
+ }
3501
+ const eventAncestors = collectAncestors(target);
3502
+ for (const ancestor of eventAncestors) {
3503
+ for (const filename of filterFilenames) {
3504
+ if (ancestor.artifacts.some((a) => a.filename === filename)) {
3505
+ addFile(graphPath("model", ancestor.path, filename), "graph");
3506
+ }
3507
+ }
3508
+ }
3294
3509
  }
3295
3510
  for (const flow of participatingFlows) {
3296
3511
  addFile(graphPath("flows", flow.path, "yg-flow.yaml"), "graph");
@@ -3311,7 +3526,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
3311
3526
 
3312
3527
  // src/core/drift-detector.ts
3313
3528
  import { access as access2 } from "fs/promises";
3314
- import path15 from "path";
3529
+ import path16 from "path";
3315
3530
  function getChildMappingExclusions(graph, nodePath) {
3316
3531
  const node = graph.nodes.get(nodePath);
3317
3532
  if (!node) return [];
@@ -3333,7 +3548,7 @@ function getChildMappingExclusions(graph, nodePath) {
3333
3548
  return exclusions;
3334
3549
  }
3335
3550
  async function detectDrift(graph, filterNodePath) {
3336
- const projectRoot = path15.dirname(graph.rootPath);
3551
+ const projectRoot = path16.dirname(graph.rootPath);
3337
3552
  const driftState = await readDriftState(graph.rootPath);
3338
3553
  const entries = [];
3339
3554
  for (const [nodePath, node] of graph.nodes) {
@@ -3415,14 +3630,14 @@ async function detectDrift(graph, filterNodePath) {
3415
3630
  };
3416
3631
  }
3417
3632
  function categorizeFile(filePath, _rootPath, projectRoot) {
3418
- const yggPrefix = path15.relative(projectRoot, _rootPath);
3419
- const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
3633
+ const yggPrefix = path16.relative(projectRoot, _rootPath);
3634
+ const normalizedPrefix = yggPrefix.split(path16.sep).join("/");
3420
3635
  const normalizedFilePath = filePath.replace(/\\/g, "/");
3421
3636
  return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
3422
3637
  }
3423
3638
  async function allPathsMissing(projectRoot, mappingPaths) {
3424
3639
  for (const mp of mappingPaths) {
3425
- const absPath = path15.join(projectRoot, mp);
3640
+ const absPath = path16.join(projectRoot, mp);
3426
3641
  try {
3427
3642
  await access2(absPath);
3428
3643
  return false;
@@ -3432,7 +3647,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
3432
3647
  return true;
3433
3648
  }
3434
3649
  async function syncDriftState(graph, nodePath) {
3435
- const projectRoot = path15.dirname(graph.rootPath);
3650
+ const projectRoot = path16.dirname(graph.rootPath);
3436
3651
  const node = graph.nodes.get(nodePath);
3437
3652
  if (!node) throw new Error(`Node not found: ${nodePath}`);
3438
3653
  if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
@@ -3752,10 +3967,10 @@ function registerTreeCommand(program2) {
3752
3967
  let roots;
3753
3968
  let showProjectName;
3754
3969
  if (options.root?.trim()) {
3755
- const path19 = options.root.trim().replace(/\/$/, "");
3756
- const node = graph.nodes.get(path19);
3970
+ const path20 = options.root.trim().replace(/\/$/, "");
3971
+ const node = graph.nodes.get(path20);
3757
3972
  if (!node) {
3758
- process.stderr.write(`Error: path '${path19}' not found
3973
+ process.stderr.write(`Error: path '${path20}' not found
3759
3974
  `);
3760
3975
  process.exit(1);
3761
3976
  }
@@ -3799,7 +4014,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
3799
4014
  }
3800
4015
 
3801
4016
  // src/cli/owner.ts
3802
- import path16 from "path";
4017
+ import path17 from "path";
3803
4018
  import { access as access3 } from "fs/promises";
3804
4019
  function normalizeForMatch(inputPath) {
3805
4020
  return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -3829,11 +4044,11 @@ function registerOwnerCommand(program2) {
3829
4044
  const graph = await loadGraph(cwd);
3830
4045
  const repoRoot = projectRootFromGraph(graph.rootPath);
3831
4046
  const rawPath = options.file.trim();
3832
- const absolute = path16.resolve(cwd, rawPath);
3833
- const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
4047
+ const absolute = path17.resolve(cwd, rawPath);
4048
+ const repoRelative = path17.relative(repoRoot, absolute).split(path17.sep).join("/");
3834
4049
  const result = findOwner(graph, repoRoot, repoRelative);
3835
4050
  if (!result.nodePath) {
3836
- const absPath = path16.resolve(repoRoot, result.file);
4051
+ const absPath = path17.resolve(repoRoot, result.file);
3837
4052
  let exists = true;
3838
4053
  try {
3839
4054
  await access3(absPath);
@@ -3867,7 +4082,7 @@ function registerOwnerCommand(program2) {
3867
4082
 
3868
4083
  // src/core/dependency-resolver.ts
3869
4084
  import { execSync } from "child_process";
3870
- import path17 from "path";
4085
+ import path18 from "path";
3871
4086
  var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
3872
4087
  var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
3873
4088
  function filterRelationType(relType, filter) {
@@ -3944,7 +4159,7 @@ function registerDepsCommand(program2) {
3944
4159
  // src/core/graph-from-git.ts
3945
4160
  import { mkdtemp, rm as rm3 } from "fs/promises";
3946
4161
  import { tmpdir } from "os";
3947
- import path18 from "path";
4162
+ import path19 from "path";
3948
4163
  import { execSync as execSync2 } from "child_process";
3949
4164
  async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3950
4165
  const yggPath = ".yggdrasil";
@@ -3955,8 +4170,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
3955
4170
  return null;
3956
4171
  }
3957
4172
  try {
3958
- tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
3959
- const archivePath = path18.join(tmpDir, "archive.tar");
4173
+ tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
4174
+ const archivePath = path19.join(tmpDir, "archive.tar");
3960
4175
  execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
3961
4176
  cwd: projectRoot,
3962
4177
  stdio: "pipe"
@@ -4026,14 +4241,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
4026
4241
  }
4027
4242
  const chains = [];
4028
4243
  for (const node of transitiveOnly) {
4029
- const path19 = [];
4244
+ const path20 = [];
4030
4245
  let current = node;
4031
4246
  while (current) {
4032
- path19.unshift(current);
4247
+ path20.unshift(current);
4033
4248
  current = parent.get(current);
4034
4249
  }
4035
- if (path19.length >= 3) {
4036
- chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
4250
+ if (path20.length >= 3) {
4251
+ chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
4037
4252
  }
4038
4253
  }
4039
4254
  return chains.sort();
@@ -4050,6 +4265,51 @@ function collectDescendants(graph, nodePath) {
4050
4265
  }
4051
4266
  return result.sort();
4052
4267
  }
4268
+ function collectIndirectDependents(graph, directlyAffected) {
4269
+ const directSet = new Set(directlyAffected);
4270
+ const reverse = /* @__PURE__ */ new Map();
4271
+ for (const [nodePath, node] of graph.nodes) {
4272
+ for (const rel of node.meta.relations ?? []) {
4273
+ if (!STRUCTURAL_TYPES.has(rel.type) && rel.type !== "emits" && rel.type !== "listens") continue;
4274
+ const deps = reverse.get(rel.target) ?? /* @__PURE__ */ new Set();
4275
+ deps.add(nodePath);
4276
+ reverse.set(rel.target, deps);
4277
+ }
4278
+ }
4279
+ const bestChain = /* @__PURE__ */ new Map();
4280
+ for (const affected of directlyAffected) {
4281
+ const parent = /* @__PURE__ */ new Map();
4282
+ const queue = [affected];
4283
+ const visited = /* @__PURE__ */ new Set([affected]);
4284
+ while (queue.length > 0) {
4285
+ const current = queue.shift();
4286
+ for (const next of reverse.get(current) ?? []) {
4287
+ if (visited.has(next)) continue;
4288
+ visited.add(next);
4289
+ parent.set(next, current);
4290
+ queue.push(next);
4291
+ }
4292
+ }
4293
+ for (const [node] of parent) {
4294
+ if (directSet.has(node)) continue;
4295
+ const path20 = [node];
4296
+ let current = node;
4297
+ while (parent.has(current)) {
4298
+ current = parent.get(current);
4299
+ path20.push(current);
4300
+ }
4301
+ const chain = path20.map((p) => `<- ${p}`).join(" ");
4302
+ const depth = path20.length;
4303
+ const existing = bestChain.get(node);
4304
+ if (!existing || depth < existing.depth) {
4305
+ bestChain.set(node, { chain, depth });
4306
+ }
4307
+ }
4308
+ }
4309
+ const indirectPaths = [...bestChain.keys()].sort();
4310
+ const chains = indirectPaths.map((p) => bestChain.get(p).chain);
4311
+ return { indirectPaths, chains };
4312
+ }
4053
4313
  async function runSimulation(graph, nodePaths, targetNodePath) {
4054
4314
  const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
4055
4315
  process.stdout.write("\nChanges in context packages:\n\n");
@@ -4129,19 +4389,32 @@ async function handleAspectImpact(graph, aspectId, simulate) {
4129
4389
  }
4130
4390
  }
4131
4391
  affected.sort((a, b) => a.path.localeCompare(b.path));
4392
+ const { indirectPaths, chains } = collectIndirectDependents(
4393
+ graph,
4394
+ affected.map((a) => a.path)
4395
+ );
4132
4396
  const propagatingFlows = graph.flows.filter((f) => (f.aspects ?? []).includes(aspectId)).map((f) => f.name);
4133
4397
  const impliedBy = graph.aspects.filter((a) => (a.implies ?? []).includes(aspectId)).map((a) => a.id);
4134
4398
  const implies = aspect.implies ?? [];
4135
4399
  process.stdout.write(`Impact of changes in aspect ${aspectId}:
4136
4400
 
4137
4401
  `);
4138
- process.stdout.write(`Affected nodes (${affected.length}):
4402
+ process.stdout.write(`Directly affected (${affected.length}):
4139
4403
  `);
4140
4404
  if (affected.length === 0) {
4141
4405
  process.stdout.write(" (none)\n");
4142
4406
  } else {
4143
4407
  for (const { path: p, source } of affected) {
4144
4408
  process.stdout.write(` ${p} (${source})
4409
+ `);
4410
+ }
4411
+ }
4412
+ if (chains.length > 0) {
4413
+ process.stdout.write(`
4414
+ Indirectly affected (structural dependents):
4415
+ `);
4416
+ for (let i = 0; i < indirectPaths.length; i++) {
4417
+ process.stdout.write(` ${indirectPaths[i]} ${chains[i]}
4145
4418
  `);
4146
4419
  }
4147
4420
  }
@@ -4155,12 +4428,13 @@ Flows propagating this aspect: ${propagatingFlows.length > 0 ? propagatingFlows.
4155
4428
  process.stdout.write(`Implies: ${implies.length > 0 ? implies.join(", ") : "(none)"}
4156
4429
  `);
4157
4430
  process.stdout.write(`
4158
- Total scope: ${affected.length} nodes, ${propagatingFlows.length} flows
4431
+ Total scope: ${affected.length + indirectPaths.length} nodes, ${propagatingFlows.length} flows
4159
4432
  `);
4160
- if (simulate && affected.length > 0) {
4433
+ const combinedPaths = [...affected.map((a) => a.path), ...indirectPaths];
4434
+ if (simulate && combinedPaths.length > 0) {
4161
4435
  await runSimulation(
4162
4436
  graph,
4163
- affected.map((a) => a.path),
4437
+ combinedPaths,
4164
4438
  null
4165
4439
  );
4166
4440
  }
@@ -4183,6 +4457,7 @@ async function handleFlowImpact(graph, flowName, simulate) {
4183
4457
  }
4184
4458
  const sorted = [...participants].sort();
4185
4459
  const flowAspects = flow.aspects ?? [];
4460
+ const { indirectPaths, chains } = collectIndirectDependents(graph, sorted);
4186
4461
  process.stdout.write(`Impact of changes in flow ${flow.name}:
4187
4462
 
4188
4463
  `);
@@ -4194,6 +4469,15 @@ async function handleFlowImpact(graph, flowName, simulate) {
4194
4469
  const isDeclared = flow.nodes.includes(p);
4195
4470
  const suffix = isDeclared ? "" : " (descendant)";
4196
4471
  process.stdout.write(` ${p}${suffix}
4472
+ `);
4473
+ }
4474
+ }
4475
+ if (chains.length > 0) {
4476
+ process.stdout.write(`
4477
+ Indirectly affected (structural dependents):
4478
+ `);
4479
+ for (let i = 0; i < indirectPaths.length; i++) {
4480
+ process.stdout.write(` ${indirectPaths[i]} ${chains[i]}
4197
4481
  `);
4198
4482
  }
4199
4483
  }
@@ -4203,10 +4487,11 @@ Flow aspects: ${flowAspects.length > 0 ? flowAspects.join(", ") : "(none)"}
4203
4487
  `
4204
4488
  );
4205
4489
  process.stdout.write(`
4206
- Total scope: ${sorted.length} nodes
4490
+ Total scope: ${sorted.length + indirectPaths.length} nodes
4207
4491
  `);
4208
- if (simulate && sorted.length > 0) {
4209
- await runSimulation(graph, sorted, null);
4492
+ const combinedPaths = [...sorted, ...indirectPaths];
4493
+ if (simulate && combinedPaths.length > 0) {
4494
+ await runSimulation(graph, combinedPaths, null);
4210
4495
  }
4211
4496
  }
4212
4497
  function registerImpactCommand(program2) {
@@ -4343,6 +4628,27 @@ function registerImpactCommand(program2) {
4343
4628
  `);
4344
4629
  }
4345
4630
  }
4631
+ const alreadyShown = /* @__PURE__ */ new Set([nodePath, ...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path)]);
4632
+ let descIndirectPaths = [];
4633
+ if (descendants.length > 0) {
4634
+ const { indirectPaths: rawIndirect, chains: rawChains } = collectIndirectDependents(graph, descendants);
4635
+ const filteredIndirect = [];
4636
+ const filteredChains = [];
4637
+ for (let i = 0; i < rawIndirect.length; i++) {
4638
+ if (!alreadyShown.has(rawIndirect[i])) {
4639
+ filteredIndirect.push(rawIndirect[i]);
4640
+ filteredChains.push(rawChains[i]);
4641
+ }
4642
+ }
4643
+ descIndirectPaths = filteredIndirect;
4644
+ if (filteredIndirect.length > 0) {
4645
+ process.stdout.write("\nIndirectly affected (structural dependents of descendants):\n");
4646
+ for (let i = 0; i < filteredIndirect.length; i++) {
4647
+ process.stdout.write(` ${filteredIndirect[i]} ${filteredChains[i]}
4648
+ `);
4649
+ }
4650
+ }
4651
+ }
4346
4652
  process.stdout.write(
4347
4653
  `
4348
4654
  Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
@@ -4372,7 +4678,7 @@ Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
4372
4678
  `);
4373
4679
  }
4374
4680
  }
4375
- const allAffected = /* @__PURE__ */ new Set([...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path)]);
4681
+ const allAffected = /* @__PURE__ */ new Set([...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path), ...descIndirectPaths]);
4376
4682
  process.stdout.write(
4377
4683
  `
4378
4684
  Total scope: ${allAffected.size} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects