@chrisdudek/yg 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 whether reading to understand, planning, or modifying.
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 readFile11, writeFile as writeFile3 } from "fs/promises";
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 readFile11(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
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 readFile12, readdir as readdir5, stat as stat3 } from "fs/promises";
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 readFile12(filePath);
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 readFile12(path11.join(projectRoot, ".gitignore"), "utf-8");
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 stat3(absPath);
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 readFile12(path11.join(directoryPath, ".gitignore"), "utf-8");
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 stat3(f);
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 chains = buildTransitiveChains(nodePath, direct, allDependents, reverse);
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
- process.stdout.write(`Impact of changes in ${nodePath}:
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 (direct.length === 0) {
3894
+ if (filteredDirect.length === 0) {
3699
3895
  process.stdout.write(" (none)\n");
3700
3896
  } else {
3701
- for (const dep of direct) {
3897
+ for (const dep of filteredDirect) {
3702
3898
  const rel = relationFrom.get(`${dep}->${nodePath}`);
3703
- const annot = rel?.consumes?.length ? ` (${rel.type}, you consume: ${rel.consumes.join(", ")})` : rel ? ` (${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([...allDependents, ...descendants]);
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 readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access3 } from "fs/promises";
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 readFile13(filePath, "utf-8");
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 : [];