@chrisdudek/yg 2.4.1 → 2.5.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
@@ -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
@@ -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(),
@@ -2045,6 +2050,77 @@ function collectAncestors(node) {
2045
2050
  }
2046
2051
  return ancestors;
2047
2052
  }
2053
+ function collectDependencyAncestors(target, config, graph) {
2054
+ const ancestors = collectAncestors(target);
2055
+ const structuralFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
2056
+ const configArtifactKeys = [...Object.keys(config.artifacts ?? {})];
2057
+ return ancestors.map((ancestor) => {
2058
+ const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
2059
+ const expanded = expandAspects(nodeAspects, graph.aspects);
2060
+ const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : configArtifactKeys;
2061
+ const availableFiles = filterFilenames.filter(
2062
+ (f) => ancestor.artifacts.some((a) => a.filename === f)
2063
+ );
2064
+ return {
2065
+ path: ancestor.path,
2066
+ name: ancestor.meta.name,
2067
+ type: ancestor.meta.type,
2068
+ aspects: expanded,
2069
+ artifactFilenames: availableFiles
2070
+ };
2071
+ });
2072
+ }
2073
+ function computeBudgetBreakdown(pkg2, graph) {
2074
+ let own = 0;
2075
+ let hierarchy = 0;
2076
+ let aspects = 0;
2077
+ let flows = 0;
2078
+ let relational = 0;
2079
+ for (const layer of pkg2.layers) {
2080
+ const tokens = estimateTokens(layer.content);
2081
+ switch (layer.type) {
2082
+ case "global":
2083
+ case "own":
2084
+ own += tokens;
2085
+ break;
2086
+ case "hierarchy":
2087
+ hierarchy += tokens;
2088
+ break;
2089
+ case "aspects":
2090
+ aspects += tokens;
2091
+ break;
2092
+ case "flows":
2093
+ flows += tokens;
2094
+ break;
2095
+ case "relational":
2096
+ relational += tokens;
2097
+ break;
2098
+ }
2099
+ }
2100
+ let depAncestorTokens = 0;
2101
+ const node = graph.nodes.get(pkg2.nodePath);
2102
+ if (node) {
2103
+ const ancestorPaths = new Set(collectAncestors(node).map((a) => a.path));
2104
+ for (const relation of node.meta.relations ?? []) {
2105
+ const target = graph.nodes.get(relation.target);
2106
+ if (!target || ancestorPaths.has(relation.target)) continue;
2107
+ const depAncestors = collectDependencyAncestors(target, graph.config, graph);
2108
+ for (const anc of depAncestors) {
2109
+ const ancNode = graph.nodes.get(anc.path);
2110
+ if (!ancNode) continue;
2111
+ for (const filename of anc.artifactFilenames) {
2112
+ const art = ancNode.artifacts.find((a) => a.filename === filename);
2113
+ if (art) {
2114
+ depAncestorTokens += estimateTokens(art.content);
2115
+ }
2116
+ }
2117
+ }
2118
+ }
2119
+ }
2120
+ const dependencies = relational + depAncestorTokens;
2121
+ const total = own + hierarchy + aspects + flows + dependencies;
2122
+ return { own, hierarchy, aspects, flows, dependencies, total };
2123
+ }
2048
2124
  function toContextMapOutput(pkg2, graph) {
2049
2125
  const node = graph.nodes.get(pkg2.nodePath);
2050
2126
  const config = graph.config;
@@ -2093,11 +2169,12 @@ function toContextMapOutput(pkg2, graph) {
2093
2169
  depRefs.push(ref);
2094
2170
  }
2095
2171
  const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2172
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
2096
2173
  const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2097
2174
  const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
2098
- const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
2175
+ const budgetStatus = breakdown.total >= errorThreshold ? "severe" : breakdown.total >= warningThreshold ? "warning" : "ok";
2099
2176
  return {
2100
- meta: { tokenCount: pkg2.tokenCount, budgetStatus },
2177
+ meta: { tokenCount: breakdown.total, budgetStatus, breakdown },
2101
2178
  project: config.name,
2102
2179
  node: {
2103
2180
  path: pkg2.nodePath,
@@ -2962,26 +3039,42 @@ async function checkAnchorPresence(graph) {
2962
3039
  }
2963
3040
  async function checkContextBudget(graph) {
2964
3041
  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) {
3042
+ const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
3043
+ const warningThreshold = budget.warning;
3044
+ const errorThreshold = budget.error;
3045
+ const ownWarningThreshold = budget.own_warning;
3046
+ for (const [nodePath] of graph.nodes) {
3047
+ const node = graph.nodes.get(nodePath);
2968
3048
  if (node.meta.blackbox) continue;
2969
3049
  try {
2970
3050
  const pkg2 = await buildContext(graph, nodePath);
2971
- if (pkg2.tokenCount >= errorThreshold) {
3051
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
3052
+ 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)})`;
3053
+ if (breakdown.total >= errorThreshold) {
2972
3054
  issues.push({
2973
3055
  severity: "warning",
2974
3056
  code: "W006",
2975
3057
  rule: "budget-error",
2976
- message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}) \u2014 blocks materialization, node must be split`,
3058
+ message: `Context is ${breakdown.total.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}).
3059
+ ${breakdownLine}`,
2977
3060
  nodePath
2978
3061
  });
2979
- } else if (pkg2.tokenCount >= warningThreshold) {
3062
+ } else if (breakdown.total >= warningThreshold) {
2980
3063
  issues.push({
2981
3064
  severity: "warning",
2982
3065
  code: "W005",
2983
3066
  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.`,
3067
+ message: `Context is ${breakdown.total.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}).
3068
+ ${breakdownLine}`,
3069
+ nodePath
3070
+ });
3071
+ }
3072
+ if (ownWarningThreshold !== void 0 && breakdown.own >= ownWarningThreshold) {
3073
+ issues.push({
3074
+ severity: "warning",
3075
+ code: "W015",
3076
+ rule: "own-budget-warning",
3077
+ message: `Own artifacts: ${breakdown.own.toLocaleString()} tokens (threshold: ${ownWarningThreshold.toLocaleString()}). Consider splitting this node's responsibilities into child nodes.`,
2985
3078
  nodePath
2986
3079
  });
2987
3080
  }
