@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 +115 -15
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +1 -0
- package/dist/templates/rules.ts +3 -1
- package/package.json +1 -1
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
|
|
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 =
|
|
2175
|
+
const budgetStatus = breakdown.total >= errorThreshold ? "severe" : breakdown.total >= warningThreshold ? "warning" : "ok";
|
|
2097
2176
|
return {
|
|
2098
|
-
meta: { tokenCount:
|
|
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
|
|
2964
|
-
const
|
|
2965
|
-
|
|
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
|
-
|
|
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 ${
|
|
3058
|
+
message: `Context is ${breakdown.total.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}).
|
|
3059
|
+
${breakdownLine}`,
|
|
2975
3060
|
nodePath
|
|
2976
3061
|
});
|
|
2977
|
-
} else if (
|
|
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 ${
|
|
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
|
|
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} -> ${
|
|
4337
|
-
` : ` Budget: ${
|
|
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})` : ""}
|