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