@chrisdudek/yg 5.0.0-alpha.1 → 5.0.0-alpha.3

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 CHANGED
@@ -172,14 +172,14 @@ var init_mapping_path = __esm({
172
172
  });
173
173
 
174
174
  // src/io/paths.ts
175
- import path8 from "path";
175
+ import path9 from "path";
176
176
  import { fileURLToPath } from "url";
177
177
  import { stat as stat2 } from "fs/promises";
178
178
  async function findYggRoot(projectRoot) {
179
- let current = path8.resolve(projectRoot);
180
- const root = path8.parse(current).root;
179
+ let current = path9.resolve(projectRoot);
180
+ const root = path9.parse(current).root;
181
181
  while (true) {
182
- const yggPath = path8.join(current, ".yggdrasil");
182
+ const yggPath = path9.join(current, ".yggdrasil");
183
183
  try {
184
184
  const st = await stat2(yggPath);
185
185
  if (!st.isDirectory()) {
@@ -193,7 +193,7 @@ async function findYggRoot(projectRoot) {
193
193
  if (current === root) {
194
194
  throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
195
195
  }
196
- current = path8.dirname(current);
196
+ current = path9.dirname(current);
197
197
  continue;
198
198
  }
199
199
  throw err;
@@ -218,20 +218,20 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
218
218
  if (normalizedInput.length === 0) {
219
219
  throw new Error("Path cannot be empty");
220
220
  }
221
- const absolute = path8.resolve(projectRoot, normalizedInput);
222
- const relative4 = path8.relative(projectRoot, absolute);
223
- const isOutside = relative4.startsWith("..") || path8.isAbsolute(relative4);
221
+ const absolute = path9.resolve(projectRoot, normalizedInput);
222
+ const relative3 = path9.relative(projectRoot, absolute);
223
+ const isOutside = relative3.startsWith("..") || path9.isAbsolute(relative3);
224
224
  if (isOutside) {
225
225
  throw new Error(`Path is outside project root: ${rawPath}`);
226
226
  }
227
- return toPosixPath(relative4);
227
+ return toPosixPath(relative3);
228
228
  }
229
229
  function projectRootFromGraph(yggRootPath) {
230
- return path8.dirname(yggRootPath);
230
+ return path9.dirname(yggRootPath);
231
231
  }
232
232
  function resolveFileArg(repoRoot, rawPath) {
233
- const absolute = path8.resolve(repoRoot, rawPath.trim());
234
- return toPosixPath(path8.relative(repoRoot, absolute));
233
+ const absolute = path9.resolve(repoRoot, rawPath.trim());
234
+ return toPosixPath(path9.relative(repoRoot, absolute));
235
235
  }
236
236
  var init_paths = __esm({
237
237
  "src/io/paths.ts"() {
@@ -252,9 +252,9 @@ var init_drift = __esm({
252
252
 
253
253
  // src/io/atomic-write.ts
254
254
  import { writeFile as writeFile3, rename, mkdir as mkdir2, rm } from "fs/promises";
255
- import path10 from "path";
255
+ import path11 from "path";
256
256
  async function atomicWriteFile(filePath, content14) {
257
- const dir = path10.dirname(filePath);
257
+ const dir = path11.dirname(filePath);
258
258
  await mkdir2(dir, { recursive: true });
259
259
  const tmpPath = `${filePath}.tmp`;
260
260
  await rm(tmpPath, { force: true });
@@ -269,7 +269,7 @@ var init_atomic_write = __esm({
269
269
 
270
270
  // src/io/drift-state-store.ts
271
271
  import { readFile as readFile10, stat as stat3, readdir as readdir3, rm as rm2 } from "fs/promises";
272
- import path11 from "path";
272
+ import path12 from "path";
273
273
  function validateBaselineShape(nodePath, parsed) {
274
274
  if (parsed === null || typeof parsed !== "object") {
275
275
  throw new OutdatedDriftBaselineError(nodePath, void 0);
@@ -285,7 +285,7 @@ function validateBaselineShape(nodePath, parsed) {
285
285
  return parsed;
286
286
  }
287
287
  function nodeStatePath(yggRoot, nodePath) {
288
- return path11.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
288
+ return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
289
289
  }
290
290
  async function scanJsonFiles(dir, baseDir) {
291
291
  const results = [];
@@ -297,12 +297,12 @@ async function scanJsonFiles(dir, baseDir) {
297
297
  return results;
298
298
  }
299
299
  for (const entry of entries) {
300
- const fullPath = path11.join(dir, entry.name);
300
+ const fullPath = path12.join(dir, entry.name);
301
301
  if (entry.isDirectory()) {
302
302
  const nested = await scanJsonFiles(fullPath, baseDir);
303
303
  results.push(...nested);
304
304
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
305
- const relPath = path11.relative(baseDir, fullPath);
305
+ const relPath = path12.relative(baseDir, fullPath);
306
306
  const nodePath = toPosix(relPath).replace(/\.json$/, "");
307
307
  results.push(nodePath);
308
308
  }
@@ -310,13 +310,13 @@ async function scanJsonFiles(dir, baseDir) {
310
310
  return results;
311
311
  }
312
312
  async function removeEmptyParents(filePath, stopDir) {
313
- let dir = path11.dirname(filePath);
313
+ let dir = path12.dirname(filePath);
314
314
  while (dir !== stopDir && dir.startsWith(stopDir)) {
315
315
  try {
316
316
  const entries = await readdir3(dir);
317
317
  if (entries.length === 0) {
318
318
  await rm2(dir, { recursive: true });
319
- dir = path11.dirname(dir);
319
+ dir = path12.dirname(dir);
320
320
  } else {
321
321
  break;
322
322
  }
@@ -358,7 +358,7 @@ async function clearDraftAspectsFromDriftState(yggRoot, nodePath, aspectIdsToCle
358
358
  await writeNodeDriftState(yggRoot, nodePath, state);
359
359
  }
360
360
  async function garbageCollectDriftState(yggRoot, validNodePaths, shouldKeep) {
361
- const driftDir = path11.join(yggRoot, DRIFT_STATE_DIR);
361
+ const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
362
362
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
363
363
  const removed = [];
364
364
  for (const nodePath of allNodePaths) {
@@ -374,7 +374,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths, shouldKeep) {
374
374
  return removed.sort();
375
375
  }
376
376
  async function readDriftState(yggRoot) {
377
- const driftPath = path11.join(yggRoot, DRIFT_STATE_DIR);
377
+ const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
378
378
  let driftStat;
379
379
  try {
380
380
  driftStat = await stat3(driftPath);
@@ -570,9 +570,25 @@ async function collectFiles(dir, projectRoot, stack) {
570
570
  }
571
571
  return results;
572
572
  }
573
+ function excludeNestedGraphSubtrees(relPaths) {
574
+ const seg = `/${YGGDRASIL_DIRNAME}/`;
575
+ const nestedRoots = /* @__PURE__ */ new Set();
576
+ for (const p2 of relPaths) {
577
+ const idx = p2.indexOf(seg);
578
+ if (idx > 0) nestedRoots.add(p2.slice(0, idx));
579
+ }
580
+ if (nestedRoots.size === 0) return relPaths;
581
+ return relPaths.filter((p2) => {
582
+ for (const root of nestedRoots) {
583
+ if (p2 === root || p2.startsWith(root + "/")) return false;
584
+ }
585
+ return true;
586
+ });
587
+ }
573
588
  async function walkRepoFiles(projectRoot) {
574
589
  const stack = await loadRootGitignoreStack(projectRoot);
575
- return collectFiles(projectRoot, projectRoot, stack);
590
+ const files = await collectFiles(projectRoot, projectRoot, stack);
591
+ return excludeNestedGraphSubtrees(files);
576
592
  }
577
593
  var require2, ignoreFactory, YGGDRASIL_DIRNAME;
578
594
  var init_repo_scanner = __esm({
@@ -587,7 +603,7 @@ var init_repo_scanner = __esm({
587
603
 
588
604
  // src/io/hash.ts
589
605
  import { readFile as readFile16, readdir as readdir7, stat as stat5 } from "fs/promises";
590
- import path15 from "path";
606
+ import path16 from "path";
591
607
  import { createHash } from "crypto";
592
608
  import { createRequire as createRequire2 } from "module";
593
609
  async function hashFile(filePath) {
@@ -597,7 +613,7 @@ async function hashFile(filePath) {
597
613
  async function loadRootGitignoreStack2(projectRoot) {
598
614
  if (!projectRoot) return [];
599
615
  try {
600
- const content14 = await readFile16(path15.join(projectRoot, ".gitignore"), "utf-8");
616
+ const content14 = await readFile16(path16.join(projectRoot, ".gitignore"), "utf-8");
601
617
  const matcher = ignoreFactory2();
602
618
  matcher.add(content14);
603
619
  return [{ basePath: projectRoot, matcher }];
@@ -607,7 +623,7 @@ async function loadRootGitignoreStack2(projectRoot) {
607
623
  }
608
624
  function isIgnoredByStack2(candidatePath, stack) {
609
625
  for (const { basePath, matcher } of stack) {
610
- const relativePath = toPosix(path15.relative(basePath, candidatePath));
626
+ const relativePath = toPosix(path16.relative(basePath, candidatePath));
611
627
  if (relativePath === "" || relativePath.startsWith("..")) continue;
612
628
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
613
629
  }
@@ -636,20 +652,30 @@ function serializeIdentity(identity) {
636
652
  `aspects={${aspectLines}}`
637
653
  ].join("\n");
638
654
  }
639
- function computeCanonicalHash(fileHashes, identity) {
655
+ function serializeVerdicts(verdicts) {
656
+ return Object.keys(verdicts).sort().map((id) => {
657
+ const v = verdicts[id];
658
+ return `id=${id}|verdict=${v.verdict}|errorSource=${v.errorSource ?? ""}`;
659
+ }).join("\n");
660
+ }
661
+ function computeCanonicalHash(fileHashes, identity, verdicts = {}) {
640
662
  const filesDigest = Object.entries(fileHashes).map(([p2, h]) => `${p2}:${h}`).sort().join("\n");
641
- return hashString(`files:
663
+ return hashString(
664
+ `files:
642
665
  ${filesDigest}
643
666
  identity:
644
- ${serializeIdentity(identity)}`);
667
+ ${serializeIdentity(identity)}
668
+ verdicts:
669
+ ${serializeVerdicts(verdicts)}`
670
+ );
645
671
  }
646
- async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes, identity) {
672
+ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes, identity, verdicts, reuseByMtime = true) {
647
673
  const fileHashes = {};
648
674
  const fileMtimes = {};
649
675
  const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
650
676
  const allFiles = [];
651
677
  for (const tf of trackedFiles) {
652
- const absPath = path15.join(projectRoot, tf.path);
678
+ const absPath = path16.join(projectRoot, tf.path);
653
679
  try {
654
680
  const st = await stat5(absPath);
655
681
  if (st.isDirectory()) {
@@ -659,7 +685,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
659
685
  });
660
686
  for (const entry of dirEntries) {
661
687
  allFiles.push({
662
- relPath: toPosixPath(path15.join(tf.path, entry.relPath)),
688
+ relPath: toPosixPath(path16.join(tf.path, entry.relPath)),
663
689
  absPath: entry.absPath,
664
690
  mtimeMs: entry.mtimeMs
665
691
  });
@@ -676,7 +702,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
676
702
  for (const entry of filtered) {
677
703
  const storedMtime = storedFileData?.mtimes[entry.relPath];
678
704
  const storedHash = storedFileData?.hashes[entry.relPath];
679
- if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
705
+ if (reuseByMtime && storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
680
706
  fileHashes[entry.relPath] = storedHash;
681
707
  } else {
682
708
  dirty.push(entry);
@@ -691,13 +717,13 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
691
717
  fileHashes[batch[j].relPath] = hashes[j];
692
718
  }
693
719
  }
694
- const canonicalHash = computeCanonicalHash(fileHashes, identity ?? EMPTY_IDENTITY);
720
+ const canonicalHash = computeCanonicalHash(fileHashes, identity ?? EMPTY_IDENTITY, verdicts ?? {});
695
721
  return { canonicalHash, fileHashes, fileMtimes };
696
722
  }
697
723
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
698
724
  let stack = options.gitignoreStack ?? [];
699
725
  try {
700
- const localContent = await readFile16(path15.join(directoryPath, ".gitignore"), "utf-8");
726
+ const localContent = await readFile16(path16.join(directoryPath, ".gitignore"), "utf-8");
701
727
  const localMatcher = ignoreFactory2();
702
728
  localMatcher.add(localContent);
703
729
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -707,7 +733,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
707
733
  const dirs = [];
708
734
  const files = [];
709
735
  for (const entry of entries) {
710
- const absoluteChildPath = path15.join(directoryPath, entry.name);
736
+ const absoluteChildPath = path16.join(directoryPath, entry.name);
711
737
  if (isIgnoredByStack2(absoluteChildPath, stack)) continue;
712
738
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
713
739
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -720,7 +746,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
720
746
  Promise.all(files.map(async (f) => {
721
747
  const fileStat = await stat5(f);
722
748
  return {
723
- relPath: toPosixPath(path15.relative(rootDirectoryPath, f)),
749
+ relPath: toPosixPath(path16.relative(rootDirectoryPath, f)),
724
750
  absPath: f,
725
751
  mtimeMs: fileStat.mtimeMs
726
752
  };
@@ -735,7 +761,7 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
735
761
  const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
736
762
  const result = [];
737
763
  for (const mp of mappingPaths) {
738
- const absPath = path15.join(projectRoot, mp);
764
+ const absPath = path16.join(projectRoot, mp);
739
765
  try {
740
766
  const st = await stat5(absPath);
741
767
  if (st.isDirectory()) {
@@ -744,7 +770,7 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
744
770
  gitignoreStack
745
771
  });
746
772
  for (const entry of dirEntries) {
747
- result.push(toPosixPath(path15.join(mp, entry.relPath)));
773
+ result.push(toPosixPath(path16.join(mp, entry.relPath)));
748
774
  }
749
775
  } else {
750
776
  result.push(toPosixPath(mp));
@@ -1081,11 +1107,16 @@ function attachmentMachineOrigin(att, node) {
1081
1107
  }
1082
1108
  function hasNonDraftEffectiveAspects(node, graph) {
1083
1109
  const statuses = computeEffectiveAspectStatuses(node, graph);
1084
- for (const s of statuses.values()) {
1085
- if (s !== "draft") return true;
1110
+ for (const [aspectId, s] of statuses) {
1111
+ if (s === "draft") continue;
1112
+ if (isAggregateAspect(graph, aspectId)) continue;
1113
+ return true;
1086
1114
  }
1087
1115
  return false;
1088
1116
  }
1117
+ function isAggregateAspect(graph, aspectId) {
1118
+ return graph.aspects.find((a) => a.id === aspectId)?.reviewer.type === "aggregate";
1119
+ }
1089
1120
  var ImpliesCycleError;
1090
1121
  var init_aspects = __esm({
1091
1122
  "src/core/graph/aspects.ts"() {
@@ -1191,19 +1222,19 @@ var init_tier_identity = __esm({
1191
1222
  });
1192
1223
 
1193
1224
  // src/core/graph/files.ts
1194
- import path18 from "path";
1225
+ import path19 from "path";
1195
1226
  import { createHash as createHash2 } from "crypto";
1196
1227
  function emptyIdentity() {
1197
1228
  return { ownSubset: sha256Hex(""), ports: {}, aspects: {} };
1198
1229
  }
1199
1230
  function yggPrefixOf(graph) {
1200
- return path18.relative(path18.dirname(graph.rootPath), graph.rootPath).split(/[\\/]/).join("/");
1231
+ return path19.relative(path19.dirname(graph.rootPath), graph.rootPath).split(/[\\/]/).join("/");
1201
1232
  }
1202
1233
  function collectTrackedFiles(node, graph, baseline) {
1203
1234
  const seen = /* @__PURE__ */ new Set();
1204
1235
  const result = [];
1205
- const projectRoot = path18.dirname(graph.rootPath);
1206
- const yggPrefix = path18.relative(projectRoot, graph.rootPath);
1236
+ const projectRoot = path19.dirname(graph.rootPath);
1237
+ const yggPrefix = path19.relative(projectRoot, graph.rootPath);
1207
1238
  const yggPrefixNormalized = toPosixPath(yggPrefix);
1208
1239
  const identityAspects = {};
1209
1240
  const identityPorts = {};
@@ -1559,7 +1590,7 @@ var init_language_registry = __esm({
1559
1590
  });
1560
1591
 
1561
1592
  // src/core/drift-cause.ts
1562
- import path24 from "path";
1593
+ import path25 from "path";
1563
1594
  function serializeCheckTouched(ct) {
1564
1595
  if (!ct) return "";
1565
1596
  return Object.keys(ct).sort().map((k) => `${k}=${ct[k]}`).join(",");
@@ -1616,13 +1647,13 @@ function diffIdentity(nodePath, stored, current) {
1616
1647
  return causes;
1617
1648
  }
1618
1649
  function categorizeFile(filePath, rootPath, projectRoot) {
1619
- const yggPrefix = toPosixPath(path24.relative(projectRoot, rootPath));
1650
+ const yggPrefix = toPosixPath(path25.relative(projectRoot, rootPath));
1620
1651
  const normalized = toPosixPath(filePath);
1621
1652
  return normalized.startsWith(yggPrefix) ? "graph" : "source";
1622
1653
  }
1623
1654
  function describeCascadeCause(filePath, layer, graph) {
1624
1655
  const normalized = toPosixPath(filePath);
1625
- const yggPrefix = toPosixPath(path24.relative(path24.dirname(graph.rootPath), graph.rootPath));
1656
+ const yggPrefix = toPosixPath(path25.relative(path25.dirname(graph.rootPath), graph.rootPath));
1626
1657
  const escPrefix = yggPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1627
1658
  if (layer === "aspects") {
1628
1659
  const match = normalized.match(new RegExp(`${escPrefix}/aspects/([^/]+(?:/[^/]+)*)/`));
@@ -1879,7 +1910,7 @@ var init_log_integrity = __esm({
1879
1910
 
1880
1911
  // src/core/approve.ts
1881
1912
  import { createHash as createHash4 } from "crypto";
1882
- import path25 from "path";
1913
+ import path26 from "path";
1883
1914
  async function approveNode(graph, nodePath, _options = {}) {
1884
1915
  const node = graph.nodes.get(nodePath);
1885
1916
  if (!node) throw new Error(`Node '${nodePath}' does not exist.`);
@@ -1973,7 +2004,7 @@ ${violations.map((v) => ` line ${v.line}: ${v.reason} \u2014 ${v.detail}`).join
1973
2004
  const pendingDriftState = baseline && action2 !== "no-change" ? { nodePath, state: { schemaVersion: DRIFT_STATE_SCHEMA_VERSION, hash: "", files: {}, identity: emptyIdentity(), aspectVerdicts: {}, log: baseline } } : void 0;
1974
2005
  return { action: action2, currentHash: "", previousHash: storedEntry?.hash, gcPaths: gcPaths2, pendingDriftState };
1975
2006
  }
1976
- const projectRoot = path25.dirname(graph.rootPath);
2007
+ const projectRoot = path26.dirname(graph.rootPath);
1977
2008
  if (!hasNonDraftEffectiveAspects(node, graph)) {
1978
2009
  const sourceChangedDraft = await sourceFilesChanged(node, graph, projectRoot, storedEntry);
1979
2010
  if (logRequired && sourceChangedDraft.length > 0 && !hasFreshLogEntry(logSnapshot.content, storedEntry?.log)) {
@@ -2085,6 +2116,7 @@ ${violations.map((v) => ` line ${v.line}: ${v.reason} \u2014 ${v.detail}`).join
2085
2116
  const statuses = computeEffectiveAspectStatuses(node, graph);
2086
2117
  for (const [aspectId, status] of statuses) {
2087
2118
  if (status === "draft") continue;
2119
+ if (isAggregateAspect(graph, aspectId)) continue;
2088
2120
  if (!storedEntry.aspectVerdicts[aspectId]) newlyActiveAspects.push(aspectId);
2089
2121
  }
2090
2122
  }
@@ -2146,7 +2178,7 @@ async function evaluateAllDraftLogGate(graph, nodePath) {
2146
2178
  if (!logRequiredFor(node, graph)) return null;
2147
2179
  const storedEntry = await readNodeDriftState(graph.rootPath, nodePath);
2148
2180
  const logSnapshot = await snapshotLog(graph.rootPath, nodePath);
2149
- const projectRoot = path25.dirname(graph.rootPath);
2181
+ const projectRoot = path26.dirname(graph.rootPath);
2150
2182
  const changed = await sourceFilesChanged(node, graph, projectRoot, storedEntry);
2151
2183
  if (changed.length > 0 && !hasFreshLogEntry(logSnapshot.content, storedEntry?.log)) {
2152
2184
  return mandatoryLogRefusal(node, nodePath, changed);
@@ -2201,7 +2233,7 @@ async function sourceFilesChanged(node, graph, projectRoot, storedEntry) {
2201
2233
  return changed;
2202
2234
  }
2203
2235
  async function snapshotLog(yggRoot, nodePath) {
2204
- const logPath2 = path25.join(yggRoot, "model", nodePath, "log.md");
2236
+ const logPath2 = path26.join(yggRoot, "model", nodePath, "log.md");
2205
2237
  try {
2206
2238
  const st = await lstatFile(logPath2);
2207
2239
  if (st.isSymbolicLink()) {
@@ -2297,7 +2329,7 @@ async function loadSourceFiles(filePaths, projectRoot) {
2297
2329
  for (const filePath of filePaths) {
2298
2330
  const posixPath4 = toPosixPath(filePath);
2299
2331
  try {
2300
- const content14 = await readTextFile(path25.join(projectRoot, filePath));
2332
+ const content14 = await readTextFile(path26.join(projectRoot, filePath));
2301
2333
  results.push({ path: posixPath4, content: content14 });
2302
2334
  } catch (err) {
2303
2335
  debugWrite(`[approve] skipped unreadable file ${posixPath4}: ${err.message}`);
@@ -2377,7 +2409,6 @@ var init_xml_escape = __esm({
2377
2409
  var aspect_verifier_exports = {};
2378
2410
  __export(aspect_verifier_exports, {
2379
2411
  buildPrompt: () => buildPrompt,
2380
- chunkSourceFiles: () => chunkSourceFiles,
2381
2412
  verifyAspects: () => verifyAspects
2382
2413
  });
2383
2414
  function buildPrompt(aspect, nodeDescription, nodePath, sourceFiles, references = []) {
@@ -2427,31 +2458,6 @@ ${aspect.content}
2427
2458
  ${files}
2428
2459
  </source-files>`;
2429
2460
  }
2430
- function chunkSourceFiles(files, maxTokens) {
2431
- const overhead = 500;
2432
- const effectiveMax = Math.max(maxTokens, 1e3);
2433
- const available = (effectiveMax - overhead) * 4;
2434
- const chunks = [];
2435
- let current = [];
2436
- let currentSize = 0;
2437
- for (const file of files) {
2438
- const fileSize = file.path.length + file.content.length + 30;
2439
- if (fileSize > available) {
2440
- const truncated = file.content.slice(0, available);
2441
- chunks.push([{ path: file.path, content: truncated + "\n[... truncated]" }]);
2442
- continue;
2443
- }
2444
- if (currentSize + fileSize > available && current.length > 0) {
2445
- chunks.push(current);
2446
- current = [];
2447
- currentSize = 0;
2448
- }
2449
- current.push(file);
2450
- currentSize += fileSize;
2451
- }
2452
- if (current.length > 0) chunks.push(current);
2453
- return chunks.length > 0 ? chunks : [[]];
2454
- }
2455
2461
  async function verifyWithConsensus(provider, prompt, consensus) {
2456
2462
  if (consensus <= 1) {
2457
2463
  return provider.verifyAspect(prompt);
@@ -2474,29 +2480,12 @@ async function verifyWithConsensus(provider, prompt, consensus) {
2474
2480
  };
2475
2481
  }
2476
2482
  async function verifyAspects(params) {
2477
- const { provider, aspects, sourceFiles, nodePath, nodeDescription, consensus = 1, maxTokens } = params;
2478
- if (sourceFiles.length === 0) {
2479
- return Object.fromEntries(aspects.map((a) => [a.id, { satisfied: true, reason: "No source files", errorSource: "codeViolation" }]));
2480
- }
2481
- const tokenBudget = maxTokens ?? 8192;
2482
- const chunks = chunkSourceFiles(sourceFiles, tokenBudget);
2483
+ const { provider, aspects, sourceFiles, nodePath, nodeDescription, consensus = 1 } = params;
2483
2484
  const results = {};
2484
2485
  for (const aspect of aspects) {
2485
- let failed = false;
2486
- let failReason = "";
2487
- let failErrorSource = "codeViolation";
2488
- for (const chunk of chunks) {
2489
- if (chunk.length === 0) continue;
2490
- const prompt = buildPrompt(aspect, nodeDescription, nodePath, chunk, aspect.references ?? []);
2491
- const result = await verifyWithConsensus(provider, prompt, consensus);
2492
- if (!result.satisfied) {
2493
- failed = true;
2494
- failReason = result.reason;
2495
- failErrorSource = result.errorSource;
2496
- break;
2497
- }
2498
- }
2499
- results[aspect.id] = failed ? { satisfied: false, reason: failReason, errorSource: failErrorSource } : { satisfied: true, reason: `All rules satisfied across ${chunks.length} file group(s)`, errorSource: "codeViolation" };
2486
+ const prompt = buildPrompt(aspect, nodeDescription, nodePath, sourceFiles, aspect.references ?? []);
2487
+ const r = await verifyWithConsensus(provider, prompt, consensus);
2488
+ results[aspect.id] = { satisfied: r.satisfied, reason: r.reason, errorSource: r.errorSource };
2500
2489
  }
2501
2490
  return results;
2502
2491
  }
@@ -2513,11 +2502,6 @@ function resolveApiKey(config) {
2513
2502
  const envVar = ENV_VAR_MAP[config.provider];
2514
2503
  return envVar ? process.env[envVar] : void 0;
2515
2504
  }
2516
- async function resolveMaxTokens(config, provider) {
2517
- if (typeof config.max_tokens === "number") return config.max_tokens;
2518
- const detected = await provider.getContextWindowSize();
2519
- return detected ?? 8192;
2520
- }
2521
2505
  async function apiFetch(url, init2, providerName, timeoutMs = 6e4) {
2522
2506
  for (let attempt = 0; attempt < 2; attempt++) {
2523
2507
  try {
@@ -2640,9 +2624,6 @@ var init_cli_base = __esm({
2640
2624
  return false;
2641
2625
  }
2642
2626
  }
2643
- async getContextWindowSize() {
2644
- return void 0;
2645
- }
2646
2627
  async verifyAspect(prompt) {
2647
2628
  const fallback = { satisfied: false, reason: "Reviewer unavailable", errorSource: "provider" };
2648
2629
  return new Promise((resolve6) => {
@@ -2725,12 +2706,10 @@ var init_ollama = __esm({
2725
2706
  endpoint;
2726
2707
  model;
2727
2708
  temperature;
2728
- contextLengthField;
2729
2709
  constructor(config) {
2730
2710
  this.endpoint = config.endpoint ?? "http://localhost:11434";
2731
2711
  this.model = config.model;
2732
2712
  this.temperature = config.temperature;
2733
- this.contextLengthField = config.context_length_field;
2734
2713
  }
2735
2714
  async isAvailable() {
2736
2715
  try {
@@ -2741,25 +2720,6 @@ var init_ollama = __esm({
2741
2720
  return false;
2742
2721
  }
2743
2722
  }
2744
- async getContextWindowSize() {
2745
- try {
2746
- const res = await apiFetch(`${this.endpoint}/api/show`, {
2747
- method: "POST",
2748
- headers: { "Content-Type": "application/json" },
2749
- body: JSON.stringify({ name: this.model })
2750
- }, "ollama", 5e3);
2751
- if (!res.ok) return void 0;
2752
- const data = await res.json();
2753
- const params = data.model_info;
2754
- if (!params) return void 0;
2755
- const key = this.contextLengthField ?? Object.keys(params).find((k) => k === "context_length" || k.endsWith(".context_length"));
2756
- const ctxLength = key ? params[key] : void 0;
2757
- return ctxLength ?? void 0;
2758
- } catch (err) {
2759
- debugWrite(`[ollama] getContextWindowSize: ${err.message}`);
2760
- return void 0;
2761
- }
2762
- }
2763
2723
  async verifyAspect(prompt) {
2764
2724
  const fallback = { satisfied: false, reason: "LLM response could not be parsed", errorSource: "provider" };
2765
2725
  const body = {
@@ -2874,9 +2834,6 @@ var init_openai = __esm({
2874
2834
  async isAvailable() {
2875
2835
  return !!this.apiKey;
2876
2836
  }
2877
- async getContextWindowSize() {
2878
- return void 0;
2879
- }
2880
2837
  };
2881
2838
  registerProvider("openai", (c) => new OpenAIProvider(c));
2882
2839
  registerProvider("openai-compatible", (c) => new OpenAIProvider(c));
@@ -2931,9 +2888,6 @@ var init_anthropic = __esm({
2931
2888
  async isAvailable() {
2932
2889
  return !!this.apiKey;
2933
2890
  }
2934
- async getContextWindowSize() {
2935
- return void 0;
2936
- }
2937
2891
  };
2938
2892
  registerProvider("anthropic", (c) => new AnthropicProvider(c));
2939
2893
  }
@@ -2987,9 +2941,6 @@ var init_google = __esm({
2987
2941
  async isAvailable() {
2988
2942
  return !!this.apiKey;
2989
2943
  }
2990
- async getContextWindowSize() {
2991
- return void 0;
2992
- }
2993
2944
  };
2994
2945
  registerProvider("google", (c) => new GoogleProvider(c));
2995
2946
  }
@@ -3073,6 +3024,7 @@ function buildAspectVerdicts(node, graph, allAspectResults) {
3073
3024
  const carryForward = [];
3074
3025
  for (const [aspectId, status] of statuses) {
3075
3026
  if (status === "draft") continue;
3027
+ if (isAggregateAspect(graph, aspectId)) continue;
3076
3028
  const res = allAspectResults[aspectId];
3077
3029
  if (res === void 0) {
3078
3030
  carryForward.push(aspectId);
@@ -3091,8 +3043,10 @@ function buildAspectVerdicts(node, graph, allAspectResults) {
3091
3043
  function reviewerAborted(node, graph, allAspectResults) {
3092
3044
  if (Object.keys(allAspectResults).length > 0) return false;
3093
3045
  const statuses = computeEffectiveAspectStatuses(node, graph);
3094
- for (const s of statuses.values()) {
3095
- if (s !== "draft") return true;
3046
+ for (const [aspectId, s] of statuses) {
3047
+ if (s === "draft") continue;
3048
+ if (isAggregateAspect(graph, aspectId)) continue;
3049
+ return true;
3096
3050
  }
3097
3051
  return false;
3098
3052
  }
@@ -3122,15 +3076,15 @@ var init_approve_verdicts = __esm({
3122
3076
  // src/ast/loader-hook.ts
3123
3077
  import { register } from "module";
3124
3078
  import { pathToFileURL } from "url";
3125
- import path26 from "path";
3079
+ import path27 from "path";
3126
3080
  import { fileURLToPath as fileURLToPath3 } from "url";
3127
3081
  import { existsSync as existsSync3 } from "fs";
3128
3082
  function ensureLoaderRegistered() {
3129
3083
  if (registered) return;
3130
- let implPath = path26.resolve(__dirname, "./loader-hook-impl.js");
3084
+ let implPath = path27.resolve(__dirname, "./loader-hook-impl.js");
3131
3085
  if (!existsSync3(implPath)) {
3132
- const pkgRoot = path26.resolve(__dirname, "../../");
3133
- implPath = path26.resolve(pkgRoot, "dist/loader-hook-impl.js");
3086
+ const pkgRoot = path27.resolve(__dirname, "../../");
3087
+ implPath = path27.resolve(pkgRoot, "dist/loader-hook-impl.js");
3134
3088
  }
3135
3089
  register(pathToFileURL(implPath));
3136
3090
  registered = true;
@@ -3140,14 +3094,14 @@ var init_loader_hook = __esm({
3140
3094
  "src/ast/loader-hook.ts"() {
3141
3095
  "use strict";
3142
3096
  __filename = fileURLToPath3(import.meta.url);
3143
- __dirname = path26.dirname(__filename);
3097
+ __dirname = path27.dirname(__filename);
3144
3098
  registered = false;
3145
3099
  }
3146
3100
  });
3147
3101
 
3148
3102
  // src/structure/ctx-fs.ts
3149
3103
  import * as fs from "fs";
3150
- import * as path27 from "path";
3104
+ import * as path28 from "path";
3151
3105
  function isAllowed(p2, set) {
3152
3106
  if (p2 === "") return false;
3153
3107
  if (set.has(p2)) return true;
@@ -3167,7 +3121,7 @@ function assertRealpathContained(abs, projectRoot, rel) {
3167
3121
  }
3168
3122
  let probe = abs;
3169
3123
  while (!fs.existsSync(probe)) {
3170
- const parent = path27.dirname(probe);
3124
+ const parent = path28.dirname(probe);
3171
3125
  if (parent === probe) return;
3172
3126
  probe = parent;
3173
3127
  }
@@ -3177,15 +3131,15 @@ function assertRealpathContained(abs, projectRoot, rel) {
3177
3131
  } catch {
3178
3132
  return;
3179
3133
  }
3180
- const relReal = toPosix(path27.relative(realRoot, realProbe));
3181
- if (relReal === ".." || relReal.startsWith("../") || path27.isAbsolute(relReal)) {
3134
+ const relReal = toPosix(path28.relative(realRoot, realProbe));
3135
+ if (relReal === ".." || relReal.startsWith("../") || path28.isAbsolute(relReal)) {
3182
3136
  throw new UndeclaredFsReadError(rel);
3183
3137
  }
3184
3138
  }
3185
3139
  function resolveAllowedReadPath(raw, allowedSet, projectRoot) {
3186
- const abs = path27.resolve(projectRoot, normalizeMappingPath(raw));
3187
- const rel = toPosix(path27.relative(projectRoot, abs));
3188
- if (rel === "" || rel.startsWith("..") || path27.isAbsolute(rel)) {
3140
+ const abs = path28.resolve(projectRoot, normalizeMappingPath(raw));
3141
+ const rel = toPosix(path28.relative(projectRoot, abs));
3142
+ if (rel === "" || rel.startsWith("..") || path28.isAbsolute(rel)) {
3189
3143
  throw new UndeclaredFsReadError(normalizeMappingPath(raw));
3190
3144
  }
3191
3145
  if (!isAllowed(rel, allowedSet)) throw new UndeclaredFsReadError(rel);
@@ -3202,7 +3156,7 @@ function createCtxFs(params) {
3202
3156
  return {
3203
3157
  exists(raw) {
3204
3158
  const p2 = assertAllowed(raw);
3205
- const abs = path27.resolve(projectRoot, p2);
3159
+ const abs = path28.resolve(projectRoot, p2);
3206
3160
  try {
3207
3161
  const stat9 = fs.statSync(abs);
3208
3162
  return stat9.isDirectory() ? "dir" : stat9.isFile() ? "file" : false;
@@ -3212,12 +3166,12 @@ function createCtxFs(params) {
3212
3166
  },
3213
3167
  read(raw) {
3214
3168
  const p2 = assertAllowed(raw);
3215
- const abs = path27.resolve(projectRoot, p2);
3169
+ const abs = path28.resolve(projectRoot, p2);
3216
3170
  return fs.readFileSync(abs, "utf8");
3217
3171
  },
3218
3172
  list(raw) {
3219
3173
  const p2 = assertAllowed(raw);
3220
- const abs = path27.resolve(projectRoot, p2);
3174
+ const abs = path28.resolve(projectRoot, p2);
3221
3175
  const entries = fs.readdirSync(abs, { withFileTypes: true });
3222
3176
  return entries.map((e) => ({
3223
3177
  name: e.name,
@@ -3233,9 +3187,9 @@ var init_ctx_fs = __esm({
3233
3187
  init_mapping_path();
3234
3188
  init_posix();
3235
3189
  UndeclaredFsReadError = class extends Error {
3236
- constructor(path44) {
3237
- super(`structure-aspect-undeclared-fs-read: ${path44}`);
3238
- this.path = path44;
3190
+ constructor(path46) {
3191
+ super(`structure-aspect-undeclared-fs-read: ${path46}`);
3192
+ this.path = path46;
3239
3193
  this.name = "UndeclaredFsReadError";
3240
3194
  }
3241
3195
  };
@@ -3263,7 +3217,7 @@ var init_expand_mapping_sync = __esm({
3263
3217
 
3264
3218
  // src/structure/ctx-graph.ts
3265
3219
  import * as fs2 from "fs";
3266
- import * as path28 from "path";
3220
+ import * as path29 from "path";
3267
3221
  function computeAllowedNodePaths(currentPath, graph) {
3268
3222
  const allowed = /* @__PURE__ */ new Set([currentPath]);
3269
3223
  const current = graph.nodes.get(currentPath);
@@ -3303,7 +3257,7 @@ function createCtxGraph(params) {
3303
3257
  for (const raw of m.meta.mapping ?? []) {
3304
3258
  const p2 = normalizeMappingPath(raw);
3305
3259
  if (!p2) continue;
3306
- const abs = path28.resolve(projectRoot, p2);
3260
+ const abs = path29.resolve(projectRoot, p2);
3307
3261
  try {
3308
3262
  const stat9 = fs2.statSync(abs);
3309
3263
  if (stat9.isFile()) {
@@ -3396,7 +3350,7 @@ var init_ctx_graph = __esm({
3396
3350
 
3397
3351
  // src/ast/parser.ts
3398
3352
  import { Parser, Language } from "web-tree-sitter";
3399
- import path29 from "path";
3353
+ import path30 from "path";
3400
3354
  import { fileURLToPath as fileURLToPath4 } from "url";
3401
3355
  import { existsSync as existsSync5 } from "fs";
3402
3356
  import { createRequire as createRequire3 } from "module";
@@ -3407,12 +3361,12 @@ async function init() {
3407
3361
  }
3408
3362
  function resolveWasm(filename, pkg2) {
3409
3363
  for (const dir of GRAMMAR_DIRS) {
3410
- const p2 = path29.join(dir, filename);
3364
+ const p2 = path30.join(dir, filename);
3411
3365
  if (existsSync5(p2)) return p2;
3412
3366
  }
3413
3367
  try {
3414
- const pkgDir = path29.dirname(_require.resolve(`${pkg2}/package.json`));
3415
- for (const candidate of [path29.join(pkgDir, filename), path29.join(pkgDir, "bindings/node", filename)]) {
3368
+ const pkgDir = path30.dirname(_require.resolve(`${pkg2}/package.json`));
3369
+ for (const candidate of [path30.join(pkgDir, filename), path30.join(pkgDir, "bindings/node", filename)]) {
3416
3370
  if (existsSync5(candidate)) return candidate;
3417
3371
  }
3418
3372
  } catch {
@@ -3437,7 +3391,7 @@ async function getParser(extension) {
3437
3391
  return parser;
3438
3392
  }
3439
3393
  async function parseFile(filePath, content14) {
3440
- const ext = path29.extname(filePath);
3394
+ const ext = path30.extname(filePath);
3441
3395
  const parser = await getParser(ext);
3442
3396
  const tree = parser.parse(content14);
3443
3397
  if (tree === null) {
@@ -3452,10 +3406,10 @@ var init_parser = __esm({
3452
3406
  init_language_registry();
3453
3407
  _require = createRequire3(import.meta.url);
3454
3408
  __filename2 = fileURLToPath4(import.meta.url);
3455
- __dirname2 = path29.dirname(__filename2);
3409
+ __dirname2 = path30.dirname(__filename2);
3456
3410
  GRAMMAR_DIRS = [
3457
- path29.resolve(__dirname2, "grammars"),
3458
- path29.resolve(__dirname2, "..", "grammars")
3411
+ path30.resolve(__dirname2, "grammars"),
3412
+ path30.resolve(__dirname2, "..", "grammars")
3459
3413
  ];
3460
3414
  initialized = false;
3461
3415
  langCache = /* @__PURE__ */ new Map();
@@ -3464,7 +3418,7 @@ var init_parser = __esm({
3464
3418
 
3465
3419
  // src/structure/ctx-parsers.ts
3466
3420
  import * as fs3 from "fs";
3467
- import * as path30 from "path";
3421
+ import * as path31 from "path";
3468
3422
  import { extname } from "path";
3469
3423
  import { parse as parseYaml13 } from "yaml";
3470
3424
  import { parse as parseTomlSmol } from "smol-toml";
@@ -3476,7 +3430,7 @@ function createCtxParsers(params) {
3476
3430
  return input;
3477
3431
  }
3478
3432
  const p2 = resolveAllowedReadPath(input, allowedSet, projectRoot);
3479
- const abs = path30.resolve(projectRoot, p2);
3433
+ const abs = path31.resolve(projectRoot, p2);
3480
3434
  const content14 = fs3.readFileSync(abs, "utf8");
3481
3435
  touchedFiles.push(p2);
3482
3436
  return { path: p2, content: content14 };
@@ -3617,7 +3571,43 @@ var init_allowed_reads = __esm({
3617
3571
  }
3618
3572
  });
3619
3573
 
3574
+ // src/ast/find-comments.ts
3575
+ import { extname as extname2 } from "path";
3576
+ function findComments(target) {
3577
+ const hasAst = "ast" in target;
3578
+ const hasRootNode = "rootNode" in target;
3579
+ if (hasAst && hasRootNode) {
3580
+ throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
3581
+ }
3582
+ let language = "language" in target ? target.language : void 0;
3583
+ if (language === void 0 && "path" in target) {
3584
+ language = getLanguageForExtension(extname2(target.path)) ?? void 0;
3585
+ }
3586
+ if (language === void 0) {
3587
+ throw new Error(
3588
+ "AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
3589
+ );
3590
+ }
3591
+ const def = LANGUAGES[language];
3592
+ if (def === void 0) {
3593
+ throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
3594
+ }
3595
+ const root = hasAst ? target.ast.rootNode : target.rootNode;
3596
+ const out = [];
3597
+ for (const type of def.commentTypes) {
3598
+ out.push(...root.descendantsOfType(type));
3599
+ }
3600
+ return out;
3601
+ }
3602
+ var init_find_comments = __esm({
3603
+ "src/ast/find-comments.ts"() {
3604
+ "use strict";
3605
+ init_language_registry();
3606
+ }
3607
+ });
3608
+
3620
3609
  // src/ast/suppress.ts
3610
+ import { extname as extname3 } from "path";
3621
3611
  function commentBody(text2) {
3622
3612
  if (text2.startsWith("//")) return text2.slice(2).trim();
3623
3613
  if (text2.startsWith("/*")) return text2.replace(/^\/\*+/, "").replace(/\*+\/$/, "").trim();
@@ -3651,7 +3641,10 @@ function parseMarker(commentText, line, file) {
3651
3641
  return null;
3652
3642
  }
3653
3643
  function collectSuppressions(tree, file, totalLines) {
3654
- const comments = tree.rootNode.descendantsOfType("comment");
3644
+ if (getLanguageForExtension(extname3(file)) === null) {
3645
+ return [];
3646
+ }
3647
+ const comments = findComments({ path: file, ast: tree });
3655
3648
  const markers = [];
3656
3649
  for (const c of comments) {
3657
3650
  const m = parseMarker(c.text, c.startPosition.row + 1, file);
@@ -3702,10 +3695,48 @@ function isLineSuppressed(ranges, aspectId, line) {
3702
3695
  return r.isWildcard || r.aspectIds.has(aspectId);
3703
3696
  });
3704
3697
  }
3698
+ function scanSuppressionMarkers(text2) {
3699
+ const lines = text2.split("\n");
3700
+ const result = [];
3701
+ for (let i = 0; i < lines.length; i++) {
3702
+ const lineNum = i + 1;
3703
+ const raw = lines[i];
3704
+ let m;
3705
+ m = raw.match(RE_DISABLE);
3706
+ if (m) {
3707
+ const ids = splitAspectList(m[1]);
3708
+ const reason = (m[2] ?? "").trim();
3709
+ for (const id of ids) {
3710
+ result.push({ line: lineNum, aspectId: id, kind: "disable", wildcard: id === "*", reason });
3711
+ }
3712
+ continue;
3713
+ }
3714
+ m = raw.match(RE_ENABLE);
3715
+ if (m) {
3716
+ const ids = splitAspectList(m[1]);
3717
+ for (const id of ids) {
3718
+ result.push({ line: lineNum, aspectId: id, kind: "enable", wildcard: id === "*", reason: "" });
3719
+ }
3720
+ continue;
3721
+ }
3722
+ m = raw.match(RE_SINGLE);
3723
+ if (m) {
3724
+ const ids = splitAspectList(m[1]);
3725
+ const reason = (m[2] ?? "").trim();
3726
+ for (const id of ids) {
3727
+ result.push({ line: lineNum, aspectId: id, kind: "single", wildcard: id === "*", reason });
3728
+ }
3729
+ continue;
3730
+ }
3731
+ }
3732
+ return result;
3733
+ }
3705
3734
  var SuppressMarkerError, RE_SINGLE, RE_DISABLE, RE_ENABLE;
3706
3735
  var init_suppress = __esm({
3707
3736
  "src/ast/suppress.ts"() {
3708
3737
  "use strict";
3738
+ init_find_comments();
3739
+ init_language_registry();
3709
3740
  SuppressMarkerError = class extends Error {
3710
3741
  constructor(message, file, line) {
3711
3742
  super(message);
@@ -3780,66 +3811,37 @@ var init_validate_check_module = __esm({
3780
3811
 
3781
3812
  // src/structure/runner.ts
3782
3813
  import * as fs4 from "fs";
3783
- import * as path31 from "path";
3814
+ import * as path32 from "path";
3784
3815
  import { pathToFileURL as pathToFileURL2 } from "url";
3785
- function buildOwnFiles(node, projectRoot, touchedFiles) {
3786
- const childMappings = /* @__PURE__ */ new Set();
3816
+ async function buildOwnFiles(node, projectRoot, touchedFiles) {
3817
+ const childMappingEntries = [];
3787
3818
  for (const child of node.children) {
3788
3819
  for (const raw of child.meta.mapping ?? []) {
3789
3820
  const p2 = normalizeMappingPath(raw);
3790
- if (p2) childMappings.add(p2);
3821
+ if (p2) childMappingEntries.push(p2);
3791
3822
  }
3792
3823
  }
3824
+ const rawMapping = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p2) => p2 !== "");
3825
+ const expanded = await expandMappingPaths(projectRoot, rawMapping);
3793
3826
  const result = [];
3794
- for (const raw of node.meta.mapping ?? []) {
3795
- const p2 = normalizeMappingPath(raw);
3796
- if (!p2 || childMappings.has(p2)) continue;
3797
- const abs = path31.resolve(projectRoot, p2);
3827
+ for (const p2 of expanded) {
3828
+ if (childMappingEntries.length > 0 && isPathInMapping(p2, childMappingEntries)) continue;
3829
+ if (BINARY_EXTENSIONS2.has(path32.extname(p2).toLowerCase())) continue;
3830
+ const abs = path32.resolve(projectRoot, p2);
3831
+ let content14;
3798
3832
  try {
3799
- const stat9 = fs4.statSync(abs);
3800
- if (stat9.isFile()) {
3801
- const content14 = fs4.readFileSync(abs, "utf8");
3802
- result.push({ path: p2, content: content14 });
3803
- touchedFiles.push(p2);
3804
- }
3833
+ content14 = fs4.readFileSync(abs, "utf8");
3805
3834
  } catch {
3835
+ continue;
3806
3836
  }
3837
+ result.push({ path: p2, content: content14 });
3838
+ touchedFiles.push(p2);
3807
3839
  }
3808
3840
  return result;
3809
3841
  }
3810
- function enumerateMappedFilesSync(mappingPaths, projectRoot) {
3811
- const out = [];
3812
- for (const raw of mappingPaths) {
3813
- const rel = normalizeMappingPath(raw);
3814
- if (!rel) continue;
3815
- const abs = path31.resolve(projectRoot, rel);
3816
- try {
3817
- const stat9 = fs4.statSync(abs);
3818
- if (stat9.isFile()) {
3819
- out.push(rel);
3820
- } else if (stat9.isDirectory()) {
3821
- for (const sub of walkDirSync(abs)) {
3822
- const relSub = path31.relative(projectRoot, sub).split(/[\\/]/).join("/");
3823
- out.push(relSub);
3824
- }
3825
- }
3826
- } catch {
3827
- }
3828
- }
3829
- return out;
3830
- }
3831
- function* walkDirSync(dir) {
3832
- let entries;
3833
- try {
3834
- entries = fs4.readdirSync(dir, { withFileTypes: true });
3835
- } catch {
3836
- return;
3837
- }
3838
- for (const e of entries) {
3839
- const child = path31.join(dir, e.name);
3840
- if (e.isDirectory()) yield* walkDirSync(child);
3841
- else if (e.isFile()) yield child;
3842
- }
3842
+ async function enumerateMappedFilesAsync(mappingPaths, projectRoot) {
3843
+ const normalized = mappingPaths.map(normalizeMappingPath).filter((p2) => p2 !== "");
3844
+ return expandMappingPaths(projectRoot, normalized);
3843
3845
  }
3844
3846
  async function runStructureAspect(params) {
3845
3847
  ensureLoaderRegistered();
@@ -3854,8 +3856,8 @@ async function runStructureAspect(params) {
3854
3856
  next: `Pass an existing node path, or add the node to the graph.`
3855
3857
  });
3856
3858
  }
3857
- const aspectDirAbs = path31.isAbsolute(aspectDir) ? aspectDir : path31.resolve(projectRoot, aspectDir);
3858
- const checkPath = path31.join(aspectDirAbs, "check.mjs");
3859
+ const aspectDirAbs = path32.isAbsolute(aspectDir) ? aspectDir : path32.resolve(projectRoot, aspectDir);
3860
+ const checkPath = path32.join(aspectDirAbs, "check.mjs");
3859
3861
  let mod;
3860
3862
  try {
3861
3863
  mod = await import(pathToFileURL2(checkPath).href);
@@ -3878,7 +3880,7 @@ async function runStructureAspect(params) {
3878
3880
  const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles });
3879
3881
  const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles });
3880
3882
  const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache });
3881
- const ownFiles = buildOwnFiles(node, projectRoot, touchedFiles);
3883
+ const ownFiles = await buildOwnFiles(node, projectRoot, touchedFiles);
3882
3884
  await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
3883
3885
  const ownFilesEnriched = enrichFilesWithAst(ownFiles, astCache);
3884
3886
  const ctx = {
@@ -3901,8 +3903,8 @@ async function runStructureAspect(params) {
3901
3903
  for (const rel of node.meta.relations ?? []) {
3902
3904
  const target = graph.nodes.get(rel.target);
3903
3905
  if (!target) continue;
3904
- for (const p2 of enumerateMappedFilesSync(target.meta.mapping ?? [], projectRoot)) {
3905
- const abs = path31.resolve(projectRoot, p2);
3906
+ for (const p2 of await enumerateMappedFilesAsync(target.meta.mapping ?? [], projectRoot)) {
3907
+ const abs = path32.resolve(projectRoot, p2);
3906
3908
  try {
3907
3909
  const content14 = fs4.readFileSync(abs, "utf8");
3908
3910
  astInputSet.push({ path: p2, content: content14 });
@@ -4007,7 +4009,7 @@ ${err.stack ?? ""}`,
4007
4009
  });
4008
4010
  return { violations: visible, touchedFiles, succeeded: true };
4009
4011
  }
4010
- var StructureRunnerError;
4012
+ var StructureRunnerError, BINARY_EXTENSIONS2;
4011
4013
  var init_runner = __esm({
4012
4014
  "src/structure/runner.ts"() {
4013
4015
  "use strict";
@@ -4017,6 +4019,7 @@ var init_runner = __esm({
4017
4019
  init_ctx_parsers();
4018
4020
  init_allowed_reads();
4019
4021
  init_expand_mapping_sync();
4022
+ init_hash();
4020
4023
  init_suppress();
4021
4024
  init_validate_check_module();
4022
4025
  StructureRunnerError = class extends Error {
@@ -4030,6 +4033,35 @@ ${data.next}`);
4030
4033
  }
4031
4034
  messageData;
4032
4035
  };
4036
+ BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
4037
+ ".gif",
4038
+ ".png",
4039
+ ".jpg",
4040
+ ".jpeg",
4041
+ ".webp",
4042
+ ".bmp",
4043
+ ".ico",
4044
+ ".svgz",
4045
+ ".woff",
4046
+ ".woff2",
4047
+ ".ttf",
4048
+ ".otf",
4049
+ ".eot",
4050
+ ".zip",
4051
+ ".gz",
4052
+ ".tgz",
4053
+ ".tar",
4054
+ ".bz2",
4055
+ ".7z",
4056
+ ".pdf",
4057
+ ".mp4",
4058
+ ".mov",
4059
+ ".webm",
4060
+ ".mp3",
4061
+ ".wav",
4062
+ ".wasm",
4063
+ ".bin"
4064
+ ]);
4033
4065
  }
4034
4066
  });
4035
4067
 
@@ -4043,15 +4075,12 @@ __export(approve_reviewer_exports, {
4043
4075
  reviewerAborted: () => reviewerAborted,
4044
4076
  runApproveWithReviewer: () => runApproveWithReviewer
4045
4077
  });
4046
- import path32 from "path";
4047
- function hasAnyCheckTouched(identity) {
4048
- return Object.values(identity.aspects).some((a) => a.checkTouched && Object.keys(a.checkTouched).length > 0);
4049
- }
4078
+ import path33 from "path";
4050
4079
  async function dispatchStructureAspects(plan, node, graph, result, projectRoot, parseCache, results, violations) {
4051
4080
  for (const entry of plan.resolved) {
4052
4081
  if (entry.kind !== "deterministic") continue;
4053
4082
  const aspect = entry.aspect;
4054
- const aspectDirAbs = path32.join(projectRoot, ".yggdrasil/aspects", aspect.id);
4083
+ const aspectDirAbs = path33.join(projectRoot, ".yggdrasil/aspects", aspect.id);
4055
4084
  try {
4056
4085
  const structResult = await runStructureAspect({
4057
4086
  aspectDir: aspectDirAbs,
@@ -4072,7 +4101,7 @@ async function dispatchStructureAspects(plan, node, graph, result, projectRoot,
4072
4101
  const p2 = toPosixPath(raw);
4073
4102
  let hash = result.pendingDriftState?.state.files[p2];
4074
4103
  if (!hash) {
4075
- const abs = path32.resolve(projectRoot, p2);
4104
+ const abs = path33.resolve(projectRoot, p2);
4076
4105
  try {
4077
4106
  hash = await hashFile(abs);
4078
4107
  } catch (e) {
@@ -4122,6 +4151,23 @@ async function commitApprovalAndCleanDrafts(rootPath, result, node, graph) {
4122
4151
  await clearDraftAspectsFromDriftState(rootPath, node.path, draftIds);
4123
4152
  }
4124
4153
  }
4154
+ async function recomputeFinalHash(result, node, graph, projectRoot) {
4155
+ if (!result.pendingDriftState) return;
4156
+ if (result.action !== "initial" && result.action !== "approved") return;
4157
+ const { trackedFiles: recomputeTracked, identity: recomputeIdentity } = collectTrackedFiles(node, graph, result.pendingDriftState.state);
4158
+ const recomputeExclusions = getChildMappingExclusions(graph, node.path);
4159
+ const { canonicalHash } = await hashTrackedFiles(
4160
+ projectRoot,
4161
+ recomputeTracked,
4162
+ void 0,
4163
+ recomputeExclusions,
4164
+ recomputeIdentity,
4165
+ result.pendingDriftState.state.aspectVerdicts
4166
+ );
4167
+ result.pendingDriftState.state.identity = recomputeIdentity;
4168
+ result.pendingDriftState.state.hash = canonicalHash;
4169
+ result.currentHash = canonicalHash;
4170
+ }
4125
4171
  function partitionCodeViolationsByStatus(codeViolations, statuses) {
4126
4172
  const enforced = [];
4127
4173
  const advisory = [];
@@ -4160,7 +4206,7 @@ async function loadAndIsolateReferences(params) {
4160
4206
  if (refs.length === 0) return { ok: true, references: [] };
4161
4207
  const out = [];
4162
4208
  for (const ref of refs) {
4163
- const absPath = path32.join(params.projectRoot, ref.path);
4209
+ const absPath = path33.join(params.projectRoot, ref.path);
4164
4210
  try {
4165
4211
  let content14 = params.cache.get(absPath);
4166
4212
  if (content14 === void 0) {
@@ -4187,7 +4233,9 @@ async function runApproveWithReviewer(input) {
4187
4233
  const allAspectIds = computeEffectiveAspects(node, graph);
4188
4234
  const allAspects = [...allAspectIds].map((id) => graph.aspects.find((a) => a.id === id)).filter((a) => a !== void 0);
4189
4235
  const statuses = computeEffectiveAspectStatuses(node, graph);
4190
- const nonDraft = allAspects.filter((a) => statuses.get(a.id) !== "draft");
4236
+ const nonDraft = allAspects.filter(
4237
+ (a) => statuses.get(a.id) !== "draft" && a.reviewer.type !== "aggregate"
4238
+ );
4191
4239
  const skippedDraftAspects = allAspects.filter((a) => statuses.get(a.id) === "draft").map((a) => a.id);
4192
4240
  for (const id of skippedDraftAspects) {
4193
4241
  process.stdout.write(`[draft] node '${node.path}': aspect '${id}' skipped (status: draft)
@@ -4197,6 +4245,7 @@ async function runApproveWithReviewer(input) {
4197
4245
  const aspectViolations = [];
4198
4246
  const allAspectResults = {};
4199
4247
  const referencesCache = /* @__PURE__ */ new Map();
4248
+ const projectRoot = toPosixPath(path33.dirname(rootPath));
4200
4249
  const finalizeAndReturn = async (extras, infra = false) => {
4201
4250
  const { verdicts, carryForward } = buildAspectVerdicts(node, graph, allAspectResults);
4202
4251
  applyAspectVerdictsToResult(result, verdicts, carryForward, storedEntry?.aspectVerdicts, filterAspectId, reviewerAborted(node, graph, allAspectResults));
@@ -4210,6 +4259,7 @@ async function runApproveWithReviewer(input) {
4210
4259
  delete result.pendingDriftState.state.log;
4211
4260
  }
4212
4261
  }
4262
+ await recomputeFinalHash(result, node, graph, projectRoot);
4213
4263
  await commitApprovalAndCleanDrafts(rootPath, result, node, graph);
4214
4264
  }
4215
4265
  const out = { ...result, ...extras, skippedDraftAspects };
@@ -4232,12 +4282,24 @@ async function runApproveWithReviewer(input) {
4232
4282
  }
4233
4283
  }, true);
4234
4284
  }
4235
- const projectRoot = toPosixPath(path32.dirname(rootPath));
4236
4285
  const { trackedFiles } = collectTrackedFiles(node, graph);
4237
4286
  const { fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, void 0, []);
4238
- const yggPrefix = toPosixPath(path32.relative(projectRoot, rootPath));
4287
+ const yggPrefix = toPosixPath(path33.relative(projectRoot, rootPath));
4239
4288
  const sourceFilePaths = Object.keys(fileHashes).map((f) => toPosixPath(f)).filter((f) => !f.startsWith(yggPrefix));
4240
4289
  const sourceFiles = await loadSourceFiles(sourceFilePaths, projectRoot);
4290
+ if (hasLlmAspects && sourceFilePaths.length === 0) {
4291
+ const llmIds = filtered.filter((a) => a.reviewer.type === "llm").map((a) => a.id).join(", ");
4292
+ const normalizedNodePath2 = toPosixPath(nodePath);
4293
+ return finalizeAndReturn({
4294
+ action: "refused",
4295
+ llmSkipped: "unavailable",
4296
+ refuseReasonData: {
4297
+ what: `No readable source files found for node '${normalizedNodePath2}', but it has effective non-draft LLM aspect(s): ${llmIds}.`,
4298
+ why: "An LLM aspect needs source files to verify \u2014 approving with no files would record a verdict over code the reviewer never saw.",
4299
+ next: `Add source files that satisfy the node mapping, or remove the LLM aspect(s), then re-run: yg approve --node ${normalizedNodePath2}`
4300
+ }
4301
+ }, true);
4302
+ }
4241
4303
  const nodeDescription = node.meta.description ?? "";
4242
4304
  const plan = graph.config.reviewer ? resolveExecutionPlan(filtered, graph.config.reviewer) : {
4243
4305
  resolved: filtered.filter((a) => a.reviewer.type === "deterministic").map((a) => ({ kind: "deterministic", aspect: a })),
@@ -4279,14 +4341,6 @@ async function runApproveWithReviewer(input) {
4279
4341
  debugWrite(`[d8.3] preserved checkTouched for ${preserved} non-evaluated deterministic aspect(s) on node ${node.path}`);
4280
4342
  }
4281
4343
  }
4282
- if (result.pendingDriftState && (result.action === "initial" || result.action === "approved") && hasAnyCheckTouched(result.pendingDriftState.state.identity)) {
4283
- const { trackedFiles: recomputeTracked, identity: recomputeIdentity } = collectTrackedFiles(node, graph, result.pendingDriftState.state);
4284
- const recomputeExclusions = getChildMappingExclusions(graph, node.path);
4285
- const { canonicalHash } = await hashTrackedFiles(projectRoot, recomputeTracked, void 0, recomputeExclusions, recomputeIdentity);
4286
- result.pendingDriftState.state.identity = recomputeIdentity;
4287
- result.pendingDriftState.state.hash = canonicalHash;
4288
- result.currentHash = canonicalHash;
4289
- }
4290
4344
  if (aspectViolations.length > 0) {
4291
4345
  const astInfraErrors = aspectViolations.filter((v) => v.errorSource !== "codeViolation");
4292
4346
  const astCodeViolations = aspectViolations.filter((v) => v.errorSource === "codeViolation");
@@ -4357,7 +4411,6 @@ async function runApproveWithReviewer(input) {
4357
4411
  aspectResults: allAspectResults
4358
4412
  }, true);
4359
4413
  }
4360
- const maxTokens = merged.max_tokens === "auto" ? await resolveMaxTokens(merged, provider) : merged.max_tokens;
4361
4414
  const aspectsForTier = [];
4362
4415
  for (const e of entries) {
4363
4416
  const loaded = await loadAndIsolateReferences({
@@ -4386,8 +4439,7 @@ async function runApproveWithReviewer(input) {
4386
4439
  sourceFiles,
4387
4440
  nodePath,
4388
4441
  nodeDescription,
4389
- consensus: merged.consensus,
4390
- maxTokens
4442
+ consensus: merged.consensus
4391
4443
  });
4392
4444
  for (const [aspectId, res] of Object.entries(llmResults)) {
4393
4445
  allAspectResults[aspectId] = res;
@@ -4454,7 +4506,6 @@ var init_approve_reviewer = __esm({
4454
4506
  "src/core/approve-reviewer.ts"() {
4455
4507
  "use strict";
4456
4508
  init_aspect_verifier();
4457
- init_api_utils();
4458
4509
  init_llm();
4459
4510
  init_approve();
4460
4511
  init_aspects();
@@ -4479,7 +4530,7 @@ import { Command as Command2 } from "commander";
4479
4530
  import chalk2 from "chalk";
4480
4531
  import { existsSync as existsSync2 } from "fs";
4481
4532
  import { mkdir as mkdir3, writeFile as writeFile7, readdir as readdir9, readFile as readFile18, stat as stat6 } from "fs/promises";
4482
- import path17 from "path";
4533
+ import path18 from "path";
4483
4534
  import { fileURLToPath as fileURLToPath2 } from "url";
4484
4535
  import * as p from "@clack/prompts";
4485
4536
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
@@ -4542,9 +4593,11 @@ The CLI (\`yg\`) reads and validates \u2014 it never modifies files. You create
4542
4593
 
4543
4594
  **Nodes** \u2014 components. \`model/<path>/yg-node.yaml\`. Nodes nest by directory \u2014 children inherit parent aspects. Schema: \`schemas/yg-node.yaml\`.
4544
4595
 
4545
- **Aspects** \u2014 enforceable rules. \`aspects/<id>/yg-aspect.yaml\` + either \`content.md\` (LLM reviewer) or \`check.mjs\` (deterministic reviewer). The \`yg-aspect.yaml\` requires a \`reviewer:\` mapping: \`reviewer.type\` is \`llm\` or \`deterministic\`. LLM aspects may also 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.
4596
+ **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
+
4598
+ Deterministic aspects ship \`check.mjs\` instead of \`content.md\` \u2014 the CLI runs the check locally at zero LLM cost and the returned violations are the verdict. A check may scope to a single file (parsing it with tree-sitter) or to the whole graph (reading the node's files, related nodes, and graph metadata through a context object). Deterministic aspects must NOT set \`reviewer.tier:\` \u2014 tiers apply only to LLM aspects. See \`yg knowledge read writing-deterministic-aspects\`.
4546
4599
 
4547
- Deterministic aspects (\`reviewer.type: deterministic\`) ship \`check.mjs\` instead of \`content.md\` \u2014 the CLI runs the check locally at zero LLM cost and the returned violations are the verdict. A check may scope to a single file (parsing it with tree-sitter) or to the whole graph (reading the node's files, related nodes, and graph metadata through a context object). Deterministic aspects must NOT set \`reviewer.tier:\` \u2014 tiers apply only to LLM aspects. See \`yg knowledge read writing-deterministic-aspects\`.
4600
+ Aggregating aspects ship neither \`content.md\` nor \`check.mjs\` but declare \`implies:\`. They act as named bundles: they expand their implied aspects onto every node where the aggregate is effective, but they have no own reviewer and produce no own verdict. Use them to group a multi-rule contract into one named attach point (the aggregate) backed by N atomic child aspects (each with its own \`content.md\` or \`check.mjs\` and one clean verdict). See \`yg knowledge read aspects-overview\`.
4548
4601
 
4549
4602
  **Flows** \u2014 business processes. \`flows/<name>/yg-flow.yaml\` with name, description, nodes (participants), aspects. Flow-level aspects propagate to all participants. Descendants of a declared participant are automatically included \u2014 adding a parent node to a flow covers all its children. Deep dive: \`yg knowledge read flows\`.
4550
4603
 
@@ -4633,13 +4686,14 @@ When \`yg check\` emits both errors AND warnings, \`suggestedNext\` points at th
4633
4686
  | \`yg log add --node <path> --reason <text>\` | Append per-node business-context entry (multi-line via \`--reason-file <path>\`) |
4634
4687
  | \`yg log read --node <path> [--top N \\| --all]\` | Read log entries (default top 10, newest first) |
4635
4688
  | \`yg log merge-resolve --node <path>\` | Reconcile log.md after a git merge (validates byte-exact ancestor + union of new entries) |
4689
+ | \`yg suppressions\` | Read-only inventory of active \`yg-suppress\` markers; warns on unknown aspect-id, wildcard, or unbounded range. Exit 0. |
4636
4690
  | \`yg knowledge list\` / \`yg knowledge read <name>\` | Browse deep-reference topics |
4637
4691
 
4638
- Full command reference (\`yg aspects\`, \`yg flows\`, \`yg owner\`, \`yg deterministic-test\`, \`yg type-suggest\`, \`yg init\`, \`yg log merge-resolve\`, all option flags): \`yg knowledge read cli-reference\`.
4692
+ Full command reference (\`yg aspects\`, \`yg flows\`, \`yg owner\`, \`yg suppressions\`, \`yg deterministic-test\`, \`yg type-suggest\`, \`yg init\`, \`yg log merge-resolve\`, all option flags): \`yg knowledge read cli-reference\`.
4639
4693
 
4640
4694
  ### Impact and Cost
4641
4695
 
4642
- Every graph change has blast radius. \`yg impact\` shows how many nodes are affected. For an LLM aspect, each affected node re-runs just the changed aspect \u2014 one LLM request (multiplied by the tier's consensus count and by the number of prompt chunks) \u2014 so an LLM aspect touching 20 nodes is at least 20 LLM calls = real cost. (A source-code edit, by contrast, is node-global: it re-runs every effective non-draft LLM aspect on that one node.) Deterministic aspects run locally and cost zero LLM calls regardless of how many nodes they touch.
4696
+ Every graph change has blast radius. \`yg impact\` shows how many nodes are affected. For an LLM aspect, each affected node re-runs just the changed aspect \u2014 one LLM request per node (multiplied by the tier's consensus count) \u2014 so an LLM aspect touching 20 nodes is at least 20 LLM calls = real cost. (A source-code edit, by contrast, is node-global: it re-runs every effective non-draft LLM aspect on that one node.) Deterministic aspects run locally and cost zero LLM calls regardless of how many nodes they touch.
4643
4697
 
4644
4698
  When code doesn't match an aspect, five options:
4645
4699
 
@@ -4658,7 +4712,7 @@ var DECISIONS = `## DECISIONS
4658
4712
 
4659
4713
  **Start of conversation:** \`yg check\`. If errors \u2014 fix before any other work. \`yg check\` failures block commits and CI. Nothing passes until check is clean.
4660
4714
 
4661
- **Before touching a source file:** \`yg context --file <path>\`. Read the files listed under \`read:\` \u2014 these are the rules the reviewer will check your code against. For LLM aspects, \`read:\` points to \`content.md\`. For deterministic aspects (\`reviewer.type: deterministic\`), \`read:\` points to \`check.mjs\` \u2014 read it to know what rules will be enforced. For blast radius: \`yg impact --file <path>\`.
4715
+ **Before touching a source file:** \`yg context --file <path>\`. Read the files listed under \`read:\` \u2014 these are the rules the reviewer will check your code against. For LLM aspects, \`read:\` points to \`content.md\`. For deterministic aspects, \`read:\` points to \`check.mjs\` \u2014 read it to know what rules will be enforced. Aggregating aspects have no \`read:\` of their own; their implied children each carry their own \`read:\` paths. For blast radius: \`yg impact --file <path>\`.
4662
4716
 
4663
4717
  **After modifying code:** \`yg check\` \u2192 fix errors \u2192 \`yg log add\` (per affected node) \u2192 \`yg approve --node <path>\`. Approve is part of the change \u2014 the change is not done until approve passes. Do not defer approval.
4664
4718
 
@@ -4926,7 +4980,7 @@ context.
4926
4980
 
4927
4981
  ### When to Create Graph Elements
4928
4982
 
4929
- **Aspect** \u2014 when the same pattern appears in 3+ files AND the reviewer can verify it against source code. Both conditions. "Every handler logs audit trail" \u2014 pattern + verifiable = aspect. "Code should be readable" \u2014 not verifiable, not an aspect. Read \`schemas/yg-aspect.yaml\` before creating. For reviewer choice (LLM or deterministic), aspect format, cost model: \`yg knowledge read aspects-overview\`. To write the rules: \`yg knowledge read writing-llm-aspects\` (or \`writing-deterministic-aspects\`). Content \`.md\` files state WHAT must be satisfied and WHY \u2014 use the user's words, never invent rationale. Things that do NOT become aspects: knowledge already visible in source code (imports, config), non-enforceable knowledge (business strategy, personas, pricing), and conventions the reviewer cannot check against code. Choose initial status: \`draft\` if content.md is still being authored or the rule is unclear (no enforcement, no cost); \`advisory\` if content.md is complete but you want to gather signal across the repo without blocking CI; \`enforced\` if the rule is vetted on a small set and you want repo-wide enforcement immediately.
4983
+ **Aspect** \u2014 when the same pattern appears in 3+ files AND the reviewer can verify it against source code. Both conditions. "Every handler logs audit trail" \u2014 pattern + verifiable = aspect. "Code should be readable" \u2014 not verifiable, not an aspect. Read \`schemas/yg-aspect.yaml\` before creating. For reviewer kind (LLM, deterministic, or aggregating), aspect format, cost model: \`yg knowledge read aspects-overview\`. To write the rules: \`yg knowledge read writing-llm-aspects\` (or \`writing-deterministic-aspects\`). Content \`.md\` files state WHAT must be satisfied and WHY \u2014 use the user's words, never invent rationale. Things that do NOT become aspects: knowledge already visible in source code (imports, config), non-enforceable knowledge (business strategy, personas, pricing), and conventions the reviewer cannot check against code. Choose initial status: \`draft\` if content.md is still being authored or the rule is unclear (no enforcement, no cost); \`advisory\` if content.md is complete but you want to gather signal across the repo without blocking CI; \`enforced\` if the rule is vetted on a small set and you want repo-wide enforcement immediately.
4930
4984
 
4931
4985
  **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.
4932
4986
 
@@ -5607,7 +5661,7 @@ import chalk from "chalk";
5607
5661
 
5608
5662
  // src/core/graph-loader.ts
5609
5663
  init_graph_fs();
5610
- import path9 from "path";
5664
+ import path10 from "path";
5611
5665
  import { gt as gt3, lt, valid as valid3 } from "semver";
5612
5666
 
5613
5667
  // src/io/config-parser.ts
@@ -5639,6 +5693,50 @@ var DEFAULT_QUALITY = {
5639
5693
  max_direct_relations: 10,
5640
5694
  max_node_chars: 4e4
5641
5695
  };
5696
+ var DEFAULT_COVERAGE = { required: ["/"], excluded: [] };
5697
+ function parseStringArray(raw, field, filename) {
5698
+ if (raw === void 0) return [];
5699
+ if (!Array.isArray(raw) || raw.some((x) => typeof x !== "string")) {
5700
+ throw new ConfigParseError({
5701
+ what: `${filename}: ${field} must be a list of strings (got ${JSON.stringify(raw)}).`,
5702
+ why: "Coverage roots are repo-relative path prefixes; a non-list value cannot be matched against files.",
5703
+ next: `Set ${field} to a YAML list, e.g.
5704
+ ${field.split(".").pop()}:
5705
+ - services/`
5706
+ }, "config-invalid");
5707
+ }
5708
+ return raw;
5709
+ }
5710
+ function parseCoverage(raw, filename) {
5711
+ if (raw === void 0) return DEFAULT_COVERAGE;
5712
+ if (typeof raw !== "object" || Array.isArray(raw) || raw === null) {
5713
+ throw new ConfigParseError({
5714
+ what: `${filename}: coverage must be a mapping`,
5715
+ why: "coverage holds the required/excluded root lists",
5716
+ next: 'replace with `coverage: { required: ["/"], excluded: [] }`'
5717
+ }, "config-invalid");
5718
+ }
5719
+ const cov = raw;
5720
+ const required = cov.required === void 0 ? ["/"] : parseStringArray(cov.required, "coverage.required", filename);
5721
+ 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
+ for (const root of [...required, ...excluded]) {
5730
+ if (root.split("/").includes("..")) {
5731
+ throw new ConfigParseError({
5732
+ what: `${filename}: coverage root '${root}' contains a '..' segment.`,
5733
+ why: "'..' is not a valid repo-relative prefix and will never match any git-tracked path, silently mis-scoping coverage enforcement.",
5734
+ next: 'Use a repo-relative path prefix without any ".." segments (e.g. - services/ instead of - services/../other/).'
5735
+ }, "config-invalid");
5736
+ }
5737
+ }
5738
+ return { required, excluded };
5739
+ }
5642
5740
  function parseMaxNodeChars(raw, filename) {
5643
5741
  if (raw === void 0) return DEFAULT_QUALITY.max_node_chars ?? 4e4;
5644
5742
  if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0) {
@@ -5716,7 +5814,8 @@ async function parseConfig(filePath) {
5716
5814
  quality,
5717
5815
  reviewer,
5718
5816
  parallel,
5719
- debug
5817
+ debug,
5818
+ coverage: parseCoverage(raw.coverage, filename)
5720
5819
  };
5721
5820
  }
5722
5821
  function parseReviewer(raw, filename) {
@@ -5863,14 +5962,6 @@ function parseTier(name, raw, filename) {
5863
5962
  }, "config-tier-unknown-key");
5864
5963
  }
5865
5964
  }
5866
- const maxTokens = c.max_tokens ?? "auto";
5867
- if (maxTokens !== "auto" && (typeof maxTokens !== "number" || maxTokens < 1)) {
5868
- throw new ConfigParseError({
5869
- what: `${filename}: tier '${name}' config.max_tokens must be 'auto' or a positive number`,
5870
- why: "max_tokens controls the LLM response budget; invalid values cause runtime errors",
5871
- next: "set to 'auto' or a positive integer (e.g. 4096)"
5872
- }, "config-tier-config-invalid");
5873
- }
5874
5965
  let references;
5875
5966
  if (t.references !== void 0) {
5876
5967
  if (t.references === null || typeof t.references !== "object" || Array.isArray(t.references)) {
@@ -5925,8 +6016,6 @@ function parseTier(name, raw, filename) {
5925
6016
  endpoint: typeof c.endpoint === "string" ? c.endpoint : void 0,
5926
6017
  temperature: typeof c.temperature === "number" ? c.temperature : 0,
5927
6018
  consensus: consensusRaw,
5928
- max_tokens: maxTokens,
5929
- context_length_field: typeof c.context_length_field === "string" ? c.context_length_field : void 0,
5930
6019
  timeout: typeof c.timeout === "number" ? c.timeout * 1e3 : void 0,
5931
6020
  ...references !== void 0 ? { references } : {}
5932
6021
  };
@@ -6380,8 +6469,10 @@ function parsePorts(rawPorts, filePath) {
6380
6469
  }
6381
6470
 
6382
6471
  // src/io/aspect-parser.ts
6472
+ init_graph_fs();
6383
6473
  init_graph();
6384
6474
  import { readFile as readFile6 } from "fs/promises";
6475
+ import path6 from "path";
6385
6476
  import { parse as parseYaml4 } from "yaml";
6386
6477
 
6387
6478
  // src/io/artifact-reader.ts
@@ -6543,7 +6634,10 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
6543
6634
  };
6544
6635
  }
6545
6636
  const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
6546
- const reviewerResult = parseReviewer2(raw.reviewer, idTrimmed);
6637
+ const hasContentMd = fileExistsSync(path6.join(aspectDir, "content.md"));
6638
+ const hasCheckMjs = fileExistsSync(path6.join(aspectDir, "check.mjs"));
6639
+ const hasImplies = Array.isArray(raw.implies) && raw.implies.length > 0;
6640
+ const reviewerResult = parseReviewer2(raw.reviewer, idTrimmed, { hasContentMd, hasCheckMjs, hasImplies });
6547
6641
  if (!reviewerResult.ok) {
6548
6642
  return { ok: false, aspectId: idTrimmed, errors: reviewerResult.errors };
6549
6643
  }
@@ -6657,6 +6751,20 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
6657
6751
  }]
6658
6752
  };
6659
6753
  }
6754
+ if (reviewer.type === "aggregate") {
6755
+ return {
6756
+ ok: false,
6757
+ aspectId: idTrimmed,
6758
+ errors: [{
6759
+ code: "aspect-references-on-aggregate",
6760
+ messageData: {
6761
+ what: `Aspect '${idTrimmed}' declares 'references:' but it is an aggregating aspect (no content.md, no check.mjs).`,
6762
+ why: "reference files are passed to the LLM reviewer in the prompt. An aggregating aspect has no own reviewer \u2014 it only bundles implied aspects, so references would never be read.",
6763
+ next: `remove 'references:' from .yggdrasil/aspects/${idTrimmed}/yg-aspect.yaml, or add a content.md and move the references onto that LLM aspect.`
6764
+ }
6765
+ }]
6766
+ };
6767
+ }
6660
6768
  references = [];
6661
6769
  const seenPaths = /* @__PURE__ */ new Set();
6662
6770
  for (let i = 0; i < raw.references.length; i++) {
@@ -6777,16 +6885,25 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
6777
6885
  }
6778
6886
  };
6779
6887
  }
6780
- function parseReviewer2(raw, aspectId) {
6888
+ function parseReviewer2(raw, aspectId, files) {
6781
6889
  if (raw === void 0 || raw === null) {
6890
+ if (files.hasContentMd && !files.hasCheckMjs) {
6891
+ return { ok: true, value: { type: "llm" } };
6892
+ }
6893
+ if (files.hasCheckMjs && !files.hasContentMd) {
6894
+ return { ok: true, value: { type: "deterministic" } };
6895
+ }
6896
+ if (!files.hasContentMd && !files.hasCheckMjs && files.hasImplies) {
6897
+ return { ok: true, value: { type: "aggregate" } };
6898
+ }
6782
6899
  return {
6783
6900
  ok: false,
6784
6901
  errors: [{
6785
6902
  code: "aspect-reviewer-missing",
6786
6903
  messageData: {
6787
- what: `aspect '${aspectId}' has no reviewer: block (field absent or null)`,
6788
- why: "every aspect must declare its reviewer explicitly (no implicit default)",
6789
- next: "add `reviewer:\\n type: llm` or `reviewer:\\n type: deterministic`"
6904
+ what: `aspect '${aspectId}' has no reviewer: block and no rule source to infer one from`,
6905
+ why: "an aspect must ship content.md (llm), check.mjs (deterministic), or declare implies (aggregating bundle); otherwise it does nothing",
6906
+ next: "add `reviewer:\\n type: llm` with a content.md, add a check.mjs, or add `implies:` to make this an aggregating aspect"
6790
6907
  }
6791
6908
  }]
6792
6909
  };
@@ -6878,7 +6995,7 @@ function parseReviewer2(raw, aspectId) {
6878
6995
 
6879
6996
  // src/io/flow-parser.ts
6880
6997
  import { readFile as readFile7 } from "fs/promises";
6881
- import path6 from "path";
6998
+ import path7 from "path";
6882
6999
  import { parse as parseYaml5 } from "yaml";
6883
7000
  async function parseFlow(flowDir, flowYamlPath) {
6884
7001
  const content14 = await readFile7(flowYamlPath, "utf-8");
@@ -6927,7 +7044,7 @@ async function parseFlow(flowDir, flowYamlPath) {
6927
7044
  }
6928
7045
  }
6929
7046
  return {
6930
- path: path6.basename(flowDir),
7047
+ path: path7.basename(flowDir),
6931
7048
  name: raw.name.trim(),
6932
7049
  description,
6933
7050
  nodes: nodePaths,
@@ -6939,16 +7056,16 @@ async function parseFlow(flowDir, flowYamlPath) {
6939
7056
 
6940
7057
  // src/io/schema-parser.ts
6941
7058
  import { readFile as readFile8 } from "fs/promises";
6942
- import path7 from "path";
7059
+ import path8 from "path";
6943
7060
  import { parse as parseYaml6 } from "yaml";
6944
7061
  async function parseSchema(filePath) {
6945
- const filename = path7.basename(filePath);
7062
+ const filename = path8.basename(filePath);
6946
7063
  const content14 = await readFile8(filePath, "utf-8");
6947
7064
  const raw = parseYaml6(content14);
6948
7065
  if (raw != null && (typeof raw !== "object" || Array.isArray(raw))) {
6949
7066
  throw new Error(`${filename} at ${filePath}: expected YAML mapping or empty document`);
6950
7067
  }
6951
- const rawName = path7.basename(filePath, path7.extname(filePath));
7068
+ const rawName = path8.basename(filePath, path8.extname(filePath));
6952
7069
  const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
6953
7070
  return { schemaType };
6954
7071
  }
@@ -7186,7 +7303,7 @@ var OutdatedSchemaVersionError = class extends Error {
7186
7303
  }
7187
7304
  };
7188
7305
  function toModelPath(absolutePath, modelDir) {
7189
- return toPosixPath(path9.relative(modelDir, absolutePath));
7306
+ return toPosixPath(path10.relative(modelDir, absolutePath));
7190
7307
  }
7191
7308
  var FALLBACK_CONFIG = {};
7192
7309
  async function loadGraph(projectRoot, options = {}) {
@@ -7203,7 +7320,7 @@ async function loadGraph(projectRoot, options = {}) {
7203
7320
  let configErrorMessage;
7204
7321
  let config = FALLBACK_CONFIG;
7205
7322
  try {
7206
- config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
7323
+ config = await parseConfig(path10.join(yggRoot, "yg-config.yaml"));
7207
7324
  } catch (error) {
7208
7325
  if (error instanceof ConfigParseError) {
7209
7326
  configErrorMessage = error.messageData;
@@ -7216,7 +7333,7 @@ async function loadGraph(projectRoot, options = {}) {
7216
7333
  }
7217
7334
  }
7218
7335
  const { architecture, error: architectureError } = await loadArchitecture(yggRoot);
7219
- const modelDir = path9.join(yggRoot, "model");
7336
+ const modelDir = path10.join(yggRoot, "model");
7220
7337
  const nodes = /* @__PURE__ */ new Map();
7221
7338
  const nodeParseErrors = [];
7222
7339
  try {
@@ -7229,9 +7346,9 @@ async function loadGraph(projectRoot, options = {}) {
7229
7346
  }
7230
7347
  throw err;
7231
7348
  }
7232
- const aspectsLoad = await loadAspects(path9.join(yggRoot, "aspects"));
7233
- const flows = await loadFlows(path9.join(yggRoot, "flows"));
7234
- const schemas = await loadSchemas(path9.join(yggRoot, "schemas"));
7349
+ const aspectsLoad = await loadAspects(path10.join(yggRoot, "aspects"));
7350
+ const flows = await loadFlows(path10.join(yggRoot, "flows"));
7351
+ const schemas = await loadSchemas(path10.join(yggRoot, "schemas"));
7235
7352
  return {
7236
7353
  config,
7237
7354
  architecture,
@@ -7249,7 +7366,7 @@ async function loadGraph(projectRoot, options = {}) {
7249
7366
  };
7250
7367
  }
7251
7368
  async function loadArchitecture(yggRoot) {
7252
- const architectureFilePath = path9.join(yggRoot, "yg-architecture.yaml");
7369
+ const architectureFilePath = path10.join(yggRoot, "yg-architecture.yaml");
7253
7370
  const emptyArch = { node_types: {} };
7254
7371
  try {
7255
7372
  const architecture = await parseArchitecture(architectureFilePath);
@@ -7287,7 +7404,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
7287
7404
  }
7288
7405
  if (hasNodeYaml) {
7289
7406
  const graphPath = toModelPath(dirPath, modelDir);
7290
- const nodeYamlPath = path9.join(dirPath, "yg-node.yaml");
7407
+ const nodeYamlPath = path10.join(dirPath, "yg-node.yaml");
7291
7408
  let meta;
7292
7409
  let nodeYamlRaw;
7293
7410
  try {
@@ -7319,7 +7436,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
7319
7436
  if (!entry.isDirectory()) continue;
7320
7437
  if (entry.name.startsWith(".")) continue;
7321
7438
  await scanModelDirectory(
7322
- path9.join(dirPath, entry.name),
7439
+ path10.join(dirPath, entry.name),
7323
7440
  modelDir,
7324
7441
  node,
7325
7442
  nodes,
@@ -7331,7 +7448,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
7331
7448
  if (!entry.isDirectory()) continue;
7332
7449
  if (entry.name.startsWith(".")) continue;
7333
7450
  await scanModelDirectory(
7334
- path9.join(dirPath, entry.name),
7451
+ path10.join(dirPath, entry.name),
7335
7452
  modelDir,
7336
7453
  null,
7337
7454
  nodes,
@@ -7353,8 +7470,8 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects, parseErrors)
7353
7470
  const entries = await readSortedDir(dirPath);
7354
7471
  const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
7355
7472
  if (hasAspectYaml) {
7356
- const id = toPosixPath(path9.relative(aspectsRoot, dirPath));
7357
- const aspectYamlPath = path9.join(dirPath, "yg-aspect.yaml");
7473
+ const id = toPosixPath(path10.relative(aspectsRoot, dirPath));
7474
+ const aspectYamlPath = path10.join(dirPath, "yg-aspect.yaml");
7358
7475
  const result = await parseAspect(dirPath, aspectYamlPath, id);
7359
7476
  if (result.ok) {
7360
7477
  aspects.push(result.aspect);
@@ -7367,7 +7484,7 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects, parseErrors)
7367
7484
  for (const entry of entries) {
7368
7485
  if (!entry.isDirectory()) continue;
7369
7486
  if (entry.name.startsWith(".")) continue;
7370
- await scanAspectsDirectory(path9.join(dirPath, entry.name), aspectsRoot, aspects, parseErrors);
7487
+ await scanAspectsDirectory(path10.join(dirPath, entry.name), aspectsRoot, aspects, parseErrors);
7371
7488
  }
7372
7489
  }
7373
7490
  async function loadFlows(flowsDir) {
@@ -7376,8 +7493,8 @@ async function loadFlows(flowsDir) {
7376
7493
  const flows = [];
7377
7494
  for (const entry of entries) {
7378
7495
  if (!entry.isDirectory()) continue;
7379
- const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
7380
- const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
7496
+ const flowYamlPath = path10.join(flowsDir, entry.name, "yg-flow.yaml");
7497
+ const flow = await parseFlow(path10.join(flowsDir, entry.name), flowYamlPath);
7381
7498
  flows.push(flow);
7382
7499
  }
7383
7500
  return flows;
@@ -7388,7 +7505,7 @@ async function loadSchemas(schemasDir) {
7388
7505
  for (const entry of entries) {
7389
7506
  if (!entry.isFile()) continue;
7390
7507
  if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
7391
- const s = await parseSchema(path9.join(schemasDir, entry.name));
7508
+ const s = await parseSchema(path10.join(schemasDir, entry.name));
7392
7509
  schemas.push(s);
7393
7510
  }
7394
7511
  return schemas;
@@ -7455,7 +7572,7 @@ async function loadGraphOrAbort(rootPath, options = {}) {
7455
7572
  // src/migrations/to-4.0.0.ts
7456
7573
  init_posix();
7457
7574
  import { readdir as readdir4, readFile as readFile11, writeFile as writeFile4, rm as rm3, stat as stat4 } from "fs/promises";
7458
- import path12 from "path";
7575
+ import path13 from "path";
7459
7576
  import { parse as parseYaml8, stringify as stringifyYaml } from "yaml";
7460
7577
  var NODE_ARTIFACTS = ["responsibility.md", "interface.md", "internals.md"];
7461
7578
  function posix(p2) {
@@ -7466,19 +7583,19 @@ async function migrateTo4(yggRoot) {
7466
7583
  const warnings = [];
7467
7584
  await splitArchitecture(yggRoot, actions);
7468
7585
  await cleanConfig(yggRoot, actions);
7469
- const modelDir = path12.join(yggRoot, "model");
7586
+ const modelDir = path13.join(yggRoot, "model");
7470
7587
  if (await dirExists(modelDir)) {
7471
7588
  await processNodesRecursive(modelDir, actions, warnings);
7472
7589
  }
7473
- const flowsDir = path12.join(yggRoot, "flows");
7590
+ const flowsDir = path13.join(yggRoot, "flows");
7474
7591
  if (await dirExists(flowsDir)) {
7475
7592
  await processFlows(flowsDir, actions);
7476
7593
  }
7477
- const aspectsDir = path12.join(yggRoot, "aspects");
7594
+ const aspectsDir = path13.join(yggRoot, "aspects");
7478
7595
  if (await dirExists(aspectsDir)) {
7479
7596
  await processAspects(aspectsDir, actions);
7480
7597
  }
7481
- const driftDir = path12.join(yggRoot, ".drift-state");
7598
+ const driftDir = path13.join(yggRoot, ".drift-state");
7482
7599
  if (await dirExists(driftDir)) {
7483
7600
  await resetDriftState(driftDir, actions);
7484
7601
  }
@@ -7496,18 +7613,18 @@ async function dirExists(dir) {
7496
7613
  }
7497
7614
  }
7498
7615
  async function splitArchitecture(yggRoot, actions) {
7499
- const configPath = path12.join(yggRoot, "yg-config.yaml");
7616
+ const configPath = path13.join(yggRoot, "yg-config.yaml");
7500
7617
  const content14 = await readFile11(configPath, "utf-8");
7501
7618
  const config = parseYaml8(content14);
7502
7619
  if (config.node_types) {
7503
7620
  const archData = { node_types: config.node_types };
7504
- const archPath = path12.join(yggRoot, "yg-architecture.yaml");
7621
+ const archPath = path13.join(yggRoot, "yg-architecture.yaml");
7505
7622
  await writeFile4(archPath, stringifyYaml(archData, { lineWidth: 0 }), "utf-8");
7506
7623
  actions.push("Extracted node_types to yg-architecture.yaml");
7507
7624
  }
7508
7625
  }
7509
7626
  async function cleanConfig(yggRoot, actions) {
7510
- const configPath = path12.join(yggRoot, "yg-config.yaml");
7627
+ const configPath = path13.join(yggRoot, "yg-config.yaml");
7511
7628
  const content14 = await readFile11(configPath, "utf-8");
7512
7629
  const config = parseYaml8(content14);
7513
7630
  let dirty = false;
@@ -7546,7 +7663,7 @@ async function cleanConfig(yggRoot, actions) {
7546
7663
  async function processNodesRecursive(dir, actions, warnings) {
7547
7664
  const entries = await readdir4(dir, { withFileTypes: true });
7548
7665
  for (const entry of entries) {
7549
- const fullPath = path12.join(dir, entry.name);
7666
+ const fullPath = path13.join(dir, entry.name);
7550
7667
  if (entry.isDirectory()) {
7551
7668
  await processNodesRecursive(fullPath, actions, warnings);
7552
7669
  continue;
@@ -7631,7 +7748,7 @@ async function processFlows(dir, actions) {
7631
7748
  const entries = await readdir4(dir, { withFileTypes: true });
7632
7749
  for (const entry of entries) {
7633
7750
  if (!entry.isDirectory()) continue;
7634
- const descPath = path12.join(dir, entry.name, "description.md");
7751
+ const descPath = path13.join(dir, entry.name, "description.md");
7635
7752
  try {
7636
7753
  await rm3(descPath);
7637
7754
  actions.push(`Deleted flow artifact: ${posix(descPath)}`);
@@ -7643,7 +7760,7 @@ async function processAspects(dir, actions) {
7643
7760
  const entries = await readdir4(dir, { withFileTypes: true });
7644
7761
  for (const entry of entries) {
7645
7762
  if (!entry.isDirectory()) continue;
7646
- const aspectPath = path12.join(dir, entry.name, "yg-aspect.yaml");
7763
+ const aspectPath = path13.join(dir, entry.name, "yg-aspect.yaml");
7647
7764
  try {
7648
7765
  const content14 = await readFile11(aspectPath, "utf-8");
7649
7766
  const aspect = parseYaml8(content14);
@@ -7662,7 +7779,7 @@ async function resetDriftState(dir, actions) {
7662
7779
  async function resetDriftStateRecursive(dir, actions) {
7663
7780
  const entries = await readdir4(dir, { withFileTypes: true });
7664
7781
  for (const entry of entries) {
7665
- const fullPath = path12.join(dir, entry.name);
7782
+ const fullPath = path13.join(dir, entry.name);
7666
7783
  if (entry.isDirectory()) {
7667
7784
  await resetDriftStateRecursive(fullPath, actions);
7668
7785
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
@@ -7674,12 +7791,12 @@ async function resetDriftStateRecursive(dir, actions) {
7674
7791
 
7675
7792
  // src/migrations/to-4.3.0.ts
7676
7793
  import { readFile as readFile12, writeFile as writeFile5 } from "fs/promises";
7677
- import path13 from "path";
7794
+ import path14 from "path";
7678
7795
  import { parse as parseYaml9, stringify as stringifyYaml2 } from "yaml";
7679
7796
  async function migrateTo43(yggRoot) {
7680
7797
  const actions = [];
7681
7798
  const warnings = [];
7682
- const archPath = path13.join(yggRoot, "yg-architecture.yaml");
7799
+ const archPath = path14.join(yggRoot, "yg-architecture.yaml");
7683
7800
  let raw;
7684
7801
  try {
7685
7802
  raw = await readFile12(archPath, "utf-8");
@@ -7724,7 +7841,7 @@ See: yg knowledge read working-with-architecture`
7724
7841
 
7725
7842
  // src/migrations/to-5.0.0.ts
7726
7843
  import { readFile as readFile17, writeFile as writeFile6, readdir as readdir8, rm as rm4 } from "fs/promises";
7727
- import path16 from "path";
7844
+ import path17 from "path";
7728
7845
  import { parse as parseYaml12, stringify as stringifyYaml3 } from "yaml";
7729
7846
 
7730
7847
  // src/core/format-detect.ts
@@ -7742,7 +7859,7 @@ init_secrets_parser();
7742
7859
  // src/migrations/aspect-status-defaults.ts
7743
7860
  init_posix();
7744
7861
  import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
7745
- import path14 from "path";
7862
+ import path15 from "path";
7746
7863
  import { parse as parseYaml11 } from "yaml";
7747
7864
  var STATUS_RANK = {
7748
7865
  draft: 0,
@@ -7766,14 +7883,14 @@ function parseAttachmentEntry(raw) {
7766
7883
  return { id, status: parseStatus(obj.status), statusInherit };
7767
7884
  }
7768
7885
  async function walkGraphDirs(rootDir, relPath, yamlName, visit) {
7769
- const dir = path14.join(rootDir, relPath);
7886
+ const dir = path15.join(rootDir, relPath);
7770
7887
  let entries;
7771
7888
  try {
7772
7889
  entries = await readdir5(dir, { withFileTypes: true });
7773
7890
  } catch {
7774
7891
  return;
7775
7892
  }
7776
- const yamlPath = path14.join(dir, yamlName);
7893
+ const yamlPath = path15.join(dir, yamlName);
7777
7894
  const relativeId = toPosix(relPath);
7778
7895
  try {
7779
7896
  await readFile14(yamlPath, "utf-8");
@@ -7782,12 +7899,12 @@ async function walkGraphDirs(rootDir, relPath, yamlName, visit) {
7782
7899
  }
7783
7900
  for (const entry of entries) {
7784
7901
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7785
- await walkGraphDirs(rootDir, path14.join(relPath, entry.name), yamlName, visit);
7902
+ await walkGraphDirs(rootDir, path15.join(relPath, entry.name), yamlName, visit);
7786
7903
  }
7787
7904
  }
7788
7905
  async function loadAspects2(yggRoot) {
7789
7906
  const result = /* @__PURE__ */ new Map();
7790
- const aspectsDir = path14.join(yggRoot, "aspects");
7907
+ const aspectsDir = path15.join(yggRoot, "aspects");
7791
7908
  await walkGraphDirs(aspectsDir, "", "yg-aspect.yaml", async (aspectId, yamlPath) => {
7792
7909
  let raw;
7793
7910
  try {
@@ -7815,7 +7932,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7815
7932
  const bump = (id) => {
7816
7933
  nodeAspectCounts.set(id, (nodeAspectCounts.get(id) ?? 0) + 1);
7817
7934
  };
7818
- const modelDir = path14.join(yggRoot, "model");
7935
+ const modelDir = path15.join(yggRoot, "model");
7819
7936
  await walkGraphDirs(modelDir, "", "yg-node.yaml", async (nodePath, yamlPath) => {
7820
7937
  let raw;
7821
7938
  try {
@@ -7857,7 +7974,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7857
7974
  }
7858
7975
  }
7859
7976
  });
7860
- const archPath = path14.join(yggRoot, "yg-architecture.yaml");
7977
+ const archPath = path15.join(yggRoot, "yg-architecture.yaml");
7861
7978
  let nodeTypes;
7862
7979
  try {
7863
7980
  const archRaw = parseYaml11(await readFile14(archPath, "utf-8"));
@@ -7882,7 +7999,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7882
7999
  });
7883
8000
  }
7884
8001
  }
7885
- const flowsDir = path14.join(yggRoot, "flows");
8002
+ const flowsDir = path15.join(yggRoot, "flows");
7886
8003
  let flowEntries = [];
7887
8004
  try {
7888
8005
  flowEntries = await readdir5(flowsDir, { withFileTypes: true });
@@ -7890,7 +8007,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7890
8007
  }
7891
8008
  for (const fe of flowEntries) {
7892
8009
  if (!fe.isDirectory()) continue;
7893
- const flowYaml = path14.join(flowsDir, fe.name, "yg-flow.yaml");
8010
+ const flowYaml = path15.join(flowsDir, fe.name, "yg-flow.yaml");
7894
8011
  let flowRaw;
7895
8012
  try {
7896
8013
  const content14 = await readFile14(flowYaml, "utf-8");
@@ -8012,8 +8129,11 @@ function rekeyDriftBaseline(oldFlat) {
8012
8129
  const aspectVerdicts = oldFlat.aspectVerdicts ?? {};
8013
8130
  const state = {
8014
8131
  schemaVersion: DRIFT_STATE_SCHEMA_VERSION,
8015
- // Recompute with the NEW canonical scheme over the SAME logical inputs.
8016
- hash: computeCanonicalHash(realFiles, identity),
8132
+ // Recompute with the NEW canonical scheme over the SAME logical inputs:
8133
+ // files + typed identity + the (possibly synthesized-empty) verdict set that
8134
+ // is also stored below. The verdict fold must use the SAME aspectVerdicts the
8135
+ // baseline persists, so a fresh `yg check` over unchanged source sees no drift.
8136
+ hash: computeCanonicalHash(realFiles, identity, aspectVerdicts),
8017
8137
  files: realFiles,
8018
8138
  identity,
8019
8139
  aspectVerdicts
@@ -8141,7 +8261,7 @@ function transformAspectReviewer(raw) {
8141
8261
  return { value: void 0, warnings, changed: false };
8142
8262
  }
8143
8263
  async function migrateConfigFile(yggRoot, actions, warnings) {
8144
- const configPath = path16.join(yggRoot, "yg-config.yaml");
8264
+ const configPath = path17.join(yggRoot, "yg-config.yaml");
8145
8265
  let content14;
8146
8266
  try {
8147
8267
  content14 = await readFile17(configPath, "utf-8");
@@ -8191,7 +8311,7 @@ async function migrateConfigFile(yggRoot, actions, warnings) {
8191
8311
  return { proceedWithAspects: true };
8192
8312
  }
8193
8313
  async function migrateAllAspects(yggRoot, actions, warnings) {
8194
- const aspectsDir = path16.join(yggRoot, "aspects");
8314
+ const aspectsDir = path17.join(yggRoot, "aspects");
8195
8315
  try {
8196
8316
  await scanAspectsDir(aspectsDir, "", actions, warnings);
8197
8317
  } catch (e) {
@@ -8199,7 +8319,7 @@ async function migrateAllAspects(yggRoot, actions, warnings) {
8199
8319
  }
8200
8320
  }
8201
8321
  async function scanAspectsDir(rootDir, relPath, actions, warnings) {
8202
- const dir = path16.join(rootDir, relPath);
8322
+ const dir = path17.join(rootDir, relPath);
8203
8323
  let entries;
8204
8324
  try {
8205
8325
  entries = await readdir8(dir, { withFileTypes: true });
@@ -8208,7 +8328,7 @@ async function scanAspectsDir(rootDir, relPath, actions, warnings) {
8208
8328
  throw e;
8209
8329
  }
8210
8330
  const aspectId = toPosix(relPath);
8211
- const aspectYamlPath = path16.join(dir, "yg-aspect.yaml");
8331
+ const aspectYamlPath = path17.join(dir, "yg-aspect.yaml");
8212
8332
  let hasAspectYaml = false;
8213
8333
  try {
8214
8334
  await readFile17(aspectYamlPath, "utf-8");
@@ -8221,7 +8341,7 @@ async function scanAspectsDir(rootDir, relPath, actions, warnings) {
8221
8341
  for (const entry of entries) {
8222
8342
  if (!entry.isDirectory()) continue;
8223
8343
  if (entry.name.startsWith(".")) continue;
8224
- await scanAspectsDir(rootDir, path16.join(relPath, entry.name), actions, warnings);
8344
+ await scanAspectsDir(rootDir, path17.join(relPath, entry.name), actions, warnings);
8225
8345
  }
8226
8346
  }
8227
8347
  async function migrateOneAspect(aspectYamlPath, aspectId, actions, warnings) {
@@ -8265,7 +8385,7 @@ async function migrateSecretsFile(yggRoot, _actions, warnings) {
8265
8385
  }
8266
8386
  var DRIFT_STATE_DIR2 = ".drift-state";
8267
8387
  async function scanDriftBaselineNodePaths(driftDir, relDir) {
8268
- const absDir = path16.join(driftDir, relDir);
8388
+ const absDir = path17.join(driftDir, relDir);
8269
8389
  let entries;
8270
8390
  try {
8271
8391
  entries = await readdir8(absDir, { withFileTypes: true });
@@ -8285,8 +8405,8 @@ async function scanDriftBaselineNodePaths(driftDir, relDir) {
8285
8405
  return results;
8286
8406
  }
8287
8407
  async function migrateOneDriftBaseline(yggRoot, driftDir, nodePath, actions, warnings) {
8288
- const filePath = path16.join(driftDir, `${nodePath}.json`);
8289
- const displayPath = toPosix(path16.join(DRIFT_STATE_DIR2, `${nodePath}.json`));
8408
+ const filePath = path17.join(driftDir, `${nodePath}.json`);
8409
+ const displayPath = toPosix(path17.join(DRIFT_STATE_DIR2, `${nodePath}.json`));
8290
8410
  let parsed;
8291
8411
  try {
8292
8412
  const content14 = await readFile17(filePath, "utf-8");
@@ -8330,7 +8450,7 @@ async function migrateOneDriftBaseline(yggRoot, driftDir, nodePath, actions, war
8330
8450
  actions.push(`${displayPath}: re-keyed drift-state baseline to typed format.`);
8331
8451
  }
8332
8452
  async function migrateDriftState(yggRoot, actions, warnings) {
8333
- const driftDir = path16.join(yggRoot, DRIFT_STATE_DIR2);
8453
+ const driftDir = path17.join(yggRoot, DRIFT_STATE_DIR2);
8334
8454
  const nodePaths = await scanDriftBaselineNodePaths(driftDir, "");
8335
8455
  for (const nodePath of nodePaths.sort()) {
8336
8456
  await migrateOneDriftBaseline(yggRoot, driftDir, nodePath, actions, warnings);
@@ -8381,34 +8501,34 @@ init_message_builder();
8381
8501
  init_debug_log();
8382
8502
  init_posix();
8383
8503
  function getPackageRoot() {
8384
- let dir = path17.dirname(fileURLToPath2(import.meta.url));
8385
- while (dir !== path17.dirname(dir)) {
8386
- if (existsSync2(path17.join(dir, "package.json"))) {
8504
+ let dir = path18.dirname(fileURLToPath2(import.meta.url));
8505
+ while (dir !== path18.dirname(dir)) {
8506
+ if (existsSync2(path18.join(dir, "package.json"))) {
8387
8507
  return dir;
8388
8508
  }
8389
- dir = path17.dirname(dir);
8509
+ dir = path18.dirname(dir);
8390
8510
  }
8391
8511
  throw new Error("Could not locate package root (no package.json found walking up from init module).");
8392
8512
  }
8393
8513
  function getGraphSchemasDir() {
8394
- return path17.join(getPackageRoot(), "graph-schemas");
8514
+ return path18.join(getPackageRoot(), "graph-schemas");
8395
8515
  }
8396
8516
  async function getCliVersion() {
8397
- const pkgPath = path17.join(getPackageRoot(), "package.json");
8517
+ const pkgPath = path18.join(getPackageRoot(), "package.json");
8398
8518
  const pkg2 = JSON.parse(await readFile18(pkgPath, "utf-8"));
8399
8519
  return pkg2.version;
8400
8520
  }
8401
8521
  async function refreshSchemas(yggRoot) {
8402
- const schemasDir = path17.join(yggRoot, "schemas");
8522
+ const schemasDir = path18.join(yggRoot, "schemas");
8403
8523
  await mkdir3(schemasDir, { recursive: true });
8404
8524
  const graphSchemasDir = getGraphSchemasDir();
8405
8525
  try {
8406
8526
  const entries = await readdir9(graphSchemasDir, { withFileTypes: true });
8407
8527
  const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
8408
8528
  for (const file of schemaFiles) {
8409
- const srcPath = path17.join(graphSchemasDir, file);
8529
+ const srcPath = path18.join(graphSchemasDir, file);
8410
8530
  const content14 = await readFile18(srcPath, "utf-8");
8411
- await writeFile7(path17.join(schemasDir, file), content14, "utf-8");
8531
+ await writeFile7(path18.join(schemasDir, file), content14, "utf-8");
8412
8532
  }
8413
8533
  } catch (e) {
8414
8534
  debugWrite(`[init] refreshSchemas schema copy failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -8593,7 +8713,7 @@ async function runReviewerConfigFlow() {
8593
8713
  return { provider, model, apiKey: apiKey || void 0, endpoint };
8594
8714
  }
8595
8715
  async function writeReviewerConfig(yggRoot, config) {
8596
- const configPath = path17.join(yggRoot, "yg-config.yaml");
8716
+ const configPath = path18.join(yggRoot, "yg-config.yaml");
8597
8717
  let raw = {};
8598
8718
  try {
8599
8719
  const content14 = await readFile18(configPath, "utf-8");
@@ -8624,7 +8744,7 @@ async function writeReviewerConfig(yggRoot, config) {
8624
8744
  await writeFile7(configPath, yamlStringify(raw), "utf-8");
8625
8745
  }
8626
8746
  async function writeSecretsFile(yggRoot, provider, apiKey) {
8627
- const secretsPath = path17.join(yggRoot, "yg-secrets.yaml");
8747
+ const secretsPath = path18.join(yggRoot, "yg-secrets.yaml");
8628
8748
  let raw = {};
8629
8749
  try {
8630
8750
  const content14 = await readFile18(secretsPath, "utf-8");
@@ -8647,7 +8767,7 @@ async function writeSecretsFile(yggRoot, provider, apiKey) {
8647
8767
  await writeFile7(secretsPath, yamlStringify(raw), { encoding: "utf-8", mode: 384 });
8648
8768
  }
8649
8769
  async function freshInit(projectRoot) {
8650
- const yggRoot = path17.join(projectRoot, ".yggdrasil");
8770
+ const yggRoot = path18.join(projectRoot, ".yggdrasil");
8651
8771
  if (!isTTY()) {
8652
8772
  process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
8653
8773
  what: "yg init requires an interactive terminal.",
@@ -8677,19 +8797,19 @@ async function freshInit(projectRoot) {
8677
8797
  p.outro(chalk2.green("Yggdrasil initialized. Run yg check to get started."));
8678
8798
  }
8679
8799
  async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
8680
- await mkdir3(path17.join(yggRoot, "model"), { recursive: true });
8681
- await mkdir3(path17.join(yggRoot, "aspects"), { recursive: true });
8682
- await mkdir3(path17.join(yggRoot, "flows"), { recursive: true });
8683
- const schemasDir = path17.join(yggRoot, "schemas");
8800
+ await mkdir3(path18.join(yggRoot, "model"), { recursive: true });
8801
+ await mkdir3(path18.join(yggRoot, "aspects"), { recursive: true });
8802
+ await mkdir3(path18.join(yggRoot, "flows"), { recursive: true });
8803
+ const schemasDir = path18.join(yggRoot, "schemas");
8684
8804
  await mkdir3(schemasDir, { recursive: true });
8685
8805
  const graphSchemasDir = getGraphSchemasDir();
8686
8806
  try {
8687
8807
  const entries = await readdir9(graphSchemasDir, { withFileTypes: true });
8688
8808
  const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
8689
8809
  for (const file of schemaFiles) {
8690
- const srcPath = path17.join(graphSchemasDir, file);
8810
+ const srcPath = path18.join(graphSchemasDir, file);
8691
8811
  const content14 = await readFile18(srcPath, "utf-8");
8692
- await writeFile7(path17.join(schemasDir, file), content14, "utf-8");
8812
+ await writeFile7(path18.join(schemasDir, file), content14, "utf-8");
8693
8813
  }
8694
8814
  } catch (err) {
8695
8815
  debugWrite(`[init] createYggdrasilStructure schema copy failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -8698,9 +8818,9 @@ async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
8698
8818
  `)
8699
8819
  );
8700
8820
  }
8701
- await writeFile7(path17.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
8702
- await writeFile7(path17.join(yggRoot, "yg-architecture.yaml"), DEFAULT_ARCHITECTURE, "utf-8");
8703
- await writeFile7(path17.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
8821
+ await writeFile7(path18.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
8822
+ await writeFile7(path18.join(yggRoot, "yg-architecture.yaml"), DEFAULT_ARCHITECTURE, "utf-8");
8823
+ await writeFile7(path18.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
8704
8824
  await installRulesForPlatform(projectRoot, platform);
8705
8825
  }
8706
8826
  async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
@@ -8709,7 +8829,7 @@ async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
8709
8829
  migrations: MIGRATIONS
8710
8830
  });
8711
8831
  await refreshSchemas(yggRoot);
8712
- const architecturePath = path17.join(yggRoot, "yg-architecture.yaml");
8832
+ const architecturePath = path18.join(yggRoot, "yg-architecture.yaml");
8713
8833
  try {
8714
8834
  await stat6(architecturePath);
8715
8835
  } catch (e) {
@@ -8721,7 +8841,7 @@ async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
8721
8841
  return { rulesPath, migrationActions, migrationWarnings, withheld };
8722
8842
  }
8723
8843
  async function existingInit(projectRoot) {
8724
- const yggRoot = path17.join(projectRoot, ".yggdrasil");
8844
+ const yggRoot = path18.join(projectRoot, ".yggdrasil");
8725
8845
  if (!isTTY()) {
8726
8846
  process.stderr.write(chalk2.yellow(buildIssueMessage({
8727
8847
  what: ".yggdrasil/ already exists.",
@@ -8753,7 +8873,7 @@ async function existingInit(projectRoot) {
8753
8873
  p.log.info("2. Run yg approve on all nodes to establish baselines");
8754
8874
  p.outro(
8755
8875
  chalk2.green(
8756
- `Migrated from ${currentVersion} to ${landedVersion}. Rules installed: ${toPosixPath(path17.relative(projectRoot, result.rulesPath))}`
8876
+ `Migrated from ${currentVersion} to ${landedVersion}. Rules installed: ${toPosixPath(path18.relative(projectRoot, result.rulesPath))}`
8757
8877
  )
8758
8878
  );
8759
8879
  return;
@@ -8771,7 +8891,7 @@ async function existingInit(projectRoot) {
8771
8891
  case "upgrade": {
8772
8892
  const platform = await promptPlatform();
8773
8893
  const result = await runVersionUpgrade2(projectRoot, yggRoot, platform);
8774
- p.outro(chalk2.green(`Rules and schemas refreshed: ${toPosixPath(path17.relative(projectRoot, result.rulesPath))}`));
8894
+ p.outro(chalk2.green(`Rules and schemas refreshed: ${toPosixPath(path18.relative(projectRoot, result.rulesPath))}`));
8775
8895
  break;
8776
8896
  }
8777
8897
  case "reviewer": {
@@ -8786,7 +8906,7 @@ async function existingInit(projectRoot) {
8786
8906
  case "platform": {
8787
8907
  const platform = await promptPlatform();
8788
8908
  const rulesPath = await installRulesForPlatform(projectRoot, platform);
8789
- p.outro(chalk2.green(`Platform changed: ${toPosixPath(path17.relative(projectRoot, rulesPath))}`));
8909
+ p.outro(chalk2.green(`Platform changed: ${toPosixPath(path18.relative(projectRoot, rulesPath))}`));
8790
8910
  break;
8791
8911
  }
8792
8912
  }
@@ -8795,7 +8915,7 @@ function registerInitCommand(program2) {
8795
8915
  program2.command("init").description("Initialize Yggdrasil graph in current project").option("--upgrade", "Non-interactive: refresh rules and schemas").option("--platform <name>", `Platform for rules file (${PLATFORMS.join(", ")})`).action(async (options) => {
8796
8916
  try {
8797
8917
  const projectRoot = process.cwd();
8798
- const yggRoot = path17.join(projectRoot, ".yggdrasil");
8918
+ const yggRoot = path18.join(projectRoot, ".yggdrasil");
8799
8919
  if (options.upgrade) {
8800
8920
  if (!options.platform) {
8801
8921
  process.stderr.write(
@@ -8875,7 +8995,7 @@ function registerInitCommand(program2) {
8875
8995
  );
8876
8996
  }
8877
8997
  process.stdout.write(
8878
- `Rules and schemas refreshed: ${toPosixPath(path17.relative(projectRoot, result.rulesPath))}
8998
+ `Rules and schemas refreshed: ${toPosixPath(path18.relative(projectRoot, result.rulesPath))}
8879
8999
  `
8880
9000
  );
8881
9001
  return;
@@ -8927,7 +9047,7 @@ init_paths();
8927
9047
  init_graph_fs();
8928
9048
  init_aspects();
8929
9049
  init_posix();
8930
- import path19 from "path";
9050
+ import path20 from "path";
8931
9051
 
8932
9052
  // src/core/graph/index.ts
8933
9053
  init_traversal();
@@ -8956,11 +9076,11 @@ function normPath(p2) {
8956
9076
  }
8957
9077
  function countDependents(graph, nodePath) {
8958
9078
  const paths = [];
8959
- for (const [path44, node] of graph.nodes) {
9079
+ for (const [path46, node] of graph.nodes) {
8960
9080
  const hasRelation = (node.meta.relations ?? []).some(
8961
9081
  (r) => r.target === nodePath && (STRUCTURAL_RELATION_TYPES2.has(r.type) || EVENT_RELATION_TYPES.has(r.type))
8962
9082
  );
8963
- if (hasRelation) paths.push(path44);
9083
+ if (hasRelation) paths.push(path46);
8964
9084
  }
8965
9085
  return { count: paths.length, paths };
8966
9086
  }
@@ -8982,7 +9102,7 @@ function buildNodeContextData(graph, nodePath) {
8982
9102
  name: aspectDef?.name ?? aspectId,
8983
9103
  description: aspectDef?.description ?? "",
8984
9104
  source,
8985
- verifiedAgainst: aspectDef?.reviewer?.type === "deterministic" ? `.yggdrasil/aspects/${aspectId}/check.mjs` : `.yggdrasil/aspects/${aspectId}/content.md`,
9105
+ verifiedAgainst: aspectDef?.reviewer?.type === "deterministic" ? `.yggdrasil/aspects/${aspectId}/check.mjs` : aspectDef?.reviewer?.type === "aggregate" ? `.yggdrasil/aspects/${aspectId}/yg-aspect.yaml` : `.yggdrasil/aspects/${aspectId}/content.md`,
8986
9106
  implies: aspectDef?.implies,
8987
9107
  status,
8988
9108
  ...refs && { references: refs }
@@ -9039,7 +9159,7 @@ function buildFileContextData(graph, filePath, ownerPath) {
9039
9159
  return {
9040
9160
  aspectId,
9041
9161
  aspectDescription: aspectDef?.description ?? aspectDef?.name ?? aspectId,
9042
- verifiedAgainst: aspectDef?.reviewer?.type === "deterministic" ? `.yggdrasil/aspects/${aspectId}/check.mjs` : `.yggdrasil/aspects/${aspectId}/content.md`,
9162
+ verifiedAgainst: aspectDef?.reviewer?.type === "deterministic" ? `.yggdrasil/aspects/${aspectId}/check.mjs` : aspectDef?.reviewer?.type === "aggregate" ? `.yggdrasil/aspects/${aspectId}/yg-aspect.yaml` : `.yggdrasil/aspects/${aspectId}/content.md`,
9043
9163
  status,
9044
9164
  ...refs && { references: refs }
9045
9165
  };
@@ -9304,7 +9424,7 @@ function issueMsg(data) {
9304
9424
  init_posix();
9305
9425
 
9306
9426
  // src/core/checks/architecture.ts
9307
- import path20 from "path";
9427
+ import path21 from "path";
9308
9428
 
9309
9429
  // src/core/file-when-evaluator.ts
9310
9430
  import { minimatch } from "minimatch";
@@ -9527,19 +9647,19 @@ function checkArchitectureParentCycles(graph) {
9527
9647
  const color = new Map(typeIds.map((id) => [id, WHITE]));
9528
9648
  const backEdges = /* @__PURE__ */ new Set();
9529
9649
  const recordedCycles = [];
9530
- function dfs(typeId, path44) {
9650
+ function dfs(typeId, path46) {
9531
9651
  if (color.get(typeId) === GRAY) {
9532
- const cycleStart = path44.indexOf(typeId);
9533
- if (cycleStart !== -1) recordedCycles.push([...path44.slice(cycleStart), typeId]);
9534
- const from = path44[path44.length - 1];
9652
+ const cycleStart = path46.indexOf(typeId);
9653
+ if (cycleStart !== -1) recordedCycles.push([...path46.slice(cycleStart), typeId]);
9654
+ const from = path46[path46.length - 1];
9535
9655
  if (from !== void 0) backEdges.add(`${from}->${typeId}`);
9536
9656
  return;
9537
9657
  }
9538
9658
  if (color.get(typeId) === BLACK) return;
9539
9659
  color.set(typeId, GRAY);
9540
- path44.push(typeId);
9541
- for (const parent of types[typeId]?.parents ?? []) dfs(parent, path44);
9542
- path44.pop();
9660
+ path46.push(typeId);
9661
+ for (const parent of types[typeId]?.parents ?? []) dfs(parent, path46);
9662
+ path46.pop();
9543
9663
  color.set(typeId, BLACK);
9544
9664
  }
9545
9665
  for (const id of typeIds) {
@@ -9642,13 +9762,13 @@ ${preview}${ellipsis}`,
9642
9762
  async function checkTypeWhenMismatch(graph, cache) {
9643
9763
  const issues = [];
9644
9764
  const unreadable = [];
9645
- const projectRoot = path20.dirname(graph.rootPath);
9765
+ const projectRoot = path21.dirname(graph.rootPath);
9646
9766
  for (const [nodePath, node] of graph.nodes) {
9647
9767
  const typeDef = graph.architecture.node_types[node.meta.type];
9648
9768
  if (typeDef === void 0 || typeDef.when === void 0) continue;
9649
9769
  const mapping = node.meta.mapping ?? [];
9650
9770
  for (const relPath of mapping) {
9651
- const absPath = path20.join(projectRoot, relPath);
9771
+ const absPath = path21.join(projectRoot, relPath);
9652
9772
  const result = await evaluateFileWhen(typeDef.when, {
9653
9773
  absPath,
9654
9774
  repoRelPath: relPath,
@@ -9876,7 +9996,7 @@ function checkDanglingAspectRefs(graph) {
9876
9996
  ...issueMsg({
9877
9997
  what: `Aspect '${aspectId}' is referenced by this node but not defined in aspects/.`,
9878
9998
  why: `Node declares an aspect that does not exist \u2014 aspect requirements cannot be verified.`,
9879
- next: `Create aspects/${aspectId}/ with yg-aspect.yaml and content.md.`
9999
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9880
10000
  })
9881
10001
  });
9882
10002
  }
@@ -9893,7 +10013,7 @@ function checkDanglingAspectRefs(graph) {
9893
10013
  ...issueMsg({
9894
10014
  what: `Aspect '${aspectId}' is referenced by port '${portName}' but not defined in aspects/.`,
9895
10015
  why: `Port declares a required aspect that does not exist \u2014 port contracts cannot be enforced.`,
9896
- next: `Create aspects/${aspectId}/ with yg-aspect.yaml and content.md.`
10016
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9897
10017
  })
9898
10018
  });
9899
10019
  }
@@ -9911,7 +10031,7 @@ function checkDanglingAspectRefs(graph) {
9911
10031
  ...issueMsg({
9912
10032
  what: `Aspect '${aspectId}' is referenced by architecture type '${typeId}' but not defined in aspects/.`,
9913
10033
  why: `Architecture declares a required aspect that does not exist.`,
9914
- next: `Create aspects/${aspectId}/ with yg-aspect.yaml and content.md.`
10034
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9915
10035
  })
9916
10036
  });
9917
10037
  }
@@ -9927,7 +10047,7 @@ function checkDanglingAspectRefs(graph) {
9927
10047
  ...issueMsg({
9928
10048
  what: `Aspect '${aspectId}' is referenced by flow '${flow.name}' but not defined in aspects/.`,
9929
10049
  why: `Flow declares an aspect that does not exist \u2014 flow requirements cannot propagate.`,
9930
- next: `Create aspects/${aspectId}/ with yg-aspect.yaml and content.md.`
10050
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9931
10051
  })
9932
10052
  });
9933
10053
  }
@@ -10223,17 +10343,45 @@ init_graph();
10223
10343
  init_graph_fs();
10224
10344
  init_aspects();
10225
10345
  init_tier_selection();
10226
- import path21 from "path";
10346
+ import path22 from "path";
10227
10347
  init_posix();
10228
10348
  function checkAspectRuleSources(graph) {
10229
10349
  const issues = [];
10230
- const projectRoot = path21.dirname(graph.rootPath);
10350
+ const projectRoot = path22.dirname(graph.rootPath);
10231
10351
  for (const aspect of graph.aspects) {
10232
10352
  const reviewer = aspect.reviewer.type;
10353
+ const aspectDir = path22.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
10354
+ const hasContentMd = fileExistsSync(path22.join(aspectDir, "content.md"));
10355
+ const hasCheckMjs = fileExistsSync(path22.join(aspectDir, "check.mjs"));
10356
+ if (reviewer === "aggregate") {
10357
+ if (hasContentMd || hasCheckMjs) {
10358
+ const present = [hasContentMd ? "content.md" : null, hasCheckMjs ? "check.mjs" : null].filter((f) => f !== null).join(" and ");
10359
+ issues.push({
10360
+ severity: "error",
10361
+ code: "aspect-unexpected-rule-source",
10362
+ rule: "aspect-rule-sources",
10363
+ ...issueMsg({
10364
+ what: `Aspect '${aspect.id}' is an aggregating aspect (no reviewer.type declared, only implies) but ships ${present}.`,
10365
+ why: `Aggregating aspects bundle implied aspects and have no own reviewer; a rule source here is never read.`,
10366
+ next: `Remove .yggdrasil/aspects/${aspect.id}/${present} to keep it aggregating, or declare reviewer.type explicitly to make it an LLM/deterministic aspect.`
10367
+ })
10368
+ });
10369
+ }
10370
+ if (!aspect.implies || aspect.implies.length === 0) {
10371
+ issues.push({
10372
+ severity: "error",
10373
+ code: "aspect-empty",
10374
+ rule: "aspect-rule-sources",
10375
+ ...issueMsg({
10376
+ what: `Aspect '${aspect.id}' has no content.md, no check.mjs, and no implies \u2014 it does nothing.`,
10377
+ why: `An aspect must ship a rule source (content.md or check.mjs) or aggregate others via implies; an empty aspect can never produce a verdict.`,
10378
+ next: `Add a content.md (llm) or check.mjs (deterministic), or add 'implies:' to .yggdrasil/aspects/${aspect.id}/yg-aspect.yaml to bundle existing aspects.`
10379
+ })
10380
+ });
10381
+ }
10382
+ continue;
10383
+ }
10233
10384
  if (reviewer !== "deterministic" && reviewer !== "llm") continue;
10234
- const aspectDir = path21.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
10235
- const hasContentMd = fileExistsSync(path21.join(aspectDir, "content.md"));
10236
- const hasCheckMjs = fileExistsSync(path21.join(aspectDir, "check.mjs"));
10237
10385
  if (hasContentMd && hasCheckMjs) {
10238
10386
  issues.push({
10239
10387
  severity: "error",
@@ -10361,7 +10509,7 @@ function formatBytes(n) {
10361
10509
  return `${Math.round(n / 1024)} KiB`;
10362
10510
  }
10363
10511
  async function checkAspectReferences(graph) {
10364
- const projectRoot = path21.dirname(graph.rootPath);
10512
+ const projectRoot = path22.dirname(graph.rootPath);
10365
10513
  const issues = [];
10366
10514
  for (const aspect of graph.aspects) {
10367
10515
  if (aspect.reviewer.type !== "llm") continue;
@@ -10395,7 +10543,7 @@ async function checkAspectReferences(graph) {
10395
10543
  const tierLabel = tierName != null ? `for tier '${tierName}'` : "for the resolved tier";
10396
10544
  let totalBytes = 0;
10397
10545
  for (const ref of aspect.references) {
10398
- const absPath = path21.join(projectRoot, ref.path);
10546
+ const absPath = path22.join(projectRoot, ref.path);
10399
10547
  const refPath = toPosixPath(ref.path);
10400
10548
  let stats;
10401
10549
  try {
@@ -10542,16 +10690,16 @@ init_hash();
10542
10690
  init_graph_fs();
10543
10691
  init_repo_scanner();
10544
10692
  init_aspects();
10545
- import path22 from "path";
10693
+ import path23 from "path";
10546
10694
  init_posix();
10547
10695
  async function checkFileMappingGitignored(graph) {
10548
- const projectRoot = path22.dirname(graph.rootPath);
10696
+ const projectRoot = path23.dirname(graph.rootPath);
10549
10697
  const tracked = new Set(await walkRepoFiles(projectRoot));
10550
10698
  const issues = [];
10551
10699
  for (const [nodePath, node] of graph.nodes) {
10552
10700
  const mapping = node.meta.mapping ?? [];
10553
10701
  for (const relPath of mapping) {
10554
- const absPath = path22.join(projectRoot, relPath);
10702
+ const absPath = path23.join(projectRoot, relPath);
10555
10703
  let st;
10556
10704
  try {
10557
10705
  st = await statPath(absPath);
@@ -10585,7 +10733,7 @@ async function checkStrictBackwardCoverage(graph, cache) {
10585
10733
  ([, def]) => def.enforce === "strict" && def.when !== void 0
10586
10734
  );
10587
10735
  if (strictTypes.length === 0) return { issues: [], unreadable: [] };
10588
- const projectRoot = path22.dirname(graph.rootPath);
10736
+ const projectRoot = path23.dirname(graph.rootPath);
10589
10737
  const fileToOwner = /* @__PURE__ */ new Map();
10590
10738
  for (const [nodePath, node] of graph.nodes) {
10591
10739
  for (const relPath of node.meta.mapping ?? []) {
@@ -10598,7 +10746,7 @@ async function checkStrictBackwardCoverage(graph, cache) {
10598
10746
  const overlapPairsSeen = /* @__PURE__ */ new Set();
10599
10747
  for (const rawRel of repoFiles) {
10600
10748
  const relPath = normalizePathForCompare(rawRel);
10601
- const absPath = path22.join(projectRoot, relPath);
10749
+ const absPath = path23.join(projectRoot, relPath);
10602
10750
  const matchingTypes = [];
10603
10751
  let fileSkipped = false;
10604
10752
  for (const [typeId2, def] of strictTypes) {
@@ -10749,11 +10897,11 @@ function checkMappingOverlap(graph) {
10749
10897
  }
10750
10898
  async function checkMappingPathsExist(graph) {
10751
10899
  const issues = [];
10752
- const projectRoot = path22.dirname(graph.rootPath);
10900
+ const projectRoot = path23.dirname(graph.rootPath);
10753
10901
  for (const [nodePath, node] of graph.nodes) {
10754
10902
  const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizePathForCompare);
10755
10903
  for (const mp of mappingPaths) {
10756
- const absPath = path22.join(projectRoot, mp);
10904
+ const absPath = path23.join(projectRoot, mp);
10757
10905
  try {
10758
10906
  await fileAccess(absPath);
10759
10907
  } catch {
@@ -10775,13 +10923,13 @@ async function checkMappingPathsExist(graph) {
10775
10923
  }
10776
10924
  function checkMappingEscapesRepo(graph) {
10777
10925
  const issues = [];
10778
- const projectRoot = path22.dirname(graph.rootPath);
10926
+ const projectRoot = path23.dirname(graph.rootPath);
10779
10927
  for (const [nodePath, node] of graph.nodes) {
10780
10928
  for (const raw of node.meta.mapping ?? []) {
10781
10929
  const norm = normalizePathForCompare(raw);
10782
- const resolved = path22.resolve(projectRoot, norm);
10783
- const rel = normalizePathForCompare(path22.relative(projectRoot, resolved));
10784
- if (path22.isAbsolute(norm) || rel === ".." || rel.startsWith("../")) {
10930
+ const resolved = path23.resolve(projectRoot, norm);
10931
+ const rel = normalizePathForCompare(path23.relative(projectRoot, resolved));
10932
+ if (path23.isAbsolute(norm) || rel === ".." || rel.startsWith("../")) {
10785
10933
  issues.push({
10786
10934
  severity: "error",
10787
10935
  code: "mapping-escapes-repo",
@@ -10830,7 +10978,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
10830
10978
  async function checkOversizedNodes(graph, cache) {
10831
10979
  const issues = [];
10832
10980
  const maxChars = graph.config.quality?.max_node_chars ?? 4e4;
10833
- const projectRoot = path22.dirname(graph.rootPath);
10981
+ const projectRoot = path23.dirname(graph.rootPath);
10834
10982
  const refsByAspect = /* @__PURE__ */ new Map();
10835
10983
  for (const aspect of graph.aspects) {
10836
10984
  if (aspect.references?.length) {
@@ -10838,8 +10986,8 @@ async function checkOversizedNodes(graph, cache) {
10838
10986
  }
10839
10987
  }
10840
10988
  async function charsOf(repoRelPath) {
10841
- if (BINARY_EXTENSIONS.has(path22.extname(repoRelPath).toLowerCase())) return 0;
10842
- const abs = path22.resolve(projectRoot, repoRelPath);
10989
+ if (BINARY_EXTENSIONS.has(path23.extname(repoRelPath).toLowerCase())) return 0;
10990
+ const abs = path23.resolve(projectRoot, repoRelPath);
10843
10991
  const res = await cache.read(abs);
10844
10992
  if (res.content !== void 0) return res.content.length;
10845
10993
  if (res.tooLarge) {
@@ -10886,7 +11034,7 @@ async function checkOversizedNodes(graph, cache) {
10886
11034
  }
10887
11035
  async function checkDirectoriesHaveNodeYaml(graph) {
10888
11036
  const issues = [];
10889
- const modelDir = path22.join(graph.rootPath, "model");
11037
+ const modelDir = path23.join(graph.rootPath, "model");
10890
11038
  async function scanDir(dirPath, segments) {
10891
11039
  const entries = await readSortedDir(dirPath);
10892
11040
  const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
@@ -10910,7 +11058,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
10910
11058
  for (const entry of entries) {
10911
11059
  if (!entry.isDirectory()) continue;
10912
11060
  if (entry.name.startsWith(".")) continue;
10913
- await scanDir(path22.join(dirPath, entry.name), [...segments, entry.name]);
11061
+ await scanDir(path23.join(dirPath, entry.name), [...segments, entry.name]);
10914
11062
  }
10915
11063
  }
10916
11064
  try {
@@ -10918,7 +11066,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
10918
11066
  for (const entry of rootEntries) {
10919
11067
  if (!entry.isDirectory()) continue;
10920
11068
  if (entry.name.startsWith(".")) continue;
10921
- await scanDir(path22.join(modelDir, entry.name), [entry.name]);
11069
+ await scanDir(path23.join(modelDir, entry.name), [entry.name]);
10922
11070
  }
10923
11071
  } catch {
10924
11072
  }
@@ -11366,7 +11514,7 @@ async function validate(graph, scope = "all") {
11366
11514
  }
11367
11515
 
11368
11516
  // src/cli/owner.ts
11369
- import path23 from "path";
11517
+ import path24 from "path";
11370
11518
  import { access as access2 } from "fs/promises";
11371
11519
  init_debug_log();
11372
11520
  init_message_builder();
@@ -11402,7 +11550,7 @@ function registerOwnerCommand(program2) {
11402
11550
  const repoRelative = resolveFileArg(repoRoot, options.file);
11403
11551
  const result = findOwner(graph, repoRoot, repoRelative);
11404
11552
  if (!result.nodePath) {
11405
- const absPath = path23.resolve(repoRoot, result.file);
11553
+ const absPath = path24.resolve(repoRoot, result.file);
11406
11554
  let exists = true;
11407
11555
  try {
11408
11556
  await access2(absPath);
@@ -11599,7 +11747,7 @@ ${errorList}`
11599
11747
 
11600
11748
  // src/cli/approve.ts
11601
11749
  import chalk4 from "chalk";
11602
- import path34 from "path";
11750
+ import path35 from "path";
11603
11751
  init_debug_log();
11604
11752
  init_approve();
11605
11753
  init_approve_reviewer();
@@ -11615,7 +11763,7 @@ init_paths();
11615
11763
  init_aspects();
11616
11764
  init_graph_fs();
11617
11765
  init_log_integrity();
11618
- import path33 from "path";
11766
+ import path34 from "path";
11619
11767
 
11620
11768
  // src/core/check-codes.ts
11621
11769
  var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
@@ -11645,8 +11793,10 @@ var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
11645
11793
  "when-unknown-port",
11646
11794
  "aspect-unexpected-rule-source",
11647
11795
  "aspect-missing-rule-source",
11796
+ "aspect-empty",
11648
11797
  "file-unreadable",
11649
11798
  "aspect-references-on-deterministic",
11799
+ "aspect-references-on-aggregate",
11650
11800
  "aspect-reference-broken",
11651
11801
  "aspect-reference-too-large",
11652
11802
  "aspect-references-total-too-large",
@@ -11655,15 +11805,103 @@ var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
11655
11805
  "aspect-reference-escape",
11656
11806
  "aspect-reference-duplicate",
11657
11807
  "aspect-tier-unknown",
11658
- "mapping-escapes-repo"
11808
+ "mapping-escapes-repo",
11809
+ // Drift-state integrity: the recorded baseline hash for a node does not match
11810
+ // a recompute over its files, typed identity, and stored verdicts, yet no file
11811
+ // or identity change explains the divergence (the stored verdicts were tampered
11812
+ // or the baseline predates a hash-scheme change). Structural because it blocks
11813
+ // CI regardless of source-file drift — the baseline itself is untrustworthy.
11814
+ "baseline-integrity"
11659
11815
  ]);
11660
11816
  var COMPLETENESS_CODES = /* @__PURE__ */ new Set(["description-missing"]);
11661
11817
 
11662
11818
  // src/core/check.ts
11663
11819
  init_log_format();
11664
11820
  init_posix();
11821
+ init_repo_scanner();
11822
+
11823
+ // src/core/check-coverage-tiers.ts
11824
+ init_posix();
11825
+ function normalizeRoot(root) {
11826
+ return toPosixPath(root.trim()).replace(/^\/+/, "").replace(/\/+$/, "").replace(/\/{2,}/g, "/");
11827
+ }
11828
+ function matchesRoot(file, normRoot) {
11829
+ return normRoot === "" || file === normRoot || file.startsWith(normRoot + "/");
11830
+ }
11831
+ function partitionByCoverageTier(uncovered, coverage) {
11832
+ const req = coverage.required.map(normalizeRoot);
11833
+ const exc = coverage.excluded.map(normalizeRoot);
11834
+ const required = [];
11835
+ const middle = [];
11836
+ for (const f of uncovered) {
11837
+ let best = { len: -1, tier: "middle" };
11838
+ for (const r of req) if (matchesRoot(f, r) && r.length > best.len) best = { len: r.length, tier: "required" };
11839
+ for (const r of exc) if (matchesRoot(f, r) && r.length >= best.len) best = { len: r.length, tier: "excluded" };
11840
+ if (best.tier === "required") required.push(f);
11841
+ else if (best.tier === "middle") middle.push(f);
11842
+ }
11843
+ return { required, middle };
11844
+ }
11845
+ function buildCoverageIssue(uncoveredFiles, totalGitFiles) {
11846
+ if (uncoveredFiles.length === 0) return null;
11847
+ const sampleSize = 5;
11848
+ const sample = uncoveredFiles.slice(0, sampleSize);
11849
+ const remaining = uncoveredFiles.length - sample.length;
11850
+ const coveragePct = totalGitFiles > 0 ? (totalGitFiles - uncoveredFiles.length) / totalGitFiles * 100 : 100;
11851
+ let coverageMd;
11852
+ if (uncoveredFiles.length <= sampleSize) {
11853
+ coverageMd = {
11854
+ what: `${uncoveredFiles.length} source file${uncoveredFiles.length === 1 ? "" : "s"} not covered by any node.
11855
+ ${sample.map((f) => " " + f).join("\n")}`,
11856
+ why: "Files without graph coverage cannot be modified under the protocol.",
11857
+ next: `Check ownership candidates: yg context --file <path>
11858
+ Then: add to existing node mapping, or create a new node.`
11859
+ };
11860
+ } else {
11861
+ const guidance = coveragePct < 50 ? "Establish coverage: create nodes for active areas first, expand coverage incrementally." : "Add to an existing node mapping, or create a new node.";
11862
+ coverageMd = {
11863
+ what: `${uncoveredFiles.length} source files have no graph coverage.
11864
+ Examples:
11865
+ ${sample.map((f) => " " + f).join("\n")}
11866
+ ... and ${remaining} more`,
11867
+ why: "Files without graph coverage cannot be modified under the protocol.",
11868
+ next: `${guidance}
11869
+ Check ownership candidates: yg context --file <path>`
11870
+ };
11871
+ }
11872
+ return {
11873
+ severity: "error",
11874
+ code: "unmapped-files",
11875
+ rule: "unmapped-file",
11876
+ messageData: coverageMd,
11877
+ uncoveredFiles,
11878
+ uncoveredCount: uncoveredFiles.length
11879
+ };
11880
+ }
11881
+ function buildCoverageAdvisoryIssue(uncoveredFiles) {
11882
+ if (uncoveredFiles.length === 0) return null;
11883
+ const sample = uncoveredFiles.slice(0, 5);
11884
+ const remaining = uncoveredFiles.length - sample.length;
11885
+ const body = uncoveredFiles.length <= 5 ? sample.map((f) => " " + f).join("\n") : `${sample.map((f) => " " + f).join("\n")}
11886
+ ... and ${remaining} more`;
11887
+ return {
11888
+ severity: "warning",
11889
+ code: "uncovered-advisory",
11890
+ rule: "uncovered-advisory",
11891
+ messageData: {
11892
+ what: `${uncoveredFiles.length} tracked file${uncoveredFiles.length === 1 ? "" : "s"} outside any required coverage root.
11893
+ ${body}`,
11894
+ why: "Not under a coverage.required root \u2014 visible but non-blocking. Bring an area under graph coverage to enforce it.",
11895
+ next: "Map these files to a node, or add their root to coverage.required to make this an error."
11896
+ },
11897
+ uncoveredFiles,
11898
+ uncoveredCount: uncoveredFiles.length
11899
+ };
11900
+ }
11901
+
11902
+ // src/core/check.ts
11665
11903
  async function classifyDrift(graph) {
11666
- const projectRoot = path33.dirname(graph.rootPath);
11904
+ const projectRoot = path34.dirname(graph.rootPath);
11667
11905
  const issues = [];
11668
11906
  for (const [nodePath, node] of graph.nodes) {
11669
11907
  const mappingPaths = normalizeMappingPaths(node.meta.mapping);
@@ -11733,7 +11971,10 @@ ${mappingPaths.map((p2) => " " + p2).join("\n")}`,
11733
11971
  trackedFiles,
11734
11972
  storedFileData,
11735
11973
  excludePrefixes,
11736
- identity
11974
+ identity,
11975
+ storedEntry.aspectVerdicts,
11976
+ false
11977
+ // never reuse stored hashes by mtime in the check gate — always re-hash content
11737
11978
  );
11738
11979
  if (canonicalHash === storedEntry.hash) return;
11739
11980
  const resolveLayer = buildLayerResolver(trackedFiles);
@@ -11791,6 +12032,21 @@ ${mappingPaths.map((p2) => " " + p2).join("\n")}`,
11791
12032
  }
11792
12033
  }
11793
12034
  }
12035
+ if (directChanges.length === 0 && cascadeCauses.length === 0) {
12036
+ const baselineIntegrityMd = {
12037
+ what: `Recorded baseline hash for '${nodePath}' does not match a recompute over its files, identity, and verdicts.`,
12038
+ why: "The drift-state was edited or is stale (a stored verdict may have been tampered, or the baseline predates a hash-scheme change). The recorded hash can no longer be trusted.",
12039
+ next: `yg approve --node ${nodePath} to re-establish the baseline, or restore it from git: git checkout HEAD -- .yggdrasil/.drift-state/${nodePath}.json`
12040
+ };
12041
+ issues.push({
12042
+ severity: "error",
12043
+ code: "baseline-integrity",
12044
+ rule: "baseline-integrity",
12045
+ messageData: baselineIntegrityMd,
12046
+ nodePath
12047
+ });
12048
+ return;
12049
+ }
11794
12050
  if (directChanges.length > 0) {
11795
12051
  const sourceFiles = directChanges.filter((f) => f.category === "source").map((f) => f.filePath);
11796
12052
  const sourceDriftMd = {
@@ -11842,7 +12098,7 @@ Verify source compliance, update if needed, then: yg approve --node ${nodePath}`
11842
12098
  async function classifyLogState(graph, projectRoot, issues) {
11843
12099
  for (const [nodePath] of graph.nodes) {
11844
12100
  const logRel = `.yggdrasil/model/${nodePath}/log.md`;
11845
- const logAbs = path33.join(projectRoot, logRel);
12101
+ const logAbs = path34.join(projectRoot, logRel);
11846
12102
  let logContent = null;
11847
12103
  try {
11848
12104
  logContent = await readTextFile(logAbs);
@@ -11896,10 +12152,11 @@ function scanUncoveredFiles(graph, gitTrackedFiles) {
11896
12152
  const paths = normalizeMappingPaths(node.meta.mapping);
11897
12153
  allMappings.push(...paths);
11898
12154
  }
11899
- const projectRoot = path33.dirname(graph.rootPath);
11900
- const yggPrefix = toPosixPath(path33.relative(projectRoot, graph.rootPath));
12155
+ const projectRoot = path34.dirname(graph.rootPath);
12156
+ const yggPrefix = toPosixPath(path34.relative(projectRoot, graph.rootPath));
11901
12157
  const uncovered = [];
11902
- for (const file of gitTrackedFiles) {
12158
+ const tracked = excludeNestedGraphSubtrees(gitTrackedFiles);
12159
+ for (const file of tracked) {
11903
12160
  const normalized = toPosixPath(file.trim());
11904
12161
  if (normalized.startsWith(yggPrefix + "/") || normalized === yggPrefix) continue;
11905
12162
  let covered = false;
@@ -11916,42 +12173,6 @@ function scanUncoveredFiles(graph, gitTrackedFiles) {
11916
12173
  }
11917
12174
  return uncovered.sort();
11918
12175
  }
11919
- function buildCoverageIssue(uncoveredFiles, totalGitFiles) {
11920
- if (uncoveredFiles.length === 0) return null;
11921
- const sampleSize = 5;
11922
- const sample = uncoveredFiles.slice(0, sampleSize);
11923
- const remaining = uncoveredFiles.length - sample.length;
11924
- const coveragePct = totalGitFiles > 0 ? (totalGitFiles - uncoveredFiles.length) / totalGitFiles * 100 : 100;
11925
- let coverageMd;
11926
- if (uncoveredFiles.length <= sampleSize) {
11927
- coverageMd = {
11928
- what: `${uncoveredFiles.length} source file${uncoveredFiles.length === 1 ? "" : "s"} not covered by any node.
11929
- ${sample.map((f) => " " + f).join("\n")}`,
11930
- why: "Files without graph coverage cannot be modified under the protocol.",
11931
- next: `Check ownership candidates: yg context --file <path>
11932
- Then: add to existing node mapping, or create a new node.`
11933
- };
11934
- } else {
11935
- const guidance = coveragePct < 50 ? "Establish coverage: create nodes for active areas first, expand coverage incrementally." : "Add to an existing node mapping, or create a new node.";
11936
- coverageMd = {
11937
- what: `${uncoveredFiles.length} source files have no graph coverage.
11938
- Examples:
11939
- ${sample.map((f) => " " + f).join("\n")}
11940
- ... and ${remaining} more`,
11941
- why: "Files without graph coverage cannot be modified under the protocol.",
11942
- next: `${guidance}
11943
- Check ownership candidates: yg context --file <path>`
11944
- };
11945
- }
11946
- return {
11947
- severity: "error",
11948
- code: "unmapped-files",
11949
- rule: "unmapped-file",
11950
- messageData: coverageMd,
11951
- uncoveredFiles,
11952
- uncoveredCount: uncoveredFiles.length
11953
- };
11954
- }
11955
12176
  async function detectOrphanedDriftState(graph) {
11956
12177
  const driftState = await readDriftState(graph.rootPath);
11957
12178
  const validNodePaths = new Set(graph.nodes.keys());
@@ -11961,20 +12182,25 @@ async function runCheck(graph, gitTrackedFiles) {
11961
12182
  const validation = await validate(graph);
11962
12183
  const validationIssues = validation.issues.filter((vi) => vi.code).map((vi) => ({ ...vi, code: vi.code }));
11963
12184
  const driftIssues = await classifyDrift(graph);
11964
- let coverageIssue = null;
12185
+ let coverageIssues = [];
11965
12186
  let coveredFiles = 0;
11966
12187
  let totalFiles = 0;
11967
12188
  if (gitTrackedFiles !== null) {
11968
- const projectRoot = path33.dirname(graph.rootPath);
11969
- const yggPrefix = toPosixPath(path33.relative(projectRoot, graph.rootPath));
11970
- const sourceFiles = gitTrackedFiles.filter((f) => {
12189
+ const projectRoot = path34.dirname(graph.rootPath);
12190
+ const yggPrefix = toPosixPath(path34.relative(projectRoot, graph.rootPath));
12191
+ const sourceFiles = excludeNestedGraphSubtrees(gitTrackedFiles).filter((f) => {
11971
12192
  const normalized = toPosixPath(f.trim());
11972
12193
  return !normalized.startsWith(yggPrefix + "/") && normalized !== yggPrefix;
11973
12194
  });
11974
12195
  totalFiles = sourceFiles.length;
11975
12196
  const uncovered = scanUncoveredFiles(graph, gitTrackedFiles);
11976
- coveredFiles = totalFiles - uncovered.length;
11977
- coverageIssue = buildCoverageIssue(uncovered, totalFiles);
12197
+ const coverage = graph.config.coverage ?? DEFAULT_COVERAGE;
12198
+ const tiers = partitionByCoverageTier(uncovered, coverage);
12199
+ coveredFiles = totalFiles - (tiers.required.length + tiers.middle.length);
12200
+ coverageIssues = [
12201
+ buildCoverageIssue(tiers.required, totalFiles),
12202
+ buildCoverageAdvisoryIssue(tiers.middle)
12203
+ ].filter((x) => x !== null);
11978
12204
  }
11979
12205
  const orphanedPaths = await detectOrphanedDriftState(graph);
11980
12206
  await garbageCollectDriftState(
@@ -11985,7 +12211,7 @@ async function runCheck(graph, gitTrackedFiles) {
11985
12211
  return n ? hasNonDraftEffectiveAspects(n, graph) : false;
11986
12212
  }
11987
12213
  );
11988
- const yggRelative = toPosixPath(path33.relative(path33.dirname(graph.rootPath), graph.rootPath));
12214
+ const yggRelative = toPosixPath(path34.relative(path34.dirname(graph.rootPath), graph.rootPath));
11989
12215
  const orphanWarnings = orphanedPaths.map((p2) => {
11990
12216
  const orphanMd = {
11991
12217
  what: `Drift state file exists for '${p2}' but node is no longer in the graph.`,
@@ -12003,7 +12229,7 @@ async function runCheck(graph, gitTrackedFiles) {
12003
12229
  const allIssues = [
12004
12230
  ...driftIssues,
12005
12231
  ...validationIssues,
12006
- ...coverageIssue ? [coverageIssue] : [],
12232
+ ...coverageIssues,
12007
12233
  ...orphanWarnings
12008
12234
  ];
12009
12235
  const nodeTypeCounts = /* @__PURE__ */ new Map();
@@ -12015,7 +12241,7 @@ async function runCheck(graph, gitTrackedFiles) {
12015
12241
  const advisoryWarnings = allIssues.filter((i) => i.code === "aspect-violation-advisory").length;
12016
12242
  const draftSkipped = countDraftAspectsAcrossGraph(graph);
12017
12243
  return {
12018
- projectName: path33.basename(path33.dirname(graph.rootPath)),
12244
+ projectName: path34.basename(path34.dirname(graph.rootPath)),
12019
12245
  nodeCount: graph.nodes.size,
12020
12246
  nodeTypeCounts,
12021
12247
  aspectCount: graph.aspects.length,
@@ -12033,6 +12259,7 @@ function emitPerAspectIssues(node, graph, baseline, issues) {
12033
12259
  const storedVerdicts = baseline.aspectVerdicts;
12034
12260
  for (const [aspectId, status] of statuses) {
12035
12261
  if (status === "draft") continue;
12262
+ if (isAggregateAspect(graph, aspectId)) continue;
12036
12263
  const verdict = storedVerdicts[aspectId];
12037
12264
  if (!verdict) {
12038
12265
  const md = aspectNewlyActiveMessage({
@@ -12106,7 +12333,7 @@ function getChildMappingExclusions2(graph, nodePath) {
12106
12333
  async function allPathsMissing(projectRoot, mappingPaths) {
12107
12334
  for (const mp of mappingPaths) {
12108
12335
  try {
12109
- await fileAccess(path33.join(projectRoot, mp));
12336
+ await fileAccess(path34.join(projectRoot, mp));
12110
12337
  return false;
12111
12338
  } catch {
12112
12339
  }
@@ -12115,7 +12342,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
12115
12342
  }
12116
12343
  function groupCascadeByCause(cascadeErrors, graph) {
12117
12344
  const groups = /* @__PURE__ */ new Map();
12118
- const yggPrefix = graph ? toPosixPath(path33.relative(path33.dirname(graph.rootPath), graph.rootPath)) : ".yggdrasil";
12345
+ const yggPrefix = graph ? toPosixPath(path34.relative(path34.dirname(graph.rootPath), graph.rootPath)) : ".yggdrasil";
12119
12346
  const escPrefix = yggPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12120
12347
  for (const issue of cascadeErrors) {
12121
12348
  if (!issue.nodePath || !issue.cascadeCauses) continue;
@@ -12534,7 +12761,7 @@ async function runDryRunForNode(params) {
12534
12761
  const { buildPrompt: buildPrompt2 } = await Promise.resolve().then(() => (init_aspect_verifier(), aspect_verifier_exports));
12535
12762
  const aspects = resolveAspects(node, graph);
12536
12763
  const statuses = computeEffectiveAspectStatuses(node, graph);
12537
- const projectRoot = path34.dirname(graph.rootPath);
12764
+ const projectRoot = path35.dirname(graph.rootPath);
12538
12765
  const { trackedFiles } = collectTrackedFiles(node, graph);
12539
12766
  const { fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, void 0, []);
12540
12767
  const sourceFilePaths = Object.keys(fileHashes).filter((f) => {
@@ -12565,7 +12792,7 @@ async function runDryRunForNode(params) {
12565
12792
  }
12566
12793
  try {
12567
12794
  const structResult = await runStructureAspect({
12568
- aspectDir: path34.join(".yggdrasil/aspects", aspect.id),
12795
+ aspectDir: path35.join(".yggdrasil/aspects", aspect.id),
12569
12796
  aspectId: aspect.id,
12570
12797
  nodePath,
12571
12798
  graph,
@@ -12754,7 +12981,7 @@ function registerApproveCommand(program2) {
12754
12981
  }
12755
12982
  const graph = await loadGraphOrAbort(process.cwd());
12756
12983
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
12757
- const yggPrefix = toPosixPath(path34.relative(path34.dirname(graph.rootPath), graph.rootPath));
12984
+ const yggPrefix = toPosixPath(path35.relative(path35.dirname(graph.rootPath), graph.rootPath));
12758
12985
  if (options.dryRun && options.node) {
12759
12986
  let allFound = true;
12760
12987
  for (const rawPath of options.node) {
@@ -12883,11 +13110,11 @@ function registerTreeCommand(program2) {
12883
13110
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
12884
13111
  let roots;
12885
13112
  if (options.root?.trim()) {
12886
- const path44 = options.root.trim().replace(/\/$/, "");
12887
- const node = graph.nodes.get(path44);
13113
+ const path46 = options.root.trim().replace(/\/$/, "");
13114
+ const node = graph.nodes.get(path46);
12888
13115
  if (!node) {
12889
13116
  process.stderr.write(chalk5.red(buildIssueMessage({
12890
- what: `Node '${path44}' not found.`,
13117
+ what: `Node '${path46}' not found.`,
12891
13118
  why: `The --root path must be a valid node path in the graph.`,
12892
13119
  next: `Run yg tree (no --root) to list all nodes, then pick a valid path.`
12893
13120
  }) + "\n"));
@@ -12987,14 +13214,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
12987
13214
  }
12988
13215
  const chains = [];
12989
13216
  for (const node of transitiveOnly) {
12990
- const path44 = [];
13217
+ const path46 = [];
12991
13218
  let current = node;
12992
13219
  while (current) {
12993
- path44.unshift(current);
13220
+ path46.unshift(current);
12994
13221
  current = parent.get(current);
12995
13222
  }
12996
- if (path44.length >= 3) {
12997
- chains.push(path44.slice(1).map((p2) => `<- ${p2}`).join(" "));
13223
+ if (path46.length >= 3) {
13224
+ chains.push(path46.slice(1).map((p2) => `<- ${p2}`).join(" "));
12998
13225
  }
12999
13226
  }
13000
13227
  return chains.sort();
@@ -13026,14 +13253,14 @@ function collectIndirectDependents(graph, directlyAffected) {
13026
13253
  }
13027
13254
  for (const [node] of parent) {
13028
13255
  if (directSet.has(node)) continue;
13029
- const path44 = [node];
13256
+ const path46 = [node];
13030
13257
  let current = node;
13031
13258
  while (parent.has(current)) {
13032
13259
  current = parent.get(current);
13033
- path44.push(current);
13260
+ path46.push(current);
13034
13261
  }
13035
- const chain = path44.map((p2) => `<- ${p2}`).join(" ");
13036
- const depth = path44.length;
13262
+ const chain = path46.map((p2) => `<- ${p2}`).join(" ");
13263
+ const depth = path46.length;
13037
13264
  const existing = bestChain.get(node);
13038
13265
  if (!existing || depth < existing.depth) {
13039
13266
  bestChain.set(node, { chain, depth });
@@ -13773,7 +14000,7 @@ import chalk8 from "chalk";
13773
14000
  init_debug_log();
13774
14001
  init_message_builder();
13775
14002
  import { execFileSync } from "child_process";
13776
- import path35 from "path";
14003
+ import path36 from "path";
13777
14004
  function registerCheckCommand(program2) {
13778
14005
  program2.command("check").description("Unified graph gate \u2014 errors, drift, coverage, completeness").action(async () => {
13779
14006
  try {
@@ -13782,7 +14009,7 @@ function registerCheckCommand(program2) {
13782
14009
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
13783
14010
  let gitFiles = null;
13784
14011
  try {
13785
- const projectRoot = path35.dirname(graph.rootPath);
14012
+ const projectRoot = path36.dirname(graph.rootPath);
13786
14013
  const output = execFileSync("git", ["ls-files", "."], {
13787
14014
  cwd: projectRoot,
13788
14015
  encoding: "utf-8",
@@ -13903,10 +14130,16 @@ function renderErrorSection(errors) {
13903
14130
  }
13904
14131
  function renderWarningSection(warnings) {
13905
14132
  const lines = [chalk8.yellow(`Warnings (${warnings.length}):`)];
13906
- for (const issue of sortByNodePath(warnings)) {
14133
+ const coverage = warnings.filter((i) => i.code === "uncovered-advisory");
14134
+ const rest = warnings.filter((i) => i.code !== "uncovered-advisory");
14135
+ for (const issue of sortByNodePath(rest)) {
13907
14136
  lines.push("");
13908
14137
  renderIssueBlock(issue, lines, "warning");
13909
14138
  }
14139
+ for (const issue of coverage) {
14140
+ lines.push("");
14141
+ renderUnmappedBlock(issue, lines, "uncovered");
14142
+ }
13910
14143
  return lines.join("\n");
13911
14144
  }
13912
14145
  function renderIssueBlock(issue, lines, mode) {
@@ -13924,14 +14157,12 @@ function renderIssueBlock(issue, lines, mode) {
13924
14157
  lines.push(` Fix: ${md.next}${fixSuffix}`);
13925
14158
  }
13926
14159
  }
13927
- function renderUnmappedBlock(issue, lines) {
14160
+ function renderUnmappedBlock(issue, lines, label = "unmapped") {
13928
14161
  const md = issue.messageData;
13929
14162
  const files = issue.uncoveredFiles ?? [];
13930
- const whatFirstLine = md.what.split("\n")[0];
13931
- const countMatch = whatFirstLine.match(/^(\d[\d,]*)/);
13932
14163
  const count = issue.uncoveredCount ?? files.length;
13933
- const countLabel = countMatch ? countMatch[1] : String(count);
13934
- lines.push(` unmapped (${countLabel})`);
14164
+ const countLabel = String(count);
14165
+ lines.push(` ${label} (${countLabel})`);
13935
14166
  const shown = files.slice(0, 10);
13936
14167
  for (const f of shown) {
13937
14168
  lines.push(` ${f}`);
@@ -14010,7 +14241,7 @@ function sortByNodePath(issues) {
14010
14241
  }
14011
14242
 
14012
14243
  // src/cli/deterministic-test.ts
14013
- import path37 from "path";
14244
+ import path38 from "path";
14014
14245
  init_debug_log();
14015
14246
 
14016
14247
  // src/ast/runner.ts
@@ -14018,7 +14249,7 @@ init_loader_hook();
14018
14249
  init_parser();
14019
14250
  init_suppress();
14020
14251
  init_validate_check_module();
14021
- import path36 from "path";
14252
+ import path37 from "path";
14022
14253
  import { readFile as readFile20 } from "fs/promises";
14023
14254
  import { pathToFileURL as pathToFileURL3 } from "url";
14024
14255
  var AstRunnerError = class extends Error {
@@ -14034,7 +14265,7 @@ ${data.next}`);
14034
14265
  };
14035
14266
  async function runAstAspect(params) {
14036
14267
  ensureLoaderRegistered();
14037
- const checkPath = path36.resolve(params.projectRoot, params.aspectDir, "check.mjs");
14268
+ const checkPath = path37.resolve(params.projectRoot, params.aspectDir, "check.mjs");
14038
14269
  let mod;
14039
14270
  try {
14040
14271
  mod = await import(pathToFileURL3(checkPath).href);
@@ -14064,7 +14295,7 @@ async function runAstAspect(params) {
14064
14295
  sourceFiles.push({ path: f.path, content: cached.content, ast: cached.ast });
14065
14296
  continue;
14066
14297
  }
14067
- const content14 = await readFile20(path36.resolve(params.projectRoot, f.path), "utf-8");
14298
+ const content14 = await readFile20(path37.resolve(params.projectRoot, f.path), "utf-8");
14068
14299
  let ast;
14069
14300
  try {
14070
14301
  ast = await parseFile(f.path, content14);
@@ -14196,7 +14427,7 @@ function registerDeterministicTestCommand(program2) {
14196
14427
  process.exit(1);
14197
14428
  return;
14198
14429
  }
14199
- const aspectDir = path37.join(".yggdrasil", "aspects", aspect.id);
14430
+ const aspectDir = path38.join(".yggdrasil", "aspects", aspect.id);
14200
14431
  if (hasNode) {
14201
14432
  const nodePath = opts.node.trim().replace(/\/$/, "");
14202
14433
  const node = graph.nodes.get(nodePath);
@@ -14324,12 +14555,12 @@ function printStructureViolations(violations) {
14324
14555
  // src/cli/log.ts
14325
14556
  import chalk9 from "chalk";
14326
14557
  import { readFile as readFile22, stat as stat8 } from "fs/promises";
14327
- import path41 from "path";
14558
+ import path42 from "path";
14328
14559
  init_debug_log();
14329
14560
  init_message_builder();
14330
14561
 
14331
14562
  // src/core/log/log-add.ts
14332
- import path38 from "path";
14563
+ import path39 from "path";
14333
14564
 
14334
14565
  // src/utils/node-path-validator.ts
14335
14566
  init_posix();
@@ -14450,7 +14681,7 @@ async function logAdd(input) {
14450
14681
  }
14451
14682
  };
14452
14683
  }
14453
- const logPath2 = path38.join(graph.rootPath, "model", nodePath, "log.md");
14684
+ const logPath2 = path39.join(graph.rootPath, "model", nodePath, "log.md");
14454
14685
  const stats = await statLogFile(logPath2);
14455
14686
  if (stats !== null) {
14456
14687
  if (stats.isSymbolicLink) {
@@ -14519,7 +14750,7 @@ function reasonHasLevel2HeaderOutsideFence(reason) {
14519
14750
  }
14520
14751
 
14521
14752
  // src/core/log/log-read.ts
14522
- import path39 from "path";
14753
+ import path40 from "path";
14523
14754
  init_log_parser();
14524
14755
  init_log_format();
14525
14756
  init_posix();
@@ -14568,7 +14799,7 @@ async function logRead(input) {
14568
14799
  }
14569
14800
  };
14570
14801
  }
14571
- const logPath2 = path39.join(graph.rootPath, "model", nodePath, "log.md");
14802
+ const logPath2 = path40.join(graph.rootPath, "model", nodePath, "log.md");
14572
14803
  const content14 = await readLogSafe(logPath2);
14573
14804
  if (content14 === "") {
14574
14805
  return { ok: true, entries: [] };
@@ -14592,7 +14823,7 @@ async function logRead(input) {
14592
14823
 
14593
14824
  // src/core/log/log-merge-resolve.ts
14594
14825
  import { createHash as createHash5 } from "crypto";
14595
- import path40 from "path";
14826
+ import path41 from "path";
14596
14827
  init_log_parser();
14597
14828
 
14598
14829
  // src/utils/git-introspect.ts
@@ -14679,7 +14910,7 @@ async function logMergeResolve(input) {
14679
14910
  }
14680
14911
  };
14681
14912
  }
14682
- const logPath2 = path40.join(yggRoot, "model", nodePath, "log.md");
14913
+ const logPath2 = path41.join(yggRoot, "model", nodePath, "log.md");
14683
14914
  let currentLog;
14684
14915
  try {
14685
14916
  currentLog = await readTextFile(logPath2);
@@ -14885,7 +15116,7 @@ ${entry.body}`);
14885
15116
  log2.command("merge-resolve").description("Reconcile log.md after a git merge (HEAD must be a merge commit)").requiredOption("--node <path>", "Node path (relative to .yggdrasil/model/)").action(async (opts) => {
14886
15117
  try {
14887
15118
  const graph = await loadGraphOrAbort(process.cwd(), { tolerateInvalidConfig: true });
14888
- const repoRoot = path41.dirname(graph.rootPath);
15119
+ const repoRoot = path42.dirname(graph.rootPath);
14889
15120
  const nodePath = toPosix(opts.node.trim()).replace(/\/$/, "");
14890
15121
  const result = await logMergeResolve({ graph, nodePath, repoRoot });
14891
15122
  if (!result.ok) {
@@ -14912,15 +15143,15 @@ import chalk10 from "chalk";
14912
15143
  // src/io/find-index.ts
14913
15144
  init_debug_log();
14914
15145
  import { readFile as readFile23, lstat as lstat3 } from "fs/promises";
14915
- import path42 from "path";
15146
+ import path43 from "path";
14916
15147
  import MiniSearch from "minisearch";
14917
15148
  var MAX_BODY_BYTES = 1048576;
14918
15149
  async function buildIndex(graph) {
14919
- const projectRoot = path42.dirname(graph.rootPath);
15150
+ const projectRoot = path43.dirname(graph.rootPath);
14920
15151
  const docs = [];
14921
15152
  for (const [nodePath, node] of graph.nodes) {
14922
15153
  const displayPath = `model/${nodePath}/`;
14923
- const logPath2 = path42.join(graph.rootPath, "model", nodePath, "log.md");
15154
+ const logPath2 = path43.join(graph.rootPath, "model", nodePath, "log.md");
14924
15155
  let body = "";
14925
15156
  try {
14926
15157
  const st = await lstat3(logPath2);
@@ -14937,7 +15168,7 @@ This does not affect append-only integrity. No action required.
14937
15168
  }
14938
15169
  body = truncated;
14939
15170
  } else if (st.isSymbolicLink()) {
14940
- process.stderr.write(`Warning: skipping symlinked log.md at ${path42.relative(projectRoot, logPath2)}
15171
+ process.stderr.write(`Warning: skipping symlinked log.md at ${path43.relative(projectRoot, logPath2)}
14941
15172
  `);
14942
15173
  } else {
14943
15174
  }
@@ -15094,14 +15325,14 @@ import { existsSync as existsSync6 } from "fs";
15094
15325
  import { resolve as resolve5 } from "path";
15095
15326
 
15096
15327
  // src/core/type-classifier.ts
15097
- import path43 from "path";
15328
+ import path44 from "path";
15098
15329
  async function classifyFile(absPath, repoRelPath, graph, cache) {
15099
15330
  const matches = [];
15100
15331
  const partialScores = [];
15101
15332
  const ctx = {
15102
15333
  absPath,
15103
15334
  repoRelPath,
15104
- projectRoot: path43.dirname(graph.rootPath),
15335
+ projectRoot: path44.dirname(graph.rootPath),
15105
15336
  cache
15106
15337
  };
15107
15338
  for (const [typeId, def] of Object.entries(graph.architecture.node_types)) {
@@ -15389,7 +15620,7 @@ See: \`yg knowledge read aspect-status\`.
15389
15620
  `;
15390
15621
 
15391
15622
  // src/templates/knowledge/aspects-overview.ts
15392
- var summary2 = "What aspects are, when to create, LLM vs deterministic reviewer choice, cost model";
15623
+ var summary2 = "What aspects are, when to create, LLM vs deterministic vs aggregating reviewer choice, cost model";
15393
15624
  var content2 = `# Aspects overview
15394
15625
 
15395
15626
  Aspects are enforceable rules attached to nodes. A reviewer (LLM or
@@ -15428,12 +15659,33 @@ While the rule is still being authored or is unclear, give the aspect
15428
15659
  \`status: draft\` \u2014 a draft aspect is WIP, so the reviewer never runs on it
15429
15660
  and it costs zero.
15430
15661
 
15431
- ## LLM vs deterministic
15662
+ ## Three reviewer kinds
15663
+
15664
+ Three reviewer kinds exist: LLM, deterministic, and aggregating. The kind is
15665
+ **inferred** from which rule source file is present in the aspect directory:
15666
+ \`content.md\` \u2192 LLM; \`check.mjs\` \u2192 deterministic; neither file but \`implies:\`
15667
+ declared \u2192 aggregating. The \`reviewer:\` block in \`yg-aspect.yaml\` is optional;
15668
+ if present, an explicit \`reviewer.type\` must agree with the inferred kind.
15669
+
15670
+ ### Aggregating aspects
15671
+
15672
+ An aggregating aspect ships neither \`content.md\` nor \`check.mjs\`. It exists
15673
+ purely to bundle other aspects under one named attach point. When an aggregating
15674
+ aspect is effective on a node, all aspects in its \`implies:\` list are expanded
15675
+ and verified individually. The aggregate itself has no own reviewer and produces
15676
+ no own verdict. It never dispatches to an LLM and never runs \`check.mjs\`.
15432
15677
 
15433
- Two reviewer types exist: LLM and deterministic. The deterministic reviewer runs
15434
- \`check.mjs\` locally \u2014 it covers both per-file syntactic rules
15435
- (single-file style) and cross-node graph-shape rules (graph-aware style), all in
15436
- one reviewer. LLM and deterministic each have a distinct sweet spot.
15678
+ Use aggregating aspects to decompose a multi-rule contract: attach the aggregate
15679
+ once (per node, per flow, per architecture type) and let each implied child carry
15680
+ one concrete, independently-verdicted rule. An aspect with neither rule source
15681
+ and no \`implies:\` is rejected by the validator.
15682
+
15683
+ ### LLM and deterministic sweet spots
15684
+
15685
+ The deterministic reviewer runs \`check.mjs\` locally \u2014 it covers both per-file
15686
+ syntactic rules (single-file style) and cross-node graph-shape rules
15687
+ (graph-aware style), all in one reviewer. LLM and deterministic each have a
15688
+ distinct sweet spot.
15437
15689
 
15438
15690
  \`check.mjs\` runs in the main Node process with full privileges \u2014 there is no
15439
15691
  security sandbox. The graph-aware allow-list (below) is a read *discipline* that
@@ -15500,10 +15752,12 @@ To author a \`check.mjs\` (both single-file and graph-aware styles):
15500
15752
  ## Cost model
15501
15753
 
15502
15754
  Every effective non-draft LLM aspect on a node = at least one reviewer call
15503
- during \`yg approve\`, multiplied by the tier's consensus count AND by the
15504
- number of prompt chunks. Deterministic aspects run locally at zero LLM
15505
- cost. A node with 5 LLM aspects = at least 5 reviewer calls. An LLM aspect
15506
- touching 20 nodes = at least 20 calls when you run \`yg approve --aspect <id>\`.
15755
+ during \`yg approve\`, multiplied by the tier's consensus count. The reviewer
15756
+ always sends the full node in a single prompt \u2014 there is no chunking.
15757
+ Deterministic aspects run locally at zero LLM cost. Aggregating aspects have no
15758
+ own reviewer call. A node with 5 LLM aspects = at least 5 reviewer calls. An LLM
15759
+ aspect touching 20 nodes = at least 20 calls when you run
15760
+ \`yg approve --aspect <id>\`.
15507
15761
 
15508
15762
  Use \`yg impact --aspect <id>\` before creating or modifying a widely-used
15509
15763
  aspect to assess the re-approval cost.
@@ -15837,9 +16091,9 @@ The runner raises typed runtime errors when the contract is broken:
15837
16091
 
15838
16092
  ## Iterating over the files
15839
16093
 
15840
- Today every mapped file (TypeScript/JavaScript family) arrives in a single
15841
- \`check.mjs\` invocation via \`ctx.files\`. Iterate the array and inspect each
15842
- file's AST:
16094
+ A node's mapping may include non-parseable files (e.g. \`.md\`, \`.sh\`,
16095
+ \`.json\`). For those files \`file.ast\` is \`undefined\`. **Always guard
16096
+ before touching \`file.ast\`**:
15843
16097
 
15844
16098
  \`\`\`javascript
15845
16099
  import { walk, report, inFile, closest } from '@chrisdudek/yg/ast';
@@ -15847,6 +16101,7 @@ import { walk, report, inFile, closest } from '@chrisdudek/yg/ast';
15847
16101
  export function check(ctx) {
15848
16102
  const violations = [];
15849
16103
  for (const file of ctx.files) {
16104
+ if (!file.ast) continue; // skip non-parseable files (no tree-sitter AST)
15850
16105
  walk(file.ast.rootNode, node => {
15851
16106
  // ... inspect node, push report(file, node, ...) on a hit ...
15852
16107
  });
@@ -15855,6 +16110,10 @@ export function check(ctx) {
15855
16110
  }
15856
16111
  \`\`\`
15857
16112
 
16113
+ Content/regex checks that only use \`file.content\` (and never touch
16114
+ \`file.ast\`) do **not** need this guard \u2014 they should iterate all files
16115
+ including non-parseable ones.
16116
+
15858
16117
  If a rule should apply only to a subset of files, filter on \`file.path\`
15859
16118
  (for example with \`inFile(file, { glob: 'src/api/**' })\`) \u2014 there is no
15860
16119
  \`ctx.language\` and no per-language invocation today; every mapped file
@@ -15886,13 +16145,19 @@ Each \`node\` object from the AST exposes:
15886
16145
  ### Reading node-types.json
15887
16146
 
15888
16147
  To learn the grammar's node types and field names, inspect the
15889
- \`node-types.json\` shipped by the tree-sitter grammar package:
16148
+ \`node-types.json\` files shipped inside the installed package under
16149
+ \`dist/grammars/\`. Each file is named after its grammar:
15890
16150
 
15891
16151
  \`\`\`
15892
- node_modules/tree-sitter-typescript/typescript/node-types.json
15893
- node_modules/tree-sitter-javascript/node-types.json
16152
+ <yg install>/dist/grammars/tree-sitter-typescript.node-types.json
16153
+ <yg install>/dist/grammars/tree-sitter-tsx.node-types.json
16154
+ <yg install>/dist/grammars/tree-sitter-javascript.node-types.json
16155
+ <yg install>/dist/grammars/tree-sitter-python.node-types.json
15894
16156
  \`\`\`
15895
16157
 
16158
+ (and so on for every other shipped grammar \u2014 one \`.node-types.json\`
16159
+ per \`.wasm\` file in the same directory.)
16160
+
15896
16161
  Each entry lists its \`type\`, whether it is \`named\`, and the \`fields\`
15897
16162
  object whose keys are the field names usable with \`childForFieldName\`.
15898
16163
 
@@ -15904,6 +16169,7 @@ import { walk, report, inFile } from '@chrisdudek/yg/ast';
15904
16169
  export function check(ctx) {
15905
16170
  const violations = [];
15906
16171
  for (const file of ctx.files) {
16172
+ if (!file.ast) continue; // skip non-parseable files (no tree-sitter AST)
15907
16173
  if (!inFile(file, { glob: 'src/api/**' })) continue;
15908
16174
  walk(file.ast.rootNode, node => {
15909
16175
  if (node.type !== 'import_statement') return;
@@ -16746,8 +17012,13 @@ files, a typed \`identity\` block holding the node's upstream identity (its own
16746
17012
  aspect-relevant metadata, a per-aspect identity for every effective aspect, and
16747
17013
  per-dependency port-aspect hashes), and a per-aspect verdict map recording the
16748
17014
  reviewer's last judgment for each non-draft aspect. The canonical drift hash is
16749
- computed over the real-file hashes together with that typed identity, so any
16750
- upstream identity change cascades exactly like a source change.
17015
+ computed over the real-file hashes, that typed identity, AND the per-aspect
17016
+ verdict map, so any upstream identity change cascades exactly like a source
17017
+ change \u2014 and a hand-edited verdict in \`.drift-state/*.json\` (e.g. flipping a
17018
+ committed \`refused\` to \`approved\`) changes the recomputed hash too. \`yg check\`
17019
+ blocks on any such divergence it cannot attribute to a file or identity change,
17020
+ reporting a \`baseline-integrity\` error; the fix is to re-approve the node (or
17021
+ restore the drift-state from git).
16751
17022
 
16752
17023
  Aspect \`status\` is deliberately NOT part of the identity: flipping an aspect
16753
17024
  between \`advisory\` and \`enforced\` does not drift the node (the recorded verdict
@@ -16886,11 +17157,15 @@ reviewer:
16886
17157
  model: qwen3 # Model identifier for this provider
16887
17158
  temperature: 0 # Sampling temperature (0 = deterministic)
16888
17159
  endpoint: http://localhost:11434 # Required for ollama and openai-compatible
16889
- max_tokens: auto # Response budget: 'auto' or positive integer
16890
17160
  # references: # optional caps on aspect reference files
16891
17161
  # max_bytes_per_file: 65536 # default: 64 KiB per reference file
16892
17162
  # max_total_bytes_per_aspect: 262144 # default: 256 KiB total per aspect
16893
17163
 
17164
+ coverage: # Optional \u2014 controls which files must be mapped
17165
+ required: # Unmapped files under these roots are a blocking error
17166
+ - "/" # Default: whole repo (previous always-map-everything behavior)
17167
+ excluded: [] # Files under these roots are silently ignored
17168
+
16894
17169
  quality:
16895
17170
  max_direct_relations: 10 # Max out-edges per node before high-fan-out warning
16896
17171
  max_node_chars: 40000 # Per-node character budget (source + aspect refs) before oversized-node error
@@ -16961,8 +17236,6 @@ Provider-specific options passed to the LLM client:
16961
17236
  | \`model\` | string | Required. Provider-specific model identifier. |
16962
17237
  | \`temperature\` | number | Defaults to 0. Higher = more varied responses. |
16963
17238
  | \`endpoint\` | string | Required for \`ollama\` and \`openai-compatible\`. |
16964
- | \`max_tokens\` | number\\|'auto' | Response budget. 'auto' queries the provider. |
16965
- | \`context_length_field\` | string | Ollama: field name in model info for context length. |
16966
17239
  | \`timeout\` | number | Timeout in seconds. Default 300. Applies to CLI providers only \u2014 non-CLI/API providers ignore it. |
16967
17240
 
16968
17241
  API keys do NOT live here \u2014 they belong in \`yg-secrets.yaml\` (api_key only).
@@ -17028,6 +17301,24 @@ the provider's standard \`*_API_KEY\`) as a fallback when not present in
17028
17301
  \`yg-config.yaml\` itself must never contain credentials. Commit it to the
17029
17302
  repository \u2014 it is safe to share.
17030
17303
 
17304
+ ## Coverage config
17305
+
17306
+ \`\`\`yaml
17307
+ coverage:
17308
+ required:
17309
+ - src/ # unmapped files under src/ are a blocking error
17310
+ excluded:
17311
+ - vendor/ # silently ignored
17312
+ \`\`\`
17313
+
17314
+ Controls which git-tracked files must be mapped to a node.
17315
+
17316
+ - \`required\` \u2014 path roots where unmapped files are a blocking \`unmapped-files\` error. Default: \`["/"]\` (whole repo \u2014 the previous always-map-everything behavior).
17317
+ - \`excluded\` \u2014 path roots that are silently ignored. Default: \`[]\`.
17318
+ - Files that match neither a required nor an excluded root produce a non-blocking \`uncovered-advisory\` warning.
17319
+ - 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.
17321
+
17031
17322
  ## Quality thresholds
17032
17323
 
17033
17324
  \`\`\`yaml
@@ -17235,6 +17526,24 @@ Use \`--reason-file <path>\` instead of \`--reason\` to supply multi-line entry
17235
17526
  content from a file. On \`yg log read\`, \`--top\` and \`--all\` are mutually
17236
17527
  exclusive \u2014 you cannot combine them.
17237
17528
 
17529
+ ## yg suppressions
17530
+
17531
+ Read-only inventory of all active \`yg-suppress\` markers in the repository's
17532
+ source files. Lists each marker's aspect path, location, reason, and kind
17533
+ (single-line, bracket, or wildcard). Exits 0 always \u2014 it is a read-only
17534
+ inspection tool.
17535
+
17536
+ \`\`\`bash
17537
+ yg suppressions
17538
+ \`\`\`
17539
+
17540
+ Emits non-blocking warnings for:
17541
+ - **Unknown aspect-id** \u2014 the aspect path in the marker does not match any known aspect.
17542
+ - **Wildcard suppress** (\`*\`) \u2014 suppresses all aspects in range; any aspect added later is also silently waived.
17543
+ - **Unbounded range** \u2014 a \`yg-suppress-disable\` marker with no matching \`yg-suppress-enable\`; the suppression extends to end of file.
17544
+
17545
+ Use \`yg suppressions\` to audit accumulated waivers before a release or a new aspect rollout. It does not affect \`yg check\` or any baseline.
17546
+
17238
17547
  ## yg type-suggest
17239
17548
 
17240
17549
  Suggest which node_type a file fits based on architecture \`when\` predicates.
@@ -17910,13 +18219,176 @@ function registerKnowledgeCommand(program2) {
17910
18219
  });
17911
18220
  }
17912
18221
 
18222
+ // src/cli/suppressions.ts
18223
+ import chalk13 from "chalk";
18224
+ import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
18225
+ import { execFileSync as execFileSync2 } from "child_process";
18226
+ import path45 from "path";
18227
+ init_debug_log();
18228
+ init_suppress();
18229
+ init_message_builder();
18230
+ init_posix();
18231
+ function isBinaryContent(buf) {
18232
+ const checkLen = Math.min(buf.length, 8192);
18233
+ for (let i = 0; i < checkLen; i++) {
18234
+ if (buf[i] === 0) return true;
18235
+ }
18236
+ return false;
18237
+ }
18238
+ function isNoiseFile(relFile) {
18239
+ const p2 = toPosixPath(relFile);
18240
+ if (p2 === ".yggdrasil" || p2.startsWith(".yggdrasil/")) return true;
18241
+ if (p2.startsWith(".cursor/")) return true;
18242
+ if (p2.startsWith(".github/copilot")) return true;
18243
+ const base = p2.includes("/") ? p2.slice(p2.lastIndexOf("/") + 1) : p2;
18244
+ if (base === ".windsurfrules" || base === ".clinerules") return true;
18245
+ if (base === "log.md") return true;
18246
+ const lower = base.toLowerCase();
18247
+ if (lower.endsWith(".md") || lower.endsWith(".mdc") || lower.endsWith(".markdown") || lower.endsWith(".txt")) {
18248
+ return true;
18249
+ }
18250
+ return false;
18251
+ }
18252
+ function runSuppressionsScan(projectRoot, gitTrackedFiles, knownAspectIds) {
18253
+ const fileEntries = [];
18254
+ const warnings = [];
18255
+ let totalMarkers = 0;
18256
+ const openDisables = /* @__PURE__ */ new Map();
18257
+ for (const relFile of gitTrackedFiles) {
18258
+ if (isNoiseFile(relFile)) continue;
18259
+ const absFile = path45.join(projectRoot, relFile);
18260
+ if (!existsSync7(absFile)) continue;
18261
+ let buf;
18262
+ try {
18263
+ buf = readFileSync5(absFile);
18264
+ } catch (error) {
18265
+ debugWrite(`[suppressions] read fallback: ${relFile}: ${error instanceof Error ? error.message : String(error)}`);
18266
+ continue;
18267
+ }
18268
+ if (isBinaryContent(buf)) continue;
18269
+ const text2 = buf.toString("utf-8");
18270
+ const markers = scanSuppressionMarkers(text2);
18271
+ if (markers.length === 0) continue;
18272
+ fileEntries.push({ file: toPosixPath(relFile), markers });
18273
+ totalMarkers += markers.length;
18274
+ const disableStack = /* @__PURE__ */ new Map();
18275
+ for (const m of markers) {
18276
+ if (m.kind === "disable") {
18277
+ const stack = disableStack.get(m.aspectId) ?? [];
18278
+ stack.push(m.line);
18279
+ disableStack.set(m.aspectId, stack);
18280
+ } else if (m.kind === "enable") {
18281
+ const stack = disableStack.get(m.aspectId);
18282
+ if (stack && stack.length > 0) {
18283
+ stack.pop();
18284
+ if (stack.length === 0) disableStack.delete(m.aspectId);
18285
+ }
18286
+ }
18287
+ }
18288
+ if (disableStack.size > 0) {
18289
+ openDisables.set(toPosixPath(relFile), disableStack);
18290
+ }
18291
+ }
18292
+ const seenWildcard = /* @__PURE__ */ new Set();
18293
+ for (const { file, markers } of fileEntries) {
18294
+ for (const m of markers) {
18295
+ if (!m.wildcard && !knownAspectIds.has(m.aspectId)) {
18296
+ const msg = buildIssueMessage({
18297
+ what: `Unknown aspect id "${m.aspectId}" in suppress marker at ${file}:${m.line}.`,
18298
+ why: "The aspect does not exist in the graph. The suppression has no effect and likely refers to a renamed or deleted aspect.",
18299
+ next: `Run \`yg aspects\` to list defined aspect ids, then update or remove this marker.`
18300
+ });
18301
+ warnings.push(msg);
18302
+ }
18303
+ if (m.wildcard && !seenWildcard.has(`${file}:${m.line}`)) {
18304
+ seenWildcard.add(`${file}:${m.line}`);
18305
+ const msg = buildIssueMessage({
18306
+ what: `Wildcard suppression "*" at ${file}:${m.line} silences ALL aspects.`,
18307
+ why: "A wildcard suppresses every current and future aspect check on the affected code \u2014 including ones not yet written. This masks problems broadly and is hard to audit.",
18308
+ next: `Replace "*" with the specific aspect id(s) you intend to suppress.`
18309
+ });
18310
+ warnings.push(msg);
18311
+ }
18312
+ }
18313
+ }
18314
+ for (const [file, disableMap] of openDisables) {
18315
+ for (const [aspectId, lines] of disableMap) {
18316
+ for (const lineNum of lines) {
18317
+ const msg = buildIssueMessage({
18318
+ what: `Unbounded yg-suppress-disable("${aspectId}") at ${file}:${lineNum} has no matching yg-suppress-enable.`,
18319
+ why: "Without a closing enable marker the suppression covers the rest of the file, which is almost always broader than intended and hides future violations added below this line.",
18320
+ next: `Add \`yg-suppress-enable(${aspectId})\` at the end of the suppressed block, or convert to a single-line \`yg-suppress(${aspectId}) <reason>\` if only one line needs suppression.`
18321
+ });
18322
+ warnings.push(msg);
18323
+ }
18324
+ }
18325
+ }
18326
+ return { fileEntries, totalMarkers, warnings };
18327
+ }
18328
+ function formatSuppressionsOutput(report) {
18329
+ const lines = [];
18330
+ if (report.fileEntries.length === 0) {
18331
+ lines.push("No active suppression markers found.");
18332
+ return lines.join("\n") + "\n";
18333
+ }
18334
+ lines.push("Active suppression markers:");
18335
+ lines.push("");
18336
+ for (const { file, markers } of report.fileEntries) {
18337
+ lines.push(` ${file}`);
18338
+ for (const m of markers) {
18339
+ const wildcardTag = m.wildcard ? chalk13.yellow(" [wildcard]") : "";
18340
+ const kindTag = m.kind === "single" ? "single" : m.kind === "disable" ? "disable" : "enable";
18341
+ const reasonPart = m.reason ? ` \u2014 ${m.reason}` : "";
18342
+ lines.push(` line ${m.line}: ${kindTag}(${m.aspectId})${wildcardTag}${reasonPart}`);
18343
+ }
18344
+ lines.push("");
18345
+ }
18346
+ const fileCount = report.fileEntries.length;
18347
+ lines.push(`Total: ${report.totalMarkers} marker${report.totalMarkers === 1 ? "" : "s"} across ${fileCount} file${fileCount === 1 ? "" : "s"}.`);
18348
+ if (report.warnings.length > 0) {
18349
+ lines.push("");
18350
+ lines.push(chalk13.yellow(`Warnings (${report.warnings.length}):`));
18351
+ for (const w of report.warnings) {
18352
+ const indented = w.split("\n").map((l) => ` ${l}`).join("\n");
18353
+ lines.push(chalk13.yellow(indented));
18354
+ }
18355
+ }
18356
+ return lines.join("\n") + "\n";
18357
+ }
18358
+ function registerSuppressionsCommand(program2) {
18359
+ program2.command("suppressions").description("Inventory active yg-suppress waivers and warn about footguns").action(async () => {
18360
+ try {
18361
+ const cwd = process.cwd();
18362
+ const graph = await loadGraphOrAbort(cwd);
18363
+ initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
18364
+ const projectRoot = path45.dirname(graph.rootPath);
18365
+ let gitFiles = [];
18366
+ try {
18367
+ const output = execFileSync2("git", ["ls-files", "."], {
18368
+ cwd: projectRoot,
18369
+ encoding: "utf-8",
18370
+ stdio: ["pipe", "pipe", "pipe"]
18371
+ });
18372
+ gitFiles = output.trim().split("\n").filter((f) => f.length > 0);
18373
+ } catch (error) {
18374
+ debugWrite(`[suppressions] git ls-files fallback: ${error instanceof Error ? error.message : String(error)}`);
18375
+ }
18376
+ const knownAspectIds = new Set(graph.aspects.map((a) => a.id));
18377
+ const report = runSuppressionsScan(projectRoot, gitFiles, knownAspectIds);
18378
+ process.stdout.write(formatSuppressionsOutput(report));
18379
+ } catch (error) {
18380
+ abortOnUnexpectedError(error, "scanning suppressions");
18381
+ }
18382
+ });
18383
+ }
18384
+
17913
18385
  // src/bin.ts
17914
- import { readFileSync as readFileSync5 } from "fs";
18386
+ import { readFileSync as readFileSync6 } from "fs";
17915
18387
  import { fileURLToPath as fileURLToPath5 } from "url";
17916
18388
  import { dirname as dirname2, join as join5 } from "path";
17917
18389
  var __filename3 = fileURLToPath5(import.meta.url);
17918
18390
  var __dirname3 = dirname2(__filename3);
17919
- var pkg = JSON.parse(readFileSync5(join5(__dirname3, "..", "package.json"), "utf-8"));
18391
+ var pkg = JSON.parse(readFileSync6(join5(__dirname3, "..", "package.json"), "utf-8"));
17920
18392
  var program = new Command2();
17921
18393
  program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
17922
18394
  registerInitCommand(program);
@@ -17933,6 +18405,7 @@ registerLogCommand(program);
17933
18405
  registerFindCommand(program);
17934
18406
  registerTypeSuggestCommand(program);
17935
18407
  registerKnowledgeCommand(program);
18408
+ registerSuppressionsCommand(program);
17936
18409
  process.on("unhandledRejection", (reason) => {
17937
18410
  const msg = reason instanceof Error ? reason.message : String(reason);
17938
18411
  process.stderr.write(`Error: ${msg}