@chrisdudek/yg 2.4.1 → 2.5.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 CHANGED
@@ -45,6 +45,7 @@ quality:
45
45
  context_budget:
46
46
  warning: 10000
47
47
  error: 20000
48
+ own_warning: 5000
48
49
  `;
49
50
 
50
51
  // src/templates/platform.ts
@@ -348,8 +349,8 @@ When reviewing graph quality (triggered by user or quality improvement):
348
349
 
349
350
  - **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
350
351
  - **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
351
- - **Budget exceeded** \u2192 if \`yg build-context\` exits with error (context package exceeds budget), warn user: "This node should be split." Do not proceed with implementation.
352
- - **Budget warning (W005)** \u2192 if \`yg validate\` shows W005 (context near budget), the node needs splitting. NEVER delete knowledge from artifacts to reduce token count \u2014 knowledge destroyed is irrecoverable. Instead: identify a cohesive subset of the node's responsibilities, propose a split to the user, create child nodes, and redistribute artifacts. The total knowledge must be preserved or increased, never reduced.
352
+ - **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
+ - **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.
353
354
  - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
354
355
  - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
355
356
  var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
@@ -439,7 +440,7 @@ When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are prese
439
440
 
440
441
  - [ ] 1. Read \`schemas/yg-flow.yaml\`
441
442
  - [ ] 2. Create \`flows/<name>/\` directory
442
- - [ ] 3. Write \`yg-flow.yaml\` \u2014 declare participants and flow-level aspects
443
+ - [ ] 3. Write \`yg-flow.yaml\` \u2014 declare nodes (participant list) and flow-level aspects
443
444
  - [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
444
445
  - [ ] 5. \`yg validate\`
445
446
 
@@ -492,7 +493,7 @@ yg drift-sync --node <path> [--recursive] | --all
492
493
  | Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
493
494
  | Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
494
495
  | Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
495
- | Business process participation | Flow (\`yg-flow.yaml participants\`) |
496
+ | Business process participation | Flow (\`yg-flow.yaml nodes\`) |
496
497
  | Process-level requirement | Flow \`aspects\` + aspect directory |
497
498
  | Context shared across a domain | Parent node artifact |
498
499
  | Technology stack | Node artifact at appropriate hierarchy level |
@@ -1185,7 +1186,7 @@ import { parse as parseYaml3 } from "yaml";
1185
1186
  var DEFAULT_QUALITY = {
1186
1187
  min_artifact_length: 50,
1187
1188
  max_direct_relations: 10,
1188
- context_budget: { warning: 1e4, error: 2e4 }
1189
+ context_budget: { warning: 1e4, error: 2e4, own_warning: void 0 }
1189
1190
  };
1190
1191
  async function parseConfig(filePath) {
1191
1192
  const content = await readFile5(filePath, "utf-8");
@@ -1250,7 +1251,8 @@ async function parseConfig(filePath) {
1250
1251
  max_direct_relations: qualityRaw.max_direct_relations ?? DEFAULT_QUALITY.max_direct_relations,
1251
1252
  context_budget: {
1252
1253
  warning: qualityRaw.context_budget?.warning ?? DEFAULT_QUALITY.context_budget.warning,
1253
- error: qualityRaw.context_budget?.error ?? DEFAULT_QUALITY.context_budget.error
1254
+ error: qualityRaw.context_budget?.error ?? DEFAULT_QUALITY.context_budget.error,
1255
+ own_warning: qualityRaw.context_budget?.own_warning
1254
1256
  }
1255
1257
  } : DEFAULT_QUALITY;
1256
1258
  if (quality.context_budget.error < quality.context_budget.warning) {
@@ -1258,6 +1260,9 @@ async function parseConfig(filePath) {
1258
1260
  `yg-config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
1259
1261
  );
1260
1262
  }
1263
+ if (quality.context_budget.own_warning !== void 0 && quality.context_budget.own_warning <= 0) {
1264
+ throw new Error("quality.context_budget.own_warning must be a positive number");
1265
+ }
1261
1266
  return {
1262
1267
  version,
1263
1268
  name: raw.name.trim(),
@@ -1504,13 +1509,17 @@ async function parseFlow(flowDir, flowYamlPath) {
1504
1509
  if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
1505
1510
  throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
1506
1511
  }
1507
- const nodes = raw.nodes;
1512
+ const nodes = raw.nodes ?? raw.participants;
1508
1513
  if (!Array.isArray(nodes) || nodes.length === 0) {
1509
- throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
1514
+ throw new Error(
1515
+ `yg-flow.yaml at ${flowYamlPath}: 'nodes' (or 'participants') must be a non-empty array`
1516
+ );
1510
1517
  }
1511
1518
  const nodePaths = nodes.filter((n) => typeof n === "string");
1512
1519
  if (nodePaths.length === 0) {
1513
- throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
1520
+ throw new Error(
1521
+ `yg-flow.yaml at ${flowYamlPath}: 'nodes' (or 'participants') must contain string node paths`
1522
+ );
1514
1523
  }
1515
1524
  let aspects;
1516
1525
  if (raw.aspects !== void 0) {
@@ -1727,19 +1736,20 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
1727
1736
  }
1728
1737
  }
