@chrisdudek/yg 2.4.0 → 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
@@ -160,6 +161,7 @@ You have broken Yggdrasil if you do any of the following:
160
161
  - \u274C Placed a cross-cutting requirement in a local artifact instead of an aspect, or used an aspect id with no \`aspects/\` directory.
161
162
  - \u274C Invented a rationale, business rule, or decision \u2014 or recorded a decision without documenting rejected alternatives and rationale (use "rationale: unknown" if unknown).
162
163
  - \u274C Used blackbox coverage for greenfield (new) code.
164
+ - \u274C Deleted or shortened graph artifact content to reduce context package size instead of splitting the node.
163
165
 
164
166
  ### Escape Hatch
165
167
 
@@ -347,7 +349,8 @@ When reviewing graph quality (triggered by user or quality improvement):
347
349
 
348
350
  - **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
349
351
  - **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
350
- - **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/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.
351
354
  - **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
352
355
  - **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
353
356
  var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
@@ -1183,7 +1186,7 @@ import { parse as parseYaml3 } from "yaml";
1183
1186
  var DEFAULT_QUALITY = {
1184
1187
  min_artifact_length: 50,
1185
1188
  max_direct_relations: 10,
1186
- context_budget: { warning: 1e4, error: 2e4 }
1189
+ context_budget: { warning: 1e4, error: 2e4, own_warning: void 0 }
1187
1190
  };
1188
1191
  async function parseConfig(filePath) {
1189
1192
  const content = await readFile5(filePath, "utf-8");
@@ -1248,7 +1251,8 @@ async function parseConfig(filePath) {
1248
1251
  max_direct_relations: qualityRaw.max_direct_relations ?? DEFAULT_QUALITY.max_direct_relations,
1249
1252
  context_budget: {
1250
1253
  warning: qualityRaw.context_budget?.warning ?? DEFAULT_QUALITY.context_budget.warning,
1251
- 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
1252
1256
  }
1253
1257
  } : DEFAULT_QUALITY;
1254
1258
  if (quality.context_budget.error < quality.context_budget.warning) {
@@ -1256,6 +1260,9 @@ async function parseConfig(filePath) {
1256
1260
  `yg-config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
1257
1261
  );
1258
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
+ }
1259
1266
  return {
1260
1267
  version,
1261
1268
  name: raw.name.trim(),
@@ -2043,6 +2050,77 @@ function collectAncestors(node) {
2043
2050
  }
2044
2051
  return ancestors;
2045
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
+ }
2046
2124
  function toContextMapOutput(pkg2, graph) {
2047
2125
  const node = graph.nodes.get(pkg2.nodePath);
2048
2126
  const config = graph.config;
@@ -2091,11 +2169,12 @@ function toContextMapOutput(pkg2, graph) {
2091
2169
  depRefs.push(ref);
2092
2170
  }
2093
2171
  const registry = buildArtifactRegistry(node, ancestors, depRefs, graph);
2172
+ const breakdown = computeBudgetBreakdown(pkg2, graph);
2094
2173
  const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
2095
2174
  const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
2096
- const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
2175
+ const budgetStatus = breakdown.total >= errorThreshold ? "severe" : breakdown.total >= warningThreshold ? "warning" : "ok";
2097
2176
  return {
2098
- meta: { tokenCount: pkg2.tokenCount, budgetStatus },
2177
+ meta: { tokenCount: breakdown.total, budgetStatus, breakdown },
2099
2178
  project: config.name,
2100
2179
  node: {
2101
2180
  path: pkg2.nodePath,
@@ -2960,26 +3039,42 @@ async function checkAnchorPresence(graph) {
2960
3039
  }
2961
3040
  async function checkContextBudget(graph) {
2962
3041
  const issues = [];
2963
- const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
2964
- const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
2965
- 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);
2966
3048
  if (node.meta.blackbox) continue;
2967
3049
  try {
2968
3050
  const pkg2 = await buildContext(graph, nodePath);
2969
- 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) {
2970
3054
  issues.push({
2971
3055
  severity: "warning",
2972
3056
  code: "W006",
2973
3057
  rule: "budget-error",
2974
- 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}`,
2975
3060
  nodePath
2976
3061
  });
2977
- } else if (pkg2.tokenCount >= warningThreshold) {
3062
+ } else if (breakdown.total >= warningThreshold) {
2978
3063
  issues.push({
2979
3064
  severity: "warning",
2980
3065
  code: "W005",
2981
3066
  rule: "budget-warning",
2982
- message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Consider splitting the node or reducing dependencies.`,
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.`,
2983
3078
  nodePath
2984
3079
  });
2985
3080
  }
@@ -2988,6 +3083,10 @@ async function checkContextBudget(graph) {
2988
3083
  }
2989
3084
  return issues;
2990
3085
  }
3086
+ function pct(value, total) {
3087
+ if (total === 0) return "0%";
3088
+ return `${Math.round(value / total * 100)}%`;
3089
+ }
2991
3090
 
2992
3091
  // src/cli/build-context.ts
2993
3092
  function collectRelevantNodePaths(graph, nodePath) {
@@ -4319,7 +4418,8 @@ async function runSimulation(graph, nodePaths, targetNodePath) {
4319
4418
  for (const dep of nodePaths) {
4320
4419
  try {
4321
4420
  const pkg2 = await buildContext(graph, dep);
4322
- 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";
4323
4423
  let baselineTokens = null;
4324
4424
  if (baselineGraph?.nodes.has(dep)) {
4325
4425
  try {
@@ -4333,8 +4433,8 @@ async function runSimulation(graph, nodePaths, targetNodePath) {
4333
4433
  );
4334
4434
  const changedLine = hasDepOnTarget ? ` + Changed dependency interface: ${targetNodePath}
4335
4435
  ` : "";
4336
- const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${pkg2.tokenCount} tokens (${status})
4337
- ` : ` Budget: ${pkg2.tokenCount} tokens (${status})
4436
+ const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${breakdown.total} tokens (${status})
4437
+ ` : ` Budget: ${breakdown.total} tokens (${status})
4338
4438
  `;
4339
4439
  const driftEntry = driftByNode.get(dep);
4340
4440
  const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}