@chrisdudek/yg 2.5.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +202 -168
- package/dist/bin.js.map +1 -1
- package/dist/templates/rules.ts +27 -11
- package/graph-schemas/yg-flow.yaml +1 -0
- package/graph-schemas/yg-node.yaml +1 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -80,7 +80,8 @@ BEFORE reading, researching, planning, OR modifying ANY mapped file:
|
|
|
80
80
|
- Assessing what is affected by a change \u2192 yg impact --node <owner>
|
|
81
81
|
- Planning modifications \u2192 both (build-context first, then impact)
|
|
82
82
|
\`yg build-context --node <path>\`. Read the YAML map for topology,
|
|
83
|
-
|
|
83
|
+
starting with the glossary at the top (aspect and flow definitions),
|
|
84
|
+
then read artifact files listed inline on each element. For quick
|
|
84
85
|
orientation, the map alone is sufficient. For implementation, read
|
|
85
86
|
all artifact files before changing code.
|
|
86
87
|
If the context package seems insufficient \u2014 enrich the graph.
|
|
@@ -148,6 +149,7 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
|
|
|
148
149
|
| "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
|
|
149
150
|
| "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
|
|
150
151
|
| "I'll grep the codebase to find where to start" | Run \`yg select --task\` first \u2014 it matches your intent against graph artifacts. Then \`yg owner\` on results. |
|
|
152
|
+
| "Drift is blocking repo-check, let me just sync it" | Drift means artifacts are stale. Update artifacts first, then sync. \`drift-sync\` without artifact update = hiding staleness. |
|
|
151
153
|
|
|
152
154
|
### Failure States
|
|
153
155
|
|
|
@@ -210,9 +212,10 @@ PREFLIGHT (every conversation, before any work):
|
|
|
210
212
|
UNDERSTANDING mapped code (questions, research, OR planning):
|
|
211
213
|
- [ ] 1. yg owner --file <path>
|
|
212
214
|
- [ ] 2. Owner found \u2192 yg build-context --node <path>. Read the YAML map
|
|
213
|
-
for topology,
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
for topology, starting with the glossary at the top for aspect and
|
|
216
|
+
flow definitions, then read artifact files listed inline on each
|
|
217
|
+
element. For quick orientation, the map alone is sufficient. For
|
|
218
|
+
implementation, read all artifact files before changing code.
|
|
216
219
|
- [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
|
|
217
220
|
Never use grep or raw file reads as primary understanding when graph coverage exists.
|
|
218
221
|
Raw reads supplement the context package \u2014 they do not replace it.
|
|
@@ -239,7 +242,7 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
239
242
|
- [ ] 1. Read specification: \`yg build-context --node <node_path>\`
|
|
240
243
|
- [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
|
|
241
244
|
- [ ] 3. Modify source code
|
|
242
|
-
- [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change)
|
|
245
|
+
- [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change). If the node's purpose changed, update \`description\` in \`yg-node.yaml\`.
|
|
243
246
|
- [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
|
|
244
247
|
- [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
|
|
245
248
|
|
|
@@ -257,7 +260,7 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
257
260
|
|
|
258
261
|
1. Create aspects first (cross-cutting requirements the new code must satisfy)
|
|
259
262
|
2. Create flows if the code participates in a business process
|
|
260
|
-
3. Create nodes with full artifacts \u2014 responsibility, interface, internals
|
|
263
|
+
3. Create nodes with full artifacts \u2014 description in \`yg-node.yaml\`, responsibility, interface, internals
|
|
261
264
|
4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
|
|
262
265
|
5. Implement code that satisfies the specification
|
|
263
266
|
6. The graph specifies WHAT and WHY; the code implements HOW (framework APIs, library choices)
|
|
@@ -282,6 +285,7 @@ Per area checklist:
|
|
|
282
285
|
- [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
|
|
283
286
|
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
284
287
|
- [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
|
|
288
|
+
- [ ] 3b. Write \`description\` in \`yg-node.yaml\` \u2014 a short summary of what the node does
|
|
285
289
|
- [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
|
|
286
290
|
- [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
|
|
287
291
|
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
@@ -352,7 +356,7 @@ When reviewing graph quality (triggered by user or quality improvement):
|
|
|
352
356
|
- **Budget warning (W005/W006)** \u2192 informational. \`yg validate\` shows a breakdown (own/hierarchy/aspects/flows/dependencies). Large inherited context means the system is complex \u2014 this is not a problem to fix, it is reality to acknowledge. Do not delete knowledge from artifacts. Do not attempt to "reduce" inherited context.
|
|
353
357
|
- **Own budget warning (W015)** \u2192 own artifacts are large. Consider splitting this node's responsibilities into child nodes. Redistribute knowledge across children so total knowledge is preserved or increased, never reduced.
|
|
354
358
|
- **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
|
|
355
|
-
- **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
|
|
359
|
+
- **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end. But NEVER run \`yg drift-sync\` to silence a failing drift check \u2014 drift is a signal that artifacts need updating. First update artifacts, then sync.`;
|
|
356
360
|
var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
|
|
357
361
|
|
|
358
362
|
### Graph Structure
|
|
@@ -389,9 +393,20 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
|
|
|
389
393
|
|
|
390
394
|
### Context Assembly
|
|
391
395
|
|
|
392
|
-
**Reading context:** \`yg build-context --node <path>\` returns a YAML map
|
|
396
|
+
**Reading context:** \`yg build-context --node <path>\` returns a YAML map structured as follows:
|
|
393
397
|
|
|
394
|
-
|
|
398
|
+
- **\`glossary\`** (top) \u2014 definitions for every aspect and flow referenced in the map, each with \`files\` listing their artifact paths. Read this first to understand IDs used throughout.
|
|
399
|
+
- **\`node\`** \u2014 the target node with inline \`files\` (its artifact paths). No \`yg-node.yaml\` in file lists.
|
|
400
|
+
- **\`hierarchy\`** \u2014 ancestor and sibling nodes, each with inline \`files\`.
|
|
401
|
+
- **\`dependencies\`** \u2014 dependency nodes, each with inline \`files\`.
|
|
402
|
+
- **\`meta\`** (bottom) \u2014 context assembly metadata.
|
|
403
|
+
- YAML comments before each section guide reading order.
|
|
404
|
+
|
|
405
|
+
All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
|
|
406
|
+
|
|
407
|
+
**Default mode (paths-only):** Use for all graph operations. Read the YAML map first \u2014 start with the \`glossary\` to understand aspects and flows, then the \`node\` section for the target. Read artifact files inline on each element using the Read tool. For quick orientation (scoping, blast radius assessment), the map alone is sufficient. For implementation or modification, read all artifact files before changing code.
|
|
408
|
+
|
|
409
|
+
The glossary at the top defines all aspects and flows \u2014 read it first to understand IDs used throughout.
|
|
395
410
|
|
|
396
411
|
**Full mode (\`--full\`):** Use only when you cannot read files individually \u2014 e.g., when pasting context into a prompt, sharing with a user, or when you have no Read tool available.
|
|
397
412
|
|
|
@@ -440,7 +455,7 @@ When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are prese
|
|
|
440
455
|
|
|
441
456
|
- [ ] 1. Read \`schemas/yg-flow.yaml\`
|
|
442
457
|
- [ ] 2. Create \`flows/<name>/\` directory
|
|
443
|
-
- [ ] 3. Write \`yg-flow.yaml\` \u2014
|
|
458
|
+
- [ ] 3. Write \`yg-flow.yaml\` \u2014 name, description, nodes (participant list), and flow-level aspects
|
|
444
459
|
- [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
|
|
445
460
|
- [ ] 5. \`yg validate\`
|
|
446
461
|
|
|
@@ -453,7 +468,8 @@ Test: "Does this describe what happens in the world, or only in the software?" I
|
|
|
453
468
|
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
454
469
|
- **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
|
|
455
470
|
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
456
|
-
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
|
|
471
|
+
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task. \`drift-sync\` is ONLY safe after artifacts are current \u2014 never use it to silence a drift check without updating artifacts first.
|
|
472
|
+
- **Description maintenance.** Every \`yg-node.yaml\`, \`yg-aspect.yaml\`, and \`yg-flow.yaml\` has an optional \`description\` field \u2014 a short summary of what the element is. Write it when creating new elements. Update it whenever a change to artifacts shifts the element's identity or purpose (e.g., responsibility split, scope change). Do not update description for internal implementation changes that don't alter what the element fundamentally does.
|
|
457
473
|
- **Completeness test:** Two checks, both required:
|
|
458
474
|
1. **Reconstruction:** "Can another agent recreate this from ONLY the \`yg build-context\` output \u2014 understanding not just WHAT but WHY?" Test: rejected alternatives, correct algorithm, design arguments.
|
|
459
475
|
2. **Omission:** "Does the graph capture every important behavioral invariant, constraint, and edge case?" Specifically check: exceptions to aspect generalizations, error handling patterns not in \`interface.md\`, concurrency behaviors not in \`internals.md\`.
|
|
@@ -1172,10 +1188,6 @@ function registerInitCommand(program2) {
|
|
|
1172
1188
|
});
|
|
1173
1189
|
}
|
|
1174
1190
|
|
|
1175
|
-
// src/cli/build-context.ts
|
|
1176
|
-
import { readFile as readFile14 } from "fs/promises";
|
|
1177
|
-
import path12 from "path";
|
|
1178
|
-
|
|
1179
1191
|
// src/core/graph-loader.ts
|
|
1180
1192
|
import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
|
|
1181
1193
|
import path9 from "path";
|
|
@@ -1298,12 +1310,14 @@ async function parseNodeYaml(filePath) {
|
|
|
1298
1310
|
if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
|
|
1299
1311
|
throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
|
|
1300
1312
|
}
|
|
1313
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1301
1314
|
const relations = parseRelations(raw.relations, filePath);
|
|
1302
1315
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
1303
1316
|
const aspects = parseAspects(raw.aspects, filePath);
|
|
1304
1317
|
return {
|
|
1305
1318
|
name: raw.name.trim(),
|
|
1306
1319
|
type: raw.type.trim(),
|
|
1320
|
+
description,
|
|
1307
1321
|
aspects,
|
|
1308
1322
|
blackbox: raw.blackbox ?? false,
|
|
1309
1323
|
relations: relations.length > 0 ? relations : void 0,
|
|
@@ -1509,6 +1523,7 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1509
1523
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
1510
1524
|
throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
|
|
1511
1525
|
}
|
|
1526
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1512
1527
|
const nodes = raw.nodes ?? raw.participants;
|
|
1513
1528
|
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
1514
1529
|
throw new Error(
|
|
@@ -1533,6 +1548,7 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1533
1548
|
return {
|
|
1534
1549
|
path: path6.basename(flowDir),
|
|
1535
1550
|
name: raw.name.trim(),
|
|
1551
|
+
description,
|
|
1536
1552
|
nodes: nodePaths,
|
|
1537
1553
|
...aspects !== void 0 && { aspects },
|
|
1538
1554
|
artifacts
|
|
@@ -1779,6 +1795,7 @@ function estimateTokens(text) {
|
|
|
1779
1795
|
// src/core/context-builder.ts
|
|
1780
1796
|
var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
1781
1797
|
var EVENT_RELATION_TYPES = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
1798
|
+
var YG_YAML_FILES = /* @__PURE__ */ new Set(["yg-node.yaml", "yg-aspect.yaml", "yg-flow.yaml"]);
|
|
1782
1799
|
async function buildContext(graph, nodePath) {
|
|
1783
1800
|
const node = graph.nodes.get(nodePath);
|
|
1784
1801
|
if (!node) {
|
|
@@ -2137,7 +2154,7 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2137
2154
|
});
|
|
2138
2155
|
const participatingFlows = collectParticipatingFlows(graph, node);
|
|
2139
2156
|
const flowRefs = participatingFlows.map((f) => {
|
|
2140
|
-
const ref = { path: f.path
|
|
2157
|
+
const ref = { path: f.path };
|
|
2141
2158
|
if (f.aspects?.length) ref.aspects = f.aspects;
|
|
2142
2159
|
return ref;
|
|
2143
2160
|
});
|
|
@@ -2145,7 +2162,7 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2145
2162
|
const hierarchyRefs = ancestors.map((a) => {
|
|
2146
2163
|
const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2147
2164
|
const expanded = expandAspects(nodeAspectIds, graph.aspects);
|
|
2148
|
-
return { path: a.path, name: a.meta.name, type: a.meta.type, aspects: expanded };
|
|
2165
|
+
return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: buildNodeFiles(a, config, `model/${a.path}`) };
|
|
2149
2166
|
});
|
|
2150
2167
|
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
2151
2168
|
const depRefs = [];
|
|
@@ -2157,23 +2174,26 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2157
2174
|
const depHierarchy = depAncestors.map((a) => {
|
|
2158
2175
|
const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2159
2176
|
const expanded = expandAspects(ids, graph.aspects);
|
|
2160
|
-
|
|
2177
|
+
const ancestorNode = graph.nodes.get(a.path);
|
|
2178
|
+
return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: ancestorNode ? buildDepNodeFiles(ancestorNode, config, `model/${a.path}`) : [] };
|
|
2161
2179
|
});
|
|
2162
2180
|
const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
|
|
2163
2181
|
const ref = {
|
|
2164
2182
|
path: target.path,
|
|
2165
2183
|
name: target.meta.name,
|
|
2166
2184
|
type: target.meta.type,
|
|
2185
|
+
description: target.meta.description,
|
|
2167
2186
|
relation: relation.type,
|
|
2168
2187
|
aspects: depEffectiveAspects,
|
|
2169
|
-
hierarchy: depHierarchy
|
|
2188
|
+
hierarchy: depHierarchy,
|
|
2189
|
+
files: buildDepNodeFiles(target, config, `model/${target.path}`)
|
|
2170
2190
|
};
|
|
2171
2191
|
if (relation.consumes?.length) ref.consumes = relation.consumes;
|
|
2172
2192
|
if (relation.failure) ref.failure = relation.failure;
|
|
2173
2193
|
if (relation.event_name) ref["event-name"] = relation.event_name;
|
|
2174
2194
|
depRefs.push(ref);
|
|
2175
2195
|
}
|
|
2176
|
-
const
|
|
2196
|
+
const glossary = buildGlossary(node, depRefs, graph);
|
|
2177
2197
|
const breakdown = computeBudgetBreakdown(pkg2, graph);
|
|
2178
2198
|
const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
|
|
2179
2199
|
const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
|
|
@@ -2185,56 +2205,29 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2185
2205
|
path: pkg2.nodePath,
|
|
2186
2206
|
name: pkg2.nodeName,
|
|
2187
2207
|
type: node.meta.type,
|
|
2208
|
+
description: node.meta.description,
|
|
2188
2209
|
mappings: normalizeMappingPaths(node.meta.mapping),
|
|
2189
2210
|
aspects: nodeAspects,
|
|
2190
|
-
flows: flowRefs
|
|
2211
|
+
flows: flowRefs,
|
|
2212
|
+
files: buildNodeFiles(node, config, `model/${pkg2.nodePath}`)
|
|
2191
2213
|
},
|
|
2192
2214
|
hierarchy: hierarchyRefs,
|
|
2193
2215
|
dependencies: depRefs,
|
|
2194
|
-
|
|
2216
|
+
glossary
|
|
2195
2217
|
};
|
|
2196
2218
|
}
|
|
2197
|
-
function
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2219
|
+
function buildNodeFiles(node, config, prefix) {
|
|
2220
|
+
const configKeys = Object.keys(config.artifacts ?? {});
|
|
2221
|
+
return configKeys.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
|
|
2222
|
+
}
|
|
2223
|
+
function buildDepNodeFiles(node, config, prefix) {
|
|
2224
|
+
const structural = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([f]) => f);
|
|
2225
|
+
const filenames = structural.length > 0 ? structural : Object.keys(config.artifacts ?? {});
|
|
2226
|
+
return filenames.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
|
|
2227
|
+
}
|
|
2228
|
+
function buildGlossary(node, dependencies, graph) {
|
|
2202
2229
|
const aspects = {};
|
|
2203
2230
|
const flows = {};
|
|
2204
|
-
function addNodeEntry(n, includeYgNodeYaml, filter) {
|
|
2205
|
-
if (nodes[n.path]) return;
|
|
2206
|
-
const files = [];
|
|
2207
|
-
if (includeYgNodeYaml) {
|
|
2208
|
-
files.push(`model/${n.path}/yg-node.yaml`);
|
|
2209
|
-
}
|
|
2210
|
-
for (const filename of filter) {
|
|
2211
|
-
if (n.artifacts.some((a) => a.filename === filename)) {
|
|
2212
|
-
files.push(`model/${n.path}/${filename}`);
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
if (files.length > 0) {
|
|
2216
|
-
nodes[n.path] = { files };
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
addNodeEntry(node, true, [...configArtifactKeys]);
|
|
2220
|
-
for (const ancestor of ancestors) {
|
|
2221
|
-
addNodeEntry(ancestor, true, [...configArtifactKeys]);
|
|
2222
|
-
}
|
|
2223
|
-
const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
|
|
2224
|
-
for (const dep of dependencies) {
|
|
2225
|
-
const target = graph.nodes.get(dep.path);
|
|
2226
|
-
if (target) {
|
|
2227
|
-
addNodeEntry(target, false, structuralFilenames);
|
|
2228
|
-
}
|
|
2229
|
-
for (const ancestor of dep.hierarchy) {
|
|
2230
|
-
if (seenDepAncestors.has(ancestor.path)) continue;
|
|
2231
|
-
seenDepAncestors.add(ancestor.path);
|
|
2232
|
-
const ancestorNode = graph.nodes.get(ancestor.path);
|
|
2233
|
-
if (ancestorNode) {
|
|
2234
|
-
addNodeEntry(ancestorNode, false, structuralFilenames);
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
2231
|
const allAspectIds = collectEffectiveAspectIds(graph, node.path);
|
|
2239
2232
|
for (const dep of dependencies) {
|
|
2240
2233
|
for (const id of dep.aspects) {
|
|
@@ -2243,33 +2236,29 @@ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
|
|
|
2243
2236
|
}
|
|
2244
2237
|
const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
|
|
2245
2238
|
for (const aspect of resolvedAspects) {
|
|
2246
|
-
const files =
|
|
2247
|
-
files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
|
|
2248
|
-
for (const art of aspect.artifacts) {
|
|
2249
|
-
files.push(`aspects/${aspect.id}/${art.filename}`);
|
|
2250
|
-
}
|
|
2239
|
+
const files = aspect.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `aspects/${aspect.id}/${a.filename}`);
|
|
2251
2240
|
const entry = {
|
|
2252
2241
|
name: aspect.name,
|
|
2253
2242
|
files
|
|
2254
2243
|
};
|
|
2244
|
+
if (aspect.description) entry.description = aspect.description;
|
|
2245
|
+
if (aspect.stability) entry.stability = aspect.stability;
|
|
2255
2246
|
if (aspect.implies?.length) entry.implies = aspect.implies;
|
|
2256
2247
|
aspects[aspect.id] = entry;
|
|
2257
2248
|
}
|
|
2258
2249
|
const participatingFlows = collectParticipatingFlows(graph, node);
|
|
2259
2250
|
for (const flow of participatingFlows) {
|
|
2260
|
-
const files =
|
|
2261
|
-
files.push(`flows/${flow.path}/yg-flow.yaml`);
|
|
2262
|
-
for (const art of flow.artifacts) {
|
|
2263
|
-
files.push(`flows/${flow.path}/${art.filename}`);
|
|
2264
|
-
}
|
|
2251
|
+
const files = flow.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `flows/${flow.path}/${a.filename}`);
|
|
2265
2252
|
const entry = {
|
|
2266
2253
|
name: flow.name,
|
|
2254
|
+
participants: flow.nodes,
|
|
2267
2255
|
files
|
|
2268
2256
|
};
|
|
2257
|
+
if (flow.description) entry.description = flow.description;
|
|
2269
2258
|
if (flow.aspects?.length) entry.aspects = flow.aspects;
|
|
2270
2259
|
flows[flow.path] = entry;
|
|
2271
2260
|
}
|
|
2272
|
-
return {
|
|
2261
|
+
return { aspects, flows };
|
|
2273
2262
|
}
|
|
2274
2263
|
function collectEffectiveAspectIds(graph, nodePath) {
|
|
2275
2264
|
const node = graph.nodes.get(nodePath);
|
|
@@ -2290,23 +2279,39 @@ function collectEffectiveAspectIds(graph, nodePath) {
|
|
|
2290
2279
|
}
|
|
2291
2280
|
|
|
2292
2281
|
// src/formatters/context-text.ts
|
|
2293
|
-
import {
|
|
2282
|
+
import { Document } from "yaml";
|
|
2294
2283
|
function formatContextYaml(data) {
|
|
2295
|
-
const output = {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2284
|
+
const output = {};
|
|
2285
|
+
output.project = data.project;
|
|
2286
|
+
output.glossary = data.glossary;
|
|
2287
|
+
output.node = data.node;
|
|
2288
|
+
if (data.hierarchy.length > 0) output.hierarchy = data.hierarchy;
|
|
2289
|
+
if (data.dependencies.length > 0) output.dependencies = data.dependencies;
|
|
2290
|
+
output.meta = {
|
|
2291
|
+
"token-count": data.meta.tokenCount,
|
|
2292
|
+
"budget-status": data.meta.budgetStatus,
|
|
2293
|
+
breakdown: data.meta.breakdown
|
|
2305
2294
|
};
|
|
2306
|
-
|
|
2307
|
-
|
|
2295
|
+
const doc = new Document(output, { aliasDuplicateObjects: false });
|
|
2296
|
+
const map = doc.contents;
|
|
2297
|
+
for (const pair of map.items) {
|
|
2298
|
+
const key = String(pair.key);
|
|
2299
|
+
switch (key) {
|
|
2300
|
+
case "glossary":
|
|
2301
|
+
pair.key.commentBefore = " Glossary: definitions of all aspects and flows referenced in this context.\n Read this first \u2014 IDs below (in node, hierarchy, dependencies) refer to entries here.";
|
|
2302
|
+
break;
|
|
2303
|
+
case "node":
|
|
2304
|
+
pair.key.commentBefore = " Target node: the component you are working on.";
|
|
2305
|
+
break;
|
|
2306
|
+
case "hierarchy":
|
|
2307
|
+
pair.key.commentBefore = " Hierarchy: ancestor modules from root to parent. Context is inherited top-down.";
|
|
2308
|
+
break;
|
|
2309
|
+
case "dependencies":
|
|
2310
|
+
pair.key.commentBefore = " Dependencies: components this node directly depends on.";
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2308
2313
|
}
|
|
2309
|
-
return
|
|
2314
|
+
return doc.toString({ lineWidth: 0 });
|
|
2310
2315
|
}
|
|
2311
2316
|
function formatFullContent(files) {
|
|
2312
2317
|
if (files.length === 0) return "";
|
|
@@ -2360,6 +2365,7 @@ async function validate(graph, scope = "all") {
|
|
|
2360
2365
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
2361
2366
|
issues.push(...await checkContextBudget(graph));
|
|
2362
2367
|
issues.push(...checkHighFanOut(graph));
|
|
2368
|
+
issues.push(...checkMissingDescriptions(graph));
|
|
2363
2369
|
}
|
|
2364
2370
|
issues.push(...checkSchemas(graph));
|
|
2365
2371
|
issues.push(...checkRelationTargets(graph));
|
|
@@ -3092,6 +3098,41 @@ function pct(value, total) {
|
|
|
3092
3098
|
if (total === 0) return "0%";
|
|
3093
3099
|
return `${Math.round(value / total * 100)}%`;
|
|
3094
3100
|
}
|
|
3101
|
+
function checkMissingDescriptions(graph) {
|
|
3102
|
+
const issues = [];
|
|
3103
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
3104
|
+
if (!node.meta.description?.trim()) {
|
|
3105
|
+
issues.push({
|
|
3106
|
+
severity: "warning",
|
|
3107
|
+
code: "W016",
|
|
3108
|
+
rule: "missing-description",
|
|
3109
|
+
message: `Node has no description`,
|
|
3110
|
+
nodePath
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
for (const aspect of graph.aspects) {
|
|
3115
|
+
if (!aspect.description?.trim()) {
|
|
3116
|
+
issues.push({
|
|
3117
|
+
severity: "warning",
|
|
3118
|
+
code: "W016",
|
|
3119
|
+
rule: "missing-description",
|
|
3120
|
+
message: `Aspect '${aspect.id}' has no description`
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
for (const flow of graph.flows) {
|
|
3125
|
+
if (!flow.description?.trim()) {
|
|
3126
|
+
issues.push({
|
|
3127
|
+
severity: "warning",
|
|
3128
|
+
code: "W016",
|
|
3129
|
+
rule: "missing-description",
|
|
3130
|
+
message: `Flow '${flow.name}' has no description`
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
return issues;
|
|
3135
|
+
}
|
|
3095
3136
|
|
|
3096
3137
|
// src/cli/build-context.ts
|
|
3097
3138
|
function collectRelevantNodePaths(graph, nodePath) {
|
|
@@ -3144,23 +3185,32 @@ function registerBuildCommand(program2) {
|
|
|
3144
3185
|
const mapOutput = toContextMapOutput(pkg2, graph);
|
|
3145
3186
|
let output = formatContextYaml(mapOutput);
|
|
3146
3187
|
if (options.full) {
|
|
3147
|
-
const allFiles = [];
|
|
3148
|
-
const allEntries = [
|
|
3149
|
-
...Object.values(mapOutput.artifacts.nodes),
|
|
3150
|
-
...Object.values(mapOutput.artifacts.aspects),
|
|
3151
|
-
...Object.values(mapOutput.artifacts.flows)
|
|
3152
|
-
];
|
|
3153
3188
|
const seen = /* @__PURE__ */ new Set();
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
}
|
|
3189
|
+
const allFiles = [];
|
|
3190
|
+
async function collectFile(filePath) {
|
|
3191
|
+
if (seen.has(filePath)) return;
|
|
3192
|
+
seen.add(filePath);
|
|
3193
|
+
const content = await findFileContent(filePath, graph);
|
|
3194
|
+
if (content !== void 0) {
|
|
3195
|
+
allFiles.push({ path: filePath, content });
|
|
3162
3196
|
}
|
|
3163
3197
|
}
|
|
3198
|
+
for (const aspect of Object.values(mapOutput.glossary.aspects)) {
|
|
3199
|
+
for (const f of aspect.files) await collectFile(f);
|
|
3200
|
+
}
|
|
3201
|
+
for (const flow of Object.values(mapOutput.glossary.flows)) {
|
|
3202
|
+
for (const f of flow.files) await collectFile(f);
|
|
3203
|
+
}
|
|
3204
|
+
for (const f of mapOutput.node.files) await collectFile(f);
|
|
3205
|
+
for (const ancestor of mapOutput.hierarchy) {
|
|
3206
|
+
for (const f of ancestor.files ?? []) await collectFile(f);
|
|
3207
|
+
}
|
|
3208
|
+
for (const dep of mapOutput.dependencies) {
|
|
3209
|
+
for (const ancestor of dep.hierarchy) {
|
|
3210
|
+
for (const f of ancestor.files ?? []) await collectFile(f);
|
|
3211
|
+
}
|
|
3212
|
+
for (const f of dep.files ?? []) await collectFile(f);
|
|
3213
|
+
}
|
|
3164
3214
|
output += formatFullContent(allFiles);
|
|
3165
3215
|
}
|
|
3166
3216
|
process.stdout.write(output);
|
|
@@ -3172,14 +3222,6 @@ function registerBuildCommand(program2) {
|
|
|
3172
3222
|
});
|
|
3173
3223
|
}
|
|
3174
3224
|
async function findFileContent(filePath, graph) {
|
|
3175
|
-
async function readYamlFromDisk(relativePath) {
|
|
3176
|
-
try {
|
|
3177
|
-
const fullPath = path12.join(graph.rootPath, relativePath);
|
|
3178
|
-
return (await readFile14(fullPath, "utf-8")).trim();
|
|
3179
|
-
} catch {
|
|
3180
|
-
return void 0;
|
|
3181
|
-
}
|
|
3182
|
-
}
|
|
3183
3225
|
if (filePath.startsWith("model/")) {
|
|
3184
3226
|
const rest = filePath.slice("model/".length);
|
|
3185
3227
|
const parts = rest.split("/");
|
|
@@ -3187,9 +3229,6 @@ async function findFileContent(filePath, graph) {
|
|
|
3187
3229
|
const nodePath = parts.join("/");
|
|
3188
3230
|
const node = graph.nodes.get(nodePath);
|
|
3189
3231
|
if (!node) return void 0;
|
|
3190
|
-
if (filename === "yg-node.yaml") {
|
|
3191
|
-
return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
|
|
3192
|
-
}
|
|
3193
3232
|
const art = node.artifacts.find((a) => a.filename === filename);
|
|
3194
3233
|
return art?.content;
|
|
3195
3234
|
}
|
|
@@ -3200,9 +3239,6 @@ async function findFileContent(filePath, graph) {
|
|
|
3200
3239
|
const filename = parts.slice(1).join("/");
|
|
3201
3240
|
const aspect = graph.aspects.find((a) => a.id === aspectId);
|
|
3202
3241
|
if (!aspect) return void 0;
|
|
3203
|
-
if (filename === "yg-aspect.yaml") {
|
|
3204
|
-
return readYamlFromDisk(filePath);
|
|
3205
|
-
}
|
|
3206
3242
|
const art = aspect.artifacts.find((a) => a.filename === filename);
|
|
3207
3243
|
return art?.content;
|
|
3208
3244
|
}
|
|
@@ -3213,9 +3249,6 @@ async function findFileContent(filePath, graph) {
|
|
|
3213
3249
|
const filename = parts.slice(1).join("/");
|
|
3214
3250
|
const flow = graph.flows.find((f) => f.path === flowPath);
|
|
3215
3251
|
if (!flow) return void 0;
|
|
3216
|
-
if (filename === "yg-flow.yaml") {
|
|
3217
|
-
return readYamlFromDisk(filePath);
|
|
3218
|
-
}
|
|
3219
3252
|
const art = flow.artifacts.find((a) => a.filename === filename);
|
|
3220
3253
|
return art?.content;
|
|
3221
3254
|
}
|
|
@@ -3271,12 +3304,12 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
3271
3304
|
import chalk2 from "chalk";
|
|
3272
3305
|
|
|
3273
3306
|
// src/io/drift-state-store.ts
|
|
3274
|
-
import { readFile as
|
|
3275
|
-
import
|
|
3307
|
+
import { readFile as readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
3308
|
+
import path12 from "path";
|
|
3276
3309
|
import { parse as yamlParse } from "yaml";
|
|
3277
3310
|
var DRIFT_STATE_DIR = ".drift-state";
|
|
3278
3311
|
function nodeStatePath(yggRoot, nodePath) {
|
|
3279
|
-
return
|
|
3312
|
+
return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
|
|
3280
3313
|
}
|
|
3281
3314
|
async function scanJsonFiles(dir, baseDir) {
|
|
3282
3315
|
const results = [];
|
|
@@ -3287,12 +3320,12 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3287
3320
|
return results;
|
|
3288
3321
|
}
|
|
3289
3322
|
for (const entry of entries) {
|
|
3290
|
-
const fullPath =
|
|
3323
|
+
const fullPath = path12.join(dir, entry.name);
|
|
3291
3324
|
if (entry.isDirectory()) {
|
|
3292
3325
|
const nested = await scanJsonFiles(fullPath, baseDir);
|
|
3293
3326
|
results.push(...nested);
|
|
3294
3327
|
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
3295
|
-
const relPath =
|
|
3328
|
+
const relPath = path12.relative(baseDir, fullPath);
|
|
3296
3329
|
const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
|
|
3297
3330
|
results.push(nodePath);
|
|
3298
3331
|
}
|
|
@@ -3300,13 +3333,13 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3300
3333
|
return results;
|
|
3301
3334
|
}
|
|
3302
3335
|
async function removeEmptyParents(filePath, stopDir) {
|
|
3303
|
-
let dir =
|
|
3336
|
+
let dir = path12.dirname(filePath);
|
|
3304
3337
|
while (dir !== stopDir && dir.startsWith(stopDir)) {
|
|
3305
3338
|
try {
|
|
3306
3339
|
const entries = await readdir6(dir);
|
|
3307
3340
|
if (entries.length === 0) {
|
|
3308
3341
|
await rm2(dir, { recursive: true });
|
|
3309
|
-
dir =
|
|
3342
|
+
dir = path12.dirname(dir);
|
|
3310
3343
|
} else {
|
|
3311
3344
|
break;
|
|
3312
3345
|
}
|
|
@@ -3318,7 +3351,7 @@ async function removeEmptyParents(filePath, stopDir) {
|
|
|
3318
3351
|
async function readNodeDriftState(yggRoot, nodePath) {
|
|
3319
3352
|
try {
|
|
3320
3353
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3321
|
-
const content = await
|
|
3354
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3322
3355
|
const parsed = JSON.parse(content);
|
|
3323
3356
|
return parsed;
|
|
3324
3357
|
} catch {
|
|
@@ -3327,12 +3360,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
|
|
|
3327
3360
|
}
|
|
3328
3361
|
async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
3329
3362
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3330
|
-
await mkdir3(
|
|
3363
|
+
await mkdir3(path12.dirname(filePath), { recursive: true });
|
|
3331
3364
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
3332
3365
|
await writeFile5(filePath, content, "utf-8");
|
|
3333
3366
|
}
|
|
3334
3367
|
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
3335
|
-
const driftDir =
|
|
3368
|
+
const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
|
|
3336
3369
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
3337
3370
|
const removed = [];
|
|
3338
3371
|
for (const nodePath of allNodePaths) {
|
|
@@ -3346,7 +3379,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
|
3346
3379
|
return removed.sort();
|
|
3347
3380
|
}
|
|
3348
3381
|
async function readDriftState(yggRoot) {
|
|
3349
|
-
const driftPath =
|
|
3382
|
+
const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
|
|
3350
3383
|
let driftStat;
|
|
3351
3384
|
try {
|
|
3352
3385
|
driftStat = await stat5(driftPath);
|
|
@@ -3354,7 +3387,7 @@ async function readDriftState(yggRoot) {
|
|
|
3354
3387
|
return {};
|
|
3355
3388
|
}
|
|
3356
3389
|
if (driftStat.isFile()) {
|
|
3357
|
-
const content = await
|
|
3390
|
+
const content = await readFile14(driftPath, "utf-8");
|
|
3358
3391
|
let raw;
|
|
3359
3392
|
try {
|
|
3360
3393
|
raw = JSON.parse(content);
|
|
@@ -3386,20 +3419,20 @@ async function readDriftState(yggRoot) {
|
|
|
3386
3419
|
}
|
|
3387
3420
|
|
|
3388
3421
|
// src/utils/hash.ts
|
|
3389
|
-
import { readFile as
|
|
3390
|
-
import
|
|
3422
|
+
import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
3423
|
+
import path13 from "path";
|
|
3391
3424
|
import { createHash } from "crypto";
|
|
3392
3425
|
import { createRequire } from "module";
|
|
3393
3426
|
var require2 = createRequire(import.meta.url);
|
|
3394
3427
|
var ignoreFactory = require2("ignore");
|
|
3395
3428
|
async function hashFile(filePath) {
|
|
3396
|
-
const content = await
|
|
3429
|
+
const content = await readFile15(filePath);
|
|
3397
3430
|
return createHash("sha256").update(content).digest("hex");
|
|
3398
3431
|
}
|
|
3399
3432
|
async function loadRootGitignoreStack(projectRoot) {
|
|
3400
3433
|
if (!projectRoot) return [];
|
|
3401
3434
|
try {
|
|
3402
|
-
const content = await
|
|
3435
|
+
const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
|
|
3403
3436
|
const matcher = ignoreFactory();
|
|
3404
3437
|
matcher.add(content);
|
|
3405
3438
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -3409,7 +3442,7 @@ async function loadRootGitignoreStack(projectRoot) {
|
|
|
3409
3442
|
}
|
|
3410
3443
|
function isIgnoredByStack(candidatePath, stack) {
|
|
3411
3444
|
for (const { basePath, matcher } of stack) {
|
|
3412
|
-
const relativePath =
|
|
3445
|
+
const relativePath = path13.relative(basePath, candidatePath);
|
|
3413
3446
|
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
3414
3447
|
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
3415
3448
|
}
|
|
@@ -3424,7 +3457,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3424
3457
|
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
3425
3458
|
const allFiles = [];
|
|
3426
3459
|
for (const tf of trackedFiles) {
|
|
3427
|
-
const absPath =
|
|
3460
|
+
const absPath = path13.join(projectRoot, tf.path);
|
|
3428
3461
|
try {
|
|
3429
3462
|
const st = await stat6(absPath);
|
|
3430
3463
|
if (st.isDirectory()) {
|
|
@@ -3434,7 +3467,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3434
3467
|
});
|
|
3435
3468
|
for (const entry of dirEntries) {
|
|
3436
3469
|
allFiles.push({
|
|
3437
|
-
relPath:
|
|
3470
|
+
relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
3438
3471
|
absPath: entry.absPath,
|
|
3439
3472
|
mtimeMs: entry.mtimeMs
|
|
3440
3473
|
});
|
|
@@ -3474,7 +3507,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3474
3507
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
3475
3508
|
let stack = options.gitignoreStack ?? [];
|
|
3476
3509
|
try {
|
|
3477
|
-
const localContent = await
|
|
3510
|
+
const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
|
|
3478
3511
|
const localMatcher = ignoreFactory();
|
|
3479
3512
|
localMatcher.add(localContent);
|
|
3480
3513
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
@@ -3484,7 +3517,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3484
3517
|
const dirs = [];
|
|
3485
3518
|
const files = [];
|
|
3486
3519
|
for (const entry of entries) {
|
|
3487
|
-
const absoluteChildPath =
|
|
3520
|
+
const absoluteChildPath = path13.join(directoryPath, entry.name);
|
|
3488
3521
|
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
3489
3522
|
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
3490
3523
|
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
@@ -3497,7 +3530,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3497
3530
|
Promise.all(files.map(async (f) => {
|
|
3498
3531
|
const fileStat = await stat6(f);
|
|
3499
3532
|
return {
|
|
3500
|
-
relPath:
|
|
3533
|
+
relPath: path13.relative(rootDirectoryPath, f),
|
|
3501
3534
|
absPath: f,
|
|
3502
3535
|
mtimeMs: fileStat.mtimeMs
|
|
3503
3536
|
};
|
|
@@ -3510,14 +3543,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3510
3543
|
}
|
|
3511
3544
|
|
|
3512
3545
|
// src/core/context-files.ts
|
|
3513
|
-
import
|
|
3546
|
+
import path14 from "path";
|
|
3514
3547
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3515
3548
|
function collectTrackedFiles(node, graph) {
|
|
3516
3549
|
const seen = /* @__PURE__ */ new Set();
|
|
3517
3550
|
const result = [];
|
|
3518
|
-
const projectRoot =
|
|
3519
|
-
const yggPrefix =
|
|
3520
|
-
const yggPrefixNormalized = yggPrefix.split(
|
|
3551
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3552
|
+
const yggPrefix = path14.relative(projectRoot, graph.rootPath);
|
|
3553
|
+
const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
|
|
3521
3554
|
const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
|
|
3522
3555
|
function addFile(filePath, category) {
|
|
3523
3556
|
if (seen.has(filePath)) return;
|
|
@@ -3630,7 +3663,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
|
|
|
3630
3663
|
|
|
3631
3664
|
// src/core/drift-detector.ts
|
|
3632
3665
|
import { access as access2 } from "fs/promises";
|
|
3633
|
-
import
|
|
3666
|
+
import path15 from "path";
|
|
3634
3667
|
function getChildMappingExclusions(graph, nodePath) {
|
|
3635
3668
|
const node = graph.nodes.get(nodePath);
|
|
3636
3669
|
if (!node) return [];
|
|
@@ -3652,7 +3685,7 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
3652
3685
|
return exclusions;
|
|
3653
3686
|
}
|
|
3654
3687
|
async function detectDrift(graph, filterNodePath) {
|
|
3655
|
-
const projectRoot =
|
|
3688
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
3656
3689
|
const driftState = await readDriftState(graph.rootPath);
|
|
3657
3690
|
const entries = [];
|
|
3658
3691
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -3734,14 +3767,14 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
3734
3767
|
};
|
|
3735
3768
|
}
|
|
3736
3769
|
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
3737
|
-
const yggPrefix =
|
|
3738
|
-
const normalizedPrefix = yggPrefix.split(
|
|
3770
|
+
const yggPrefix = path15.relative(projectRoot, _rootPath);
|
|
3771
|
+
const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
|
|
3739
3772
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
3740
3773
|
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
3741
3774
|
}
|
|
3742
3775
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
3743
3776
|
for (const mp of mappingPaths) {
|
|
3744
|
-
const absPath =
|
|
3777
|
+
const absPath = path15.join(projectRoot, mp);
|
|
3745
3778
|
try {
|
|
3746
3779
|
await access2(absPath);
|
|
3747
3780
|
return false;
|
|
@@ -3751,7 +3784,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
3751
3784
|
return true;
|
|
3752
3785
|
}
|
|
3753
3786
|
async function syncDriftState(graph, nodePath) {
|
|
3754
|
-
const projectRoot =
|
|
3787
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
3755
3788
|
const node = graph.nodes.get(nodePath);
|
|
3756
3789
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
3757
3790
|
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
@@ -4071,10 +4104,10 @@ function registerTreeCommand(program2) {
|
|
|
4071
4104
|
let roots;
|
|
4072
4105
|
let showProjectName;
|
|
4073
4106
|
if (options.root?.trim()) {
|
|
4074
|
-
const
|
|
4075
|
-
const node = graph.nodes.get(
|
|
4107
|
+
const path19 = options.root.trim().replace(/\/$/, "");
|
|
4108
|
+
const node = graph.nodes.get(path19);
|
|
4076
4109
|
if (!node) {
|
|
4077
|
-
process.stderr.write(`Error: path '${
|
|
4110
|
+
process.stderr.write(`Error: path '${path19}' not found
|
|
4078
4111
|
`);
|
|
4079
4112
|
process.exit(1);
|
|
4080
4113
|
}
|
|
@@ -4118,7 +4151,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
4118
4151
|
}
|
|
4119
4152
|
|
|
4120
4153
|
// src/cli/owner.ts
|
|
4121
|
-
import
|
|
4154
|
+
import path16 from "path";
|
|
4122
4155
|
import { access as access3 } from "fs/promises";
|
|
4123
4156
|
function normalizeForMatch(inputPath) {
|
|
4124
4157
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -4148,11 +4181,11 @@ function registerOwnerCommand(program2) {
|
|
|
4148
4181
|
const graph = await loadGraph(cwd);
|
|
4149
4182
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
4150
4183
|
const rawPath = options.file.trim();
|
|
4151
|
-
const absolute =
|
|
4152
|
-
const repoRelative =
|
|
4184
|
+
const absolute = path16.resolve(cwd, rawPath);
|
|
4185
|
+
const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
|
|
4153
4186
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
4154
4187
|
if (!result.nodePath) {
|
|
4155
|
-
const absPath =
|
|
4188
|
+
const absPath = path16.resolve(repoRoot, result.file);
|
|
4156
4189
|
let exists = true;
|
|
4157
4190
|
try {
|
|
4158
4191
|
await access3(absPath);
|
|
@@ -4186,7 +4219,7 @@ function registerOwnerCommand(program2) {
|
|
|
4186
4219
|
|
|
4187
4220
|
// src/core/dependency-resolver.ts
|
|
4188
4221
|
import { execSync } from "child_process";
|
|
4189
|
-
import
|
|
4222
|
+
import path17 from "path";
|
|
4190
4223
|
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
4191
4224
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
4192
4225
|
function filterRelationType(relType, filter) {
|
|
@@ -4263,7 +4296,7 @@ function registerDepsCommand(program2) {
|
|
|
4263
4296
|
// src/core/graph-from-git.ts
|
|
4264
4297
|
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
4265
4298
|
import { tmpdir } from "os";
|
|
4266
|
-
import
|
|
4299
|
+
import path18 from "path";
|
|
4267
4300
|
import { execSync as execSync2 } from "child_process";
|
|
4268
4301
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
4269
4302
|
const yggPath = ".yggdrasil";
|
|
@@ -4274,8 +4307,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
4274
4307
|
return null;
|
|
4275
4308
|
}
|
|
4276
4309
|
try {
|
|
4277
|
-
tmpDir = await mkdtemp(
|
|
4278
|
-
const archivePath =
|
|
4310
|
+
tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
|
|
4311
|
+
const archivePath = path18.join(tmpDir, "archive.tar");
|
|
4279
4312
|
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
4280
4313
|
cwd: projectRoot,
|
|
4281
4314
|
stdio: "pipe"
|
|
@@ -4345,14 +4378,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
4345
4378
|
}
|
|
4346
4379
|
const chains = [];
|
|
4347
4380
|
for (const node of transitiveOnly) {
|
|
4348
|
-
const
|
|
4381
|
+
const path19 = [];
|
|
4349
4382
|
let current = node;
|
|
4350
4383
|
while (current) {
|
|
4351
|
-
|
|
4384
|
+
path19.unshift(current);
|
|
4352
4385
|
current = parent.get(current);
|
|
4353
4386
|
}
|
|
4354
|
-
if (
|
|
4355
|
-
chains.push(
|
|
4387
|
+
if (path19.length >= 3) {
|
|
4388
|
+
chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
4356
4389
|
}
|
|
4357
4390
|
}
|
|
4358
4391
|
return chains.sort();
|
|
@@ -4396,14 +4429,14 @@ function collectIndirectDependents(graph, directlyAffected) {
|
|
|
4396
4429
|
}
|
|
4397
4430
|
for (const [node] of parent) {
|
|
4398
4431
|
if (directSet.has(node)) continue;
|
|
4399
|
-
const
|
|
4432
|
+
const path19 = [node];
|
|
4400
4433
|
let current = node;
|
|
4401
4434
|
while (parent.has(current)) {
|
|
4402
4435
|
current = parent.get(current);
|
|
4403
|
-
|
|
4436
|
+
path19.push(current);
|
|
4404
4437
|
}
|
|
4405
|
-
const chain =
|
|
4406
|
-
const depth =
|
|
4438
|
+
const chain = path19.map((p) => `<- ${p}`).join(" ");
|
|
4439
|
+
const depth = path19.length;
|
|
4407
4440
|
const existing = bestChain.get(node);
|
|
4408
4441
|
if (!existing || depth < existing.depth) {
|
|
4409
4442
|
bestChain.set(node, { chain, depth });
|
|
@@ -4845,6 +4878,7 @@ function registerFlowsCommand(program2) {
|
|
|
4845
4878
|
participants: flow.nodes.length,
|
|
4846
4879
|
nodes: flow.nodes.sort()
|
|
4847
4880
|
};
|
|
4881
|
+
if (flow.description) entry.description = flow.description;
|
|
4848
4882
|
if (flow.aspects && flow.aspects.length > 0) entry.aspects = flow.aspects;
|
|
4849
4883
|
return entry;
|
|
4850
4884
|
});
|