@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/README.md +1 -1
- package/dist/bin.js +461 -155
- package/dist/bin.js.map +1 -1
- package/dist/templates/rules.ts +26 -13
- package/package.json +1 -1
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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. **
|
|
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
|
|
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
|
|
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>.
|
|
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
|
|
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
|
-
|
|
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
|
|
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, """);
|
|
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" &&
|
|
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
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
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
|
|
2985
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
3100
|
-
import
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
3229
|
-
const yggPrefix =
|
|
3230
|
-
const yggPrefixNormalized = yggPrefix.split(
|
|
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
|
|
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 =
|
|
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 =
|
|
3419
|
-
const normalizedPrefix = yggPrefix.split(
|
|
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 =
|
|
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 =
|
|
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
|
|
3756
|
-
const node = graph.nodes.get(
|
|
3970
|
+
const path20 = options.root.trim().replace(/\/$/, "");
|
|
3971
|
+
const node = graph.nodes.get(path20);
|
|
3757
3972
|
if (!node) {
|
|
3758
|
-
process.stderr.write(`Error: path '${
|
|
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
|
|
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 =
|
|
3833
|
-
const repoRelative =
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
3959
|
-
const archivePath =
|
|
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
|
|
4244
|
+
const path20 = [];
|
|
4030
4245
|
let current = node;
|
|
4031
4246
|
while (current) {
|
|
4032
|
-
|
|
4247
|
+
path20.unshift(current);
|
|
4033
4248
|
current = parent.get(current);
|
|
4034
4249
|
}
|
|
4035
|
-
if (
|
|
4036
|
-
chains.push(
|
|
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(`
|
|
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
|
-
|
|
4433
|
+
const combinedPaths = [...affected.map((a) => a.path), ...indirectPaths];
|
|
4434
|
+
if (simulate && combinedPaths.length > 0) {
|
|
4161
4435
|
await runSimulation(
|
|
4162
4436
|
graph,
|
|
4163
|
-
|
|
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
|
-
|
|
4209
|
-
|
|
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
|