@chrisdudek/yg 5.0.0-alpha.3 → 5.0.0-alpha.4
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/ast.d.ts +7 -2
- package/dist/ast.js +7 -2
- package/dist/bin.js +405 -150
- package/dist/structure.js +111 -49
- package/graph-schemas/yg-architecture.yaml +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -162,9 +162,23 @@ var init_graph = __esm({
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
// src/utils/mapping-path.ts
|
|
165
|
+
import { minimatch } from "minimatch";
|
|
165
166
|
function normalizeMappingPath(p2) {
|
|
166
167
|
return p2.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
167
168
|
}
|
|
169
|
+
function isGlobPattern(entry) {
|
|
170
|
+
return entry.includes("*");
|
|
171
|
+
}
|
|
172
|
+
function globMatch(file, pattern) {
|
|
173
|
+
return minimatch(file, pattern, { dot: true });
|
|
174
|
+
}
|
|
175
|
+
function mappingEntryMatchesFile(entry, file) {
|
|
176
|
+
const e = normalizeMappingPath(entry);
|
|
177
|
+
const f = normalizeMappingPath(file);
|
|
178
|
+
if (e === "") return false;
|
|
179
|
+
if (isGlobPattern(e)) return globMatch(f, e);
|
|
180
|
+
return f === e || f.startsWith(e + "/");
|
|
181
|
+
}
|
|
168
182
|
var init_mapping_path = __esm({
|
|
169
183
|
"src/utils/mapping-path.ts"() {
|
|
170
184
|
"use strict";
|
|
@@ -675,6 +689,11 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
675
689
|
const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
|
|
676
690
|
const allFiles = [];
|
|
677
691
|
for (const tf of trackedFiles) {
|
|
692
|
+
if (isGlobPattern(tf.path)) {
|
|
693
|
+
const entries = await expandGlobEntry(projectRoot, tf.path, gitignoreStack);
|
|
694
|
+
for (const entry of entries) allFiles.push(entry);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
678
697
|
const absPath = path16.join(projectRoot, tf.path);
|
|
679
698
|
try {
|
|
680
699
|
const st = await stat5(absPath);
|
|
@@ -697,7 +716,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
697
716
|
continue;
|
|
698
717
|
}
|
|
699
718
|
}
|
|
700
|
-
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) =>
|
|
719
|
+
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => mappingEntryMatchesFile(prefix, entry.relPath))) : allFiles;
|
|
701
720
|
const dirty = [];
|
|
702
721
|
for (const entry of filtered) {
|
|
703
722
|
const storedMtime = storedFileData?.mtimes[entry.relPath];
|
|
@@ -757,26 +776,50 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
757
776
|
result.push(...fileStats);
|
|
758
777
|
return result;
|
|
759
778
|
}
|
|
779
|
+
async function expandGlobEntry(projectRoot, glob, gitignoreStack) {
|
|
780
|
+
const segments = glob.split("/");
|
|
781
|
+
const firstGlobIdx = segments.findIndex((s) => isGlobPattern(s));
|
|
782
|
+
const baseSegments = firstGlobIdx > 0 ? segments.slice(0, firstGlobIdx) : [];
|
|
783
|
+
const baseDir = baseSegments.length > 0 ? path16.join(projectRoot, ...baseSegments) : projectRoot;
|
|
784
|
+
try {
|
|
785
|
+
const dirEntries = await collectDirectoryFilePaths(baseDir, projectRoot, {
|
|
786
|
+
projectRoot,
|
|
787
|
+
gitignoreStack
|
|
788
|
+
});
|
|
789
|
+
return dirEntries.filter((entry) => globMatch(entry.relPath, glob)).map((entry) => ({
|
|
790
|
+
relPath: toPosixPath(entry.relPath),
|
|
791
|
+
absPath: entry.absPath,
|
|
792
|
+
mtimeMs: entry.mtimeMs
|
|
793
|
+
}));
|
|
794
|
+
} catch {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
}
|
|
760
798
|
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
761
799
|
const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
|
|
762
800
|
const result = [];
|
|
763
801
|
for (const mp of mappingPaths) {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
802
|
+
if (isGlobPattern(mp)) {
|
|
803
|
+
const entries = await expandGlobEntry(projectRoot, mp, gitignoreStack);
|
|
804
|
+
for (const entry of entries) result.push(entry.relPath);
|
|
805
|
+
} else {
|
|
806
|
+
const absPath = path16.join(projectRoot, mp);
|
|
807
|
+
try {
|
|
808
|
+
const st = await stat5(absPath);
|
|
809
|
+
if (st.isDirectory()) {
|
|
810
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
811
|
+
projectRoot,
|
|
812
|
+
gitignoreStack
|
|
813
|
+
});
|
|
814
|
+
for (const entry of dirEntries) {
|
|
815
|
+
result.push(toPosixPath(path16.join(mp, entry.relPath)));
|
|
816
|
+
}
|
|
817
|
+
} else {
|
|
818
|
+
result.push(toPosixPath(mp));
|
|
774
819
|
}
|
|
775
|
-
}
|
|
776
|
-
|
|
820
|
+
} catch {
|
|
821
|
+
continue;
|
|
777
822
|
}
|
|
778
|
-
} catch {
|
|
779
|
-
continue;
|
|
780
823
|
}
|
|
781
824
|
}
|
|
782
825
|
return result;
|
|
@@ -786,6 +829,7 @@ var init_hash = __esm({
|
|
|
786
829
|
"src/io/hash.ts"() {
|
|
787
830
|
"use strict";
|
|
788
831
|
init_posix();
|
|
832
|
+
init_mapping_path();
|
|
789
833
|
init_repo_scanner();
|
|
790
834
|
require3 = createRequire2(import.meta.url);
|
|
791
835
|
ignoreFactory2 = require3("ignore");
|
|
@@ -1047,10 +1091,10 @@ function computeEffectiveAspectStatuses(node, graph) {
|
|
|
1047
1091
|
for (const a of graph.aspects) idToAspect.set(a.id, a);
|
|
1048
1092
|
let changed = true;
|
|
1049
1093
|
let iterations = 0;
|
|
1050
|
-
const maxIterations = graph.aspects.length +
|
|
1094
|
+
const maxIterations = graph.aspects.length + 2;
|
|
1051
1095
|
while (changed) {
|
|
1052
1096
|
if (++iterations > maxIterations) {
|
|
1053
|
-
throw new ImpliesCycleError(
|
|
1097
|
+
throw new ImpliesCycleError("implies fix-point exceeded its iteration bound (internal invariant violated)");
|
|
1054
1098
|
}
|
|
1055
1099
|
changed = false;
|
|
1056
1100
|
const currentIds = [...result.keys()];
|
|
@@ -1258,8 +1302,7 @@ function collectTrackedFiles(node, graph, baseline) {
|
|
|
1258
1302
|
}
|
|
1259
1303
|
const allAspectIds = computeEffectiveAspects(node, graph);
|
|
1260
1304
|
const mappingPathsList = normalizeMappingPaths(node.meta.mapping);
|
|
1261
|
-
const
|
|
1262
|
-
const isOwnedByMapping = (p2) => mappingPathsSet.has(p2) || mappingPathsList.some((m) => p2.startsWith(m + "/"));
|
|
1305
|
+
const isOwnedByMapping = (p2) => mappingPathsList.some((m) => mappingEntryMatchesFile(m, p2));
|
|
1263
1306
|
for (const aspectId of allAspectIds) {
|
|
1264
1307
|
const aspect = graph.aspects.find((a) => a.id === aspectId);
|
|
1265
1308
|
if (!aspect) continue;
|
|
@@ -1363,6 +1406,7 @@ var init_files = __esm({
|
|
|
1363
1406
|
"src/core/graph/files.ts"() {
|
|
1364
1407
|
"use strict";
|
|
1365
1408
|
init_paths();
|
|
1409
|
+
init_mapping_path();
|
|
1366
1410
|
init_traversal();
|
|
1367
1411
|
init_aspects();
|
|
1368
1412
|
init_tier_selection();
|
|
@@ -3106,9 +3150,8 @@ function isAllowed(p2, set) {
|
|
|
3106
3150
|
if (p2 === "") return false;
|
|
3107
3151
|
if (set.has(p2)) return true;
|
|
3108
3152
|
for (const a of set) {
|
|
3109
|
-
if (a === p2) return true;
|
|
3110
3153
|
if (a.startsWith(p2 + "/")) return true;
|
|
3111
|
-
if (
|
|
3154
|
+
if (mappingEntryMatchesFile(a, p2)) return true;
|
|
3112
3155
|
}
|
|
3113
3156
|
return false;
|
|
3114
3157
|
}
|
|
@@ -3200,13 +3243,7 @@ var init_ctx_fs = __esm({
|
|
|
3200
3243
|
function isPathInMapping(candidate, mapping) {
|
|
3201
3244
|
const c = normalizeMappingPath(candidate);
|
|
3202
3245
|
if (c === "") return false;
|
|
3203
|
-
|
|
3204
|
-
const n = normalizeMappingPath(raw);
|
|
3205
|
-
if (n === "") continue;
|
|
3206
|
-
if (c === n) return true;
|
|
3207
|
-
if (c.startsWith(n + "/")) return true;
|
|
3208
|
-
}
|
|
3209
|
-
return false;
|
|
3246
|
+
return mapping.some((raw) => mappingEntryMatchesFile(raw, c));
|
|
3210
3247
|
}
|
|
3211
3248
|
var init_expand_mapping_sync = __esm({
|
|
3212
3249
|
"src/structure/expand-mapping-sync.ts"() {
|
|
@@ -3247,15 +3284,16 @@ function computeAllowedNodePaths(currentPath, graph) {
|
|
|
3247
3284
|
return allowed;
|
|
3248
3285
|
}
|
|
3249
3286
|
function createCtxGraph(params) {
|
|
3250
|
-
const { currentNodePath, graph, projectRoot, touchedFiles } = params;
|
|
3287
|
+
const { currentNodePath, graph, projectRoot, touchedFiles, expandedFilesByNode } = params;
|
|
3251
3288
|
const allowed = computeAllowedNodePaths(currentNodePath, graph);
|
|
3252
3289
|
function assertAllowed(id) {
|
|
3253
3290
|
if (!allowed.has(id)) throw new UndeclaredGraphReadError(id);
|
|
3254
3291
|
}
|
|
3255
3292
|
function toPublicNode(m) {
|
|
3256
3293
|
const files = [];
|
|
3257
|
-
|
|
3258
|
-
|
|
3294
|
+
const preExpanded = expandedFilesByNode?.get(m.path);
|
|
3295
|
+
const candidatePaths = preExpanded ?? (m.meta.mapping ?? []).map(normalizeMappingPath);
|
|
3296
|
+
for (const p2 of candidatePaths) {
|
|
3259
3297
|
if (!p2) continue;
|
|
3260
3298
|
const abs = path29.resolve(projectRoot, p2);
|
|
3261
3299
|
try {
|
|
@@ -3354,10 +3392,14 @@ import path30 from "path";
|
|
|
3354
3392
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
3355
3393
|
import { existsSync as existsSync5 } from "fs";
|
|
3356
3394
|
import { createRequire as createRequire3 } from "module";
|
|
3357
|
-
|
|
3358
|
-
if (
|
|
3359
|
-
|
|
3360
|
-
|
|
3395
|
+
function init() {
|
|
3396
|
+
if (initPromise === null) {
|
|
3397
|
+
initPromise = Parser.init();
|
|
3398
|
+
initPromise.catch(() => {
|
|
3399
|
+
initPromise = null;
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
3402
|
+
return initPromise;
|
|
3361
3403
|
}
|
|
3362
3404
|
function resolveWasm(filename, pkg2) {
|
|
3363
3405
|
for (const dir of GRAMMAR_DIRS) {
|
|
@@ -3380,12 +3422,16 @@ async function getParser(extension) {
|
|
|
3380
3422
|
throw new Error(`no parser for extension '${extension}'`);
|
|
3381
3423
|
}
|
|
3382
3424
|
const cacheKey = info.wasmFile;
|
|
3383
|
-
let
|
|
3384
|
-
if (
|
|
3425
|
+
let langP = langCache.get(cacheKey);
|
|
3426
|
+
if (langP === void 0) {
|
|
3385
3427
|
const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
|
|
3386
|
-
|
|
3387
|
-
langCache.set(cacheKey,
|
|
3428
|
+
langP = Language.load(wasmPath);
|
|
3429
|
+
langCache.set(cacheKey, langP);
|
|
3430
|
+
langP.catch(() => {
|
|
3431
|
+
if (langCache.get(cacheKey) === langP) langCache.delete(cacheKey);
|
|
3432
|
+
});
|
|
3388
3433
|
}
|
|
3434
|
+
const lang = await langP;
|
|
3389
3435
|
const parser = new Parser();
|
|
3390
3436
|
parser.setLanguage(lang);
|
|
3391
3437
|
return parser;
|
|
@@ -3399,7 +3445,7 @@ async function parseFile(filePath, content14) {
|
|
|
3399
3445
|
}
|
|
3400
3446
|
return tree;
|
|
3401
3447
|
}
|
|
3402
|
-
var _require, __filename2, __dirname2, GRAMMAR_DIRS,
|
|
3448
|
+
var _require, __filename2, __dirname2, GRAMMAR_DIRS, initPromise, langCache;
|
|
3403
3449
|
var init_parser = __esm({
|
|
3404
3450
|
"src/ast/parser.ts"() {
|
|
3405
3451
|
"use strict";
|
|
@@ -3411,7 +3457,7 @@ var init_parser = __esm({
|
|
|
3411
3457
|
path30.resolve(__dirname2, "grammars"),
|
|
3412
3458
|
path30.resolve(__dirname2, "..", "grammars")
|
|
3413
3459
|
];
|
|
3414
|
-
|
|
3460
|
+
initPromise = null;
|
|
3415
3461
|
langCache = /* @__PURE__ */ new Map();
|
|
3416
3462
|
}
|
|
3417
3463
|
});
|
|
@@ -3640,15 +3686,23 @@ function parseMarker(commentText, line, file) {
|
|
|
3640
3686
|
if (m) return makeMarker("single", m, line, file);
|
|
3641
3687
|
return null;
|
|
3642
3688
|
}
|
|
3643
|
-
function collectSuppressions(tree, file, totalLines) {
|
|
3644
|
-
|
|
3645
|
-
return [];
|
|
3646
|
-
}
|
|
3647
|
-
const comments = findComments({ path: file, ast: tree });
|
|
3689
|
+
function collectSuppressions(tree, file, totalLines, content14) {
|
|
3690
|
+
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
3648
3691
|
const markers = [];
|
|
3649
|
-
|
|
3650
|
-
const
|
|
3651
|
-
|
|
3692
|
+
if (hasGrammar && tree) {
|
|
3693
|
+
const comments = findComments({ path: file, ast: tree });
|
|
3694
|
+
for (const c of comments) {
|
|
3695
|
+
const m = parseMarker(c.text, c.startPosition.row + 1, file);
|
|
3696
|
+
if (m) markers.push(m);
|
|
3697
|
+
}
|
|
3698
|
+
} else if (content14 !== void 0) {
|
|
3699
|
+
const lines = content14.split("\n");
|
|
3700
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3701
|
+
const m = parseMarker(lines[i], i + 1, file);
|
|
3702
|
+
if (m) markers.push(m);
|
|
3703
|
+
}
|
|
3704
|
+
} else {
|
|
3705
|
+
return [];
|
|
3652
3706
|
}
|
|
3653
3707
|
markers.sort((a, b) => a.line - b.line);
|
|
3654
3708
|
const ranges = [];
|
|
@@ -3746,8 +3800,8 @@ var init_suppress = __esm({
|
|
|
3746
3800
|
}
|
|
3747
3801
|
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
3748
3802
|
};
|
|
3749
|
-
RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)
|
|
3750
|
-
RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)
|
|
3803
|
+
RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
3804
|
+
RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
3751
3805
|
RE_ENABLE = /\byg-suppress-enable\(\s*([^)]+?)\s*\)/;
|
|
3752
3806
|
}
|
|
3753
3807
|
});
|
|
@@ -3878,7 +3932,12 @@ async function runStructureAspect(params) {
|
|
|
3878
3932
|
const checkFn = mod.check;
|
|
3879
3933
|
const allowedSet = collectAllowedReadsForAspect(nodePath, graph);
|
|
3880
3934
|
const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles });
|
|
3881
|
-
const
|
|
3935
|
+
const expandedFilesByNode = /* @__PURE__ */ new Map();
|
|
3936
|
+
for (const id of computeAllowedNodePaths(nodePath, graph)) {
|
|
3937
|
+
const m = graph.nodes.get(id);
|
|
3938
|
+
if (m) expandedFilesByNode.set(id, await enumerateMappedFilesAsync(m.meta.mapping ?? [], projectRoot));
|
|
3939
|
+
}
|
|
3940
|
+
const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles, expandedFilesByNode });
|
|
3882
3941
|
const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache });
|
|
3883
3942
|
const ownFiles = await buildOwnFiles(node, projectRoot, touchedFiles);
|
|
3884
3943
|
await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
|
|
@@ -3992,12 +4051,22 @@ ${err.stack ?? ""}`,
|
|
|
3992
4051
|
}
|
|
3993
4052
|
violations.push(vv);
|
|
3994
4053
|
}
|
|
4054
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
4055
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
4056
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
4057
|
+
}
|
|
3995
4058
|
const rangesByFile = /* @__PURE__ */ new Map();
|
|
3996
4059
|
function rangesFor(filePath) {
|
|
3997
4060
|
const existing = rangesByFile.get(filePath);
|
|
3998
4061
|
if (existing !== void 0) return existing;
|
|
3999
4062
|
const cached = astCache.get(filePath);
|
|
4000
|
-
|
|
4063
|
+
let ranges;
|
|
4064
|
+
if (cached) {
|
|
4065
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
4066
|
+
} else {
|
|
4067
|
+
const content14 = contentByPath.get(filePath);
|
|
4068
|
+
ranges = content14 !== void 0 ? collectSuppressions(void 0, filePath, content14.split("\n").length, content14) : null;
|
|
4069
|
+
}
|
|
4001
4070
|
rangesByFile.set(filePath, ranges);
|
|
4002
4071
|
return ranges;
|
|
4003
4072
|
}
|
|
@@ -4591,7 +4660,7 @@ The CLI (\`yg\`) reads and validates \u2014 it never modifies files. You create
|
|
|
4591
4660
|
.drift-state/ \u2190 generated by CLI; never edit manually
|
|
4592
4661
|
\`\`\`
|
|
4593
4662
|
|
|
4594
|
-
**Nodes** \u2014 components. \`model/<path>/yg-node.yaml\`. Nodes nest by directory \u2014 children inherit parent aspects. Schema: \`schemas/yg-node.yaml\`.
|
|
4663
|
+
**Nodes** \u2014 components. \`model/<path>/yg-node.yaml\`. Nodes nest by directory \u2014 children inherit parent aspects. Schema: \`schemas/yg-node.yaml\`. Node \`mapping:\` entries and architecture \`when.path\` both accept minimatch glob patterns \u2014 \`*\` matches within a single path segment, \`**\` matches across segments (e.g. \`src/db/*Repository.cs\` maps only repository files in that directory; \`src/**/*.ts\` maps all TypeScript files under src).
|
|
4595
4664
|
|
|
4596
4665
|
**Aspects** \u2014 enforceable rules. \`aspects/<id>/yg-aspect.yaml\` + zero or one rule source files. The reviewer kind is inferred from which rule source is present: \`content.md\` \u2192 LLM reviewer; \`check.mjs\` \u2192 deterministic reviewer; neither file but \`implies:\` declared \u2192 aggregating aspect (a named bundle with no own reviewer). The \`reviewer:\` block in \`yg-aspect.yaml\` is optional \u2014 kind is inferred automatically. If present, an explicit \`reviewer.type\` must agree with the inferred kind. LLM aspects may set \`reviewer.tier:\` to pick a named tier from \`yg-config.yaml\` (otherwise the configured default tier is used). An aspect can declare \`implies: [other-aspect]\` \u2014 implied aspects are included recursively (must be acyclic). LLM aspects may declare \`references:\` \u2014 supporting files (lookup tables, catalogues) included in the reviewer prompt and exposed to the agent under \`read:\`. Schema: \`schemas/yg-aspect.yaml\`. Aspects also carry a \`status:\` field (default \`enforced\`) \u2014 three levels \`draft / advisory / enforced\` control whether the reviewer runs and whether violations block.
|
|
4597
4666
|
|
|
@@ -4984,7 +5053,7 @@ context.
|
|
|
4984
5053
|
|
|
4985
5054
|
**Flow** \u2014 when you see a sequence of steps toward a business goal. Not code call sequences \u2014 real-world processes. "User places an order" = flow. "Handler calls service" = relation between nodes. Read \`schemas/yg-flow.yaml\` and \`yg knowledge read flows\` before creating.
|
|
4986
5055
|
|
|
4987
|
-
**Node** \u2014 one per cohesive feature area. Not per directory, not per file. If a node's mapped source (plus any aspect reference files) exceeds the per-node character budget (\`quality.max_node_chars\`, default 40000) or it covers >3 distinct workflows, split into children \u2014 \`yg check\` enforces the budget as an \`oversized-node\` error. Why: the reviewer
|
|
5056
|
+
**Node** \u2014 one per cohesive feature area. Not per directory, not per file. If a node's mapped source (plus any aspect reference files) exceeds the per-node character budget (\`quality.max_node_chars\`, default 40000) or it covers >3 distinct workflows, split into children \u2014 \`yg check\` enforces the budget as an \`oversized-node\` error. Why: the LLM reviewer sends ALL of a node's files in one prompt, so an oversized context dilutes focus and risks window truncation that falsely rejects unchanged code. The budget therefore applies ONLY to nodes an LLM reviewer actually reads \u2014 those with at least one non-draft LLM aspect; deterministic-only and aspect-less nodes carry no budget (a \`check.mjs\` reads files programmatically, with no context window). Keep LLM-reviewed nodes well under the budget. Binary files (images, fonts, archives, etc.) count 0 toward the budget automatically. A node mapping a single unsplittable generated **text** artifact (e.g. a large lockfile) can opt out with \`sizeExempt: { reason: "..." }\`. Read \`schemas/yg-node.yaml\` before creating.
|
|
4988
5057
|
|
|
4989
5058
|
**Port / relation** \u2014 when a critical aspect must cross a node boundary, or when a new typed dependency is needed. Bare relations do NOT propagate aspects; ports do. Six relation types exist (\`calls\`, \`uses\`, \`extends\`, \`implements\`, \`emits\`, \`listens\`); event relations must be paired. Deep dive: \`yg knowledge read ports-and-relations\`.
|
|
4990
5059
|
|
|
@@ -5719,13 +5788,6 @@ function parseCoverage(raw, filename) {
|
|
|
5719
5788
|
const cov = raw;
|
|
5720
5789
|
const required = cov.required === void 0 ? ["/"] : parseStringArray(cov.required, "coverage.required", filename);
|
|
5721
5790
|
const excluded = parseStringArray(cov.excluded, "coverage.excluded", filename);
|
|
5722
|
-
if (required.length === 0) {
|
|
5723
|
-
throw new ConfigParseError({
|
|
5724
|
-
what: `${filename}: coverage.required must list at least one root.`,
|
|
5725
|
-
why: "An empty required list silently turns every unmapped file into a non-blocking warning, disabling coverage enforcement.",
|
|
5726
|
-
next: "Omit the coverage block to require the whole repo, or list real roots (e.g. - services/)."
|
|
5727
|
-
}, "config-invalid");
|
|
5728
|
-
}
|
|
5729
5791
|
for (const root of [...required, ...excluded]) {
|
|
5730
5792
|
if (root.split("/").includes("..")) {
|
|
5731
5793
|
throw new ConfigParseError({
|
|
@@ -5748,6 +5810,17 @@ function parseMaxNodeChars(raw, filename) {
|
|
|
5748
5810
|
}
|
|
5749
5811
|
return raw;
|
|
5750
5812
|
}
|
|
5813
|
+
function parseMaxDirectRelations(raw, filename) {
|
|
5814
|
+
if (raw === void 0) return DEFAULT_QUALITY.max_direct_relations ?? 10;
|
|
5815
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0) {
|
|
5816
|
+
throw new ConfigParseError({
|
|
5817
|
+
what: `${filename}: quality.max_direct_relations must be a positive integer (got ${JSON.stringify(raw)}).`,
|
|
5818
|
+
why: "It is the per-node relation-count budget; a zero, negative, or fractional value makes the threshold nonsensical.",
|
|
5819
|
+
next: "Set quality.max_direct_relations to a positive integer (default 10), or remove it to use the default."
|
|
5820
|
+
}, "config-invalid");
|
|
5821
|
+
}
|
|
5822
|
+
return raw;
|
|
5823
|
+
}
|
|
5751
5824
|
var PROVIDER_DEFAULTS = {
|
|
5752
5825
|
"claude-code": { model: "haiku" },
|
|
5753
5826
|
"codex": { model: "o4-mini" },
|
|
@@ -5775,7 +5848,7 @@ async function parseConfig(filePath) {
|
|
|
5775
5848
|
}
|
|
5776
5849
|
const qualityMap = qualityRaw;
|
|
5777
5850
|
const quality = qualityMap ? {
|
|
5778
|
-
max_direct_relations:
|
|
5851
|
+
max_direct_relations: parseMaxDirectRelations(qualityMap.max_direct_relations, filename),
|
|
5779
5852
|
max_node_chars: parseMaxNodeChars(qualityMap.max_node_chars, filename)
|
|
5780
5853
|
} : DEFAULT_QUALITY;
|
|
5781
5854
|
let reviewer;
|
|
@@ -5952,6 +6025,13 @@ function parseTier(name, raw, filename) {
|
|
|
5952
6025
|
next: "add `model: <model-name>` under config:"
|
|
5953
6026
|
}, "config-tier-config-missing");
|
|
5954
6027
|
}
|
|
6028
|
+
if (t.provider === "openai-compatible" && (typeof c.endpoint !== "string" || !c.endpoint.trim())) {
|
|
6029
|
+
throw new ConfigParseError({
|
|
6030
|
+
what: `${filename}: tier '${name}' (provider 'openai-compatible') is missing config.endpoint`,
|
|
6031
|
+
why: `'openai-compatible' has no default host \u2014 without an explicit endpoint it silently falls back to the public OpenAI API (api.openai.com).`,
|
|
6032
|
+
next: "add `endpoint: <url>` under config: pointing at your compatible server."
|
|
6033
|
+
}, "config-tier-endpoint-missing");
|
|
6034
|
+
}
|
|
5955
6035
|
const allowed = /* @__PURE__ */ new Set(["provider", "consensus", "config", "references"]);
|
|
5956
6036
|
for (const k of Object.keys(t)) {
|
|
5957
6037
|
if (!allowed.has(k)) {
|
|
@@ -6375,6 +6455,10 @@ function parseRelations(raw, filePath) {
|
|
|
6375
6455
|
);
|
|
6376
6456
|
}
|
|
6377
6457
|
rel.consumes = consumesArr;
|
|
6458
|
+
} else if (obj.consumes !== void 0) {
|
|
6459
|
+
throw new Error(
|
|
6460
|
+
`yg-node.yaml at ${filePath}: relations[${index}].consumes must be an array of string port names (got ${Array.isArray(obj.consumes) ? "array" : typeof obj.consumes}). A scalar value is silently ignored, so the consumed port's required aspects would not be enforced. Use consumes: [<port-name>].`
|
|
6461
|
+
);
|
|
6378
6462
|
}
|
|
6379
6463
|
if (typeof obj.event_name === "string" && obj.event_name.trim()) {
|
|
6380
6464
|
rel.event_name = obj.event_name.trim();
|
|
@@ -6933,13 +7017,13 @@ function parseReviewer2(raw, aspectId, files) {
|
|
|
6933
7017
|
next: "add `type: llm` or `type: deterministic` under reviewer:"
|
|
6934
7018
|
}
|
|
6935
7019
|
});
|
|
6936
|
-
} else if (obj.type !== "llm" && obj.type !== "deterministic") {
|
|
7020
|
+
} else if (obj.type !== "llm" && obj.type !== "deterministic" && obj.type !== "aggregate") {
|
|
6937
7021
|
errors.push({
|
|
6938
7022
|
code: "aspect-reviewer-type-invalid",
|
|
6939
7023
|
messageData: {
|
|
6940
7024
|
what: `aspect '${aspectId}' has invalid reviewer.type: '${String(obj.type)}'`,
|
|
6941
|
-
why: 'only "llm"
|
|
6942
|
-
next: "change to type: llm or type:
|
|
7025
|
+
why: 'only "llm", "deterministic", or "aggregate" are valid',
|
|
7026
|
+
next: "change to type: llm, type: deterministic, or type: aggregate"
|
|
6943
7027
|
}
|
|
6944
7028
|
});
|
|
6945
7029
|
} else {
|
|
@@ -9427,7 +9511,7 @@ init_posix();
|
|
|
9427
9511
|
import path21 from "path";
|
|
9428
9512
|
|
|
9429
9513
|
// src/core/file-when-evaluator.ts
|
|
9430
|
-
|
|
9514
|
+
init_mapping_path();
|
|
9431
9515
|
var YGGDRASIL_PREFIX = ".yggdrasil/";
|
|
9432
9516
|
function safeRegexTest(re, str) {
|
|
9433
9517
|
const HEAD_LIMIT = 256 * 1024;
|
|
@@ -9506,7 +9590,7 @@ async function evaluateAtomic2(predicate, ctx) {
|
|
|
9506
9590
|
);
|
|
9507
9591
|
}
|
|
9508
9592
|
if (predicate.path !== void 0) {
|
|
9509
|
-
const matches =
|
|
9593
|
+
const matches = globMatch(ctx.repoRelPath, predicate.path);
|
|
9510
9594
|
return {
|
|
9511
9595
|
result: matches,
|
|
9512
9596
|
trace: { kind: "atom-path", pattern: predicate.path, result: matches }
|
|
@@ -9549,7 +9633,15 @@ async function evaluateAtomic2(predicate, ctx) {
|
|
|
9549
9633
|
}
|
|
9550
9634
|
};
|
|
9551
9635
|
}
|
|
9552
|
-
|
|
9636
|
+
let regex;
|
|
9637
|
+
try {
|
|
9638
|
+
regex = new RegExp(predicate.content);
|
|
9639
|
+
} catch {
|
|
9640
|
+
return {
|
|
9641
|
+
result: false,
|
|
9642
|
+
trace: { kind: "atom-content", pattern: predicate.content, result: false, detail: "invalid content regex" }
|
|
9643
|
+
};
|
|
9644
|
+
}
|
|
9553
9645
|
const { match: matches } = safeRegexTest(regex, fileContent.content);
|
|
9554
9646
|
return {
|
|
9555
9647
|
result: matches,
|
|
@@ -9610,6 +9702,9 @@ function renderNode(node, indent, lines) {
|
|
|
9610
9702
|
}
|
|
9611
9703
|
|
|
9612
9704
|
// src/core/checks/architecture.ts
|
|
9705
|
+
init_hash();
|
|
9706
|
+
init_mapping_path();
|
|
9707
|
+
init_posix();
|
|
9613
9708
|
function checkTypeUnknownParent(graph) {
|
|
9614
9709
|
const issues = [];
|
|
9615
9710
|
const knownTypes = new Set(Object.keys(graph.architecture.node_types));
|
|
@@ -9767,7 +9862,15 @@ async function checkTypeWhenMismatch(graph, cache) {
|
|
|
9767
9862
|
const typeDef = graph.architecture.node_types[node.meta.type];
|
|
9768
9863
|
if (typeDef === void 0 || typeDef.when === void 0) continue;
|
|
9769
9864
|
const mapping = node.meta.mapping ?? [];
|
|
9770
|
-
|
|
9865
|
+
const pathsToCheck = [];
|
|
9866
|
+
for (const entry of mapping) {
|
|
9867
|
+
if (isGlobPattern(entry)) {
|
|
9868
|
+
pathsToCheck.push(...(await expandMappingPaths(projectRoot, [entry])).map(toPosixPath));
|
|
9869
|
+
} else {
|
|
9870
|
+
pathsToCheck.push(entry);
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
for (const relPath of pathsToCheck) {
|
|
9771
9874
|
const absPath = path21.join(projectRoot, relPath);
|
|
9772
9875
|
const result = await evaluateFileWhen(typeDef.when, {
|
|
9773
9876
|
absPath,
|
|
@@ -10266,6 +10369,23 @@ function checkWhenReferences(graph) {
|
|
|
10266
10369
|
})
|
|
10267
10370
|
});
|
|
10268
10371
|
}
|
|
10372
|
+
} else if (match.consumes_port !== void 0) {
|
|
10373
|
+
const port = match.consumes_port;
|
|
10374
|
+
const known = [...graph.nodes.values()].some(
|
|
10375
|
+
(n) => n.meta.ports !== void 0 && port in n.meta.ports
|
|
10376
|
+
);
|
|
10377
|
+
if (!known) {
|
|
10378
|
+
issues.push({
|
|
10379
|
+
severity: "error",
|
|
10380
|
+
code: "when-unknown-port",
|
|
10381
|
+
rule: "when-unknown-port",
|
|
10382
|
+
...issueMsg({
|
|
10383
|
+
what: `Port '${port}' in when at ${ctx}/${relType}.consumes_port is not declared on any node.`,
|
|
10384
|
+
why: "The predicate references a port that no node defines, so it can never match.",
|
|
10385
|
+
next: `Fix the port name, or declare it under ports: on the node(s) this relation targets.`
|
|
10386
|
+
})
|
|
10387
|
+
});
|
|
10388
|
+
}
|
|
10269
10389
|
}
|
|
10270
10390
|
}
|
|
10271
10391
|
};
|
|
@@ -10657,7 +10777,7 @@ function checkAspectStatusDowngrade(graph) {
|
|
|
10657
10777
|
for (const source of sources) {
|
|
10658
10778
|
if (!sourceIsExplicit(source, node, aspectId, graph)) continue;
|
|
10659
10779
|
const otherDeclared = sources.filter((s) => s !== source).map((s) => s.declared);
|
|
10660
|
-
const anchor = otherDeclared
|
|
10780
|
+
const anchor = [...otherDeclared, aspectDefault].reduce(
|
|
10661
10781
|
(acc, cur) => STATUS_ORDER[cur] > STATUS_ORDER[acc] ? cur : acc,
|
|
10662
10782
|
"draft"
|
|
10663
10783
|
);
|
|
@@ -10687,6 +10807,7 @@ function checkAspectStatusDowngrade(graph) {
|
|
|
10687
10807
|
// src/core/checks/mapping.ts
|
|
10688
10808
|
init_paths();
|
|
10689
10809
|
init_hash();
|
|
10810
|
+
init_mapping_path();
|
|
10690
10811
|
init_graph_fs();
|
|
10691
10812
|
init_repo_scanner();
|
|
10692
10813
|
init_aspects();
|
|
@@ -10734,12 +10855,6 @@ async function checkStrictBackwardCoverage(graph, cache) {
|
|
|
10734
10855
|
);
|
|
10735
10856
|
if (strictTypes.length === 0) return { issues: [], unreadable: [] };
|
|
10736
10857
|
const projectRoot = path23.dirname(graph.rootPath);
|
|
10737
|
-
const fileToOwner = /* @__PURE__ */ new Map();
|
|
10738
|
-
for (const [nodePath, node] of graph.nodes) {
|
|
10739
|
-
for (const relPath of node.meta.mapping ?? []) {
|
|
10740
|
-
if (!fileToOwner.has(relPath)) fileToOwner.set(relPath, { nodePath, nodeType: node.meta.type });
|
|
10741
|
-
}
|
|
10742
|
-
}
|
|
10743
10858
|
const repoFiles = await walkRepoFiles(projectRoot);
|
|
10744
10859
|
const issues = [];
|
|
10745
10860
|
const unreadable = [];
|
|
@@ -10802,7 +10917,14 @@ Run: yg impact --type ${sorted[j]}`
|
|
|
10802
10917
|
}
|
|
10803
10918
|
if (matchingTypes.length === 0) continue;
|
|
10804
10919
|
const { typeId, trace } = matchingTypes[0];
|
|
10805
|
-
|
|
10920
|
+
let owner;
|
|
10921
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
10922
|
+
const entries = node.meta.mapping ?? [];
|
|
10923
|
+
if (entries.some((entry) => mappingEntryMatchesFile(entry, relPath))) {
|
|
10924
|
+
owner = { nodePath, nodeType: node.meta.type };
|
|
10925
|
+
break;
|
|
10926
|
+
}
|
|
10927
|
+
}
|
|
10806
10928
|
if (owner === void 0) {
|
|
10807
10929
|
issues.push({
|
|
10808
10930
|
severity: "error",
|
|
@@ -10847,7 +10969,7 @@ function arePathsOverlapping(pathA, pathB) {
|
|
|
10847
10969
|
function isAncestorNode(possibleAncestor, possibleDescendant) {
|
|
10848
10970
|
return possibleDescendant.startsWith(possibleAncestor + "/");
|
|
10849
10971
|
}
|
|
10850
|
-
function checkMappingOverlap(graph) {
|
|
10972
|
+
async function checkMappingOverlap(graph) {
|
|
10851
10973
|
const issues = [];
|
|
10852
10974
|
const ownership = [];
|
|
10853
10975
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -10893,6 +11015,46 @@ function checkMappingOverlap(graph) {
|
|
|
10893
11015
|
});
|
|
10894
11016
|
}
|
|
10895
11017
|
}
|
|
11018
|
+
const anyGlob = [...graph.nodes.values()].some(
|
|
11019
|
+
(n) => (n.meta.mapping ?? []).some((e) => isGlobPattern(e))
|
|
11020
|
+
);
|
|
11021
|
+
if (anyGlob) {
|
|
11022
|
+
const projectRoot = path23.dirname(graph.rootPath);
|
|
11023
|
+
const repoFiles = await walkRepoFiles(projectRoot);
|
|
11024
|
+
const reported = /* @__PURE__ */ new Set();
|
|
11025
|
+
for (const rawRel of repoFiles) {
|
|
11026
|
+
const relPath = normalizePathForCompare(rawRel);
|
|
11027
|
+
const owners = [];
|
|
11028
|
+
let viaGlob = false;
|
|
11029
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
11030
|
+
let matched = false;
|
|
11031
|
+
for (const entry of node.meta.mapping ?? []) {
|
|
11032
|
+
if (!mappingEntryMatchesFile(entry, relPath)) continue;
|
|
11033
|
+
matched = true;
|
|
11034
|
+
if (isGlobPattern(entry)) viaGlob = true;
|
|
11035
|
+
}
|
|
11036
|
+
if (matched) owners.push(nodePath);
|
|
11037
|
+
}
|
|
11038
|
+
if (owners.length < 2 || !viaGlob || reported.has(relPath)) continue;
|
|
11039
|
+
const leaves = owners.filter(
|
|
11040
|
+
(o) => !owners.some((other) => other !== o && isAncestorNode(o, other))
|
|
11041
|
+
);
|
|
11042
|
+
if (leaves.length < 2) continue;
|
|
11043
|
+
reported.add(relPath);
|
|
11044
|
+
issues.push({
|
|
11045
|
+
severity: "error",
|
|
11046
|
+
code: "overlapping-mapping",
|
|
11047
|
+
rule: "overlapping-mapping",
|
|
11048
|
+
...issueMsg({
|
|
11049
|
+
what: `File '${relPath}' is owned by multiple non-hierarchical nodes:
|
|
11050
|
+
${leaves.map((n) => " " + n).join("\n")}`,
|
|
11051
|
+
why: `Each source file must have exactly one owner node. A glob mapping in one node resolves to a file also claimed by another node.`,
|
|
11052
|
+
next: `Narrow the glob, or remove the file from one node's mapping and model the dependency via a relation.`
|
|
11053
|
+
}),
|
|
11054
|
+
nodePath: leaves[0]
|
|
11055
|
+
});
|
|
11056
|
+
}
|
|
11057
|
+
}
|
|
10896
11058
|
return issues;
|
|
10897
11059
|
}
|
|
10898
11060
|
async function checkMappingPathsExist(graph) {
|
|
@@ -10901,21 +11063,38 @@ async function checkMappingPathsExist(graph) {
|
|
|
10901
11063
|
for (const [nodePath, node] of graph.nodes) {
|
|
10902
11064
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizePathForCompare);
|
|
10903
11065
|
for (const mp of mappingPaths) {
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
}
|
|
11066
|
+
if (isGlobPattern(mp)) {
|
|
11067
|
+
const matched = await expandMappingPaths(projectRoot, [mp]);
|
|
11068
|
+
if (matched.length === 0) {
|
|
11069
|
+
issues.push({
|
|
11070
|
+
severity: "error",
|
|
11071
|
+
code: "mapping-path-missing",
|
|
11072
|
+
rule: "mapping-path-missing",
|
|
11073
|
+
...issueMsg({
|
|
11074
|
+
what: `Glob '${mp}' matches no files on disk.`,
|
|
11075
|
+
why: `Node maps a glob pattern that currently resolves to no files \u2014 possibly all matching files were deleted or the pattern is wrong.`,
|
|
11076
|
+
next: `Update mapping in yg-node.yaml: fix the glob or remove the entry.`
|
|
11077
|
+
}),
|
|
11078
|
+
nodePath
|
|
11079
|
+
});
|
|
11080
|
+
}
|
|
11081
|
+
} else {
|
|
11082
|
+
const absPath = path23.join(projectRoot, mp);
|
|
11083
|
+
try {
|
|
11084
|
+
await fileAccess(absPath);
|
|
11085
|
+
} catch {
|
|
11086
|
+
issues.push({
|
|
11087
|
+
severity: "error",
|
|
11088
|
+
code: "mapping-path-missing",
|
|
11089
|
+
rule: "mapping-path-missing",
|
|
11090
|
+
...issueMsg({
|
|
11091
|
+
what: `Mapping path '${mp}' does not exist on disk.`,
|
|
11092
|
+
why: `Node maps a file that was deleted or moved.`,
|
|
11093
|
+
next: `Update mapping in yg-node.yaml: fix the path or remove the entry.`
|
|
11094
|
+
}),
|
|
11095
|
+
nodePath
|
|
11096
|
+
});
|
|
11097
|
+
}
|
|
10919
11098
|
}
|
|
10920
11099
|
}
|
|
10921
11100
|
}
|
|
@@ -10985,6 +11164,7 @@ async function checkOversizedNodes(graph, cache) {
|
|
|
10985
11164
|
refsByAspect.set(aspect.id, aspect.references.map((r) => r.path));
|
|
10986
11165
|
}
|
|
10987
11166
|
}
|
|
11167
|
+
const aspectById = new Map(graph.aspects.map((a) => [a.id, a]));
|
|
10988
11168
|
async function charsOf(repoRelPath) {
|
|
10989
11169
|
if (BINARY_EXTENSIONS.has(path23.extname(repoRelPath).toLowerCase())) return 0;
|
|
10990
11170
|
const abs = path23.resolve(projectRoot, repoRelPath);
|
|
@@ -11004,13 +11184,24 @@ async function checkOversizedNodes(graph, cache) {
|
|
|
11004
11184
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
11005
11185
|
if (mappingPaths.length === 0) continue;
|
|
11006
11186
|
const sourceFiles = await expandMappingPaths(projectRoot, mappingPaths);
|
|
11007
|
-
const refPaths = /* @__PURE__ */ new Set();
|
|
11008
11187
|
let effectiveAspects;
|
|
11009
11188
|
try {
|
|
11010
11189
|
effectiveAspects = computeEffectiveAspects(node, graph);
|
|
11011
11190
|
} catch {
|
|
11012
11191
|
effectiveAspects = /* @__PURE__ */ new Set();
|
|
11013
11192
|
}
|
|
11193
|
+
let statuses;
|
|
11194
|
+
try {
|
|
11195
|
+
statuses = computeEffectiveAspectStatuses(node, graph);
|
|
11196
|
+
} catch {
|
|
11197
|
+
statuses = /* @__PURE__ */ new Map();
|
|
11198
|
+
}
|
|
11199
|
+
const isLlmReviewed = [...effectiveAspects].some((id) => {
|
|
11200
|
+
const def = aspectById.get(id);
|
|
11201
|
+
return def?.reviewer.type === "llm" && (statuses.get(id) ?? "enforced") !== "draft";
|
|
11202
|
+
});
|
|
11203
|
+
if (!isLlmReviewed) continue;
|
|
11204
|
+
const refPaths = /* @__PURE__ */ new Set();
|
|
11014
11205
|
for (const aspectId of effectiveAspects) {
|
|
11015
11206
|
for (const rp of refsByAspect.get(aspectId) ?? []) refPaths.add(rp);
|
|
11016
11207
|
}
|
|
@@ -11448,7 +11639,7 @@ async function validate(graph, scope = "all") {
|
|
|
11448
11639
|
issues.push(...checkSchemas(graph));
|
|
11449
11640
|
issues.push(...checkRelationTargets(graph));
|
|
11450
11641
|
issues.push(...checkNoCycles(graph));
|
|
11451
|
-
issues.push(...checkMappingOverlap(graph));
|
|
11642
|
+
issues.push(...await checkMappingOverlap(graph));
|
|
11452
11643
|
issues.push(...checkMappingEscapesRepo(graph));
|
|
11453
11644
|
issues.push(...await checkMappingPathsExist(graph));
|
|
11454
11645
|
issues.push(...checkBrokenFlowRefs(graph));
|
|
@@ -11520,6 +11711,7 @@ init_debug_log();
|
|
|
11520
11711
|
init_message_builder();
|
|
11521
11712
|
init_paths();
|
|
11522
11713
|
init_posix();
|
|
11714
|
+
init_mapping_path();
|
|
11523
11715
|
function normalizeForMatch(inputPath) {
|
|
11524
11716
|
return toPosixPath(inputPath.trim());
|
|
11525
11717
|
}
|
|
@@ -11529,17 +11721,25 @@ function findOwner(graph, projectRoot, rawPath) {
|
|
|
11529
11721
|
for (const [nodePath, node] of graph.nodes) {
|
|
11530
11722
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizeForMatch).filter((mappingPath) => mappingPath.length > 0);
|
|
11531
11723
|
for (const mappingPath of mappingPaths) {
|
|
11532
|
-
if (
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11724
|
+
if (isGlobPattern(mappingPath)) {
|
|
11725
|
+
if (mappingEntryMatchesFile(mappingPath, file)) {
|
|
11726
|
+
if (!best || mappingPath.length > best.mappingPath.length) {
|
|
11727
|
+
best = { nodePath, mappingPath, exact: true };
|
|
11728
|
+
}
|
|
11729
|
+
}
|
|
11730
|
+
} else {
|
|
11731
|
+
if (file === mappingPath) {
|
|
11732
|
+
return { file, nodePath, mappingPath, direct: true };
|
|
11733
|
+
}
|
|
11734
|
+
if (file.startsWith(mappingPath + "/")) {
|
|
11735
|
+
if (!best || mappingPath.length > best.mappingPath.length) {
|
|
11736
|
+
best = { nodePath, mappingPath, exact: false };
|
|
11737
|
+
}
|
|
11538
11738
|
}
|
|
11539
11739
|
}
|
|
11540
11740
|
}
|
|
11541
11741
|
}
|
|
11542
|
-
return best ? { file, nodePath: best.nodePath, mappingPath: best.mappingPath, direct:
|
|
11742
|
+
return best ? { file, nodePath: best.nodePath, mappingPath: best.mappingPath, direct: best.exact } : { file, nodePath: null };
|
|
11543
11743
|
}
|
|
11544
11744
|
function registerOwnerCommand(program2) {
|
|
11545
11745
|
program2.command("owner").description("Find which graph node owns a source file").requiredOption("--file <path>", "File path (relative to repository root)").action(async (options) => {
|
|
@@ -11559,11 +11759,21 @@ function registerOwnerCommand(program2) {
|
|
|
11559
11759
|
exists = false;
|
|
11560
11760
|
}
|
|
11561
11761
|
if (exists) {
|
|
11562
|
-
process.stdout.write(
|
|
11563
|
-
|
|
11762
|
+
process.stdout.write(
|
|
11763
|
+
buildIssueMessage({
|
|
11764
|
+
what: `${result.file} -> no graph coverage`,
|
|
11765
|
+
why: "This file exists but no graph node maps it, so its code is not verified against any aspect.",
|
|
11766
|
+
next: `Add '${result.file}' to a node's mapping in yg-node.yaml, or create a node for it.`
|
|
11767
|
+
}) + "\n"
|
|
11768
|
+
);
|
|
11564
11769
|
} else {
|
|
11565
|
-
process.stdout.write(
|
|
11566
|
-
|
|
11770
|
+
process.stdout.write(
|
|
11771
|
+
buildIssueMessage({
|
|
11772
|
+
what: `${result.file} -> no graph coverage (file not found)`,
|
|
11773
|
+
why: "This path does not exist on disk and is not mapped by any graph node.",
|
|
11774
|
+
next: `Check the path for typos; once the file exists, add it to a node's mapping in yg-node.yaml.`
|
|
11775
|
+
}) + "\n"
|
|
11776
|
+
);
|
|
11567
11777
|
}
|
|
11568
11778
|
} else {
|
|
11569
11779
|
process.stdout.write(`${result.file} -> ${result.nodePath}
|
|
@@ -11791,6 +12001,13 @@ var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
|
|
|
11791
12001
|
"when-unknown-type",
|
|
11792
12002
|
"when-unknown-node",
|
|
11793
12003
|
"when-unknown-port",
|
|
12004
|
+
// Port-contract codes — blocking architecture-gate errors (documented in the
|
|
12005
|
+
// ports-and-relations knowledge topic); belong in the single-source structural set.
|
|
12006
|
+
"port-missing-consumes",
|
|
12007
|
+
"port-undefined",
|
|
12008
|
+
"port-missing-aspect",
|
|
12009
|
+
"consumes-without-ports",
|
|
12010
|
+
"relation-target-forbidden",
|
|
11794
12011
|
"aspect-unexpected-rule-source",
|
|
11795
12012
|
"aspect-missing-rule-source",
|
|
11796
12013
|
"aspect-empty",
|
|
@@ -11819,14 +12036,16 @@ var COMPLETENESS_CODES = /* @__PURE__ */ new Set(["description-missing"]);
|
|
|
11819
12036
|
init_log_format();
|
|
11820
12037
|
init_posix();
|
|
11821
12038
|
init_repo_scanner();
|
|
12039
|
+
init_mapping_path();
|
|
11822
12040
|
|
|
11823
12041
|
// src/core/check-coverage-tiers.ts
|
|
11824
12042
|
init_posix();
|
|
12043
|
+
init_mapping_path();
|
|
11825
12044
|
function normalizeRoot(root) {
|
|
11826
12045
|
return toPosixPath(root.trim()).replace(/^\/+/, "").replace(/\/+$/, "").replace(/\/{2,}/g, "/");
|
|
11827
12046
|
}
|
|
11828
12047
|
function matchesRoot(file, normRoot) {
|
|
11829
|
-
return normRoot === "" ||
|
|
12048
|
+
return normRoot === "" || mappingEntryMatchesFile(normRoot, file);
|
|
11830
12049
|
}
|
|
11831
12050
|
function partitionByCoverageTier(uncovered, coverage) {
|
|
11832
12051
|
const req = coverage.required.map(normalizeRoot);
|
|
@@ -12159,14 +12378,7 @@ function scanUncoveredFiles(graph, gitTrackedFiles) {
|
|
|
12159
12378
|
for (const file of tracked) {
|
|
12160
12379
|
const normalized = toPosixPath(file.trim());
|
|
12161
12380
|
if (normalized.startsWith(yggPrefix + "/") || normalized === yggPrefix) continue;
|
|
12162
|
-
|
|
12163
|
-
for (const rawMp of allMappings) {
|
|
12164
|
-
const mp = toPosixPath(rawMp);
|
|
12165
|
-
if (normalized === mp || normalized.startsWith(mp + "/")) {
|
|
12166
|
-
covered = true;
|
|
12167
|
-
break;
|
|
12168
|
-
}
|
|
12169
|
-
}
|
|
12381
|
+
const covered = allMappings.some((mp) => mappingEntryMatchesFile(mp, normalized));
|
|
12170
12382
|
if (!covered) {
|
|
12171
12383
|
uncovered.push(normalized);
|
|
12172
12384
|
}
|
|
@@ -12332,10 +12544,15 @@ function getChildMappingExclusions2(graph, nodePath) {
|
|
|
12332
12544
|
}
|
|
12333
12545
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
12334
12546
|
for (const mp of mappingPaths) {
|
|
12335
|
-
|
|
12336
|
-
await
|
|
12337
|
-
return false;
|
|
12338
|
-
}
|
|
12547
|
+
if (isGlobPattern(mp)) {
|
|
12548
|
+
const matched = await expandMappingPaths(projectRoot, [mp]);
|
|
12549
|
+
if (matched.length > 0) return false;
|
|
12550
|
+
} else {
|
|
12551
|
+
try {
|
|
12552
|
+
await fileAccess(path34.join(projectRoot, mp));
|
|
12553
|
+
return false;
|
|
12554
|
+
} catch {
|
|
12555
|
+
}
|
|
12339
12556
|
}
|
|
12340
12557
|
}
|
|
12341
12558
|
return true;
|
|
@@ -12444,7 +12661,7 @@ function computeSuggestedNext(issues, graph) {
|
|
|
12444
12661
|
addRemaining(coverageErrors.length > 0 ? coverageErrors[0].uncoveredCount ?? 0 : 0, "files need coverage");
|
|
12445
12662
|
const then = remaining.length > 0 ? `
|
|
12446
12663
|
Then: ${remaining.join(", ")}` : "";
|
|
12447
|
-
return `Fix ${first.code} in ${first.nodePath ?? ".yggdrasil
|
|
12664
|
+
return `Fix ${first.code} in ${first.nodePath ?? ".yggdrasil"}
|
|
12448
12665
|
1 of ${structuralErrors.length} structural error${structuralErrors.length === 1 ? "" : "s"}${then}`;
|
|
12449
12666
|
}
|
|
12450
12667
|
if (coverageErrors.length > 0) {
|
|
@@ -14249,6 +14466,7 @@ init_loader_hook();
|
|
|
14249
14466
|
init_parser();
|
|
14250
14467
|
init_suppress();
|
|
14251
14468
|
init_validate_check_module();
|
|
14469
|
+
init_language_registry();
|
|
14252
14470
|
import path37 from "path";
|
|
14253
14471
|
import { readFile as readFile20 } from "fs/promises";
|
|
14254
14472
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
@@ -14296,18 +14514,15 @@ async function runAstAspect(params) {
|
|
|
14296
14514
|
continue;
|
|
14297
14515
|
}
|
|
14298
14516
|
const content14 = await readFile20(path37.resolve(params.projectRoot, f.path), "utf-8");
|
|
14517
|
+
if (getLanguageForExtension(path37.extname(f.path).toLowerCase()) === null) {
|
|
14518
|
+
sourceFiles.push({ path: f.path, content: content14, ast: void 0 });
|
|
14519
|
+
continue;
|
|
14520
|
+
}
|
|
14299
14521
|
let ast;
|
|
14300
14522
|
try {
|
|
14301
14523
|
ast = await parseFile(f.path, content14);
|
|
14302
14524
|
} catch (e) {
|
|
14303
14525
|
const msg = e.message ?? String(e);
|
|
14304
|
-
if (msg.startsWith("no parser for extension")) {
|
|
14305
|
-
throw new AstRunnerError("AST_NO_PARSER_FOR_EXTENSION", {
|
|
14306
|
-
what: msg + ` (file: ${f.path})`,
|
|
14307
|
-
why: `v1 supports only .ts/.tsx/.js/.mjs/.cjs/.jsx.`,
|
|
14308
|
-
next: `Remove ${f.path} from the node's mapping.`
|
|
14309
|
-
});
|
|
14310
|
-
}
|
|
14311
14526
|
throw new AstRunnerError("AST_GRAMMAR_LOAD_FAILED", {
|
|
14312
14527
|
what: `Failed to load tree-sitter grammar for ${f.path}: ${msg}`,
|
|
14313
14528
|
why: `The bundled WASM grammar could not be loaded.`,
|
|
@@ -14328,7 +14543,7 @@ async function runAstAspect(params) {
|
|
|
14328
14543
|
const rangesPerFile = /* @__PURE__ */ new Map();
|
|
14329
14544
|
for (const f of sourceFiles) {
|
|
14330
14545
|
const totalLines = f.content.split("\n").length;
|
|
14331
|
-
rangesPerFile.set(f.path, collectSuppressions(f.ast, f.path, totalLines));
|
|
14546
|
+
rangesPerFile.set(f.path, collectSuppressions(f.ast, f.path, totalLines, f.content));
|
|
14332
14547
|
}
|
|
14333
14548
|
const ctx = { files: sourceFiles };
|
|
14334
14549
|
let raw;
|
|
@@ -15610,6 +15825,20 @@ predicate satisfaction fraction, or edge-case messages for files inside
|
|
|
15610
15825
|
Run this whenever you add or modify a type's \`when\` predicate and want
|
|
15611
15826
|
to verify that existing files are classified as expected.
|
|
15612
15827
|
|
|
15828
|
+
## Glob patterns in mapping and when.path
|
|
15829
|
+
|
|
15830
|
+
Both node \`mapping:\` entries and architecture \`when.path\` predicates accept
|
|
15831
|
+
minimatch glob patterns. \`*\` matches any characters within a single path
|
|
15832
|
+
segment (does not cross \`/\`); \`**\` matches across path segments.
|
|
15833
|
+
|
|
15834
|
+
Examples:
|
|
15835
|
+
- \`src/db/*Repository.cs\` \u2014 owns only files matching \`*Repository.cs\` directly
|
|
15836
|
+
inside \`src/db/\`, not subdirectory files or non-matching files like \`Helper.cs\`.
|
|
15837
|
+
- \`src/**/*.ts\` \u2014 owns all \`.ts\` files anywhere under \`src/\` at any depth.
|
|
15838
|
+
|
|
15839
|
+
Plain (non-glob) entries remain unchanged: an exact file path or a directory
|
|
15840
|
+
prefix (e.g. \`src/handlers\`) covers that file or all files beneath it.
|
|
15841
|
+
|
|
15613
15842
|
## Aspect status in architecture default aspects
|
|
15614
15843
|
|
|
15615
15844
|
Architecture-level default aspects (channel 3) may declare \`status:\` to
|
|
@@ -16013,7 +16242,11 @@ var content4 = `# Writing deterministic aspects
|
|
|
16013
16242
|
A deterministic aspect declares \`reviewer: { type: deterministic }\` and ships
|
|
16014
16243
|
a \`check.mjs\` file. The check runs locally at zero LLM cost and returns a
|
|
16015
16244
|
\`Violation[]\`. Deterministic aspects do not use reviewer tiers \u2014
|
|
16016
|
-
\`reviewer.tier:\` is rejected together with \`type: deterministic\`.
|
|
16245
|
+
\`reviewer.tier:\` is rejected together with \`type: deterministic\`. Because the
|
|
16246
|
+
check reads files programmatically (no LLM prompt), the per-node character budget
|
|
16247
|
+
(\`quality.max_node_chars\` / \`oversized-node\`) does NOT apply to a node whose
|
|
16248
|
+
only effective aspects are deterministic \u2014 such a node may map an arbitrarily
|
|
16249
|
+
large area.
|
|
16017
16250
|
|
|
16018
16251
|
There are two ways to scope a deterministic aspect:
|
|
16019
16252
|
|
|
@@ -16126,7 +16359,7 @@ arrives in the one \`check.mjs\` invocation.
|
|
|
16126
16359
|
| \`walk(node, visitor)\` | \`(node, (n) => boolean|void) => void\` | DFS traversal; visitor returning \`false\` skips descent into that subtree |
|
|
16127
16360
|
| \`report(file, node, message)\` | \`(file, TreeNode, string) => Violation\` | Build a \`{ file, line, column, message }\` \u2014 \`line\` 1-based, \`column\` 0-based |
|
|
16128
16361
|
| \`inFile(file, pattern)\` | \`(file, { glob } | { regex } | { contains }) => boolean\` | Path filter (discriminated object form) |
|
|
16129
|
-
| \`findComments(target)\` | \`(file
|
|
16362
|
+
| \`findComments(target)\` | \`(file) => TreeNode[]\` | Returns comment nodes within a file (language derived from its path) |
|
|
16130
16363
|
| \`closest(node, types)\` | \`(TreeNode, string[]) => TreeNode | null\` | Nearest ancestor whose \`type\` is in \`types\` |
|
|
16131
16364
|
|
|
16132
16365
|
## tree-sitter node API
|
|
@@ -16385,7 +16618,7 @@ parsed AST trees via \`ctx.parseAst\`:
|
|
|
16385
16618
|
| \`closest(node, types)\` | \`(TreeNode, string[]) => TreeNode | null\` | Nearest ancestor of one of the given types |
|
|
16386
16619
|
| \`report(file, node, message)\` | \`(file, TreeNode, string) => Violation\` | Build \`{ file, line, column, message }\` \u2014 line 1-based, column 0-based |
|
|
16387
16620
|
| \`inFile(file, pattern)\` | \`(file, { glob } | { regex } | { contains }) => boolean\` | Path filter |
|
|
16388
|
-
| \`findComments(target)\` | \`(file
|
|
16621
|
+
| \`findComments(target)\` | \`(file) => TreeNode[]\` | Returns comment nodes |
|
|
16389
16622
|
|
|
16390
16623
|
These helpers are optional \u2014 most graph-aware checks work purely with \`ctx.graph\`
|
|
16391
16624
|
and \`ctx.fs\` without parsing AST trees at all.
|
|
@@ -16890,6 +17123,21 @@ For generated files where the entire file is exempt, place the marker at
|
|
|
16890
17123
|
the file level (outside any function or class). At file level, the
|
|
16891
17124
|
contextual scope is the whole file.
|
|
16892
17125
|
|
|
17126
|
+
## Language support
|
|
17127
|
+
|
|
17128
|
+
Markers are recognized in any source language, using whichever comment syntax
|
|
17129
|
+
the language provides \u2014 \`//\` and \`/* */\` (C-family), \`#\` (shell, Python),
|
|
17130
|
+
\`--\` (SQL), and so on. The marker token \`yg-suppress(...)\` is what is matched,
|
|
17131
|
+
not a specific comment style.
|
|
17132
|
+
|
|
17133
|
+
For a file whose extension has a registered grammar, markers are read from the
|
|
17134
|
+
file's comments, so a \`yg-suppress(...)\` that merely appears inside a string
|
|
17135
|
+
literal is NOT treated as a marker. For a file whose extension has no registered
|
|
17136
|
+
grammar (e.g. \`.sql\`, \`.md\`, \`.sh\`), there is no parse tree, so markers are
|
|
17137
|
+
found by scanning the raw lines \u2014 which is what lets a content-only deterministic
|
|
17138
|
+
check waive a violation in such a file. (In that raw-scan mode a marker token
|
|
17139
|
+
sitting inside a string literal would also match, so keep markers in comments.)
|
|
17140
|
+
|
|
16893
17141
|
## Reason text
|
|
16894
17142
|
|
|
16895
17143
|
The reason text after the aspect-id is permanent. Future maintainers and
|
|
@@ -17156,7 +17404,7 @@ reviewer:
|
|
|
17156
17404
|
config:
|
|
17157
17405
|
model: qwen3 # Model identifier for this provider
|
|
17158
17406
|
temperature: 0 # Sampling temperature (0 = deterministic)
|
|
17159
|
-
endpoint: http://localhost:11434 # Required for ollama
|
|
17407
|
+
endpoint: http://localhost:11434 # Required for openai-compatible (no default host); ollama defaults to localhost:11434
|
|
17160
17408
|
# references: # optional caps on aspect reference files
|
|
17161
17409
|
# max_bytes_per_file: 65536 # default: 64 KiB per reference file
|
|
17162
17410
|
# max_total_bytes_per_aspect: 262144 # default: 256 KiB total per aspect
|
|
@@ -17168,7 +17416,7 @@ coverage: # Optional \u2014 controls which files must
|
|
|
17168
17416
|
|
|
17169
17417
|
quality:
|
|
17170
17418
|
max_direct_relations: 10 # Max out-edges per node before high-fan-out warning
|
|
17171
|
-
max_node_chars: 40000 # Per-node
|
|
17419
|
+
max_node_chars: 40000 # Per-node char budget (source + aspect refs); oversized-node error \u2014 LLM-reviewed nodes only
|
|
17172
17420
|
|
|
17173
17421
|
parallel: 1 # Concurrent aspect verifications across nodes (default: 1)
|
|
17174
17422
|
|
|
@@ -17235,7 +17483,7 @@ Provider-specific options passed to the LLM client:
|
|
|
17235
17483
|
|---|---|---|
|
|
17236
17484
|
| \`model\` | string | Required. Provider-specific model identifier. |
|
|
17237
17485
|
| \`temperature\` | number | Defaults to 0. Higher = more varied responses. |
|
|
17238
|
-
| \`endpoint\` | string | Required for \`
|
|
17486
|
+
| \`endpoint\` | string | Required for \`openai-compatible\` (no default host \u2014 else falls back to api.openai.com); \`ollama\` defaults to http://localhost:11434. |
|
|
17239
17487
|
| \`timeout\` | number | Timeout in seconds. Default 300. Applies to CLI providers only \u2014 non-CLI/API providers ignore it. |
|
|
17240
17488
|
|
|
17241
17489
|
API keys do NOT live here \u2014 they belong in \`yg-secrets.yaml\` (api_key only).
|
|
@@ -17306,18 +17554,20 @@ repository \u2014 it is safe to share.
|
|
|
17306
17554
|
\`\`\`yaml
|
|
17307
17555
|
coverage:
|
|
17308
17556
|
required:
|
|
17309
|
-
- src/
|
|
17557
|
+
- src/ # unmapped files under src/ are a blocking error
|
|
17310
17558
|
excluded:
|
|
17311
|
-
- vendor/
|
|
17559
|
+
- vendor/ # silently ignored
|
|
17560
|
+
- "**/*.generated.ts" # glob: drop generated files anywhere
|
|
17312
17561
|
\`\`\`
|
|
17313
17562
|
|
|
17314
17563
|
Controls which git-tracked files must be mapped to a node.
|
|
17315
17564
|
|
|
17316
|
-
- \`required\` \u2014
|
|
17317
|
-
- \`excluded\` \u2014
|
|
17565
|
+
- \`required\` \u2014 roots where unmapped files are a blocking \`unmapped-files\` error. Default: \`["/"]\` (whole repo \u2014 the previous always-map-everything behavior). An explicit empty list \`[]\` means require nothing: every uncovered file (outside \`excluded\`/nested) becomes a non-blocking \`uncovered-advisory\` warning and nothing blocks (pure-advisory adoption). Empty only counts when written explicitly; omitting the \`coverage\` block keeps the \`["/"]\` default.
|
|
17566
|
+
- \`excluded\` \u2014 roots that are silently ignored. Default: \`[]\`.
|
|
17567
|
+
- Roots accept the same forms as a node \`mapping:\` entry: an exact file, a directory prefix (e.g. \`src/\` covers everything beneath it), or a glob (\`*\` within a segment, \`**\` across) \u2014 so \`excluded: ["**/*.generated.ts"]\` drops generated files anywhere and \`required: ["services/*/api/**"]\` scopes the blocking tier to a pattern. \`/\` still means the whole repo.
|
|
17318
17568
|
- Files that match neither a required nor an excluded root produce a non-blocking \`uncovered-advisory\` warning.
|
|
17319
17569
|
- Subtrees containing their own nested \`.yggdrasil/\` are auto-skipped by all repo-walking checks (they are governed by their own graph).
|
|
17320
|
-
- Longest-match wins; on a tie between required and excluded, excluded wins.
|
|
17570
|
+
- Longest-match wins (by normalized root/pattern length); on a tie between required and excluded, excluded wins.
|
|
17321
17571
|
|
|
17322
17572
|
## Quality thresholds
|
|
17323
17573
|
|
|
@@ -17330,6 +17580,9 @@ quality:
|
|
|
17330
17580
|
\`max_direct_relations\` fires a warning when exceeded. \`max_node_chars\` is a
|
|
17331
17581
|
blocking error: a node whose mapped source plus aspect reference files exceed it
|
|
17332
17582
|
(binary files do not count) must be split into children to stay under the budget.
|
|
17583
|
+
The budget applies only to nodes an LLM reviewer actually reads \u2014 those with at
|
|
17584
|
+
least one non-draft LLM aspect; deterministic-only and aspect-less nodes are not
|
|
17585
|
+
bounded (a check.mjs reads files programmatically, with no context window).
|
|
17333
17586
|
For a node mapping a single unsplittable generated or binary artifact (a lockfile,
|
|
17334
17587
|
an append-only changelog, an image), opt out per-node with
|
|
17335
17588
|
\`sizeExempt: { reason: "<why it cannot be split>" }\`.
|
|
@@ -17396,8 +17649,9 @@ Unified gate \u2014 runs all validators in sequence.
|
|
|
17396
17649
|
yg check
|
|
17397
17650
|
\`\`\`
|
|
17398
17651
|
|
|
17399
|
-
Detects: drift (source + cascade), validation errors, coverage gaps
|
|
17400
|
-
\`unmapped-files
|
|
17652
|
+
Detects: drift (source + cascade), validation errors, coverage gaps
|
|
17653
|
+
(\`unmapped-files\` errors under required roots, \`uncovered-advisory\` warnings
|
|
17654
|
+
outside them), type-when mismatches, strict orphans/misplaced files.
|
|
17401
17655
|
|
|
17402
17656
|
Exit 0 = clean. Exit 1 = errors found. CI blocks on exit 1.
|
|
17403
17657
|
|
|
@@ -17808,8 +18062,10 @@ ports: # map keyed by port name (NOT a list)
|
|
|
17808
18062
|
\`\`\`
|
|
17809
18063
|
|
|
17810
18064
|
Every aspect id listed in a port's \`aspects\` must be defined under
|
|
17811
|
-
\`aspects
|
|
17812
|
-
\`
|
|
18065
|
+
\`aspects/\`. An undefined id is caught unconditionally by the
|
|
18066
|
+
reference-integrity check (code \`aspect-undefined\`); when the port is
|
|
18067
|
+
actually consumed, the missing aspect additionally surfaces as
|
|
18068
|
+
\`port-missing-aspect\`.
|
|
17813
18069
|
|
|
17814
18070
|
A consumer references the port via the relation's \`consumes\`. In
|
|
17815
18071
|
\`yg-node.yaml\`, \`relations:\` is a flat list and each entry carries its own
|
|
@@ -17854,12 +18110,11 @@ a blocking error on the missing port contract, surfacing the gap.
|
|
|
17854
18110
|
|
|
17855
18111
|
If a target node declares ports and the consumer's relation does NOT
|
|
17856
18112
|
declare \`consumes\`, \`yg check\` emits a blocking error (code
|
|
17857
|
-
\`port-missing-consumes\`) that fails the architecture gate
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
\`\`\`
|
|
18113
|
+
\`port-missing-consumes\`) that fails the architecture gate. Like every
|
|
18114
|
+
diagnostic, it is rendered in the what/why/next form: it names the
|
|
18115
|
+
relation, explains that the target's port-required aspects won't be
|
|
18116
|
+
verified without a \`consumes\` declaration, and tells you to add
|
|
18117
|
+
\`consumes: [<port-names>]\` to the relation.
|
|
17863
18118
|
|
|
17864
18119
|
There is no "accept the gap" mechanism. Resolve it one of two ways:
|
|
17865
18120
|
declare which port(s) you consume on the relation, or remove the ports
|