@chrisdudek/yg 5.0.0-alpha.2 → 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 +582 -187
- package/dist/structure.d.ts +8 -0
- package/dist/structure.js +111 -49
- package/graph-schemas/yg-architecture.yaml +1 -1
- package/graph-schemas/yg-config.yaml +6 -0
- package/package.json +1 -1
package/dist/structure.d.ts
CHANGED
|
@@ -158,6 +158,12 @@ interface ReviewerConfig {
|
|
|
158
158
|
/** At least one entry required; key is the tier name */
|
|
159
159
|
tiers: Record<string, LlmConfig>;
|
|
160
160
|
}
|
|
161
|
+
interface CoverageConfig {
|
|
162
|
+
/** Roots (POSIX, from repo root; "/" = whole repo) where an uncovered file is an error. */
|
|
163
|
+
required: string[];
|
|
164
|
+
/** Roots where an uncovered file is silent (no issue). */
|
|
165
|
+
excluded: string[];
|
|
166
|
+
}
|
|
161
167
|
interface YggConfig {
|
|
162
168
|
version?: string;
|
|
163
169
|
quality?: QualityConfig;
|
|
@@ -167,6 +173,8 @@ interface YggConfig {
|
|
|
167
173
|
reviewer?: ReviewerConfig;
|
|
168
174
|
parallel?: number;
|
|
169
175
|
debug?: boolean;
|
|
176
|
+
/** Coverage scope. Absent ⇒ DEFAULT_COVERAGE (whole repo required = today's behavior). */
|
|
177
|
+
coverage?: CoverageConfig;
|
|
170
178
|
}
|
|
171
179
|
interface ArchitectureNodeType {
|
|
172
180
|
description: string;
|
package/dist/structure.js
CHANGED
|
@@ -28,9 +28,23 @@ import * as fs from "fs";
|
|
|
28
28
|
import * as path2 from "path";
|
|
29
29
|
|
|
30
30
|
// src/utils/mapping-path.ts
|
|
31
|
+
import { minimatch } from "minimatch";
|
|
31
32
|
function normalizeMappingPath(p) {
|
|
32
33
|
return p.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
33
34
|
}
|
|
35
|
+
function isGlobPattern(entry) {
|
|
36
|
+
return entry.includes("*");
|
|
37
|
+
}
|
|
38
|
+
function globMatch(file, pattern) {
|
|
39
|
+
return minimatch(file, pattern, { dot: true });
|
|
40
|
+
}
|
|
41
|
+
function mappingEntryMatchesFile(entry, file) {
|
|
42
|
+
const e = normalizeMappingPath(entry);
|
|
43
|
+
const f = normalizeMappingPath(file);
|
|
44
|
+
if (e === "") return false;
|
|
45
|
+
if (isGlobPattern(e)) return globMatch(f, e);
|
|
46
|
+
return f === e || f.startsWith(e + "/");
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
// src/utils/posix.ts
|
|
36
50
|
function toPosix(p) {
|
|
@@ -52,9 +66,8 @@ function isAllowed(p, set) {
|
|
|
52
66
|
if (p === "") return false;
|
|
53
67
|
if (set.has(p)) return true;
|
|
54
68
|
for (const a of set) {
|
|
55
|
-
if (a === p) return true;
|
|
56
69
|
if (a.startsWith(p + "/")) return true;
|
|
57
|
-
if (
|
|
70
|
+
if (mappingEntryMatchesFile(a, p)) return true;
|
|
58
71
|
}
|
|
59
72
|
return false;
|
|
60
73
|
}
|
|
@@ -135,13 +148,7 @@ import * as path3 from "path";
|
|
|
135
148
|
function isPathInMapping(candidate, mapping) {
|
|
136
149
|
const c = normalizeMappingPath(candidate);
|
|
137
150
|
if (c === "") return false;
|
|
138
|
-
|
|
139
|
-
const n = normalizeMappingPath(raw);
|
|
140
|
-
if (n === "") continue;
|
|
141
|
-
if (c === n) return true;
|
|
142
|
-
if (c.startsWith(n + "/")) return true;
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
151
|
+
return mapping.some((raw) => mappingEntryMatchesFile(raw, c));
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
// src/structure/ctx-graph.ts
|
|
@@ -181,15 +188,16 @@ function computeAllowedNodePaths(currentPath, graph) {
|
|
|
181
188
|
return allowed;
|
|
182
189
|
}
|
|
183
190
|
function createCtxGraph(params) {
|
|
184
|
-
const { currentNodePath, graph, projectRoot, touchedFiles } = params;
|
|
191
|
+
const { currentNodePath, graph, projectRoot, touchedFiles, expandedFilesByNode } = params;
|
|
185
192
|
const allowed = computeAllowedNodePaths(currentNodePath, graph);
|
|
186
193
|
function assertAllowed(id) {
|
|
187
194
|
if (!allowed.has(id)) throw new UndeclaredGraphReadError(id);
|
|
188
195
|
}
|
|
189
196
|
function toPublicNode(m) {
|
|
190
197
|
const files = [];
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
const preExpanded = expandedFilesByNode?.get(m.path);
|
|
199
|
+
const candidatePaths = preExpanded ?? (m.meta.mapping ?? []).map(normalizeMappingPath);
|
|
200
|
+
for (const p of candidatePaths) {
|
|
193
201
|
if (!p) continue;
|
|
194
202
|
const abs = path3.resolve(projectRoot, p);
|
|
195
203
|
try {
|
|
@@ -500,12 +508,16 @@ var GRAMMAR_DIRS = [
|
|
|
500
508
|
path4.resolve(__dirname2, "grammars"),
|
|
501
509
|
path4.resolve(__dirname2, "..", "grammars")
|
|
502
510
|
];
|
|
503
|
-
var
|
|
511
|
+
var initPromise = null;
|
|
504
512
|
var langCache = /* @__PURE__ */ new Map();
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
513
|
+
function init() {
|
|
514
|
+
if (initPromise === null) {
|
|
515
|
+
initPromise = Parser.init();
|
|
516
|
+
initPromise.catch(() => {
|
|
517
|
+
initPromise = null;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return initPromise;
|
|
509
521
|
}
|
|
510
522
|
function resolveWasm(filename, pkg) {
|
|
511
523
|
for (const dir of GRAMMAR_DIRS) {
|
|
@@ -528,12 +540,16 @@ async function getParser(extension) {
|
|
|
528
540
|
throw new Error(`no parser for extension '${extension}'`);
|
|
529
541
|
}
|
|
530
542
|
const cacheKey = info.wasmFile;
|
|
531
|
-
let
|
|
532
|
-
if (
|
|
543
|
+
let langP = langCache.get(cacheKey);
|
|
544
|
+
if (langP === void 0) {
|
|
533
545
|
const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
|
|
534
|
-
|
|
535
|
-
langCache.set(cacheKey,
|
|
546
|
+
langP = Language.load(wasmPath);
|
|
547
|
+
langCache.set(cacheKey, langP);
|
|
548
|
+
langP.catch(() => {
|
|
549
|
+
if (langCache.get(cacheKey) === langP) langCache.delete(cacheKey);
|
|
550
|
+
});
|
|
536
551
|
}
|
|
552
|
+
const lang = await langP;
|
|
537
553
|
const parser = new Parser();
|
|
538
554
|
parser.setLanguage(lang);
|
|
539
555
|
return parser;
|
|
@@ -764,26 +780,50 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
764
780
|
result.push(...fileStats);
|
|
765
781
|
return result;
|
|
766
782
|
}
|
|
783
|
+
async function expandGlobEntry(projectRoot, glob, gitignoreStack) {
|
|
784
|
+
const segments = glob.split("/");
|
|
785
|
+
const firstGlobIdx = segments.findIndex((s) => isGlobPattern(s));
|
|
786
|
+
const baseSegments = firstGlobIdx > 0 ? segments.slice(0, firstGlobIdx) : [];
|
|
787
|
+
const baseDir = baseSegments.length > 0 ? path7.join(projectRoot, ...baseSegments) : projectRoot;
|
|
788
|
+
try {
|
|
789
|
+
const dirEntries = await collectDirectoryFilePaths(baseDir, projectRoot, {
|
|
790
|
+
projectRoot,
|
|
791
|
+
gitignoreStack
|
|
792
|
+
});
|
|
793
|
+
return dirEntries.filter((entry) => globMatch(entry.relPath, glob)).map((entry) => ({
|
|
794
|
+
relPath: toPosixPath(entry.relPath),
|
|
795
|
+
absPath: entry.absPath,
|
|
796
|
+
mtimeMs: entry.mtimeMs
|
|
797
|
+
}));
|
|
798
|
+
} catch {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
}
|
|
767
802
|
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
768
803
|
const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
|
|
769
804
|
const result = [];
|
|
770
805
|
for (const mp of mappingPaths) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
806
|
+
if (isGlobPattern(mp)) {
|
|
807
|
+
const entries = await expandGlobEntry(projectRoot, mp, gitignoreStack);
|
|
808
|
+
for (const entry of entries) result.push(entry.relPath);
|
|
809
|
+
} else {
|
|
810
|
+
const absPath = path7.join(projectRoot, mp);
|
|
811
|
+
try {
|
|
812
|
+
const st = await stat(absPath);
|
|
813
|
+
if (st.isDirectory()) {
|
|
814
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
815
|
+
projectRoot,
|
|
816
|
+
gitignoreStack
|
|
817
|
+
});
|
|
818
|
+
for (const entry of dirEntries) {
|
|
819
|
+
result.push(toPosixPath(path7.join(mp, entry.relPath)));
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
result.push(toPosixPath(mp));
|
|
781
823
|
}
|
|
782
|
-
}
|
|
783
|
-
|
|
824
|
+
} catch {
|
|
825
|
+
continue;
|
|
784
826
|
}
|
|
785
|
-
} catch {
|
|
786
|
-
continue;
|
|
787
827
|
}
|
|
788
828
|
}
|
|
789
829
|
return result;
|
|
@@ -831,8 +871,8 @@ var SuppressMarkerError = class extends Error {
|
|
|
831
871
|
}
|
|
832
872
|
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
833
873
|
};
|
|
834
|
-
var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)
|
|
835
|
-
var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)
|
|
874
|
+
var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
875
|
+
var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
836
876
|
var RE_ENABLE = /\byg-suppress-enable\(\s*([^)]+?)\s*\)/;
|
|
837
877
|
function commentBody(text) {
|
|
838
878
|
if (text.startsWith("//")) return text.slice(2).trim();
|
|
@@ -866,15 +906,23 @@ function parseMarker(commentText, line, file) {
|
|
|
866
906
|
if (m) return makeMarker("single", m, line, file);
|
|
867
907
|
return null;
|
|
868
908
|
}
|
|
869
|
-
function collectSuppressions(tree, file, totalLines) {
|
|
870
|
-
|
|
871
|
-
return [];
|
|
872
|
-
}
|
|
873
|
-
const comments = findComments({ path: file, ast: tree });
|
|
909
|
+
function collectSuppressions(tree, file, totalLines, content) {
|
|
910
|
+
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
874
911
|
const markers = [];
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
|
|
912
|
+
if (hasGrammar && tree) {
|
|
913
|
+
const comments = findComments({ path: file, ast: tree });
|
|
914
|
+
for (const c of comments) {
|
|
915
|
+
const m = parseMarker(c.text, c.startPosition.row + 1, file);
|
|
916
|
+
if (m) markers.push(m);
|
|
917
|
+
}
|
|
918
|
+
} else if (content !== void 0) {
|
|
919
|
+
const lines = content.split("\n");
|
|
920
|
+
for (let i = 0; i < lines.length; i++) {
|
|
921
|
+
const m = parseMarker(lines[i], i + 1, file);
|
|
922
|
+
if (m) markers.push(m);
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
return [];
|
|
878
926
|
}
|
|
879
927
|
markers.sort((a, b) => a.line - b.line);
|
|
880
928
|
const ranges = [];
|
|
@@ -1080,7 +1128,12 @@ async function runStructureAspect(params) {
|
|
|
1080
1128
|
const checkFn = mod.check;
|
|
1081
1129
|
const allowedSet = collectAllowedReadsForAspect(nodePath, graph);
|
|
1082
1130
|
const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles });
|
|
1083
|
-
const
|
|
1131
|
+
const expandedFilesByNode = /* @__PURE__ */ new Map();
|
|
1132
|
+
for (const id of computeAllowedNodePaths(nodePath, graph)) {
|
|
1133
|
+
const m = graph.nodes.get(id);
|
|
1134
|
+
if (m) expandedFilesByNode.set(id, await enumerateMappedFilesAsync(m.meta.mapping ?? [], projectRoot));
|
|
1135
|
+
}
|
|
1136
|
+
const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles, expandedFilesByNode });
|
|
1084
1137
|
const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache });
|
|
1085
1138
|
const ownFiles = await buildOwnFiles(node, projectRoot, touchedFiles);
|
|
1086
1139
|
await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
|
|
@@ -1194,12 +1247,22 @@ ${err.stack ?? ""}`,
|
|
|
1194
1247
|
}
|
|
1195
1248
|
violations.push(vv);
|
|
1196
1249
|
}
|
|
1250
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
1251
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
1252
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
1253
|
+
}
|
|
1197
1254
|
const rangesByFile = /* @__PURE__ */ new Map();
|
|
1198
1255
|
function rangesFor(filePath) {
|
|
1199
1256
|
const existing = rangesByFile.get(filePath);
|
|
1200
1257
|
if (existing !== void 0) return existing;
|
|
1201
1258
|
const cached = astCache.get(filePath);
|
|
1202
|
-
|
|
1259
|
+
let ranges;
|
|
1260
|
+
if (cached) {
|
|
1261
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1262
|
+
} else {
|
|
1263
|
+
const content = contentByPath.get(filePath);
|
|
1264
|
+
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1265
|
+
}
|
|
1203
1266
|
rangesByFile.set(filePath, ranges);
|
|
1204
1267
|
return ranges;
|
|
1205
1268
|
}
|
|
@@ -1242,9 +1305,8 @@ function report(file, node, message) {
|
|
|
1242
1305
|
}
|
|
1243
1306
|
|
|
1244
1307
|
// src/ast/file-path.ts
|
|
1245
|
-
import { minimatch } from "minimatch";
|
|
1246
1308
|
function inFile(file, pattern) {
|
|
1247
|
-
if ("glob" in pattern) return
|
|
1309
|
+
if ("glob" in pattern) return globMatch(file.path, pattern.glob);
|
|
1248
1310
|
if ("regex" in pattern) return pattern.regex.test(file.path);
|
|
1249
1311
|
if ("contains" in pattern) return file.path.includes(pattern.contains);
|
|
1250
1312
|
return false;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
node_types:
|
|
11
11
|
<type-id>:
|
|
12
12
|
description: <string> # required — what this type is for, when to use it.
|
|
13
|
-
#
|
|
13
|
+
# absence is a FATAL architecture-invalid error (the whole type system is rejected).
|
|
14
14
|
|
|
15
15
|
when: <file-predicate> # optional — per-file classification.
|
|
16
16
|
# Types WITH `when` are file-classifying: every file in
|
|
@@ -14,6 +14,12 @@ parallel: 1 # optional — concurrency limit for batch app
|
|
|
14
14
|
debug: false # optional — when true, appends all command output to .yggdrasil/.debug.log
|
|
15
15
|
# Default: false (off). Log is append-only; rotate or delete manually.
|
|
16
16
|
|
|
17
|
+
coverage: # optional — scopes the unmapped-files gate. Absent = whole repo required (today's behavior).
|
|
18
|
+
required: ["/"] # roots where an uncovered tracked file is an ERROR (blocks). "/" = whole repo.
|
|
19
|
+
excluded: [] # roots where an uncovered file is SILENT (no warning).
|
|
20
|
+
# Files outside required and excluded are a non-blocking WARNING.
|
|
21
|
+
# Subtrees containing their own nested .yggdrasil/ are auto-skipped by every check.
|
|
22
|
+
|
|
17
23
|
reviewer: # required — aspect verification during yg approve
|
|
18
24
|
default: standard # required when more than one tier is configured; optional with exactly one tier.
|
|
19
25
|
# Must reference one of the keys under reviewer.tiers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrisdudek/yg",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.4",
|
|
4
4
|
"description": "Architecture rules your coding agent can't ignore. Written in Markdown, verified on every change, enforced in the agent's loop — not after on a PR. Works with Claude Code, Cursor, Copilot, Codex, Cline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|