@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 +133 -30
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +1 -0
- package/dist/templates/rules.ts +4 -4
- package/graph-schemas/yg-flow.yaml +1 -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
|
|
@@ -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
|
|
352
|
-
- **
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
2180
|
+
const budgetStatus = breakdown.total >= errorThreshold ? "severe" : breakdown.total >= warningThreshold ? "warning" : "ok";
|
|
2099
2181
|
return {
|
|
2100
|
-
meta: { tokenCount:
|
|
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
|
|
2966
|
-
const
|
|
2967
|
-
|
|
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
|
-
|
|
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 ${
|
|
3063
|
+
message: `Context is ${breakdown.total.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}).
|
|
3064
|
+
${breakdownLine}`,
|
|
2977
3065
|
nodePath
|
|
2978
3066
|
});
|
|
2979
|
-
} else if (
|
|
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 ${
|
|
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
|
|
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} -> ${
|
|
4339
|
-
` : ` Budget: ${
|
|
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})` : ""}
|