1729
1738
  async function loadFlows(flowsDir) {
1739
+ let entries;
1730
1740
  try {
1731
- const entries = await readdir4(flowsDir, { withFileTypes: true });
1732
- const flows = [];
1733
- for (const entry of entries) {
1734
- if (!entry.isDirectory()) continue;
1735
- const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
1736
- const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
1737
- flows.push(flow);
1738
- }
1739
- return flows;
1741
+ entries = await readdir4(flowsDir, { withFileTypes: true });
1740
1742
  } catch {
1741
1743
  return [];
1742
1744
  }
1745
+ const flows = [];
1746
+ for (const entry of entries) {
1747
+ if (!entry.isDirectory()) continue;
1748
+ const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
1749
+ const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
1750
+ flows.push(flow);
1751
+ }
1752
+ return flows;
1743
1753
  }
1744
1754
  async function loadSchemas(schemasDir) {
1745
1755
  try {
@@ -2045,6 +2055,77 @@ function collectAncestors(node) {
2045
2055
  }
2046
2056
  return ancestors;
2047
2057
  }
2058
+ function collectDependencyAncestors(target, config, graph) {
2059
+ const ancestors = collectAncestors(target);
2060
+ const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2061
+ const configArtifactKeys = [...Object.keys(config.artifacts ?? {})];
2062
+ return ancestors.map((ancestor) => {
2063
+ const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
2064
+ const expanded = expandAspects(nodeAspects, graph.aspects);
2065
+ const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : configArtifactKeys;
2066
+ const availableFiles = filterFilenames.filter(
2067
+ (f) => ancestor.artifacts.some((a) => a.filename === f)
2068
+ );
2069
+ return {
2070
+ path: ancestor.path,
2071
+ name: ancestor.meta.name,
2072
+ type: ancestor.meta.type,
2073
+ aspects: expanded,
2074
+ artifactFilenames: availableFiles
2075
+ };
2076
+ });
2077
+ }
2078
+ function computeBudgetBreakdown(pkg2, graph) {
2079
+ let own = 0;
2080
+ let hierarchy = 0;
2081
+ let aspects = 0;
2082
+ let flows = 0;
2083
+ let relational = 0;
2084
+ for (const layer of pkg2.layers) {
2085
+ const tokens = estimateTokens(layer.content);
2086
+ switch (layer.type) {
2087
+ case "global":
2088
+ case "own":
2089
+ own += tokens;
2090
+ break;
2091
+ case "hierarchy":
2092
+ hierarchy += tokens;
2093
+ break;
2094
+ case "aspects":
2095
+ aspects += tokens;
2096
+ break;
2097
+ case "flows":
2098
+ flows += tokens;
2099
+ break;
2100
+ case "relational":
2101
+ relational += tokens;
2102
+ break;
2103
+ }
2104
+ }
2105
+ let depAncestorTokens = 0;
2106
+ const node = graph.nodes.get(pkg2.nodePath);
2107
+ if (node) {
2108
+ const ancestorPaths = new Set(collectAncestors(node).map((a) => a.path));
2109
+ for (const relation of node.meta.relations ?? []) {
2110
+ const target = graph.nodes.get(relation.target);
2111
+ if (!target || ancestorPaths.has(relation.target)) continue;
2112
+ const depAncestors = collectDependencyAncestors(target, graph.config, graph);
2113
+ for (const anc of depAncestors) {
2114
+ const ancNode = graph.nodes.get(anc.path);
2115
+ if (!ancNode) continue;
2116
+ for (const filename of anc.artifactFilenames) {
2117
+ const art = ancNode.artifacts.find((a) => a.filename === filename);
2118
+ if (art) {
2119
+ depAncestorTokens += estimateTokens(art.content);
2120
+ }
2121
+ }
2122
+ }
2123
+ }
2124
+ }
2125
+ const dependencies = relational + depAncestorTokens;
2126
+ const total = own + hierarchy + aspects + flows + dependencies;
2127
+ return { own, hierarchy, aspects, flows, dependencies, total };
2128
+ }
2048
2129
  function toContextMapOutput(pkg2, graph) {
2049
2130
  const node = graph.nodes.get(pkg2.nodePath);
2050
2131
  const config = graph.config;
@@ -2093,11 +2174,12 @@ function toContextMapOutput(pkg2, graph) {
2093
2174
  depRefs.push(ref);
2094
2175
  }
2095
2176
  const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2177
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
2096
2178
  const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2097
2179
  const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
2098
- const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
2180
+ const budgetStatus = breakdown.total >= errorThreshold ? "severe" : breakdown.total >= warningThreshold ? "warning" : "ok";
2099
2181
  return {
2100
- meta: { tokenCount: pkg2.tokenCount, budgetStatus },
2182
+ meta: { tokenCount: breakdown.total, budgetStatus, breakdown },
2101
2183
  project: config.name,
2102
2184
  node: {
2103
2185
  path: pkg2.nodePath,
@@ -2962,26 +3044,42 @@ async function checkAnchorPresence(graph) {
2962
3044
  }
2963
3045
  async function checkContextBudget(graph) {
2964
3046
  const issues = [];
2965
- const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
2966
- const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
2967
- for (const [nodePath, node] of graph.nodes) {
3047
+ const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
3048
+ const warningThreshold = budget.warning;
3049
+ const errorThreshold = budget.error;
3050
+ const ownWarningThreshold = budget.own_warning;
3051
+ for (const [nodePath] of graph.nodes) {
3052
+ const node = graph.nodes.get(nodePath);
2968
3053
  if (node.meta.blackbox) continue;
2969
3054
  try {
2970
3055
  const pkg2 = await buildContext(graph, nodePath);
2971
- if (pkg2.tokenCount >= errorThreshold) {
3056
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
3057
+ const breakdownLine = `own: ${breakdown.own.toLocaleString()} (${pct(breakdown.own, breakdown.total)}) | hierarchy: ${breakdown.hierarchy.toLocaleString()} (${pct(breakdown.hierarchy, breakdown.total)}) | aspects: ${breakdown.aspects.toLocaleString()} (${pct(breakdown.aspects, breakdown.total)}) | flows: ${breakdown.flows.toLocaleString()} (${pct(breakdown.flows, breakdown.total)}) | dependencies: ${breakdown.dependencies.toLocaleString()} (${pct(breakdown.dependencies, breakdown.total)})`;
3058
+ if (breakdown.total >= errorThreshold) {
2972
3059
  issues.push({
2973
3060
  severity: "warning",
2974
3061
  code: "W006",
2975
3062
  rule: "budget-error",
2976
- message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}) \u2014 blocks materialization, node must be split`,
3063
+ message: `Context is ${breakdown.total.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}).
3064
+ ${breakdownLine}`,
2977
3065
  nodePath
2978
3066
  });
2979
- } else if (pkg2.tokenCount >= warningThreshold) {
3067
+ } else if (breakdown.total >= warningThreshold) {
2980
3068
  issues.push({
2981
3069
  severity: "warning",
2982
3070
  code: "W005",
2983
3071
  rule: "budget-warning",
2984
- message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Split the node into smaller units \u2014 do not delete knowledge from artifacts to reduce size.`,
3072
+ message: `Context is ${breakdown.total.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}).
3073
+ ${breakdownLine}`,
3074
+ nodePath
3075
+ });
3076
+ }
3077
+ if (ownWarningThreshold !== void 0 && breakdown.own >= ownWarningThreshold) {
3078
+ issues.push({
3079
+ severity: "warning",
3080
+ code: "W015",
3081
+ rule: "own-budget-warning",
3082
+ message: `Own artifacts: ${breakdown.own.toLocaleString()} tokens (threshold: ${ownWarningThreshold.toLocaleString()}). Consider splitting this node's responsibilities into child nodes.`,
2985
3083
  nodePath
2986
3084
  });
2987
3085
  }
@@ -2990,6 +3088,10 @@ async function checkContextBudget(graph) {
2990
3088
  }
2991
3089
  return issues;
2992
3090
  }
3091
+ function pct(value, total) {
3092
+ if (total === 0) return "0%";
3093
+ return `${Math.round(value / total * 100)}%`;
3094
+ }
2993
3095
 
2994
3096
  // src/cli/build-context.ts
2995
3097
  function collectRelevantNodePaths(graph, nodePath) {
@@ -4321,7 +4423,8 @@ async function runSimulation(graph, nodePaths, targetNodePath) {
4321
4423
  for (const dep of nodePaths) {
4322
4424
  try {
4323
4425
  const pkg2 = await buildContext(graph, dep);
4324
- const status = pkg2.tokenCount >= budget.error ? "error" : pkg2.tokenCount >= budget.warning ? "warning" : "ok";
4426
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
4427
+ const status = breakdown.total >= budget.error ? "severe" : breakdown.total >= budget.warning ? "warning" : "ok";
4325
4428
  let baselineTokens = null;
4326
4429
  if (baselineGraph?.nodes.has(dep)) {
4327
4430
  try {
@@ -4335,8 +4438,8 @@ async function runSimulation(graph, nodePaths, targetNodePath) {
4335
4438
  );
4336
4439
  const changedLine = hasDepOnTarget ? ` + Changed dependency interface: ${targetNodePath}
4337
4440
  ` : "";
4338
- const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${pkg2.tokenCount} tokens (${status})
4339
- ` : ` Budget: ${pkg2.tokenCount} tokens (${status})
4441
+ const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${breakdown.total} tokens (${status})
4442
+ ` : ` Budget: ${breakdown.total} tokens (${status})
4340
4443
  `;
4341
4444
  const driftEntry = driftByNode.get(dep);
4342
4445
  const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}