@chrisdudek/yg 2.5.0 → 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 +220 -181
- package/dist/bin.js.map +1 -1
- package/dist/templates/rules.ts +28 -12
- package/graph-schemas/yg-flow.yaml +2 -1
- 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\`.
|
|
@@ -493,7 +509,7 @@ yg drift-sync --node <path> [--recursive] | --all
|
|
|
493
509
|
| Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
|
|
494
510
|
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
495
511
|
| Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
|
|
496
|
-
| Business process participation | Flow (\`yg-flow.yaml
|
|
512
|
+
| Business process participation | Flow (\`yg-flow.yaml nodes\`) |
|
|
497
513
|
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
498
514
|
| Context shared across a domain | Parent node artifact |
|
|
499
515
|
| Technology stack | Node artifact at appropriate hierarchy level |
|
|
@@ -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,13 +1523,18 @@ 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
|
}
|
|
1512
|
-
const
|
|
1526
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1527
|
+
const nodes = raw.nodes ?? raw.participants;
|
|
1513
1528
|
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
1514
|
-
throw new Error(
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
`yg-flow.yaml at ${flowYamlPath}: 'nodes' (or 'participants') must be a non-empty array`
|
|
1531
|
+
);
|
|
1515
1532
|
}
|
|
1516
1533
|
const nodePaths = nodes.filter((n) => typeof n === "string");
|
|
1517
1534
|
if (nodePaths.length === 0) {
|
|
1518
|
-
throw new Error(
|
|
1535
|
+
throw new Error(
|
|
1536
|
+
`yg-flow.yaml at ${flowYamlPath}: 'nodes' (or 'participants') must contain string node paths`
|
|
1537
|
+
);
|
|
1519
1538
|
}
|
|
1520
1539
|
let aspects;
|
|
1521
1540
|
if (raw.aspects !== void 0) {
|
|
@@ -1529,6 +1548,7 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1529
1548
|
return {
|
|
1530
1549
|
path: path6.basename(flowDir),
|
|
1531
1550
|
name: raw.name.trim(),
|
|
1551
|
+
description,
|
|
1532
1552
|
nodes: nodePaths,
|
|
1533
1553
|
...aspects !== void 0 && { aspects },
|
|
1534
1554
|
artifacts
|
|
@@ -1732,19 +1752,20 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
|
1732
1752
|
}
|
|
1733
1753
|
}
|
|
1734
1754
|
async function loadFlows(flowsDir) {
|
|
1755
|
+
let entries;
|
|
1735
1756
|
try {
|
|
1736
|
-
|
|
1737
|
-
const flows = [];
|
|
1738
|
-
for (const entry of entries) {
|
|
1739
|
-
if (!entry.isDirectory()) continue;
|
|
1740
|
-
const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
|
|
1741
|
-
const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
|
|
1742
|
-
flows.push(flow);
|
|
1743
|
-
}
|
|
1744
|
-
return flows;
|
|
1757
|
+
entries = await readdir4(flowsDir, { withFileTypes: true });
|
|
1745
1758
|
} catch {
|
|
1746
1759
|
return [];
|
|
1747
1760
|
}
|
|
1761
|
+
const flows = [];
|
|
1762
|
+
for (const entry of entries) {
|
|
1763
|
+
if (!entry.isDirectory()) continue;
|
|
1764
|
+
const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
|
|
1765
|
+
const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
|
|
1766
|
+
flows.push(flow);
|
|
1767
|
+
}
|
|
1768
|
+
return flows;
|
|
1748
1769
|
}
|
|
1749
1770
|
async function loadSchemas(schemasDir) {
|
|
1750
1771
|
try {
|
|
@@ -1774,6 +1795,7 @@ function estimateTokens(text) {
|
|
|
1774
1795
|
// src/core/context-builder.ts
|
|
1775
1796
|
var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
1776
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"]);
|
|
1777
1799
|
async function buildContext(graph, nodePath) {
|
|
1778
1800
|
const node = graph.nodes.get(nodePath);
|
|
1779
1801
|
if (!node) {
|
|
@@ -2132,7 +2154,7 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2132
2154
|
});
|
|
2133
2155
|
const participatingFlows = collectParticipatingFlows(graph, node);
|
|
2134
2156
|
const flowRefs = participatingFlows.map((f) => {
|
|
2135
|
-
const ref = { path: f.path
|
|
2157
|
+
const ref = { path: f.path };
|
|
2136
2158
|
if (f.aspects?.length) ref.aspects = f.aspects;
|
|
2137
2159
|
return ref;
|
|
2138
2160
|
});
|
|
@@ -2140,7 +2162,7 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2140
2162
|
const hierarchyRefs = ancestors.map((a) => {
|
|
2141
2163
|
const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2142
2164
|
const expanded = expandAspects(nodeAspectIds, graph.aspects);
|
|
2143
|
-
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}`) };
|
|
2144
2166
|
});
|
|
2145
2167
|
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
2146
2168
|
const depRefs = [];
|
|
@@ -2152,23 +2174,26 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2152
2174
|
const depHierarchy = depAncestors.map((a) => {
|
|
2153
2175
|
const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2154
2176
|
const expanded = expandAspects(ids, graph.aspects);
|
|
2155
|
-
|
|
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}`) : [] };
|
|
2156
2179
|
});
|
|
2157
2180
|
const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
|
|
2158
2181
|
const ref = {
|
|
2159
2182
|
path: target.path,
|
|
2160
2183
|
name: target.meta.name,
|
|
2161
2184
|
type: target.meta.type,
|
|
2185
|
+
description: target.meta.description,
|
|
2162
2186
|
relation: relation.type,
|
|
2163
2187
|
aspects: depEffectiveAspects,
|
|
2164
|
-
hierarchy: depHierarchy
|
|
2188
|
+
hierarchy: depHierarchy,
|
|
2189
|
+
files: buildDepNodeFiles(target, config, `model/${target.path}`)
|
|
2165
2190
|
};
|
|
2166
2191
|
if (relation.consumes?.length) ref.consumes = relation.consumes;
|
|
2167
2192
|
if (relation.failure) ref.failure = relation.failure;
|
|
2168
2193
|
if (relation.event_name) ref["event-name"] = relation.event_name;
|
|
2169
2194
|
depRefs.push(ref);
|
|
2170
2195
|
}
|
|
2171
|
-
const
|
|
2196
|
+
const glossary = buildGlossary(node, depRefs, graph);
|
|
2172
2197
|
const breakdown = computeBudgetBreakdown(pkg2, graph);
|
|
2173
2198
|
const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
|
|
2174
2199
|
const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
|
|
@@ -2180,56 +2205,29 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2180
2205
|
path: pkg2.nodePath,
|
|
2181
2206
|
name: pkg2.nodeName,
|
|
2182
2207
|
type: node.meta.type,
|
|
2208
|
+
description: node.meta.description,
|
|
2183
2209
|
mappings: normalizeMappingPaths(node.meta.mapping),
|
|
2184
2210
|
aspects: nodeAspects,
|
|
2185
|
-
flows: flowRefs
|
|
2211
|
+
flows: flowRefs,
|
|
2212
|
+
files: buildNodeFiles(node, config, `model/${pkg2.nodePath}`)
|
|
2186
2213
|
},
|
|
2187
2214
|
hierarchy: hierarchyRefs,
|
|
2188
2215
|
dependencies: depRefs,
|
|
2189
|
-
|
|
2216
|
+
glossary
|
|
2190
2217
|
};
|
|
2191
2218
|
}
|
|
2192
|
-
function
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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) {
|
|
2197
2229
|
const aspects = {};
|
|
2198
2230
|
const flows = {};
|
|
2199
|
-
function addNodeEntry(n, includeYgNodeYaml, filter) {
|
|
2200
|
-
if (nodes[n.path]) return;
|
|
2201
|
-
const files = [];
|
|
2202
|
-
if (includeYgNodeYaml) {
|
|
2203
|
-
files.push(`model/${n.path}/yg-node.yaml`);
|
|
2204
|
-
}
|
|
2205
|
-
for (const filename of filter) {
|
|
2206
|
-
if (n.artifacts.some((a) => a.filename === filename)) {
|
|
2207
|
-
files.push(`model/${n.path}/${filename}`);
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
if (files.length > 0) {
|
|
2211
|
-
nodes[n.path] = { files };
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
addNodeEntry(node, true, [...configArtifactKeys]);
|
|
2215
|
-
for (const ancestor of ancestors) {
|
|
2216
|
-
addNodeEntry(ancestor, true, [...configArtifactKeys]);
|
|
2217
|
-
}
|
|
2218
|
-
const seenDepAncestors = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
|
|
2219
|
-
for (const dep of dependencies) {
|
|
2220
|
-
const target = graph.nodes.get(dep.path);
|
|
2221
|
-
if (target) {
|
|
2222
|
-
addNodeEntry(target, false, structuralFilenames);
|
|
2223
|
-
}
|
|
2224
|
-
for (const ancestor of dep.hierarchy) {
|
|
2225
|
-
if (seenDepAncestors.has(ancestor.path)) continue;
|
|
2226
|
-
seenDepAncestors.add(ancestor.path);
|
|
2227
|
-
const ancestorNode = graph.nodes.get(ancestor.path);
|
|
2228
|
-
if (ancestorNode) {
|
|
2229
|
-
addNodeEntry(ancestorNode, false, structuralFilenames);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
2231
|
const allAspectIds = collectEffectiveAspectIds(graph, node.path);
|
|
2234
2232
|
for (const dep of dependencies) {
|
|
2235
2233
|
for (const id of dep.aspects) {
|
|
@@ -2238,33 +2236,29 @@ function buildArtifactRegistry(node, ancestors, dependencies, graph) {
|
|
|
2238
2236
|
}
|
|
2239
2237
|
const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
|
|
2240
2238
|
for (const aspect of resolvedAspects) {
|
|
2241
|
-
const files =
|
|
2242
|
-
files.push(`aspects/${aspect.id}/yg-aspect.yaml`);
|
|
2243
|
-
for (const art of aspect.artifacts) {
|
|
2244
|
-
files.push(`aspects/${aspect.id}/${art.filename}`);
|
|
2245
|
-
}
|
|
2239
|
+
const files = aspect.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `aspects/${aspect.id}/${a.filename}`);
|
|
2246
2240
|
const entry = {
|
|
2247
2241
|
name: aspect.name,
|
|
2248
2242
|
files
|
|
2249
2243
|
};
|
|
2244
|
+
if (aspect.description) entry.description = aspect.description;
|
|
2245
|
+
if (aspect.stability) entry.stability = aspect.stability;
|
|
2250
2246
|
if (aspect.implies?.length) entry.implies = aspect.implies;
|
|
2251
2247
|
aspects[aspect.id] = entry;
|
|
2252
2248
|
}
|
|
2253
2249
|
const participatingFlows = collectParticipatingFlows(graph, node);
|
|
2254
2250
|
for (const flow of participatingFlows) {
|
|
2255
|
-
const files =
|
|
2256
|
-
files.push(`flows/${flow.path}/yg-flow.yaml`);
|
|
2257
|
-
for (const art of flow.artifacts) {
|
|
2258
|
-
files.push(`flows/${flow.path}/${art.filename}`);
|
|
2259
|
-
}
|
|
2251
|
+
const files = flow.artifacts.filter((a) => !YG_YAML_FILES.has(a.filename)).map((a) => `flows/${flow.path}/${a.filename}`);
|
|
2260
2252
|
const entry = {
|
|
2261
2253
|
name: flow.name,
|
|
2254
|
+
participants: flow.nodes,
|
|
2262
2255
|
files
|
|
2263
2256
|
};
|
|
2257
|
+
if (flow.description) entry.description = flow.description;
|
|
2264
2258
|
if (flow.aspects?.length) entry.aspects = flow.aspects;
|
|
2265
2259
|
flows[flow.path] = entry;
|
|
2266
2260
|
}
|
|
2267
|
-
return {
|
|
2261
|
+
return { aspects, flows };
|
|
2268
2262
|
}
|
|
2269
2263
|
function collectEffectiveAspectIds(graph, nodePath) {
|
|
2270
2264
|
const node = graph.nodes.get(nodePath);
|
|
@@ -2285,23 +2279,39 @@ function collectEffectiveAspectIds(graph, nodePath) {
|
|
|
2285
2279
|
}
|
|
2286
2280
|
|
|
2287
2281
|
// src/formatters/context-text.ts
|
|
2288
|
-
import {
|
|
2282
|
+
import { Document } from "yaml";
|
|
2289
2283
|
function formatContextYaml(data) {
|
|
2290
|
-
const output = {
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
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
|
|
2300
2294
|
};
|
|
2301
|
-
|
|
2302
|
-
|
|
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
|
+
}
|
|
2303
2313
|
}
|
|
2304
|
-
return
|
|
2314
|
+
return doc.toString({ lineWidth: 0 });
|
|
2305
2315
|
}
|
|
2306
2316
|
function formatFullContent(files) {
|
|
2307
2317
|
if (files.length === 0) return "";
|
|
@@ -2355,6 +2365,7 @@ async function validate(graph, scope = "all") {
|
|
|
2355
2365
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
2356
2366
|
issues.push(...await checkContextBudget(graph));
|
|
2357
2367
|
issues.push(...checkHighFanOut(graph));
|
|
2368
|
+
issues.push(...checkMissingDescriptions(graph));
|
|
2358
2369
|
}
|
|
2359
2370
|
issues.push(...checkSchemas(graph));
|
|
2360
2371
|
issues.push(...checkRelationTargets(graph));
|
|
@@ -3087,6 +3098,41 @@ function pct(value, total) {
|
|
|
3087
3098
|
if (total === 0) return "0%";
|
|
3088
3099
|
return `${Math.round(value / total * 100)}%`;
|
|
3089
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
|
+
}
|
|
3090
3136
|
|
|
3091
3137
|
// src/cli/build-context.ts
|
|
3092
3138
|
function collectRelevantNodePaths(graph, nodePath) {
|
|
@@ -3139,22 +3185,31 @@ function registerBuildCommand(program2) {
|
|
|
3139
3185
|
const mapOutput = toContextMapOutput(pkg2, graph);
|
|
3140
3186
|
let output = formatContextYaml(mapOutput);
|
|
3141
3187
|
if (options.full) {
|
|
3142
|
-
const allFiles = [];
|
|
3143
|
-
const allEntries = [
|
|
3144
|
-
...Object.values(mapOutput.artifacts.nodes),
|
|
3145
|
-
...Object.values(mapOutput.artifacts.aspects),
|
|
3146
|
-
...Object.values(mapOutput.artifacts.flows)
|
|
3147
|
-
];
|
|
3148
3188
|
const seen = /* @__PURE__ */ new Set();
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
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 });
|
|
3196
|
+
}
|
|
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);
|
|
3157
3211
|
}
|
|
3212
|
+
for (const f of dep.files ?? []) await collectFile(f);
|
|
3158
3213
|
}
|
|
3159
3214
|
output += formatFullContent(allFiles);
|
|
3160
3215
|
}
|
|
@@ -3167,14 +3222,6 @@ function registerBuildCommand(program2) {
|
|
|
3167
3222
|
});
|
|
3168
3223
|
}
|
|
3169
3224
|
async function findFileContent(filePath, graph) {
|
|
3170
|
-
async function readYamlFromDisk(relativePath) {
|
|
3171
|
-
try {
|
|
3172
|
-
const fullPath = path12.join(graph.rootPath, relativePath);
|
|
3173
|
-
return (await readFile14(fullPath, "utf-8")).trim();
|
|
3174
|
-
} catch {
|
|
3175
|
-
return void 0;
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
3225
|
if (filePath.startsWith("model/")) {
|
|
3179
3226
|
const rest = filePath.slice("model/".length);
|
|
3180
3227
|
const parts = rest.split("/");
|
|
@@ -3182,9 +3229,6 @@ async function findFileContent(filePath, graph) {
|
|
|
3182
3229
|
const nodePath = parts.join("/");
|
|
3183
3230
|
const node = graph.nodes.get(nodePath);
|
|
3184
3231
|
if (!node) return void 0;
|
|
3185
|
-
if (filename === "yg-node.yaml") {
|
|
3186
|
-
return node.nodeYamlRaw?.trim() ?? await readYamlFromDisk(filePath);
|
|
3187
|
-
}
|
|
3188
3232
|
const art = node.artifacts.find((a) => a.filename === filename);
|
|
3189
3233
|
return art?.content;
|
|
3190
3234
|
}
|
|
@@ -3195,9 +3239,6 @@ async function findFileContent(filePath, graph) {
|
|
|
3195
3239
|
const filename = parts.slice(1).join("/");
|
|
3196
3240
|
const aspect = graph.aspects.find((a) => a.id === aspectId);
|
|
3197
3241
|
if (!aspect) return void 0;
|
|
3198
|
-
if (filename === "yg-aspect.yaml") {
|
|
3199
|
-
return readYamlFromDisk(filePath);
|
|
3200
|
-
}
|
|
3201
3242
|
const art = aspect.artifacts.find((a) => a.filename === filename);
|
|
3202
3243
|
return art?.content;
|
|
3203
3244
|
}
|
|
@@ -3208,9 +3249,6 @@ async function findFileContent(filePath, graph) {
|
|
|
3208
3249
|
const filename = parts.slice(1).join("/");
|
|
3209
3250
|
const flow = graph.flows.find((f) => f.path === flowPath);
|
|
3210
3251
|
if (!flow) return void 0;
|
|
3211
|
-
if (filename === "yg-flow.yaml") {
|
|
3212
|
-
return readYamlFromDisk(filePath);
|
|
3213
|
-
}
|
|
3214
3252
|
const art = flow.artifacts.find((a) => a.filename === filename);
|
|
3215
3253
|
return art?.content;
|
|
3216
3254
|
}
|
|
@@ -3266,12 +3304,12 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
3266
3304
|
import chalk2 from "chalk";
|
|
3267
3305
|
|
|
3268
3306
|
// src/io/drift-state-store.ts
|
|
3269
|
-
import { readFile as
|
|
3270
|
-
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";
|
|
3271
3309
|
import { parse as yamlParse } from "yaml";
|
|
3272
3310
|
var DRIFT_STATE_DIR = ".drift-state";
|
|
3273
3311
|
function nodeStatePath(yggRoot, nodePath) {
|
|
3274
|
-
return
|
|
3312
|
+
return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
|
|
3275
3313
|
}
|
|
3276
3314
|
async function scanJsonFiles(dir, baseDir) {
|
|
3277
3315
|
const results = [];
|
|
@@ -3282,12 +3320,12 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3282
3320
|
return results;
|
|
3283
3321
|
}
|
|
3284
3322
|
for (const entry of entries) {
|
|
3285
|
-
const fullPath =
|
|
3323
|
+
const fullPath = path12.join(dir, entry.name);
|
|
3286
3324
|
if (entry.isDirectory()) {
|
|
3287
3325
|
const nested = await scanJsonFiles(fullPath, baseDir);
|
|
3288
3326
|
results.push(...nested);
|
|
3289
3327
|
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
3290
|
-
const relPath =
|
|
3328
|
+
const relPath = path12.relative(baseDir, fullPath);
|
|
3291
3329
|
const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
|
|
3292
3330
|
results.push(nodePath);
|
|
3293
3331
|
}
|
|
@@ -3295,13 +3333,13 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3295
3333
|
return results;
|
|
3296
3334
|
}
|
|
3297
3335
|
async function removeEmptyParents(filePath, stopDir) {
|
|
3298
|
-
let dir =
|
|
3336
|
+
let dir = path12.dirname(filePath);
|
|
3299
3337
|
while (dir !== stopDir && dir.startsWith(stopDir)) {
|
|
3300
3338
|
try {
|
|
3301
3339
|
const entries = await readdir6(dir);
|
|
3302
3340
|
if (entries.length === 0) {
|
|
3303
3341
|
await rm2(dir, { recursive: true });
|
|
3304
|
-
dir =
|
|
3342
|
+
dir = path12.dirname(dir);
|
|
3305
3343
|
} else {
|
|
3306
3344
|
break;
|
|
3307
3345
|
}
|
|
@@ -3313,7 +3351,7 @@ async function removeEmptyParents(filePath, stopDir) {
|
|
|
3313
3351
|
async function readNodeDriftState(yggRoot, nodePath) {
|
|
3314
3352
|
try {
|
|
3315
3353
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3316
|
-
const content = await
|
|
3354
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3317
3355
|
const parsed = JSON.parse(content);
|
|
3318
3356
|
return parsed;
|
|
3319
3357
|
} catch {
|
|
@@ -3322,12 +3360,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
|
|
|
3322
3360
|
}
|
|
3323
3361
|
async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
3324
3362
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3325
|
-
await mkdir3(
|
|
3363
|
+
await mkdir3(path12.dirname(filePath), { recursive: true });
|
|
3326
3364
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
3327
3365
|
await writeFile5(filePath, content, "utf-8");
|
|
3328
3366
|
}
|
|
3329
3367
|
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
3330
|
-
const driftDir =
|
|
3368
|
+
const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
|
|
3331
3369
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
3332
3370
|
const removed = [];
|
|
3333
3371
|
for (const nodePath of allNodePaths) {
|
|
@@ -3341,7 +3379,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
|
3341
3379
|
return removed.sort();
|
|
3342
3380
|
}
|
|
3343
3381
|
async function readDriftState(yggRoot) {
|
|
3344
|
-
const driftPath =
|
|
3382
|
+
const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
|
|
3345
3383
|
let driftStat;
|
|
3346
3384
|
try {
|
|
3347
3385
|
driftStat = await stat5(driftPath);
|
|
@@ -3349,7 +3387,7 @@ async function readDriftState(yggRoot) {
|
|
|
3349
3387
|
return {};
|
|
3350
3388
|
}
|
|
3351
3389
|
if (driftStat.isFile()) {
|
|
3352
|
-
const content = await
|
|
3390
|
+
const content = await readFile14(driftPath, "utf-8");
|
|
3353
3391
|
let raw;
|
|
3354
3392
|
try {
|
|
3355
3393
|
raw = JSON.parse(content);
|
|
@@ -3381,20 +3419,20 @@ async function readDriftState(yggRoot) {
|
|
|
3381
3419
|
}
|
|
3382
3420
|
|
|
3383
3421
|
// src/utils/hash.ts
|
|
3384
|
-
import { readFile as
|
|
3385
|
-
import
|
|
3422
|
+
import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
3423
|
+
import path13 from "path";
|
|
3386
3424
|
import { createHash } from "crypto";
|
|
3387
3425
|
import { createRequire } from "module";
|
|
3388
3426
|
var require2 = createRequire(import.meta.url);
|
|
3389
3427
|
var ignoreFactory = require2("ignore");
|
|
3390
3428
|
async function hashFile(filePath) {
|
|
3391
|
-
const content = await
|
|
3429
|
+
const content = await readFile15(filePath);
|
|
3392
3430
|
return createHash("sha256").update(content).digest("hex");
|
|
3393
3431
|
}
|
|
3394
3432
|
async function loadRootGitignoreStack(projectRoot) {
|
|
3395
3433
|
if (!projectRoot) return [];
|
|
3396
3434
|
try {
|
|
3397
|
-
const content = await
|
|
3435
|
+
const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
|
|
3398
3436
|
const matcher = ignoreFactory();
|
|
3399
3437
|
matcher.add(content);
|
|
3400
3438
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -3404,7 +3442,7 @@ async function loadRootGitignoreStack(projectRoot) {
|
|
|
3404
3442
|
}
|
|
3405
3443
|
function isIgnoredByStack(candidatePath, stack) {
|
|
3406
3444
|
for (const { basePath, matcher } of stack) {
|
|
3407
|
-
const relativePath =
|
|
3445
|
+
const relativePath = path13.relative(basePath, candidatePath);
|
|
3408
3446
|
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
3409
3447
|
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
3410
3448
|
}
|
|
@@ -3419,7 +3457,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3419
3457
|
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
3420
3458
|
const allFiles = [];
|
|
3421
3459
|
for (const tf of trackedFiles) {
|
|
3422
|
-
const absPath =
|
|
3460
|
+
const absPath = path13.join(projectRoot, tf.path);
|
|
3423
3461
|
try {
|
|
3424
3462
|
const st = await stat6(absPath);
|
|
3425
3463
|
if (st.isDirectory()) {
|
|
@@ -3429,7 +3467,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3429
3467
|
});
|
|
3430
3468
|
for (const entry of dirEntries) {
|
|
3431
3469
|
allFiles.push({
|
|
3432
|
-
relPath:
|
|
3470
|
+
relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
3433
3471
|
absPath: entry.absPath,
|
|
3434
3472
|
mtimeMs: entry.mtimeMs
|
|
3435
3473
|
});
|
|
@@ -3469,7 +3507,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3469
3507
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
3470
3508
|
let stack = options.gitignoreStack ?? [];
|
|
3471
3509
|
try {
|
|
3472
|
-
const localContent = await
|
|
3510
|
+
const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
|
|
3473
3511
|
const localMatcher = ignoreFactory();
|
|
3474
3512
|
localMatcher.add(localContent);
|
|
3475
3513
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
@@ -3479,7 +3517,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3479
3517
|
const dirs = [];
|
|
3480
3518
|
const files = [];
|
|
3481
3519
|
for (const entry of entries) {
|
|
3482
|
-
const absoluteChildPath =
|
|
3520
|
+
const absoluteChildPath = path13.join(directoryPath, entry.name);
|
|
3483
3521
|
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
3484
3522
|
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
3485
3523
|
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
@@ -3492,7 +3530,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3492
3530
|
Promise.all(files.map(async (f) => {
|
|
3493
3531
|
const fileStat = await stat6(f);
|
|
3494
3532
|
return {
|
|
3495
|
-
relPath:
|
|
3533
|
+
relPath: path13.relative(rootDirectoryPath, f),
|
|
3496
3534
|
absPath: f,
|
|
3497
3535
|
mtimeMs: fileStat.mtimeMs
|
|
3498
3536
|
};
|
|
@@ -3505,14 +3543,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3505
3543
|
}
|
|
3506
3544
|
|
|
3507
3545
|
// src/core/context-files.ts
|
|
3508
|
-
import
|
|
3546
|
+
import path14 from "path";
|
|
3509
3547
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3510
3548
|
function collectTrackedFiles(node, graph) {
|
|
3511
3549
|
const seen = /* @__PURE__ */ new Set();
|
|
3512
3550
|
const result = [];
|
|
3513
|
-
const projectRoot =
|
|
3514
|
-
const yggPrefix =
|
|
3515
|
-
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("/");
|
|
3516
3554
|
const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
|
|
3517
3555
|
function addFile(filePath, category) {
|
|
3518
3556
|
if (seen.has(filePath)) return;
|
|
@@ -3625,7 +3663,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
|
|
|
3625
3663
|
|
|
3626
3664
|
// src/core/drift-detector.ts
|
|
3627
3665
|
import { access as access2 } from "fs/promises";
|
|
3628
|
-
import
|
|
3666
|
+
import path15 from "path";
|
|
3629
3667
|
function getChildMappingExclusions(graph, nodePath) {
|
|
3630
3668
|
const node = graph.nodes.get(nodePath);
|
|
3631
3669
|
if (!node) return [];
|
|
@@ -3647,7 +3685,7 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
3647
3685
|
return exclusions;
|
|
3648
3686
|
}
|
|
3649
3687
|
async function detectDrift(graph, filterNodePath) {
|
|
3650
|
-
const projectRoot =
|
|
3688
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
3651
3689
|
const driftState = await readDriftState(graph.rootPath);
|
|
3652
3690
|
const entries = [];
|
|
3653
3691
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -3729,14 +3767,14 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
3729
3767
|
};
|
|
3730
3768
|
}
|
|
3731
3769
|
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
3732
|
-
const yggPrefix =
|
|
3733
|
-
const normalizedPrefix = yggPrefix.split(
|
|
3770
|
+
const yggPrefix = path15.relative(projectRoot, _rootPath);
|
|
3771
|
+
const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
|
|
3734
3772
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
3735
3773
|
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
3736
3774
|
}
|
|
3737
3775
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
3738
3776
|
for (const mp of mappingPaths) {
|
|
3739
|
-
const absPath =
|
|
3777
|
+
const absPath = path15.join(projectRoot, mp);
|
|
3740
3778
|
try {
|
|
3741
3779
|
await access2(absPath);
|
|
3742
3780
|
return false;
|
|
@@ -3746,7 +3784,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
3746
3784
|
return true;
|
|
3747
3785
|
}
|
|
3748
3786
|
async function syncDriftState(graph, nodePath) {
|
|
3749
|
-
const projectRoot =
|
|
3787
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
3750
3788
|
const node = graph.nodes.get(nodePath);
|
|
3751
3789
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
3752
3790
|
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
@@ -4066,10 +4104,10 @@ function registerTreeCommand(program2) {
|
|
|
4066
4104
|
let roots;
|
|
4067
4105
|
let showProjectName;
|
|
4068
4106
|
if (options.root?.trim()) {
|
|
4069
|
-
const
|
|
4070
|
-
const node = graph.nodes.get(
|
|
4107
|
+
const path19 = options.root.trim().replace(/\/$/, "");
|
|
4108
|
+
const node = graph.nodes.get(path19);
|
|
4071
4109
|
if (!node) {
|
|
4072
|
-
process.stderr.write(`Error: path '${
|
|
4110
|
+
process.stderr.write(`Error: path '${path19}' not found
|
|
4073
4111
|
`);
|
|
4074
4112
|
process.exit(1);
|
|
4075
4113
|
}
|
|
@@ -4113,7 +4151,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
4113
4151
|
}
|
|
4114
4152
|
|
|
4115
4153
|
// src/cli/owner.ts
|
|
4116
|
-
import
|
|
4154
|
+
import path16 from "path";
|
|
4117
4155
|
import { access as access3 } from "fs/promises";
|
|
4118
4156
|
function normalizeForMatch(inputPath) {
|
|
4119
4157
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -4143,11 +4181,11 @@ function registerOwnerCommand(program2) {
|
|
|
4143
4181
|
const graph = await loadGraph(cwd);
|
|
4144
4182
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
4145
4183
|
const rawPath = options.file.trim();
|
|
4146
|
-
const absolute =
|
|
4147
|
-
const repoRelative =
|
|
4184
|
+
const absolute = path16.resolve(cwd, rawPath);
|
|
4185
|
+
const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
|
|
4148
4186
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
4149
4187
|
if (!result.nodePath) {
|
|
4150
|
-
const absPath =
|
|
4188
|
+
const absPath = path16.resolve(repoRoot, result.file);
|
|
4151
4189
|
let exists = true;
|
|
4152
4190
|
try {
|
|
4153
4191
|
await access3(absPath);
|
|
@@ -4181,7 +4219,7 @@ function registerOwnerCommand(program2) {
|
|
|
4181
4219
|
|
|
4182
4220
|
// src/core/dependency-resolver.ts
|
|
4183
4221
|
import { execSync } from "child_process";
|
|
4184
|
-
import
|
|
4222
|
+
import path17 from "path";
|
|
4185
4223
|
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
4186
4224
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
4187
4225
|
function filterRelationType(relType, filter) {
|
|
@@ -4258,7 +4296,7 @@ function registerDepsCommand(program2) {
|
|
|
4258
4296
|
// src/core/graph-from-git.ts
|
|
4259
4297
|
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
4260
4298
|
import { tmpdir } from "os";
|
|
4261
|
-
import
|
|
4299
|
+
import path18 from "path";
|
|
4262
4300
|
import { execSync as execSync2 } from "child_process";
|
|
4263
4301
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
4264
4302
|
const yggPath = ".yggdrasil";
|
|
@@ -4269,8 +4307,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
4269
4307
|
return null;
|
|
4270
4308
|
}
|
|
4271
4309
|
try {
|
|
4272
|
-
tmpDir = await mkdtemp(
|
|
4273
|
-
const archivePath =
|
|
4310
|
+
tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
|
|
4311
|
+
const archivePath = path18.join(tmpDir, "archive.tar");
|
|
4274
4312
|
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
4275
4313
|
cwd: projectRoot,
|
|
4276
4314
|
stdio: "pipe"
|
|
@@ -4340,14 +4378,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
4340
4378
|
}
|
|
4341
4379
|
const chains = [];
|
|
4342
4380
|
for (const node of transitiveOnly) {
|
|
4343
|
-
const
|
|
4381
|
+
const path19 = [];
|
|
4344
4382
|
let current = node;
|
|
4345
4383
|
while (current) {
|
|
4346
|
-
|
|
4384
|
+
path19.unshift(current);
|
|
4347
4385
|
current = parent.get(current);
|
|
4348
4386
|
}
|
|
4349
|
-
if (
|
|
4350
|
-
chains.push(
|
|
4387
|
+
if (path19.length >= 3) {
|
|
4388
|
+
chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
4351
4389
|
}
|
|
4352
4390
|
}
|
|
4353
4391
|
return chains.sort();
|
|
@@ -4391,14 +4429,14 @@ function collectIndirectDependents(graph, directlyAffected) {
|
|
|
4391
4429
|
}
|
|
4392
4430
|
for (const [node] of parent) {
|
|
4393
4431
|
if (directSet.has(node)) continue;
|
|
4394
|
-
const
|
|
4432
|
+
const path19 = [node];
|
|
4395
4433
|
let current = node;
|
|
4396
4434
|
while (parent.has(current)) {
|
|
4397
4435
|
current = parent.get(current);
|
|
4398
|
-
|
|
4436
|
+
path19.push(current);
|
|
4399
4437
|
}
|
|
4400
|
-
const chain =
|
|
4401
|
-
const depth =
|
|
4438
|
+
const chain = path19.map((p) => `<- ${p}`).join(" ");
|
|
4439
|
+
const depth = path19.length;
|
|
4402
4440
|
const existing = bestChain.get(node);
|
|
4403
4441
|
if (!existing || depth < existing.depth) {
|
|
4404
4442
|
bestChain.set(node, { chain, depth });
|
|
@@ -4840,6 +4878,7 @@ function registerFlowsCommand(program2) {
|
|
|
4840
4878
|
participants: flow.nodes.length,
|
|
4841
4879
|
nodes: flow.nodes.sort()
|
|
4842
4880
|
};
|
|
4881
|
+
if (flow.description) entry.description = flow.description;
|
|
4843
4882
|
if (flow.aspects && flow.aspects.length > 0) entry.aspects = flow.aspects;
|
|
4844
4883
|
return entry;
|
|
4845
4884
|
});
|