@chrisdudek/yg 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +225 -21
- package/dist/bin.js.map +1 -1
- package/dist/templates/rules.ts +32 -2
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -92,11 +92,29 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
|
|
|
92
92
|
4. **Always capture why \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
|
|
93
93
|
5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
|
|
94
94
|
|
|
95
|
+
### Recognizing Graph-Required Actions
|
|
96
|
+
|
|
97
|
+
What matters is the ACTION you are performing, not what instructed it. If the action involves understanding mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a workflow step, or your own initiative.
|
|
98
|
+
|
|
99
|
+
**Actions that require \`yg owner\` + \`yg build-context\` first:**
|
|
100
|
+
|
|
101
|
+
- Reading or exploring source files to understand a component
|
|
102
|
+
- Proposing approaches, designs, or plans for changing code
|
|
103
|
+
- Reviewing or debugging code
|
|
104
|
+
- Any form of reasoning about how mapped code works or should change
|
|
105
|
+
|
|
106
|
+
**Actions that do NOT require yg:**
|
|
107
|
+
|
|
108
|
+
- Git operations (log, diff, status, blame)
|
|
109
|
+
- Reading documentation, READMEs, or config files outside \`.yggdrasil/\`
|
|
110
|
+
- Running tests, builds, or linters
|
|
111
|
+
- Working with files that \`yg owner\` reports as unmapped
|
|
112
|
+
|
|
95
113
|
### Failure States
|
|
96
114
|
|
|
97
115
|
You have broken Yggdrasil if you do any of the following:
|
|
98
116
|
|
|
99
|
-
- \u274C Worked on a mapped file without running \`yg owner\` + \`yg build-context\` first \u2014
|
|
117
|
+
- \u274C Worked on a mapped file without running \`yg owner\` + \`yg build-context\` first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
|
|
100
118
|
- \u274C Modified source code without updating graph artifacts in the same response, or vice versa.
|
|
101
119
|
- \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
|
|
102
120
|
- \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
|
|
@@ -206,6 +224,7 @@ Per area checklist:
|
|
|
206
224
|
- [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
|
|
207
225
|
- [ ] 5. Identify relations \u2014 add to \`node.yaml\`
|
|
208
226
|
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
227
|
+
- [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add to \`node.yaml\` \`anchors\` field
|
|
209
228
|
- [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
|
|
210
229
|
- [ ] 8. \`yg validate\` \u2014 fix errors
|
|
211
230
|
- [ ] 9. \`yg drift-sync --node <path>\`
|
|
@@ -308,6 +327,8 @@ Three artifacts capture node knowledge at three levels:
|
|
|
308
327
|
- **interface.md** (required when node has consumers) \u2014 HOW TO USE: public methods, parameters, return types, contracts, failure modes, exposed data structures. Everything another node needs to interact with this one.
|
|
309
328
|
- **internals.md** (optional, highest value for cross-module nodes) \u2014 HOW IT WORKS + WHY: algorithms, control flow, business rules, invariants, state machines, lifecycle, and design decisions with rejected alternatives. Use sections within the file: ## Logic, ## Constraints, ## State, ## Decisions (with "Chose X over Y because Z" format).
|
|
310
329
|
|
|
330
|
+
**Enrichment priority (when adding incrementally):** \`interface.md\` first (highest cross-module ROI \u2014 contracts enable other nodes to reason about interactions), then \`responsibility.md\` (identity and boundaries), then \`internals.md\` (depth for complex nodes). A node with only \`interface.md\` provides more cross-module value than one with only \`internals.md\`.
|
|
331
|
+
|
|
311
332
|
### Context Assembly
|
|
312
333
|
|
|
313
334
|
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global config, hierarchy, own artifacts, aspects, and relational context. It is your architectural map. For implementation-level claims (exact call patterns, error handling, await vs fire-and-forget) \u2014 verify against source code. If the package is insufficient, enrich the graph.
|
|
@@ -343,6 +364,14 @@ When a node follows an aspect's pattern with exceptions, record exceptions in \`
|
|
|
343
364
|
|
|
344
365
|
**Aspect lifecycle warning.** Aspects decay CATASTROPHICALLY \u2014 a pattern either exists or it doesn't. When a pattern changes, ALL aspect claims become wrong at once. This differs from other artifacts: \`interface.md\` and \`responsibility.md\` are most stable (~9-year half-life); \`internals.md\` has moderate stability (~2.5-year half-life); aspects are least stable (~2.4-year half-life, binary decay). After any significant feature addition, review ALL aspects touching the affected area. Don't wait for drift \u2014 aspects can be 100% wrong without any mapped file changing.
|
|
345
366
|
|
|
367
|
+
**Aspect stability tiers.** If an aspect has a \`stability\` field in \`aspect.yaml\`, use it to calibrate review urgency:
|
|
368
|
+
|
|
369
|
+
- \`schema\` \u2014 enforced by data model; review only when data model changes (most stable)
|
|
370
|
+
- \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
|
|
371
|
+
- \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
|
|
372
|
+
|
|
373
|
+
When code anchors (\`anchors\` field in \`node.yaml\`) are present for an aspect, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
|
|
374
|
+
|
|
346
375
|
### Creating Flows
|
|
347
376
|
|
|
348
377
|
- [ ] 1. Read \`schemas/flow.yaml\`
|
|
@@ -379,6 +408,7 @@ yg flows List flows with metadata (YAML output).
|
|
|
379
408
|
yg deps --node <path> [--depth N] [--type structural|event|all]
|
|
380
409
|
Show dependencies.
|
|
381
410
|
yg impact --node <path> --simulate Simulate blast radius of a planned change.
|
|
411
|
+
yg impact --node <path> --method <name> Filter impact to dependents consuming a specific method.
|
|
382
412
|
yg impact --aspect <id> Show all nodes where aspect is effective.
|
|
383
413
|
yg impact --flow <name> Show flow participants and descendants.
|
|
384
414
|
yg status Graph health: nodes, coverage, drift summary.
|
|
@@ -405,7 +435,7 @@ yg journal-archive Archive consolidated journal entries.
|
|
|
405
435
|
| Context shared across a domain | Parent node artifact |
|
|
406
436
|
| Technology stack | \`config.yaml stack\` (+ \`rationale\` field) |
|
|
407
437
|
| Global coding standards | \`config.yaml standards\` |`;
|
|
408
|
-
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n");
|
|
438
|
+
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
|
|
409
439
|
|
|
410
440
|
// src/templates/platform.ts
|
|
411
441
|
var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
|
|
@@ -862,6 +892,7 @@ async function parseNodeYaml(filePath) {
|
|
|
862
892
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
863
893
|
const aspects = parseStringArray(raw.aspects) ?? parseStringArray(raw.tags);
|
|
864
894
|
const aspectExceptions = parseAspectExceptions(raw.aspect_exceptions, aspects, filePath);
|
|
895
|
+
const anchors = parseAnchors(raw.anchors, filePath);
|
|
865
896
|
return {
|
|
866
897
|
name: raw.name.trim(),
|
|
867
898
|
type: raw.type.trim(),
|
|
@@ -869,9 +900,37 @@ async function parseNodeYaml(filePath) {
|
|
|
869
900
|
aspect_exceptions: aspectExceptions,
|
|
870
901
|
blackbox: raw.blackbox ?? false,
|
|
871
902
|
relations: relations.length > 0 ? relations : void 0,
|
|
872
|
-
mapping
|
|
903
|
+
mapping,
|
|
904
|
+
anchors
|
|
873
905
|
};
|
|
874
906
|
}
|
|
907
|
+
function parseAnchors(raw, filePath) {
|
|
908
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
909
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`node.yaml at ${filePath}: 'anchors' must be an object mapping aspect ids to arrays of strings`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
const obj = raw;
|
|
915
|
+
const entries = Object.entries(obj);
|
|
916
|
+
if (entries.length === 0) return void 0;
|
|
917
|
+
const result = {};
|
|
918
|
+
for (const [key, value] of entries) {
|
|
919
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const strings = value.filter((v) => typeof v === "string");
|
|
925
|
+
if (strings.length === 0) {
|
|
926
|
+
throw new Error(
|
|
927
|
+
`node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
result[key] = strings;
|
|
931
|
+
}
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
875
934
|
function parseAspectExceptions(raw, aspects, filePath) {
|
|
876
935
|
if (raw === void 0 || raw === null) return void 0;
|
|
877
936
|
if (!Array.isArray(raw)) {
|
|
@@ -1002,6 +1061,7 @@ async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles
|
|
|
1002
1061
|
}
|
|
1003
1062
|
|
|
1004
1063
|
// src/io/aspect-parser.ts
|
|
1064
|
+
var VALID_STABILITY_VALUES = ["schema", "protocol", "implementation"];
|
|
1005
1065
|
async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
1006
1066
|
const idTrimmed = id?.trim() ?? "";
|
|
1007
1067
|
if (!idTrimmed) {
|
|
@@ -1024,11 +1084,21 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1024
1084
|
}
|
|
1025
1085
|
implies = raw.implies.filter((t) => typeof t === "string");
|
|
1026
1086
|
}
|
|
1087
|
+
let stability;
|
|
1088
|
+
if (raw.stability !== void 0) {
|
|
1089
|
+
if (typeof raw.stability !== "string" || !VALID_STABILITY_VALUES.includes(raw.stability)) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`Aspect file ${aspectYamlPath}: 'stability' must be one of: ${VALID_STABILITY_VALUES.join(", ")}`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
stability = raw.stability;
|
|
1095
|
+
}
|
|
1027
1096
|
return {
|
|
1028
1097
|
name: raw.name.trim(),
|
|
1029
1098
|
id: idTrimmed,
|
|
1030
1099
|
description,
|
|
1031
1100
|
implies,
|
|
1101
|
+
stability,
|
|
1032
1102
|
artifacts
|
|
1033
1103
|
};
|
|
1034
1104
|
}
|
|
@@ -1538,6 +1608,10 @@ Consumes: ${relation.consumes.join(", ")}`;
|
|
|
1538
1608
|
function buildAspectLayer(aspect, exceptionNote) {
|
|
1539
1609
|
let content = aspect.artifacts.map((a) => `### ${a.filename}
|
|
1540
1610
|
${a.content}`).join("\n\n");
|
|
1611
|
+
if (aspect.stability) {
|
|
1612
|
+
content += `
|
|
1613
|
+
**Stability tier:** ${aspect.stability}`;
|
|
1614
|
+
}
|
|
1541
1615
|
if (exceptionNote) {
|
|
1542
1616
|
content += `
|
|
1543
1617
|
|
|
@@ -1613,7 +1687,7 @@ function collectEffectiveAspectIds(graph, nodePath) {
|
|
|
1613
1687
|
}
|
|
1614
1688
|
|
|
1615
1689
|
// src/core/validator.ts
|
|
1616
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
1690
|
+
import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
|
|
1617
1691
|
import path9 from "path";
|
|
1618
1692
|
var RESERVED_DIRS = /* @__PURE__ */ new Set();
|
|
1619
1693
|
async function validate(graph, scope = "all") {
|
|
@@ -1644,6 +1718,7 @@ async function validate(graph, scope = "all") {
|
|
|
1644
1718
|
issues.push(...checkImpliesNoCycles(graph));
|
|
1645
1719
|
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
1646
1720
|
issues.push(...checkAspectExceptions(graph));
|
|
1721
|
+
issues.push(...await checkAnchorPresence(graph));
|
|
1647
1722
|
issues.push(...checkRequiredArtifacts(graph));
|
|
1648
1723
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
1649
1724
|
issues.push(...await checkContextBudget(graph));
|
|
@@ -2286,6 +2361,81 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2286
2361
|
}
|
|
2287
2362
|
return issues;
|
|
2288
2363
|
}
|
|
2364
|
+
async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
2365
|
+
const files = [];
|
|
2366
|
+
async function collectFiles(absPath) {
|
|
2367
|
+
try {
|
|
2368
|
+
const s = await stat3(absPath);
|
|
2369
|
+
if (s.isFile()) {
|
|
2370
|
+
files.push(absPath);
|
|
2371
|
+
} else if (s.isDirectory()) {
|
|
2372
|
+
const entries = await readdir4(absPath, { withFileTypes: true });
|
|
2373
|
+
for (const entry of entries) {
|
|
2374
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2375
|
+
const entryPath = path9.join(absPath, entry.name);
|
|
2376
|
+
if (entry.isFile()) {
|
|
2377
|
+
files.push(entryPath);
|
|
2378
|
+
} else if (entry.isDirectory()) {
|
|
2379
|
+
await collectFiles(entryPath);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
for (const mp of mappingPaths) {
|
|
2387
|
+
await collectFiles(path9.join(projectRoot, mp));
|
|
2388
|
+
}
|
|
2389
|
+
return files;
|
|
2390
|
+
}
|
|
2391
|
+
async function checkAnchorPresence(graph) {
|
|
2392
|
+
const issues = [];
|
|
2393
|
+
const projectRoot = path9.dirname(graph.rootPath);
|
|
2394
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
2395
|
+
const anchors = node.meta.anchors;
|
|
2396
|
+
if (!anchors) continue;
|
|
2397
|
+
const nodeAspects = new Set(node.meta.aspects ?? []);
|
|
2398
|
+
for (const aspectId of Object.keys(anchors)) {
|
|
2399
|
+
if (!nodeAspects.has(aspectId)) {
|
|
2400
|
+
issues.push({
|
|
2401
|
+
severity: "error",
|
|
2402
|
+
code: "E019",
|
|
2403
|
+
rule: "invalid-anchor-ref",
|
|
2404
|
+
message: `anchors references aspect '${aspectId}' which is not in this node's aspects list (${[...nodeAspects].join(", ") || "none"})`,
|
|
2405
|
+
nodePath
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2410
|
+
if (mappingPaths.length === 0) continue;
|
|
2411
|
+
const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
|
|
2412
|
+
if (sourceFiles.length === 0) continue;
|
|
2413
|
+
const fileContents = [];
|
|
2414
|
+
for (const filePath of sourceFiles) {
|
|
2415
|
+
try {
|
|
2416
|
+
const content = await readFile11(filePath, "utf-8");
|
|
2417
|
+
fileContents.push(content);
|
|
2418
|
+
} catch {
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
for (const [aspectId, anchorList] of Object.entries(anchors)) {
|
|
2422
|
+
if (!nodeAspects.has(aspectId)) continue;
|
|
2423
|
+
for (const anchor of anchorList) {
|
|
2424
|
+
const found = fileContents.some((content) => content.includes(anchor));
|
|
2425
|
+
if (!found) {
|
|
2426
|
+
issues.push({
|
|
2427
|
+
severity: "warning",
|
|
2428
|
+
code: "W014",
|
|
2429
|
+
rule: "anchor-not-found",
|
|
2430
|
+
message: `Anchor '${anchor}' for aspect '${aspectId}' not found in mapped source files`,
|
|
2431
|
+
nodePath
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return issues;
|
|
2438
|
+
}
|
|
2289
2439
|
async function checkContextBudget(graph) {
|
|
2290
2440
|
const issues = [];
|
|
2291
2441
|
const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
|
|
@@ -2502,13 +2652,13 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
2502
2652
|
import chalk2 from "chalk";
|
|
2503
2653
|
|
|
2504
2654
|
// src/io/drift-state-store.ts
|
|
2505
|
-
import { readFile as
|
|
2655
|
+
import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
|
|
2506
2656
|
import path10 from "path";
|
|
2507
2657
|
import { parse as yamlParse } from "yaml";
|
|
2508
2658
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2509
2659
|
async function readDriftState(yggRoot) {
|
|
2510
2660
|
try {
|
|
2511
|
-
const content = await
|
|
2661
|
+
const content = await readFile12(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
|
|
2512
2662
|
let raw;
|
|
2513
2663
|
try {
|
|
2514
2664
|
raw = JSON.parse(content);
|
|
@@ -2533,20 +2683,20 @@ async function writeDriftState(yggRoot, state) {
|
|
|
2533
2683
|
}
|
|
2534
2684
|
|
|
2535
2685
|
// src/utils/hash.ts
|
|
2536
|
-
import { readFile as
|
|
2686
|
+
import { readFile as readFile13, readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
2537
2687
|
import path11 from "path";
|
|
2538
2688
|
import { createHash } from "crypto";
|
|
2539
2689
|
import { createRequire } from "module";
|
|
2540
2690
|
var require2 = createRequire(import.meta.url);
|
|
2541
2691
|
var ignoreFactory = require2("ignore");
|
|
2542
2692
|
async function hashFile(filePath) {
|
|
2543
|
-
const content = await
|
|
2693
|
+
const content = await readFile13(filePath);
|
|
2544
2694
|
return createHash("sha256").update(content).digest("hex");
|
|
2545
2695
|
}
|
|
2546
2696
|
async function loadRootGitignoreStack(projectRoot) {
|
|
2547
2697
|
if (!projectRoot) return [];
|
|
2548
2698
|
try {
|
|
2549
|
-
const content = await
|
|
2699
|
+
const content = await readFile13(path11.join(projectRoot, ".gitignore"), "utf-8");
|
|
2550
2700
|
const matcher = ignoreFactory();
|
|
2551
2701
|
matcher.add(content);
|
|
2552
2702
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -2573,7 +2723,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2573
2723
|
for (const tf of trackedFiles) {
|
|
2574
2724
|
const absPath = path11.join(projectRoot, tf.path);
|
|
2575
2725
|
try {
|
|
2576
|
-
const st = await
|
|
2726
|
+
const st = await stat4(absPath);
|
|
2577
2727
|
if (st.isDirectory()) {
|
|
2578
2728
|
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2579
2729
|
projectRoot,
|
|
@@ -2621,7 +2771,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2621
2771
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
2622
2772
|
let stack = options.gitignoreStack ?? [];
|
|
2623
2773
|
try {
|
|
2624
|
-
const localContent = await
|
|
2774
|
+
const localContent = await readFile13(path11.join(directoryPath, ".gitignore"), "utf-8");
|
|
2625
2775
|
const localMatcher = ignoreFactory();
|
|
2626
2776
|
localMatcher.add(localContent);
|
|
2627
2777
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
@@ -2642,7 +2792,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
2642
2792
|
gitignoreStack: stack
|
|
2643
2793
|
}))),
|
|
2644
2794
|
Promise.all(files.map(async (f) => {
|
|
2645
|
-
const fileStat = await
|
|
2795
|
+
const fileStat = await stat4(f);
|
|
2646
2796
|
return {
|
|
2647
2797
|
relPath: path11.relative(rootDirectoryPath, f),
|
|
2648
2798
|
absPath: f,
|
|
@@ -3642,7 +3792,7 @@ Total scope: ${sorted.length} nodes
|
|
|
3642
3792
|
}
|
|
3643
3793
|
}
|
|
3644
3794
|
function registerImpactCommand(program2) {
|
|
3645
|
-
program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
|
|
3795
|
+
program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--method <name>", "Filter impact to dependents consuming a specific method (requires --node)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
|
|
3646
3796
|
async (options) => {
|
|
3647
3797
|
try {
|
|
3648
3798
|
const modeCount = [options.node, options.aspect, options.flow].filter(Boolean).length;
|
|
@@ -3673,11 +3823,56 @@ function registerImpactCommand(program2) {
|
|
|
3673
3823
|
`);
|
|
3674
3824
|
process.exit(1);
|
|
3675
3825
|
}
|
|
3826
|
+
if (options.method && !options.node) {
|
|
3827
|
+
process.stderr.write("Error: --method requires --node\n");
|
|
3828
|
+
process.exit(1);
|
|
3829
|
+
}
|
|
3676
3830
|
const { direct, allDependents, reverse, relationFrom } = collectReverseDependents(
|
|
3677
3831
|
graph,
|
|
3678
3832
|
nodePath
|
|
3679
3833
|
);
|
|
3680
|
-
const
|
|
3834
|
+
const methodFilter = options.method?.trim();
|
|
3835
|
+
let filteredDirect = direct;
|
|
3836
|
+
let filteredAllDependents = allDependents;
|
|
3837
|
+
if (methodFilter) {
|
|
3838
|
+
filteredDirect = direct.filter((dep) => {
|
|
3839
|
+
const rel = relationFrom.get(`${dep}->${nodePath}`);
|
|
3840
|
+
return rel?.consumes?.includes(methodFilter) || !rel?.consumes?.length;
|
|
3841
|
+
});
|
|
3842
|
+
const filteredSet = new Set(filteredDirect);
|
|
3843
|
+
filteredAllDependents = allDependents.filter((dep) => filteredSet.has(dep));
|
|
3844
|
+
}
|
|
3845
|
+
const chains = buildTransitiveChains(nodePath, filteredDirect, filteredAllDependents, reverse);
|
|
3846
|
+
const eventDependents = [];
|
|
3847
|
+
for (const [np, n] of graph.nodes) {
|
|
3848
|
+
for (const rel of n.meta.relations ?? []) {
|
|
3849
|
+
if (rel.target === nodePath && (rel.type === "emits" || rel.type === "listens")) {
|
|
3850
|
+
eventDependents.push({
|
|
3851
|
+
path: np,
|
|
3852
|
+
type: rel.type,
|
|
3853
|
+
eventName: rel.event_name ?? n.meta.name
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
const targetNode = graph.nodes.get(nodePath);
|
|
3859
|
+
for (const rel of targetNode.meta.relations ?? []) {
|
|
3860
|
+
if (rel.type === "emits") {
|
|
3861
|
+
const eventName = rel.event_name ?? rel.target;
|
|
3862
|
+
for (const [np, n] of graph.nodes) {
|
|
3863
|
+
if (np === nodePath) continue;
|
|
3864
|
+
for (const r of n.meta.relations ?? []) {
|
|
3865
|
+
if (r.type === "listens" && r.target === rel.target) {
|
|
3866
|
+
eventDependents.push({
|
|
3867
|
+
path: np,
|
|
3868
|
+
type: "listens",
|
|
3869
|
+
eventName: r.event_name ?? eventName
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3681
3876
|
const flows = [];
|
|
3682
3877
|
for (const flow of graph.flows) {
|
|
3683
3878
|
if (flow.nodes.includes(nodePath)) {
|
|
@@ -3691,17 +3886,25 @@ function registerImpactCommand(program2) {
|
|
|
3691
3886
|
aspectsInScope.push(aspect.name);
|
|
3692
3887
|
}
|
|
3693
3888
|
}
|
|
3694
|
-
|
|
3889
|
+
const methodLabel = methodFilter ? ` (method: ${methodFilter})` : "";
|
|
3890
|
+
process.stdout.write(`Impact of changes in ${nodePath}${methodLabel}:
|
|
3695
3891
|
|
|
3696
3892
|
`);
|
|
3697
3893
|
process.stdout.write("Directly dependent:\n");
|
|
3698
|
-
if (
|
|
3894
|
+
if (filteredDirect.length === 0) {
|
|
3699
3895
|
process.stdout.write(" (none)\n");
|
|
3700
3896
|
} else {
|
|
3701
|
-
for (const dep of
|
|
3897
|
+
for (const dep of filteredDirect) {
|
|
3702
3898
|
const rel = relationFrom.get(`${dep}->${nodePath}`);
|
|
3703
|
-
const annot = rel?.consumes?.length ? ` (${rel.type},
|
|
3899
|
+
const annot = rel?.consumes?.length ? ` (${rel.type}, consumes: ${rel.consumes.join(", ")})` : rel ? ` (${rel.type})` : "";
|
|
3704
3900
|
process.stdout.write(` <- ${dep}${annot}
|
|
3901
|
+
`);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
if (eventDependents.length > 0 && !methodFilter) {
|
|
3905
|
+
process.stdout.write("\nEvent-connected:\n");
|
|
3906
|
+
for (const { path: p, type, eventName } of eventDependents.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
3907
|
+
process.stdout.write(` ${p} (${type}: ${eventName})
|
|
3705
3908
|
`);
|
|
3706
3909
|
}
|
|
3707
3910
|
}
|
|
@@ -3751,7 +3954,7 @@ Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
|
|
|
3751
3954
|
`);
|
|
3752
3955
|
}
|
|
3753
3956
|
}
|
|
3754
|
-
const allAffected = /* @__PURE__ */ new Set([...
|
|
3957
|
+
const allAffected = /* @__PURE__ */ new Set([...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path)]);
|
|
3755
3958
|
process.stdout.write(
|
|
3756
3959
|
`
|
|
3757
3960
|
Total scope: ${allAffected.size} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects
|
|
@@ -3780,6 +3983,7 @@ function registerAspectsCommand(program2) {
|
|
|
3780
3983
|
const entry = { id: aspect.id, name: aspect.name };
|
|
3781
3984
|
if (aspect.description) entry.description = aspect.description;
|
|
3782
3985
|
if (aspect.implies && aspect.implies.length > 0) entry.implies = aspect.implies;
|
|
3986
|
+
if (aspect.stability) entry.stability = aspect.stability;
|
|
3783
3987
|
return entry;
|
|
3784
3988
|
});
|
|
3785
3989
|
process.stdout.write(yamlStringify(output));
|
|
@@ -3833,7 +4037,7 @@ function registerFlowsCommand(program2) {
|
|
|
3833
4037
|
}
|
|
3834
4038
|
|
|
3835
4039
|
// src/io/journal-store.ts
|
|
3836
|
-
import { readFile as
|
|
4040
|
+
import { readFile as readFile14, writeFile as writeFile4, mkdir as mkdir3, rename, access as access3 } from "fs/promises";
|
|
3837
4041
|
import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
|
|
3838
4042
|
import path17 from "path";
|
|
3839
4043
|
var JOURNAL_FILE = ".journal.yaml";
|
|
@@ -3841,7 +4045,7 @@ var ARCHIVE_DIR = "journals-archive";
|
|
|
3841
4045
|
async function readJournal(yggRoot) {
|
|
3842
4046
|
const filePath = path17.join(yggRoot, JOURNAL_FILE);
|
|
3843
4047
|
try {
|
|
3844
|
-
const content = await
|
|
4048
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3845
4049
|
const raw = parseYaml6(content);
|
|
3846
4050
|
const entries = raw.entries ?? [];
|
|
3847
4051
|
return Array.isArray(entries) ? entries : [];
|