@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.
@@ -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 (p.startsWith(a + "/")) return true;
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
- for (const raw of mapping) {
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
- for (const raw of m.meta.mapping ?? []) {
192
- const p = normalizeMappingPath(raw);
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 initialized = false;
511
+ var initPromise = null;
504
512
  var langCache = /* @__PURE__ */ new Map();
505
- async function init() {
506
- if (initialized) return;
507
- await Parser.init();
508
- initialized = true;
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 lang = langCache.get(cacheKey);
532
- if (!lang) {
543
+ let langP = langCache.get(cacheKey);
544
+ if (langP === void 0) {
533
545
  const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
534
- lang = await Language.load(wasmPath);
535
- langCache.set(cacheKey, lang);
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
- const absPath = path7.join(projectRoot, mp);
772
- try {
773
- const st = await stat(absPath);
774
- if (st.isDirectory()) {
775
- const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
776
- projectRoot,
777
- gitignoreStack
778
- });
779
- for (const entry of dirEntries) {
780
- result.push(toPosixPath(path7.join(mp, entry.relPath)));
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
- } else {
783
- result.push(toPosixPath(mp));
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
- if (getLanguageForExtension(extname3(file)) === null) {
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
- for (const c of comments) {
876
- const m = parseMarker(c.text, c.startPosition.row + 1, file);
877
- if (m) markers.push(m);
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 ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles });
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
- const ranges = cached ? collectSuppressions(cached.ast, filePath, cached.content.split("\n").length) : null;
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 minimatch(file.path, pattern.glob);
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
- # Validator emits description-missing if absent.
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.2",
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": {