@@ -2990,6 +3083,10 @@ async function checkContextBudget(graph) {
2990
3083
  }
2991
3084
  return issues;
2992
3085
  }
3086
+ function pct(value, total) {
3087
+ if (total === 0) return "0%";
3088
+ return `${Math.round(value / total * 100)}%`;
3089
+ }
2993
3090
 
2994
3091
  // src/cli/build-context.ts
2995
3092
  function collectRelevantNodePaths(graph, nodePath) {
@@ -4321,7 +4418,8 @@ async function runSimulation(graph, nodePaths, targetNodePath) {
4321
4418
  for (const dep of nodePaths) {
4322
4419
  try {
4323
4420
  const pkg2 = await buildContext(graph, dep);
4324
- const status = pkg2.tokenCount >= budget.error ? "error" : pkg2.tokenCount >= budget.warning ? "warning" : "ok";
4421
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
4422
+ const status = breakdown.total >= budget.error ? "severe" : breakdown.total >= budget.warning ? "warning" : "ok";
4325
4423
  let baselineTokens = null;
4326
4424
  if (baselineGraph?.nodes.has(dep)) {
4327
4425
  try {
@@ -4335,8 +4433,8 @@ async function runSimulation(graph, nodePaths, targetNodePath) {
4335
4433
  );
4336
4434
  const changedLine = hasDepOnTarget ? ` + Changed dependency interface: ${targetNodePath}
4337
4435
  ` : "";
4338
- const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${pkg2.tokenCount} tokens (${status})
4339
- ` : ` Budget: ${pkg2.tokenCount} tokens (${status})
4436
+ const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${breakdown.total} tokens (${status})
4437
+ ` : ` Budget: ${breakdown.total} tokens (${status})
4340
4438
  `;
4341
4439
  const driftEntry = driftByNode.get(dep);
4342
4440
  const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}