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

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);
@@ -587,7 +587,7 @@ var init_repo_scanner = __esm({
587
587
 
588
588
  // src/io/hash.ts
589
589
  import { readFile as readFile16, readdir as readdir7, stat as stat5 } from "fs/promises";
590
- import path15 from "path";
590
+ import path16 from "path";
591
591
  import { createHash } from "crypto";
592
592
  import { createRequire as createRequire2 } from "module";
593
593
  async function hashFile(filePath) {
@@ -597,7 +597,7 @@ async function hashFile(filePath) {
597
597
  async function loadRootGitignoreStack2(projectRoot) {
598
598
  if (!projectRoot) return [];
599
599
  try {
600
- const content14 = await readFile16(path15.join(projectRoot, ".gitignore"), "utf-8");
600
+ const content14 = await readFile16(path16.join(projectRoot, ".gitignore"), "utf-8");
601
601
  const matcher = ignoreFactory2();
602
602
  matcher.add(content14);
603
603
  return [{ basePath: projectRoot, matcher }];
@@ -607,7 +607,7 @@ async function loadRootGitignoreStack2(projectRoot) {
607
607
  }
608
608
  function isIgnoredByStack2(candidatePath, stack) {
609
609
  for (const { basePath, matcher } of stack) {
610
- const relativePath = toPosix(path15.relative(basePath, candidatePath));
610
+ const relativePath = toPosix(path16.relative(basePath, candidatePath));
611
611
  if (relativePath === "" || relativePath.startsWith("..")) continue;
612
612
  if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
613
613
  }
@@ -636,20 +636,30 @@ function serializeIdentity(identity) {
636
636
  `aspects={${aspectLines}}`
637
637
  ].join("\n");
638
638
  }
639
- function computeCanonicalHash(fileHashes, identity) {
639
+ function serializeVerdicts(verdicts) {
640
+ return Object.keys(verdicts).sort().map((id) => {
641
+ const v = verdicts[id];
642
+ return `id=${id}|verdict=${v.verdict}|errorSource=${v.errorSource ?? ""}`;
643
+ }).join("\n");
644
+ }
645
+ function computeCanonicalHash(fileHashes, identity, verdicts = {}) {
640
646
  const filesDigest = Object.entries(fileHashes).map(([p2, h]) => `${p2}:${h}`).sort().join("\n");
641
- return hashString(`files:
647
+ return hashString(
648
+ `files:
642
649
  ${filesDigest}
643
650
  identity:
644
- ${serializeIdentity(identity)}`);
651
+ ${serializeIdentity(identity)}
652
+ verdicts:
653
+ ${serializeVerdicts(verdicts)}`
654
+ );
645
655
  }
646
- async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes, identity) {
656
+ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes, identity, verdicts, reuseByMtime = true) {
647
657
  const fileHashes = {};
648
658
  const fileMtimes = {};
649
659
  const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
650
660
  const allFiles = [];
651
661
  for (const tf of trackedFiles) {
652
- const absPath = path15.join(projectRoot, tf.path);
662
+ const absPath = path16.join(projectRoot, tf.path);
653
663
  try {
654
664
  const st = await stat5(absPath);
655
665
  if (st.isDirectory()) {
@@ -659,7 +669,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
659
669
  });
660
670
  for (const entry of dirEntries) {
661
671
  allFiles.push({
662
- relPath: toPosixPath(path15.join(tf.path, entry.relPath)),
672
+ relPath: toPosixPath(path16.join(tf.path, entry.relPath)),
663
673
  absPath: entry.absPath,
664
674
  mtimeMs: entry.mtimeMs
665
675
  });
@@ -676,7 +686,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
676
686
  for (const entry of filtered) {
677
687
  const storedMtime = storedFileData?.mtimes[entry.relPath];
678
688
  const storedHash = storedFileData?.hashes[entry.relPath];
679
- if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
689
+ if (reuseByMtime && storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
680
690
  fileHashes[entry.relPath] = storedHash;
681
691
  } else {
682
692
  dirty.push(entry);
@@ -691,13 +701,13 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
691
701
  fileHashes[batch[j].relPath] = hashes[j];
692
702
  }
693
703
  }
694
- const canonicalHash = computeCanonicalHash(fileHashes, identity ?? EMPTY_IDENTITY);
704
+ const canonicalHash = computeCanonicalHash(fileHashes, identity ?? EMPTY_IDENTITY, verdicts ?? {});
695
705
  return { canonicalHash, fileHashes, fileMtimes };
696
706
  }
697
707
  async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
698
708
  let stack = options.gitignoreStack ?? [];
699
709
  try {
700
- const localContent = await readFile16(path15.join(directoryPath, ".gitignore"), "utf-8");
710
+ const localContent = await readFile16(path16.join(directoryPath, ".gitignore"), "utf-8");
701
711
  const localMatcher = ignoreFactory2();
702
712
  localMatcher.add(localContent);
703
713
  stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
@@ -707,7 +717,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
707
717
  const dirs = [];
708
718
  const files = [];
709
719
  for (const entry of entries) {
710
- const absoluteChildPath = path15.join(directoryPath, entry.name);
720
+ const absoluteChildPath = path16.join(directoryPath, entry.name);
711
721
  if (isIgnoredByStack2(absoluteChildPath, stack)) continue;
712
722
  if (entry.isDirectory()) dirs.push(absoluteChildPath);
713
723
  else if (entry.isFile()) files.push(absoluteChildPath);
@@ -720,7 +730,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
720
730
  Promise.all(files.map(async (f) => {
721
731
  const fileStat = await stat5(f);
722
732
  return {
723
- relPath: toPosixPath(path15.relative(rootDirectoryPath, f)),
733
+ relPath: toPosixPath(path16.relative(rootDirectoryPath, f)),
724
734
  absPath: f,
725
735
  mtimeMs: fileStat.mtimeMs
726
736
  };
@@ -735,7 +745,7 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
735
745
  const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
736
746
  const result = [];
737
747
  for (const mp of mappingPaths) {
738
- const absPath = path15.join(projectRoot, mp);
748
+ const absPath = path16.join(projectRoot, mp);
739
749
  try {
740
750
  const st = await stat5(absPath);
741
751
  if (st.isDirectory()) {
@@ -744,7 +754,7 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
744
754
  gitignoreStack
745
755
  });
746
756
  for (const entry of dirEntries) {
747
- result.push(toPosixPath(path15.join(mp, entry.relPath)));
757
+ result.push(toPosixPath(path16.join(mp, entry.relPath)));
748
758
  }
749
759
  } else {
750
760
  result.push(toPosixPath(mp));
@@ -1081,11 +1091,16 @@ function attachmentMachineOrigin(att, node) {
1081
1091
  }
1082
1092
  function hasNonDraftEffectiveAspects(node, graph) {
1083
1093
  const statuses = computeEffectiveAspectStatuses(node, graph);
1084
- for (const s of statuses.values()) {
1085
- if (s !== "draft") return true;
1094
+ for (const [aspectId, s] of statuses) {
1095
+ if (s === "draft") continue;
1096
+ if (isAggregateAspect(graph, aspectId)) continue;
1097
+ return true;
1086
1098
  }
1087
1099
  return false;
1088
1100
  }
1101
+ function isAggregateAspect(graph, aspectId) {
1102
+ return graph.aspects.find((a) => a.id === aspectId)?.reviewer.type === "aggregate";
1103
+ }
1089
1104
  var ImpliesCycleError;
1090
1105
  var init_aspects = __esm({
1091
1106
  "src/core/graph/aspects.ts"() {
@@ -1191,19 +1206,19 @@ var init_tier_identity = __esm({
1191
1206
  });
1192
1207
 
1193
1208
  // src/core/graph/files.ts
1194
- import path18 from "path";
1209
+ import path19 from "path";
1195
1210
  import { createHash as createHash2 } from "crypto";
1196
1211
  function emptyIdentity() {
1197
1212
  return { ownSubset: sha256Hex(""), ports: {}, aspects: {} };
1198
1213
  }
1199
1214
  function yggPrefixOf(graph) {
1200
- return path18.relative(path18.dirname(graph.rootPath), graph.rootPath).split(/[\\/]/).join("/");
1215
+ return path19.relative(path19.dirname(graph.rootPath), graph.rootPath).split(/[\\/]/).join("/");
1201
1216
  }
1202
1217
  function collectTrackedFiles(node, graph, baseline) {
1203
1218
  const seen = /* @__PURE__ */ new Set();
1204
1219
  const result = [];
1205
- const projectRoot = path18.dirname(graph.rootPath);
1206
- const yggPrefix = path18.relative(projectRoot, graph.rootPath);
1220
+ const projectRoot = path19.dirname(graph.rootPath);
1221
+ const yggPrefix = path19.relative(projectRoot, graph.rootPath);
1207
1222
  const yggPrefixNormalized = toPosixPath(yggPrefix);
1208
1223
  const identityAspects = {};
1209
1224
  const identityPorts = {};
@@ -1559,7 +1574,7 @@ var init_language_registry = __esm({
1559
1574
  });
1560
1575
 
1561
1576
  // src/core/drift-cause.ts
1562
- import path24 from "path";
1577
+ import path25 from "path";
1563
1578
  function serializeCheckTouched(ct) {
1564
1579
  if (!ct) return "";
1565
1580
  return Object.keys(ct).sort().map((k) => `${k}=${ct[k]}`).join(",");
@@ -1616,13 +1631,13 @@ function diffIdentity(nodePath, stored, current) {
1616
1631
  return causes;
1617
1632
  }
1618
1633
  function categorizeFile(filePath, rootPath, projectRoot) {
1619
- const yggPrefix = toPosixPath(path24.relative(projectRoot, rootPath));
1634
+ const yggPrefix = toPosixPath(path25.relative(projectRoot, rootPath));
1620
1635
  const normalized = toPosixPath(filePath);
1621
1636
  return normalized.startsWith(yggPrefix) ? "graph" : "source";
1622
1637
  }
1623
1638
  function describeCascadeCause(filePath, layer, graph) {
1624
1639
  const normalized = toPosixPath(filePath);
1625
- const yggPrefix = toPosixPath(path24.relative(path24.dirname(graph.rootPath), graph.rootPath));
1640
+ const yggPrefix = toPosixPath(path25.relative(path25.dirname(graph.rootPath), graph.rootPath));
1626
1641
  const escPrefix = yggPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1627
1642
  if (layer === "aspects") {
1628
1643
  const match = normalized.match(new RegExp(`${escPrefix}/aspects/([^/]+(?:/[^/]+)*)/`));
@@ -1879,7 +1894,7 @@ var init_log_integrity = __esm({
1879
1894
 
1880
1895
  // src/core/approve.ts
1881
1896
  import { createHash as createHash4 } from "crypto";
1882
- import path25 from "path";
1897
+ import path26 from "path";
1883
1898
  async function approveNode(graph, nodePath, _options = {}) {
1884
1899
  const node = graph.nodes.get(nodePath);
1885
1900
  if (!node) throw new Error(`Node '${nodePath}' does not exist.`);
@@ -1973,7 +1988,7 @@ ${violations.map((v) => ` line ${v.line}: ${v.reason} \u2014 ${v.detail}`).join
1973
1988
  const pendingDriftState = baseline && action2 !== "no-change" ? { nodePath, state: { schemaVersion: DRIFT_STATE_SCHEMA_VERSION, hash: "", files: {}, identity: emptyIdentity(), aspectVerdicts: {}, log: baseline } } : void 0;
1974
1989
  return { action: action2, currentHash: "", previousHash: storedEntry?.hash, gcPaths: gcPaths2, pendingDriftState };
1975
1990
  }
1976
- const projectRoot = path25.dirname(graph.rootPath);
1991
+ const projectRoot = path26.dirname(graph.rootPath);
1977
1992
  if (!hasNonDraftEffectiveAspects(node, graph)) {
1978
1993
  const sourceChangedDraft = await sourceFilesChanged(node, graph, projectRoot, storedEntry);
1979
1994
  if (logRequired && sourceChangedDraft.length > 0 && !hasFreshLogEntry(logSnapshot.content, storedEntry?.log)) {
@@ -2085,6 +2100,7 @@ ${violations.map((v) => ` line ${v.line}: ${v.reason} \u2014 ${v.detail}`).join
2085
2100
  const statuses = computeEffectiveAspectStatuses(node, graph);
2086
2101
  for (const [aspectId, status] of statuses) {
2087
2102
  if (status === "draft") continue;
2103
+ if (isAggregateAspect(graph, aspectId)) continue;
2088
2104
  if (!storedEntry.aspectVerdicts[aspectId]) newlyActiveAspects.push(aspectId);
2089
2105
  }
2090
2106
  }
@@ -2146,7 +2162,7 @@ async function evaluateAllDraftLogGate(graph, nodePath) {
2146
2162
  if (!logRequiredFor(node, graph)) return null;
2147
2163
  const storedEntry = await readNodeDriftState(graph.rootPath, nodePath);
2148
2164
  const logSnapshot = await snapshotLog(graph.rootPath, nodePath);
2149
- const projectRoot = path25.dirname(graph.rootPath);
2165
+ const projectRoot = path26.dirname(graph.rootPath);
2150
2166
  const changed = await sourceFilesChanged(node, graph, projectRoot, storedEntry);
2151
2167
  if (changed.length > 0 && !hasFreshLogEntry(logSnapshot.content, storedEntry?.log)) {
2152
2168
  return mandatoryLogRefusal(node, nodePath, changed);
@@ -2201,7 +2217,7 @@ async function sourceFilesChanged(node, graph, projectRoot, storedEntry) {
2201
2217
  return changed;
2202
2218
  }
2203
2219
  async function snapshotLog(yggRoot, nodePath) {
2204
- const logPath2 = path25.join(yggRoot, "model", nodePath, "log.md");
2220
+ const logPath2 = path26.join(yggRoot, "model", nodePath, "log.md");
2205
2221
  try {
2206
2222
  const st = await lstatFile(logPath2);
2207
2223
  if (st.isSymbolicLink()) {
@@ -2297,7 +2313,7 @@ async function loadSourceFiles(filePaths, projectRoot) {
2297
2313
  for (const filePath of filePaths) {
2298
2314
  const posixPath4 = toPosixPath(filePath);
2299
2315
  try {
2300
- const content14 = await readTextFile(path25.join(projectRoot, filePath));
2316
+ const content14 = await readTextFile(path26.join(projectRoot, filePath));
2301
2317
  results.push({ path: posixPath4, content: content14 });
2302
2318
  } catch (err) {
2303
2319
  debugWrite(`[approve] skipped unreadable file ${posixPath4}: ${err.message}`);
@@ -2377,7 +2393,6 @@ var init_xml_escape = __esm({
2377
2393
  var aspect_verifier_exports = {};
2378
2394
  __export(aspect_verifier_exports, {
2379
2395
  buildPrompt: () => buildPrompt,
2380
- chunkSourceFiles: () => chunkSourceFiles,
2381
2396
  verifyAspects: () => verifyAspects
2382
2397
  });
2383
2398
  function buildPrompt(aspect, nodeDescription, nodePath, sourceFiles, references = []) {
@@ -2427,31 +2442,6 @@ ${aspect.content}
2427
2442
  ${files}
2428
2443
  </source-files>`;
2429
2444
  }
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
2445
  async function verifyWithConsensus(provider, prompt, consensus) {
2456
2446
  if (consensus <= 1) {
2457
2447
  return provider.verifyAspect(prompt);
@@ -2474,29 +2464,12 @@ async function verifyWithConsensus(provider, prompt, consensus) {
2474
2464
  };
2475
2465
  }
2476
2466
  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);
2467
+ const { provider, aspects, sourceFiles, nodePath, nodeDescription, consensus = 1 } = params;
2483
2468
  const results = {};
2484
2469
  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" };
2470
+ const prompt = buildPrompt(aspect, nodeDescription, nodePath, sourceFiles, aspect.references ?? []);
2471
+ const r = await verifyWithConsensus(provider, prompt, consensus);
2472
+ results[aspect.id] = { satisfied: r.satisfied, reason: r.reason, errorSource: r.errorSource };
2500
2473
  }
2501
2474
  return results;
2502
2475
  }
@@ -2513,11 +2486,6 @@ function resolveApiKey(config) {
2513
2486
  const envVar = ENV_VAR_MAP[config.provider];
2514
2487
  return envVar ? process.env[envVar] : void 0;
2515
2488
  }
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
2489
  async function apiFetch(url, init2, providerName, timeoutMs = 6e4) {
2522
2490
  for (let attempt = 0; attempt < 2; attempt++) {
2523
2491
  try {
@@ -2640,9 +2608,6 @@ var init_cli_base = __esm({
2640
2608
  return false;
2641
2609
  }
2642
2610
  }
2643
- async getContextWindowSize() {
2644
- return void 0;
2645
- }
2646
2611
  async verifyAspect(prompt) {
2647
2612
  const fallback = { satisfied: false, reason: "Reviewer unavailable", errorSource: "provider" };
2648
2613
  return new Promise((resolve6) => {
@@ -2725,12 +2690,10 @@ var init_ollama = __esm({
2725
2690
  endpoint;
2726
2691
  model;
2727
2692
  temperature;
2728
- contextLengthField;
2729
2693
  constructor(config) {
2730
2694
  this.endpoint = config.endpoint ?? "http://localhost:11434";
2731
2695
  this.model = config.model;
2732
2696
  this.temperature = config.temperature;
2733
- this.contextLengthField = config.context_length_field;
2734
2697
  }
2735
2698
  async isAvailable() {
2736
2699
  try {
@@ -2741,25 +2704,6 @@ var init_ollama = __esm({
2741
2704
  return false;
2742
2705
  }
2743
2706
  }
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
2707
  async verifyAspect(prompt) {
2764
2708
  const fallback = { satisfied: false, reason: "LLM response could not be parsed", errorSource: "provider" };
2765
2709
  const body = {
@@ -2874,9 +2818,6 @@ var init_openai = __esm({
2874
2818
  async isAvailable() {
2875
2819
  return !!this.apiKey;
2876
2820
  }
2877
- async getContextWindowSize() {
2878
- return void 0;
2879
- }
2880
2821
  };
2881
2822
  registerProvider("openai", (c) => new OpenAIProvider(c));
2882
2823
  registerProvider("openai-compatible", (c) => new OpenAIProvider(c));
@@ -2931,9 +2872,6 @@ var init_anthropic = __esm({
2931
2872
  async isAvailable() {
2932
2873
  return !!this.apiKey;
2933
2874
  }
2934
- async getContextWindowSize() {
2935
- return void 0;
2936
- }
2937
2875
  };
2938
2876
  registerProvider("anthropic", (c) => new AnthropicProvider(c));
2939
2877
  }
@@ -2987,9 +2925,6 @@ var init_google = __esm({
2987
2925
  async isAvailable() {
2988
2926
  return !!this.apiKey;
2989
2927
  }
2990
- async getContextWindowSize() {
2991
- return void 0;
2992
- }
2993
2928
  };
2994
2929
  registerProvider("google", (c) => new GoogleProvider(c));
2995
2930
  }
@@ -3073,6 +3008,7 @@ function buildAspectVerdicts(node, graph, allAspectResults) {
3073
3008
  const carryForward = [];
3074
3009
  for (const [aspectId, status] of statuses) {
3075
3010
  if (status === "draft") continue;
3011
+ if (isAggregateAspect(graph, aspectId)) continue;
3076
3012
  const res = allAspectResults[aspectId];
3077
3013
  if (res === void 0) {
3078
3014
  carryForward.push(aspectId);
@@ -3091,8 +3027,10 @@ function buildAspectVerdicts(node, graph, allAspectResults) {
3091
3027
  function reviewerAborted(node, graph, allAspectResults) {
3092
3028
  if (Object.keys(allAspectResults).length > 0) return false;
3093
3029
  const statuses = computeEffectiveAspectStatuses(node, graph);
3094
- for (const s of statuses.values()) {
3095
- if (s !== "draft") return true;
3030
+ for (const [aspectId, s] of statuses) {
3031
+ if (s === "draft") continue;
3032
+ if (isAggregateAspect(graph, aspectId)) continue;
3033
+ return true;
3096
3034
  }
3097
3035
  return false;
3098
3036
  }
@@ -3122,15 +3060,15 @@ var init_approve_verdicts = __esm({
3122
3060
  // src/ast/loader-hook.ts
3123
3061
  import { register } from "module";
3124
3062
  import { pathToFileURL } from "url";
3125
- import path26 from "path";
3063
+ import path27 from "path";
3126
3064
  import { fileURLToPath as fileURLToPath3 } from "url";
3127
3065
  import { existsSync as existsSync3 } from "fs";
3128
3066
  function ensureLoaderRegistered() {
3129
3067
  if (registered) return;
3130
- let implPath = path26.resolve(__dirname, "./loader-hook-impl.js");
3068
+ let implPath = path27.resolve(__dirname, "./loader-hook-impl.js");
3131
3069
  if (!existsSync3(implPath)) {
3132
- const pkgRoot = path26.resolve(__dirname, "../../");
3133
- implPath = path26.resolve(pkgRoot, "dist/loader-hook-impl.js");
3070
+ const pkgRoot = path27.resolve(__dirname, "../../");
3071
+ implPath = path27.resolve(pkgRoot, "dist/loader-hook-impl.js");
3134
3072
  }
3135
3073
  register(pathToFileURL(implPath));
3136
3074
  registered = true;
@@ -3140,14 +3078,14 @@ var init_loader_hook = __esm({
3140
3078
  "src/ast/loader-hook.ts"() {
3141
3079
  "use strict";
3142
3080
  __filename = fileURLToPath3(import.meta.url);
3143
- __dirname = path26.dirname(__filename);
3081
+ __dirname = path27.dirname(__filename);
3144
3082
  registered = false;
3145
3083
  }
3146
3084
  });
3147
3085
 
3148
3086
  // src/structure/ctx-fs.ts
3149
3087
  import * as fs from "fs";
3150
- import * as path27 from "path";
3088
+ import * as path28 from "path";
3151
3089
  function isAllowed(p2, set) {
3152
3090
  if (p2 === "") return false;
3153
3091
  if (set.has(p2)) return true;
@@ -3167,7 +3105,7 @@ function assertRealpathContained(abs, projectRoot, rel) {
3167
3105
  }
3168
3106
  let probe = abs;
3169
3107
  while (!fs.existsSync(probe)) {
3170
- const parent = path27.dirname(probe);
3108
+ const parent = path28.dirname(probe);
3171
3109
  if (parent === probe) return;
3172
3110
  probe = parent;
3173
3111
  }
@@ -3177,15 +3115,15 @@ function assertRealpathContained(abs, projectRoot, rel) {
3177
3115
  } catch {
3178
3116
  return;
3179
3117
  }
3180
- const relReal = toPosix(path27.relative(realRoot, realProbe));
3181
- if (relReal === ".." || relReal.startsWith("../") || path27.isAbsolute(relReal)) {
3118
+ const relReal = toPosix(path28.relative(realRoot, realProbe));
3119
+ if (relReal === ".." || relReal.startsWith("../") || path28.isAbsolute(relReal)) {
3182
3120
  throw new UndeclaredFsReadError(rel);
3183
3121
  }
3184
3122
  }
3185
3123
  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)) {
3124
+ const abs = path28.resolve(projectRoot, normalizeMappingPath(raw));
3125
+ const rel = toPosix(path28.relative(projectRoot, abs));
3126
+ if (rel === "" || rel.startsWith("..") || path28.isAbsolute(rel)) {
3189
3127
  throw new UndeclaredFsReadError(normalizeMappingPath(raw));
3190
3128
  }
3191
3129
  if (!isAllowed(rel, allowedSet)) throw new UndeclaredFsReadError(rel);
@@ -3202,7 +3140,7 @@ function createCtxFs(params) {
3202
3140
  return {
3203
3141
  exists(raw) {
3204
3142
  const p2 = assertAllowed(raw);
3205
- const abs = path27.resolve(projectRoot, p2);
3143
+ const abs = path28.resolve(projectRoot, p2);
3206
3144
  try {
3207
3145
  const stat9 = fs.statSync(abs);
3208
3146
  return stat9.isDirectory() ? "dir" : stat9.isFile() ? "file" : false;
@@ -3212,12 +3150,12 @@ function createCtxFs(params) {
3212
3150
  },
3213
3151
  read(raw) {
3214
3152
  const p2 = assertAllowed(raw);
3215
- const abs = path27.resolve(projectRoot, p2);
3153
+ const abs = path28.resolve(projectRoot, p2);
3216
3154
  return fs.readFileSync(abs, "utf8");
3217
3155
  },
3218
3156
  list(raw) {
3219
3157
  const p2 = assertAllowed(raw);
3220
- const abs = path27.resolve(projectRoot, p2);
3158
+ const abs = path28.resolve(projectRoot, p2);
3221
3159
  const entries = fs.readdirSync(abs, { withFileTypes: true });
3222
3160
  return entries.map((e) => ({
3223
3161
  name: e.name,
@@ -3233,9 +3171,9 @@ var init_ctx_fs = __esm({
3233
3171
  init_mapping_path();
3234
3172
  init_posix();
3235
3173
  UndeclaredFsReadError = class extends Error {
3236
- constructor(path44) {
3237
- super(`structure-aspect-undeclared-fs-read: ${path44}`);
3238
- this.path = path44;
3174
+ constructor(path46) {
3175
+ super(`structure-aspect-undeclared-fs-read: ${path46}`);
3176
+ this.path = path46;
3239
3177
  this.name = "UndeclaredFsReadError";
3240
3178
  }
3241
3179
  };
@@ -3263,7 +3201,7 @@ var init_expand_mapping_sync = __esm({
3263
3201
 
3264
3202
  // src/structure/ctx-graph.ts
3265
3203
  import * as fs2 from "fs";
3266
- import * as path28 from "path";
3204
+ import * as path29 from "path";
3267
3205
  function computeAllowedNodePaths(currentPath, graph) {
3268
3206
  const allowed = /* @__PURE__ */ new Set([currentPath]);
3269
3207
  const current = graph.nodes.get(currentPath);
@@ -3303,7 +3241,7 @@ function createCtxGraph(params) {
3303
3241
  for (const raw of m.meta.mapping ?? []) {
3304
3242
  const p2 = normalizeMappingPath(raw);
3305
3243
  if (!p2) continue;
3306
- const abs = path28.resolve(projectRoot, p2);
3244
+ const abs = path29.resolve(projectRoot, p2);
3307
3245
  try {
3308
3246
  const stat9 = fs2.statSync(abs);
3309
3247
  if (stat9.isFile()) {
@@ -3396,7 +3334,7 @@ var init_ctx_graph = __esm({
3396
3334
 
3397
3335
  // src/ast/parser.ts
3398
3336
  import { Parser, Language } from "web-tree-sitter";
3399
- import path29 from "path";
3337
+ import path30 from "path";
3400
3338
  import { fileURLToPath as fileURLToPath4 } from "url";
3401
3339
  import { existsSync as existsSync5 } from "fs";
3402
3340
  import { createRequire as createRequire3 } from "module";
@@ -3407,12 +3345,12 @@ async function init() {
3407
3345
  }
3408
3346
  function resolveWasm(filename, pkg2) {
3409
3347
  for (const dir of GRAMMAR_DIRS) {
3410
- const p2 = path29.join(dir, filename);
3348
+ const p2 = path30.join(dir, filename);
3411
3349
  if (existsSync5(p2)) return p2;
3412
3350
  }
3413
3351
  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)]) {
3352
+ const pkgDir = path30.dirname(_require.resolve(`${pkg2}/package.json`));
3353
+ for (const candidate of [path30.join(pkgDir, filename), path30.join(pkgDir, "bindings/node", filename)]) {
3416
3354
  if (existsSync5(candidate)) return candidate;
3417
3355
  }
3418
3356
  } catch {
@@ -3437,7 +3375,7 @@ async function getParser(extension) {
3437
3375
  return parser;
3438
3376
  }
3439
3377
  async function parseFile(filePath, content14) {
3440
- const ext = path29.extname(filePath);
3378
+ const ext = path30.extname(filePath);
3441
3379
  const parser = await getParser(ext);
3442
3380
  const tree = parser.parse(content14);
3443
3381
  if (tree === null) {
@@ -3452,10 +3390,10 @@ var init_parser = __esm({
3452
3390
  init_language_registry();
3453
3391
  _require = createRequire3(import.meta.url);
3454
3392
  __filename2 = fileURLToPath4(import.meta.url);
3455
- __dirname2 = path29.dirname(__filename2);
3393
+ __dirname2 = path30.dirname(__filename2);
3456
3394
  GRAMMAR_DIRS = [
3457
- path29.resolve(__dirname2, "grammars"),
3458
- path29.resolve(__dirname2, "..", "grammars")
3395
+ path30.resolve(__dirname2, "grammars"),
3396
+ path30.resolve(__dirname2, "..", "grammars")
3459
3397
  ];
3460
3398
  initialized = false;
3461
3399
  langCache = /* @__PURE__ */ new Map();
@@ -3464,7 +3402,7 @@ var init_parser = __esm({
3464
3402
 
3465
3403
  // src/structure/ctx-parsers.ts
3466
3404
  import * as fs3 from "fs";
3467
- import * as path30 from "path";
3405
+ import * as path31 from "path";
3468
3406
  import { extname } from "path";
3469
3407
  import { parse as parseYaml13 } from "yaml";
3470
3408
  import { parse as parseTomlSmol } from "smol-toml";
@@ -3476,7 +3414,7 @@ function createCtxParsers(params) {
3476
3414
  return input;
3477
3415
  }
3478
3416
  const p2 = resolveAllowedReadPath(input, allowedSet, projectRoot);
3479
- const abs = path30.resolve(projectRoot, p2);
3417
+ const abs = path31.resolve(projectRoot, p2);
3480
3418
  const content14 = fs3.readFileSync(abs, "utf8");
3481
3419
  touchedFiles.push(p2);
3482
3420
  return { path: p2, content: content14 };
@@ -3617,7 +3555,43 @@ var init_allowed_reads = __esm({
3617
3555
  }
3618
3556
  });
3619
3557
 
3558
+ // src/ast/find-comments.ts
3559
+ import { extname as extname2 } from "path";
3560
+ function findComments(target) {
3561
+ const hasAst = "ast" in target;
3562
+ const hasRootNode = "rootNode" in target;
3563
+ if (hasAst && hasRootNode) {
3564
+ throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
3565
+ }
3566
+ let language = "language" in target ? target.language : void 0;
3567
+ if (language === void 0 && "path" in target) {
3568
+ language = getLanguageForExtension(extname2(target.path)) ?? void 0;
3569
+ }
3570
+ if (language === void 0) {
3571
+ throw new Error(
3572
+ "AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
3573
+ );
3574
+ }
3575
+ const def = LANGUAGES[language];
3576
+ if (def === void 0) {
3577
+ throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
3578
+ }
3579
+ const root = hasAst ? target.ast.rootNode : target.rootNode;
3580
+ const out = [];
3581
+ for (const type of def.commentTypes) {
3582
+ out.push(...root.descendantsOfType(type));
3583
+ }
3584
+ return out;
3585
+ }
3586
+ var init_find_comments = __esm({
3587
+ "src/ast/find-comments.ts"() {
3588
+ "use strict";
3589
+ init_language_registry();
3590
+ }
3591
+ });
3592
+
3620
3593
  // src/ast/suppress.ts
3594
+ import { extname as extname3 } from "path";
3621
3595
  function commentBody(text2) {
3622
3596
  if (text2.startsWith("//")) return text2.slice(2).trim();
3623
3597
  if (text2.startsWith("/*")) return text2.replace(/^\/\*+/, "").replace(/\*+\/$/, "").trim();
@@ -3651,7 +3625,10 @@ function parseMarker(commentText, line, file) {
3651
3625
  return null;
3652
3626
  }
3653
3627
  function collectSuppressions(tree, file, totalLines) {
3654
- const comments = tree.rootNode.descendantsOfType("comment");
3628
+ if (getLanguageForExtension(extname3(file)) === null) {
3629
+ return [];
3630
+ }
3631
+ const comments = findComments({ path: file, ast: tree });
3655
3632
  const markers = [];
3656
3633
  for (const c of comments) {
3657
3634
  const m = parseMarker(c.text, c.startPosition.row + 1, file);
@@ -3702,10 +3679,48 @@ function isLineSuppressed(ranges, aspectId, line) {
3702
3679
  return r.isWildcard || r.aspectIds.has(aspectId);
3703
3680
  });
3704
3681
  }
3682
+ function scanSuppressionMarkers(text2) {
3683
+ const lines = text2.split("\n");
3684
+ const result = [];
3685
+ for (let i = 0; i < lines.length; i++) {
3686
+ const lineNum = i + 1;
3687
+ const raw = lines[i];
3688
+ let m;
3689
+ m = raw.match(RE_DISABLE);
3690
+ if (m) {
3691
+ const ids = splitAspectList(m[1]);
3692
+ const reason = (m[2] ?? "").trim();
3693
+ for (const id of ids) {
3694
+ result.push({ line: lineNum, aspectId: id, kind: "disable", wildcard: id === "*", reason });
3695
+ }
3696
+ continue;
3697
+ }
3698
+ m = raw.match(RE_ENABLE);
3699
+ if (m) {
3700
+ const ids = splitAspectList(m[1]);
3701
+ for (const id of ids) {
3702
+ result.push({ line: lineNum, aspectId: id, kind: "enable", wildcard: id === "*", reason: "" });
3703
+ }
3704
+ continue;
3705
+ }
3706
+ m = raw.match(RE_SINGLE);
3707
+ if (m) {
3708
+ const ids = splitAspectList(m[1]);
3709
+ const reason = (m[2] ?? "").trim();
3710
+ for (const id of ids) {
3711
+ result.push({ line: lineNum, aspectId: id, kind: "single", wildcard: id === "*", reason });
3712
+ }
3713
+ continue;
3714
+ }
3715
+ }
3716
+ return result;
3717
+ }
3705
3718
  var SuppressMarkerError, RE_SINGLE, RE_DISABLE, RE_ENABLE;
3706
3719
  var init_suppress = __esm({
3707
3720
  "src/ast/suppress.ts"() {
3708
3721
  "use strict";
3722
+ init_find_comments();
3723
+ init_language_registry();
3709
3724
  SuppressMarkerError = class extends Error {
3710
3725
  constructor(message, file, line) {
3711
3726
  super(message);
@@ -3780,66 +3795,37 @@ var init_validate_check_module = __esm({
3780
3795
 
3781
3796
  // src/structure/runner.ts
3782
3797
  import * as fs4 from "fs";
3783
- import * as path31 from "path";
3798
+ import * as path32 from "path";
3784
3799
  import { pathToFileURL as pathToFileURL2 } from "url";
3785
- function buildOwnFiles(node, projectRoot, touchedFiles) {
3786
- const childMappings = /* @__PURE__ */ new Set();
3800
+ async function buildOwnFiles(node, projectRoot, touchedFiles) {
3801
+ const childMappingEntries = [];
3787
3802
  for (const child of node.children) {
3788
3803
  for (const raw of child.meta.mapping ?? []) {
3789
3804
  const p2 = normalizeMappingPath(raw);
3790
- if (p2) childMappings.add(p2);
3805
+ if (p2) childMappingEntries.push(p2);
3791
3806
  }
3792
3807
  }
3808
+ const rawMapping = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p2) => p2 !== "");
3809
+ const expanded = await expandMappingPaths(projectRoot, rawMapping);
3793
3810
  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);
3811
+ for (const p2 of expanded) {
3812
+ if (childMappingEntries.length > 0 && isPathInMapping(p2, childMappingEntries)) continue;
3813
+ if (BINARY_EXTENSIONS2.has(path32.extname(p2).toLowerCase())) continue;
3814
+ const abs = path32.resolve(projectRoot, p2);
3815
+ let content14;
3798
3816
  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
- }
3817
+ content14 = fs4.readFileSync(abs, "utf8");
3805
3818
  } catch {
3819
+ continue;
3806
3820
  }
3821
+ result.push({ path: p2, content: content14 });
3822
+ touchedFiles.push(p2);
3807
3823
  }
3808
3824
  return result;
3809
3825
  }
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
- }
3826
+ async function enumerateMappedFilesAsync(mappingPaths, projectRoot) {
3827
+ const normalized = mappingPaths.map(normalizeMappingPath).filter((p2) => p2 !== "");
3828
+ return expandMappingPaths(projectRoot, normalized);
3843
3829
  }
3844
3830
  async function runStructureAspect(params) {
3845
3831
  ensureLoaderRegistered();
@@ -3854,8 +3840,8 @@ async function runStructureAspect(params) {
3854
3840
  next: `Pass an existing node path, or add the node to the graph.`
3855
3841
  });
3856
3842
  }
3857
- const aspectDirAbs = path31.isAbsolute(aspectDir) ? aspectDir : path31.resolve(projectRoot, aspectDir);
3858
- const checkPath = path31.join(aspectDirAbs, "check.mjs");
3843
+ const aspectDirAbs = path32.isAbsolute(aspectDir) ? aspectDir : path32.resolve(projectRoot, aspectDir);
3844
+ const checkPath = path32.join(aspectDirAbs, "check.mjs");
3859
3845
  let mod;
3860
3846
  try {
3861
3847
  mod = await import(pathToFileURL2(checkPath).href);
@@ -3878,7 +3864,7 @@ async function runStructureAspect(params) {
3878
3864
  const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles });
3879
3865
  const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles });
3880
3866
  const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache });
3881
- const ownFiles = buildOwnFiles(node, projectRoot, touchedFiles);
3867
+ const ownFiles = await buildOwnFiles(node, projectRoot, touchedFiles);
3882
3868
  await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
3883
3869
  const ownFilesEnriched = enrichFilesWithAst(ownFiles, astCache);
3884
3870
  const ctx = {
@@ -3901,8 +3887,8 @@ async function runStructureAspect(params) {
3901
3887
  for (const rel of node.meta.relations ?? []) {
3902
3888
  const target = graph.nodes.get(rel.target);
3903
3889
  if (!target) continue;
3904
- for (const p2 of enumerateMappedFilesSync(target.meta.mapping ?? [], projectRoot)) {
3905
- const abs = path31.resolve(projectRoot, p2);
3890
+ for (const p2 of await enumerateMappedFilesAsync(target.meta.mapping ?? [], projectRoot)) {
3891
+ const abs = path32.resolve(projectRoot, p2);
3906
3892
  try {
3907
3893
  const content14 = fs4.readFileSync(abs, "utf8");
3908
3894
  astInputSet.push({ path: p2, content: content14 });
@@ -4007,7 +3993,7 @@ ${err.stack ?? ""}`,
4007
3993
  });
4008
3994
  return { violations: visible, touchedFiles, succeeded: true };
4009
3995
  }
4010
- var StructureRunnerError;
3996
+ var StructureRunnerError, BINARY_EXTENSIONS2;
4011
3997
  var init_runner = __esm({
4012
3998
  "src/structure/runner.ts"() {
4013
3999
  "use strict";
@@ -4017,6 +4003,7 @@ var init_runner = __esm({
4017
4003
  init_ctx_parsers();
4018
4004
  init_allowed_reads();
4019
4005
  init_expand_mapping_sync();
4006
+ init_hash();
4020
4007
  init_suppress();
4021
4008
  init_validate_check_module();
4022
4009
  StructureRunnerError = class extends Error {
@@ -4030,6 +4017,35 @@ ${data.next}`);
4030
4017
  }
4031
4018
  messageData;
4032
4019
  };
4020
+ BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
4021
+ ".gif",
4022
+ ".png",
4023
+ ".jpg",
4024
+ ".jpeg",
4025
+ ".webp",
4026
+ ".bmp",
4027
+ ".ico",
4028
+ ".svgz",
4029
+ ".woff",
4030
+ ".woff2",
4031
+ ".ttf",
4032
+ ".otf",
4033
+ ".eot",
4034
+ ".zip",
4035
+ ".gz",
4036
+ ".tgz",
4037
+ ".tar",
4038
+ ".bz2",
4039
+ ".7z",
4040
+ ".pdf",
4041
+ ".mp4",
4042
+ ".mov",
4043
+ ".webm",
4044
+ ".mp3",
4045
+ ".wav",
4046
+ ".wasm",
4047
+ ".bin"
4048
+ ]);
4033
4049
  }
4034
4050
  });
4035
4051
 
@@ -4043,15 +4059,12 @@ __export(approve_reviewer_exports, {
4043
4059
  reviewerAborted: () => reviewerAborted,
4044
4060
  runApproveWithReviewer: () => runApproveWithReviewer
4045
4061
  });
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
- }
4062
+ import path33 from "path";
4050
4063
  async function dispatchStructureAspects(plan, node, graph, result, projectRoot, parseCache, results, violations) {
4051
4064
  for (const entry of plan.resolved) {
4052
4065
  if (entry.kind !== "deterministic") continue;
4053
4066
  const aspect = entry.aspect;
4054
- const aspectDirAbs = path32.join(projectRoot, ".yggdrasil/aspects", aspect.id);
4067
+ const aspectDirAbs = path33.join(projectRoot, ".yggdrasil/aspects", aspect.id);
4055
4068
  try {
4056
4069
  const structResult = await runStructureAspect({
4057
4070
  aspectDir: aspectDirAbs,
@@ -4072,7 +4085,7 @@ async function dispatchStructureAspects(plan, node, graph, result, projectRoot,
4072
4085
  const p2 = toPosixPath(raw);
4073
4086
  let hash = result.pendingDriftState?.state.files[p2];
4074
4087
  if (!hash) {
4075
- const abs = path32.resolve(projectRoot, p2);
4088
+ const abs = path33.resolve(projectRoot, p2);
4076
4089
  try {
4077
4090
  hash = await hashFile(abs);
4078
4091
  } catch (e) {
@@ -4122,6 +4135,23 @@ async function commitApprovalAndCleanDrafts(rootPath, result, node, graph) {
4122
4135
  await clearDraftAspectsFromDriftState(rootPath, node.path, draftIds);
4123
4136
  }
4124
4137
  }
4138
+ async function recomputeFinalHash(result, node, graph, projectRoot) {
4139
+ if (!result.pendingDriftState) return;
4140
+ if (result.action !== "initial" && result.action !== "approved") return;
4141
+ const { trackedFiles: recomputeTracked, identity: recomputeIdentity } = collectTrackedFiles(node, graph, result.pendingDriftState.state);
4142
+ const recomputeExclusions = getChildMappingExclusions(graph, node.path);
4143
+ const { canonicalHash } = await hashTrackedFiles(
4144
+ projectRoot,
4145
+ recomputeTracked,
4146
+ void 0,
4147
+ recomputeExclusions,
4148
+ recomputeIdentity,
4149
+ result.pendingDriftState.state.aspectVerdicts
4150
+ );
4151
+ result.pendingDriftState.state.identity = recomputeIdentity;
4152
+ result.pendingDriftState.state.hash = canonicalHash;
4153
+ result.currentHash = canonicalHash;
4154
+ }
4125
4155
  function partitionCodeViolationsByStatus(codeViolations, statuses) {
4126
4156
  const enforced = [];
4127
4157
  const advisory = [];
@@ -4160,7 +4190,7 @@ async function loadAndIsolateReferences(params) {
4160
4190
  if (refs.length === 0) return { ok: true, references: [] };
4161
4191
  const out = [];
4162
4192
  for (const ref of refs) {
4163
- const absPath = path32.join(params.projectRoot, ref.path);
4193
+ const absPath = path33.join(params.projectRoot, ref.path);
4164
4194
  try {
4165
4195
  let content14 = params.cache.get(absPath);
4166
4196
  if (content14 === void 0) {
@@ -4187,7 +4217,9 @@ async function runApproveWithReviewer(input) {
4187
4217
  const allAspectIds = computeEffectiveAspects(node, graph);
4188
4218
  const allAspects = [...allAspectIds].map((id) => graph.aspects.find((a) => a.id === id)).filter((a) => a !== void 0);
4189
4219
  const statuses = computeEffectiveAspectStatuses(node, graph);
4190
- const nonDraft = allAspects.filter((a) => statuses.get(a.id) !== "draft");
4220
+ const nonDraft = allAspects.filter(
4221
+ (a) => statuses.get(a.id) !== "draft" && a.reviewer.type !== "aggregate"
4222
+ );
4191
4223
  const skippedDraftAspects = allAspects.filter((a) => statuses.get(a.id) === "draft").map((a) => a.id);
4192
4224
  for (const id of skippedDraftAspects) {
4193
4225
  process.stdout.write(`[draft] node '${node.path}': aspect '${id}' skipped (status: draft)
@@ -4197,6 +4229,7 @@ async function runApproveWithReviewer(input) {
4197
4229
  const aspectViolations = [];
4198
4230
  const allAspectResults = {};
4199
4231
  const referencesCache = /* @__PURE__ */ new Map();
4232
+ const projectRoot = toPosixPath(path33.dirname(rootPath));
4200
4233
  const finalizeAndReturn = async (extras, infra = false) => {
4201
4234
  const { verdicts, carryForward } = buildAspectVerdicts(node, graph, allAspectResults);
4202
4235
  applyAspectVerdictsToResult(result, verdicts, carryForward, storedEntry?.aspectVerdicts, filterAspectId, reviewerAborted(node, graph, allAspectResults));
@@ -4210,6 +4243,7 @@ async function runApproveWithReviewer(input) {
4210
4243
  delete result.pendingDriftState.state.log;
4211
4244
  }
4212
4245
  }
4246
+ await recomputeFinalHash(result, node, graph, projectRoot);
4213
4247
  await commitApprovalAndCleanDrafts(rootPath, result, node, graph);
4214
4248
  }
4215
4249
  const out = { ...result, ...extras, skippedDraftAspects };
@@ -4232,12 +4266,24 @@ async function runApproveWithReviewer(input) {
4232
4266
  }
4233
4267
  }, true);
4234
4268
  }
4235
- const projectRoot = toPosixPath(path32.dirname(rootPath));
4236
4269
  const { trackedFiles } = collectTrackedFiles(node, graph);
4237
4270
  const { fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, void 0, []);
4238
- const yggPrefix = toPosixPath(path32.relative(projectRoot, rootPath));
4271
+ const yggPrefix = toPosixPath(path33.relative(projectRoot, rootPath));
4239
4272
  const sourceFilePaths = Object.keys(fileHashes).map((f) => toPosixPath(f)).filter((f) => !f.startsWith(yggPrefix));
4240
4273
  const sourceFiles = await loadSourceFiles(sourceFilePaths, projectRoot);
4274
+ if (hasLlmAspects && sourceFilePaths.length === 0) {
4275
+ const llmIds = filtered.filter((a) => a.reviewer.type === "llm").map((a) => a.id).join(", ");
4276
+ const normalizedNodePath2 = toPosixPath(nodePath);
4277
+ return finalizeAndReturn({
4278
+ action: "refused",
4279
+ llmSkipped: "unavailable",
4280
+ refuseReasonData: {
4281
+ what: `No readable source files found for node '${normalizedNodePath2}', but it has effective non-draft LLM aspect(s): ${llmIds}.`,
4282
+ why: "An LLM aspect needs source files to verify \u2014 approving with no files would record a verdict over code the reviewer never saw.",
4283
+ next: `Add source files that satisfy the node mapping, or remove the LLM aspect(s), then re-run: yg approve --node ${normalizedNodePath2}`
4284
+ }
4285
+ }, true);
4286
+ }
4241
4287
  const nodeDescription = node.meta.description ?? "";
4242
4288
  const plan = graph.config.reviewer ? resolveExecutionPlan(filtered, graph.config.reviewer) : {
4243
4289
  resolved: filtered.filter((a) => a.reviewer.type === "deterministic").map((a) => ({ kind: "deterministic", aspect: a })),
@@ -4279,14 +4325,6 @@ async function runApproveWithReviewer(input) {
4279
4325
  debugWrite(`[d8.3] preserved checkTouched for ${preserved} non-evaluated deterministic aspect(s) on node ${node.path}`);
4280
4326
  }
4281
4327
  }
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
4328
  if (aspectViolations.length > 0) {
4291
4329
  const astInfraErrors = aspectViolations.filter((v) => v.errorSource !== "codeViolation");
4292
4330
  const astCodeViolations = aspectViolations.filter((v) => v.errorSource === "codeViolation");
@@ -4357,7 +4395,6 @@ async function runApproveWithReviewer(input) {
4357
4395
  aspectResults: allAspectResults
4358
4396
  }, true);
4359
4397
  }
4360
- const maxTokens = merged.max_tokens === "auto" ? await resolveMaxTokens(merged, provider) : merged.max_tokens;
4361
4398
  const aspectsForTier = [];
4362
4399
  for (const e of entries) {
4363
4400
  const loaded = await loadAndIsolateReferences({
@@ -4386,8 +4423,7 @@ async function runApproveWithReviewer(input) {
4386
4423
  sourceFiles,
4387
4424
  nodePath,
4388
4425
  nodeDescription,
4389
- consensus: merged.consensus,
4390
- maxTokens
4426
+ consensus: merged.consensus
4391
4427
  });
4392
4428
  for (const [aspectId, res] of Object.entries(llmResults)) {
4393
4429
  allAspectResults[aspectId] = res;
@@ -4454,7 +4490,6 @@ var init_approve_reviewer = __esm({
4454
4490
  "src/core/approve-reviewer.ts"() {
4455
4491
  "use strict";
4456
4492
  init_aspect_verifier();
4457
- init_api_utils();
4458
4493
  init_llm();
4459
4494
  init_approve();
4460
4495
  init_aspects();
@@ -4479,7 +4514,7 @@ import { Command as Command2 } from "commander";
4479
4514
  import chalk2 from "chalk";
4480
4515
  import { existsSync as existsSync2 } from "fs";
4481
4516
  import { mkdir as mkdir3, writeFile as writeFile7, readdir as readdir9, readFile as readFile18, stat as stat6 } from "fs/promises";
4482
- import path17 from "path";
4517
+ import path18 from "path";
4483
4518
  import { fileURLToPath as fileURLToPath2 } from "url";
4484
4519
  import * as p from "@clack/prompts";
4485
4520
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
@@ -4542,9 +4577,11 @@ The CLI (\`yg\`) reads and validates \u2014 it never modifies files. You create
4542
4577
 
4543
4578
  **Nodes** \u2014 components. \`model/<path>/yg-node.yaml\`. Nodes nest by directory \u2014 children inherit parent aspects. Schema: \`schemas/yg-node.yaml\`.
4544
4579
 
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.
4580
+ **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.
4546
4581
 
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\`.
4582
+ 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\`.
4583
+
4584
+ 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
4585
 
4549
4586
  **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
4587
 
@@ -4633,13 +4670,14 @@ When \`yg check\` emits both errors AND warnings, \`suggestedNext\` points at th
4633
4670
  | \`yg log add --node <path> --reason <text>\` | Append per-node business-context entry (multi-line via \`--reason-file <path>\`) |
4634
4671
  | \`yg log read --node <path> [--top N \\| --all]\` | Read log entries (default top 10, newest first) |
4635
4672
  | \`yg log merge-resolve --node <path>\` | Reconcile log.md after a git merge (validates byte-exact ancestor + union of new entries) |
4673
+ | \`yg suppressions\` | Read-only inventory of active \`yg-suppress\` markers; warns on unknown aspect-id, wildcard, or unbounded range. Exit 0. |
4636
4674
  | \`yg knowledge list\` / \`yg knowledge read <name>\` | Browse deep-reference topics |
4637
4675
 
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\`.
4676
+ 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
4677
 
4640
4678
  ### Impact and Cost
4641
4679
 
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.
4680
+ 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
4681
 
4644
4682
  When code doesn't match an aspect, five options:
4645
4683
 
@@ -4658,7 +4696,7 @@ var DECISIONS = `## DECISIONS
4658
4696
 
4659
4697
  **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
4698
 
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>\`.
4699
+ **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
4700
 
4663
4701
  **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
4702
 
@@ -4926,7 +4964,7 @@ context.
4926
4964
 
4927
4965
  ### When to Create Graph Elements
4928
4966
 
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.
4967
+ **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
4968
 
4931
4969
  **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
4970
 
@@ -5607,7 +5645,7 @@ import chalk from "chalk";
5607
5645
 
5608
5646
  // src/core/graph-loader.ts
5609
5647
  init_graph_fs();
5610
- import path9 from "path";
5648
+ import path10 from "path";
5611
5649
  import { gt as gt3, lt, valid as valid3 } from "semver";
5612
5650
 
5613
5651
  // src/io/config-parser.ts
@@ -5863,14 +5901,6 @@ function parseTier(name, raw, filename) {
5863
5901
  }, "config-tier-unknown-key");
5864
5902
  }
5865
5903
  }
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
5904
  let references;
5875
5905
  if (t.references !== void 0) {
5876
5906
  if (t.references === null || typeof t.references !== "object" || Array.isArray(t.references)) {
@@ -5925,8 +5955,6 @@ function parseTier(name, raw, filename) {
5925
5955
  endpoint: typeof c.endpoint === "string" ? c.endpoint : void 0,
5926
5956
  temperature: typeof c.temperature === "number" ? c.temperature : 0,
5927
5957
  consensus: consensusRaw,
5928
- max_tokens: maxTokens,
5929
- context_length_field: typeof c.context_length_field === "string" ? c.context_length_field : void 0,
5930
5958
  timeout: typeof c.timeout === "number" ? c.timeout * 1e3 : void 0,
5931
5959
  ...references !== void 0 ? { references } : {}
5932
5960
  };
@@ -6380,8 +6408,10 @@ function parsePorts(rawPorts, filePath) {
6380
6408
  }
6381
6409
 
6382
6410
  // src/io/aspect-parser.ts
6411
+ init_graph_fs();
6383
6412
  init_graph();
6384
6413
  import { readFile as readFile6 } from "fs/promises";
6414
+ import path6 from "path";
6385
6415
  import { parse as parseYaml4 } from "yaml";
6386
6416
 
6387
6417
  // src/io/artifact-reader.ts
@@ -6543,7 +6573,10 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
6543
6573
  };
6544
6574
  }
6545
6575
  const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
6546
- const reviewerResult = parseReviewer2(raw.reviewer, idTrimmed);
6576
+ const hasContentMd = fileExistsSync(path6.join(aspectDir, "content.md"));
6577
+ const hasCheckMjs = fileExistsSync(path6.join(aspectDir, "check.mjs"));
6578
+ const hasImplies = Array.isArray(raw.implies) && raw.implies.length > 0;
6579
+ const reviewerResult = parseReviewer2(raw.reviewer, idTrimmed, { hasContentMd, hasCheckMjs, hasImplies });
6547
6580
  if (!reviewerResult.ok) {
6548
6581
  return { ok: false, aspectId: idTrimmed, errors: reviewerResult.errors };
6549
6582
  }
@@ -6657,6 +6690,20 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
6657
6690
  }]
6658
6691
  };
6659
6692
  }
6693
+ if (reviewer.type === "aggregate") {
6694
+ return {
6695
+ ok: false,
6696
+ aspectId: idTrimmed,
6697
+ errors: [{
6698
+ code: "aspect-references-on-aggregate",
6699
+ messageData: {
6700
+ what: `Aspect '${idTrimmed}' declares 'references:' but it is an aggregating aspect (no content.md, no check.mjs).`,
6701
+ 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.",
6702
+ next: `remove 'references:' from .yggdrasil/aspects/${idTrimmed}/yg-aspect.yaml, or add a content.md and move the references onto that LLM aspect.`
6703
+ }
6704
+ }]
6705
+ };
6706
+ }
6660
6707
  references = [];
6661
6708
  const seenPaths = /* @__PURE__ */ new Set();
6662
6709
  for (let i = 0; i < raw.references.length; i++) {
@@ -6777,16 +6824,25 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
6777
6824
  }
6778
6825
  };
6779
6826
  }
6780
- function parseReviewer2(raw, aspectId) {
6827
+ function parseReviewer2(raw, aspectId, files) {
6781
6828
  if (raw === void 0 || raw === null) {
6829
+ if (files.hasContentMd && !files.hasCheckMjs) {
6830
+ return { ok: true, value: { type: "llm" } };
6831
+ }
6832
+ if (files.hasCheckMjs && !files.hasContentMd) {
6833
+ return { ok: true, value: { type: "deterministic" } };
6834
+ }
6835
+ if (!files.hasContentMd && !files.hasCheckMjs && files.hasImplies) {
6836
+ return { ok: true, value: { type: "aggregate" } };
6837
+ }
6782
6838
  return {
6783
6839
  ok: false,
6784
6840
  errors: [{
6785
6841
  code: "aspect-reviewer-missing",
6786
6842
  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`"
6843
+ what: `aspect '${aspectId}' has no reviewer: block and no rule source to infer one from`,
6844
+ why: "an aspect must ship content.md (llm), check.mjs (deterministic), or declare implies (aggregating bundle); otherwise it does nothing",
6845
+ next: "add `reviewer:\\n type: llm` with a content.md, add a check.mjs, or add `implies:` to make this an aggregating aspect"
6790
6846
  }
6791
6847
  }]
6792
6848
  };
@@ -6878,7 +6934,7 @@ function parseReviewer2(raw, aspectId) {
6878
6934
 
6879
6935
  // src/io/flow-parser.ts
6880
6936
  import { readFile as readFile7 } from "fs/promises";
6881
- import path6 from "path";
6937
+ import path7 from "path";
6882
6938
  import { parse as parseYaml5 } from "yaml";
6883
6939
  async function parseFlow(flowDir, flowYamlPath) {
6884
6940
  const content14 = await readFile7(flowYamlPath, "utf-8");
@@ -6927,7 +6983,7 @@ async function parseFlow(flowDir, flowYamlPath) {
6927
6983
  }
6928
6984
  }
6929
6985
  return {
6930
- path: path6.basename(flowDir),
6986
+ path: path7.basename(flowDir),
6931
6987
  name: raw.name.trim(),
6932
6988
  description,
6933
6989
  nodes: nodePaths,
@@ -6939,16 +6995,16 @@ async function parseFlow(flowDir, flowYamlPath) {
6939
6995
 
6940
6996
  // src/io/schema-parser.ts
6941
6997
  import { readFile as readFile8 } from "fs/promises";
6942
- import path7 from "path";
6998
+ import path8 from "path";
6943
6999
  import { parse as parseYaml6 } from "yaml";
6944
7000
  async function parseSchema(filePath) {
6945
- const filename = path7.basename(filePath);
7001
+ const filename = path8.basename(filePath);
6946
7002
  const content14 = await readFile8(filePath, "utf-8");
6947
7003
  const raw = parseYaml6(content14);
6948
7004
  if (raw != null && (typeof raw !== "object" || Array.isArray(raw))) {
6949
7005
  throw new Error(`${filename} at ${filePath}: expected YAML mapping or empty document`);
6950
7006
  }
6951
- const rawName = path7.basename(filePath, path7.extname(filePath));
7007
+ const rawName = path8.basename(filePath, path8.extname(filePath));
6952
7008
  const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
6953
7009
  return { schemaType };
6954
7010
  }
@@ -7186,7 +7242,7 @@ var OutdatedSchemaVersionError = class extends Error {
7186
7242
  }
7187
7243
  };
7188
7244
  function toModelPath(absolutePath, modelDir) {
7189
- return toPosixPath(path9.relative(modelDir, absolutePath));
7245
+ return toPosixPath(path10.relative(modelDir, absolutePath));
7190
7246
  }
7191
7247
  var FALLBACK_CONFIG = {};
7192
7248
  async function loadGraph(projectRoot, options = {}) {
@@ -7203,7 +7259,7 @@ async function loadGraph(projectRoot, options = {}) {
7203
7259
  let configErrorMessage;
7204
7260
  let config = FALLBACK_CONFIG;
7205
7261
  try {
7206
- config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
7262
+ config = await parseConfig(path10.join(yggRoot, "yg-config.yaml"));
7207
7263
  } catch (error) {
7208
7264
  if (error instanceof ConfigParseError) {
7209
7265
  configErrorMessage = error.messageData;
@@ -7216,7 +7272,7 @@ async function loadGraph(projectRoot, options = {}) {
7216
7272
  }
7217
7273
  }
7218
7274
  const { architecture, error: architectureError } = await loadArchitecture(yggRoot);
7219
- const modelDir = path9.join(yggRoot, "model");
7275
+ const modelDir = path10.join(yggRoot, "model");
7220
7276
  const nodes = /* @__PURE__ */ new Map();
7221
7277
  const nodeParseErrors = [];
7222
7278
  try {
@@ -7229,9 +7285,9 @@ async function loadGraph(projectRoot, options = {}) {
7229
7285
  }
7230
7286
  throw err;
7231
7287
  }
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"));
7288
+ const aspectsLoad = await loadAspects(path10.join(yggRoot, "aspects"));
7289
+ const flows = await loadFlows(path10.join(yggRoot, "flows"));
7290
+ const schemas = await loadSchemas(path10.join(yggRoot, "schemas"));
7235
7291
  return {
7236
7292
  config,
7237
7293
  architecture,
@@ -7249,7 +7305,7 @@ async function loadGraph(projectRoot, options = {}) {
7249
7305
  };
7250
7306
  }
7251
7307
  async function loadArchitecture(yggRoot) {
7252
- const architectureFilePath = path9.join(yggRoot, "yg-architecture.yaml");
7308
+ const architectureFilePath = path10.join(yggRoot, "yg-architecture.yaml");
7253
7309
  const emptyArch = { node_types: {} };
7254
7310
  try {
7255
7311
  const architecture = await parseArchitecture(architectureFilePath);
@@ -7287,7 +7343,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
7287
7343
  }
7288
7344
  if (hasNodeYaml) {
7289
7345
  const graphPath = toModelPath(dirPath, modelDir);
7290
- const nodeYamlPath = path9.join(dirPath, "yg-node.yaml");
7346
+ const nodeYamlPath = path10.join(dirPath, "yg-node.yaml");
7291
7347
  let meta;
7292
7348
  let nodeYamlRaw;
7293
7349
  try {
@@ -7319,7 +7375,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
7319
7375
  if (!entry.isDirectory()) continue;
7320
7376
  if (entry.name.startsWith(".")) continue;
7321
7377
  await scanModelDirectory(
7322
- path9.join(dirPath, entry.name),
7378
+ path10.join(dirPath, entry.name),
7323
7379
  modelDir,
7324
7380
  node,
7325
7381
  nodes,
@@ -7331,7 +7387,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
7331
7387
  if (!entry.isDirectory()) continue;
7332
7388
  if (entry.name.startsWith(".")) continue;
7333
7389
  await scanModelDirectory(
7334
- path9.join(dirPath, entry.name),
7390
+ path10.join(dirPath, entry.name),
7335
7391
  modelDir,
7336
7392
  null,
7337
7393
  nodes,
@@ -7353,8 +7409,8 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects, parseErrors)
7353
7409
  const entries = await readSortedDir(dirPath);
7354
7410
  const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
7355
7411
  if (hasAspectYaml) {
7356
- const id = toPosixPath(path9.relative(aspectsRoot, dirPath));
7357
- const aspectYamlPath = path9.join(dirPath, "yg-aspect.yaml");
7412
+ const id = toPosixPath(path10.relative(aspectsRoot, dirPath));
7413
+ const aspectYamlPath = path10.join(dirPath, "yg-aspect.yaml");
7358
7414
  const result = await parseAspect(dirPath, aspectYamlPath, id);
7359
7415
  if (result.ok) {
7360
7416
  aspects.push(result.aspect);
@@ -7367,7 +7423,7 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects, parseErrors)
7367
7423
  for (const entry of entries) {
7368
7424
  if (!entry.isDirectory()) continue;
7369
7425
  if (entry.name.startsWith(".")) continue;
7370
- await scanAspectsDirectory(path9.join(dirPath, entry.name), aspectsRoot, aspects, parseErrors);
7426
+ await scanAspectsDirectory(path10.join(dirPath, entry.name), aspectsRoot, aspects, parseErrors);
7371
7427
  }
7372
7428
  }
7373
7429
  async function loadFlows(flowsDir) {
@@ -7376,8 +7432,8 @@ async function loadFlows(flowsDir) {
7376
7432
  const flows = [];
7377
7433
  for (const entry of entries) {
7378
7434
  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);
7435
+ const flowYamlPath = path10.join(flowsDir, entry.name, "yg-flow.yaml");
7436
+ const flow = await parseFlow(path10.join(flowsDir, entry.name), flowYamlPath);
7381
7437
  flows.push(flow);
7382
7438
  }
7383
7439
  return flows;
@@ -7388,7 +7444,7 @@ async function loadSchemas(schemasDir) {
7388
7444
  for (const entry of entries) {
7389
7445
  if (!entry.isFile()) continue;
7390
7446
  if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
7391
- const s = await parseSchema(path9.join(schemasDir, entry.name));
7447
+ const s = await parseSchema(path10.join(schemasDir, entry.name));
7392
7448
  schemas.push(s);
7393
7449
  }
7394
7450
  return schemas;
@@ -7455,7 +7511,7 @@ async function loadGraphOrAbort(rootPath, options = {}) {
7455
7511
  // src/migrations/to-4.0.0.ts
7456
7512
  init_posix();
7457
7513
  import { readdir as readdir4, readFile as readFile11, writeFile as writeFile4, rm as rm3, stat as stat4 } from "fs/promises";
7458
- import path12 from "path";
7514
+ import path13 from "path";
7459
7515
  import { parse as parseYaml8, stringify as stringifyYaml } from "yaml";
7460
7516
  var NODE_ARTIFACTS = ["responsibility.md", "interface.md", "internals.md"];
7461
7517
  function posix(p2) {
@@ -7466,19 +7522,19 @@ async function migrateTo4(yggRoot) {
7466
7522
  const warnings = [];
7467
7523
  await splitArchitecture(yggRoot, actions);
7468
7524
  await cleanConfig(yggRoot, actions);
7469
- const modelDir = path12.join(yggRoot, "model");
7525
+ const modelDir = path13.join(yggRoot, "model");
7470
7526
  if (await dirExists(modelDir)) {
7471
7527
  await processNodesRecursive(modelDir, actions, warnings);
7472
7528
  }
7473
- const flowsDir = path12.join(yggRoot, "flows");
7529
+ const flowsDir = path13.join(yggRoot, "flows");
7474
7530
  if (await dirExists(flowsDir)) {
7475
7531
  await processFlows(flowsDir, actions);
7476
7532
  }
7477
- const aspectsDir = path12.join(yggRoot, "aspects");
7533
+ const aspectsDir = path13.join(yggRoot, "aspects");
7478
7534
  if (await dirExists(aspectsDir)) {
7479
7535
  await processAspects(aspectsDir, actions);
7480
7536
  }
7481
- const driftDir = path12.join(yggRoot, ".drift-state");
7537
+ const driftDir = path13.join(yggRoot, ".drift-state");
7482
7538
  if (await dirExists(driftDir)) {
7483
7539
  await resetDriftState(driftDir, actions);
7484
7540
  }
@@ -7496,18 +7552,18 @@ async function dirExists(dir) {
7496
7552
  }
7497
7553
  }
7498
7554
  async function splitArchitecture(yggRoot, actions) {
7499
- const configPath = path12.join(yggRoot, "yg-config.yaml");
7555
+ const configPath = path13.join(yggRoot, "yg-config.yaml");
7500
7556
  const content14 = await readFile11(configPath, "utf-8");
7501
7557
  const config = parseYaml8(content14);
7502
7558
  if (config.node_types) {
7503
7559
  const archData = { node_types: config.node_types };
7504
- const archPath = path12.join(yggRoot, "yg-architecture.yaml");
7560
+ const archPath = path13.join(yggRoot, "yg-architecture.yaml");
7505
7561
  await writeFile4(archPath, stringifyYaml(archData, { lineWidth: 0 }), "utf-8");
7506
7562
  actions.push("Extracted node_types to yg-architecture.yaml");
7507
7563
  }
7508
7564
  }
7509
7565
  async function cleanConfig(yggRoot, actions) {
7510
- const configPath = path12.join(yggRoot, "yg-config.yaml");
7566
+ const configPath = path13.join(yggRoot, "yg-config.yaml");
7511
7567
  const content14 = await readFile11(configPath, "utf-8");
7512
7568
  const config = parseYaml8(content14);
7513
7569
  let dirty = false;
@@ -7546,7 +7602,7 @@ async function cleanConfig(yggRoot, actions) {
7546
7602
  async function processNodesRecursive(dir, actions, warnings) {
7547
7603
  const entries = await readdir4(dir, { withFileTypes: true });
7548
7604
  for (const entry of entries) {
7549
- const fullPath = path12.join(dir, entry.name);
7605
+ const fullPath = path13.join(dir, entry.name);
7550
7606
  if (entry.isDirectory()) {
7551
7607
  await processNodesRecursive(fullPath, actions, warnings);
7552
7608
  continue;
@@ -7631,7 +7687,7 @@ async function processFlows(dir, actions) {
7631
7687
  const entries = await readdir4(dir, { withFileTypes: true });
7632
7688
  for (const entry of entries) {
7633
7689
  if (!entry.isDirectory()) continue;
7634
- const descPath = path12.join(dir, entry.name, "description.md");
7690
+ const descPath = path13.join(dir, entry.name, "description.md");
7635
7691
  try {
7636
7692
  await rm3(descPath);
7637
7693
  actions.push(`Deleted flow artifact: ${posix(descPath)}`);
@@ -7643,7 +7699,7 @@ async function processAspects(dir, actions) {
7643
7699
  const entries = await readdir4(dir, { withFileTypes: true });
7644
7700
  for (const entry of entries) {
7645
7701
  if (!entry.isDirectory()) continue;
7646
- const aspectPath = path12.join(dir, entry.name, "yg-aspect.yaml");
7702
+ const aspectPath = path13.join(dir, entry.name, "yg-aspect.yaml");
7647
7703
  try {
7648
7704
  const content14 = await readFile11(aspectPath, "utf-8");
7649
7705
  const aspect = parseYaml8(content14);
@@ -7662,7 +7718,7 @@ async function resetDriftState(dir, actions) {
7662
7718
  async function resetDriftStateRecursive(dir, actions) {
7663
7719
  const entries = await readdir4(dir, { withFileTypes: true });
7664
7720
  for (const entry of entries) {
7665
- const fullPath = path12.join(dir, entry.name);
7721
+ const fullPath = path13.join(dir, entry.name);
7666
7722
  if (entry.isDirectory()) {
7667
7723
  await resetDriftStateRecursive(fullPath, actions);
7668
7724
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
@@ -7674,12 +7730,12 @@ async function resetDriftStateRecursive(dir, actions) {
7674
7730
 
7675
7731
  // src/migrations/to-4.3.0.ts
7676
7732
  import { readFile as readFile12, writeFile as writeFile5 } from "fs/promises";
7677
- import path13 from "path";
7733
+ import path14 from "path";
7678
7734
  import { parse as parseYaml9, stringify as stringifyYaml2 } from "yaml";
7679
7735
  async function migrateTo43(yggRoot) {
7680
7736
  const actions = [];
7681
7737
  const warnings = [];
7682
- const archPath = path13.join(yggRoot, "yg-architecture.yaml");
7738
+ const archPath = path14.join(yggRoot, "yg-architecture.yaml");
7683
7739
  let raw;
7684
7740
  try {
7685
7741
  raw = await readFile12(archPath, "utf-8");
@@ -7724,7 +7780,7 @@ See: yg knowledge read working-with-architecture`
7724
7780
 
7725
7781
  // src/migrations/to-5.0.0.ts
7726
7782
  import { readFile as readFile17, writeFile as writeFile6, readdir as readdir8, rm as rm4 } from "fs/promises";
7727
- import path16 from "path";
7783
+ import path17 from "path";
7728
7784
  import { parse as parseYaml12, stringify as stringifyYaml3 } from "yaml";
7729
7785
 
7730
7786
  // src/core/format-detect.ts
@@ -7742,7 +7798,7 @@ init_secrets_parser();
7742
7798
  // src/migrations/aspect-status-defaults.ts
7743
7799
  init_posix();
7744
7800
  import { readFile as readFile14, readdir as readdir5 } from "fs/promises";
7745
- import path14 from "path";
7801
+ import path15 from "path";
7746
7802
  import { parse as parseYaml11 } from "yaml";
7747
7803
  var STATUS_RANK = {
7748
7804
  draft: 0,
@@ -7766,14 +7822,14 @@ function parseAttachmentEntry(raw) {
7766
7822
  return { id, status: parseStatus(obj.status), statusInherit };
7767
7823
  }
7768
7824
  async function walkGraphDirs(rootDir, relPath, yamlName, visit) {
7769
- const dir = path14.join(rootDir, relPath);
7825
+ const dir = path15.join(rootDir, relPath);
7770
7826
  let entries;
7771
7827
  try {
7772
7828
  entries = await readdir5(dir, { withFileTypes: true });
7773
7829
  } catch {
7774
7830
  return;
7775
7831
  }
7776
- const yamlPath = path14.join(dir, yamlName);
7832
+ const yamlPath = path15.join(dir, yamlName);
7777
7833
  const relativeId = toPosix(relPath);
7778
7834
  try {
7779
7835
  await readFile14(yamlPath, "utf-8");
@@ -7782,12 +7838,12 @@ async function walkGraphDirs(rootDir, relPath, yamlName, visit) {
7782
7838
  }
7783
7839
  for (const entry of entries) {
7784
7840
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7785
- await walkGraphDirs(rootDir, path14.join(relPath, entry.name), yamlName, visit);
7841
+ await walkGraphDirs(rootDir, path15.join(relPath, entry.name), yamlName, visit);
7786
7842
  }
7787
7843
  }
7788
7844
  async function loadAspects2(yggRoot) {
7789
7845
  const result = /* @__PURE__ */ new Map();
7790
- const aspectsDir = path14.join(yggRoot, "aspects");
7846
+ const aspectsDir = path15.join(yggRoot, "aspects");
7791
7847
  await walkGraphDirs(aspectsDir, "", "yg-aspect.yaml", async (aspectId, yamlPath) => {
7792
7848
  let raw;
7793
7849
  try {
@@ -7815,7 +7871,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7815
7871
  const bump = (id) => {
7816
7872
  nodeAspectCounts.set(id, (nodeAspectCounts.get(id) ?? 0) + 1);
7817
7873
  };
7818
- const modelDir = path14.join(yggRoot, "model");
7874
+ const modelDir = path15.join(yggRoot, "model");
7819
7875
  await walkGraphDirs(modelDir, "", "yg-node.yaml", async (nodePath, yamlPath) => {
7820
7876
  let raw;
7821
7877
  try {
@@ -7857,7 +7913,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7857
7913
  }
7858
7914
  }
7859
7915
  });
7860
- const archPath = path14.join(yggRoot, "yg-architecture.yaml");
7916
+ const archPath = path15.join(yggRoot, "yg-architecture.yaml");
7861
7917
  let nodeTypes;
7862
7918
  try {
7863
7919
  const archRaw = parseYaml11(await readFile14(archPath, "utf-8"));
@@ -7882,7 +7938,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7882
7938
  });
7883
7939
  }
7884
7940
  }
7885
- const flowsDir = path14.join(yggRoot, "flows");
7941
+ const flowsDir = path15.join(yggRoot, "flows");
7886
7942
  let flowEntries = [];
7887
7943
  try {
7888
7944
  flowEntries = await readdir5(flowsDir, { withFileTypes: true });
@@ -7890,7 +7946,7 @@ async function loadAttachSitesAndNodeAspects(yggRoot) {
7890
7946
  }
7891
7947
  for (const fe of flowEntries) {
7892
7948
  if (!fe.isDirectory()) continue;
7893
- const flowYaml = path14.join(flowsDir, fe.name, "yg-flow.yaml");
7949
+ const flowYaml = path15.join(flowsDir, fe.name, "yg-flow.yaml");
7894
7950
  let flowRaw;
7895
7951
  try {
7896
7952
  const content14 = await readFile14(flowYaml, "utf-8");
@@ -8012,8 +8068,11 @@ function rekeyDriftBaseline(oldFlat) {
8012
8068
  const aspectVerdicts = oldFlat.aspectVerdicts ?? {};
8013
8069
  const state = {
8014
8070
  schemaVersion: DRIFT_STATE_SCHEMA_VERSION,
8015
- // Recompute with the NEW canonical scheme over the SAME logical inputs.
8016
- hash: computeCanonicalHash(realFiles, identity),
8071
+ // Recompute with the NEW canonical scheme over the SAME logical inputs:
8072
+ // files + typed identity + the (possibly synthesized-empty) verdict set that
8073
+ // is also stored below. The verdict fold must use the SAME aspectVerdicts the
8074
+ // baseline persists, so a fresh `yg check` over unchanged source sees no drift.
8075
+ hash: computeCanonicalHash(realFiles, identity, aspectVerdicts),
8017
8076
  files: realFiles,
8018
8077
  identity,
8019
8078
  aspectVerdicts
@@ -8141,7 +8200,7 @@ function transformAspectReviewer(raw) {
8141
8200
  return { value: void 0, warnings, changed: false };
8142
8201
  }
8143
8202
  async function migrateConfigFile(yggRoot, actions, warnings) {
8144
- const configPath = path16.join(yggRoot, "yg-config.yaml");
8203
+ const configPath = path17.join(yggRoot, "yg-config.yaml");
8145
8204
  let content14;
8146
8205
  try {
8147
8206
  content14 = await readFile17(configPath, "utf-8");
@@ -8191,7 +8250,7 @@ async function migrateConfigFile(yggRoot, actions, warnings) {
8191
8250
  return { proceedWithAspects: true };
8192
8251
  }
8193
8252
  async function migrateAllAspects(yggRoot, actions, warnings) {
8194
- const aspectsDir = path16.join(yggRoot, "aspects");
8253
+ const aspectsDir = path17.join(yggRoot, "aspects");
8195
8254
  try {
8196
8255
  await scanAspectsDir(aspectsDir, "", actions, warnings);
8197
8256
  } catch (e) {
@@ -8199,7 +8258,7 @@ async function migrateAllAspects(yggRoot, actions, warnings) {
8199
8258
  }
8200
8259
  }
8201
8260
  async function scanAspectsDir(rootDir, relPath, actions, warnings) {
8202
- const dir = path16.join(rootDir, relPath);
8261
+ const dir = path17.join(rootDir, relPath);
8203
8262
  let entries;
8204
8263
  try {
8205
8264
  entries = await readdir8(dir, { withFileTypes: true });
@@ -8208,7 +8267,7 @@ async function scanAspectsDir(rootDir, relPath, actions, warnings) {
8208
8267
  throw e;
8209
8268
  }
8210
8269
  const aspectId = toPosix(relPath);
8211
- const aspectYamlPath = path16.join(dir, "yg-aspect.yaml");
8270
+ const aspectYamlPath = path17.join(dir, "yg-aspect.yaml");
8212
8271
  let hasAspectYaml = false;
8213
8272
  try {
8214
8273
  await readFile17(aspectYamlPath, "utf-8");
@@ -8221,7 +8280,7 @@ async function scanAspectsDir(rootDir, relPath, actions, warnings) {
8221
8280
  for (const entry of entries) {
8222
8281
  if (!entry.isDirectory()) continue;
8223
8282
  if (entry.name.startsWith(".")) continue;
8224
- await scanAspectsDir(rootDir, path16.join(relPath, entry.name), actions, warnings);
8283
+ await scanAspectsDir(rootDir, path17.join(relPath, entry.name), actions, warnings);
8225
8284
  }
8226
8285
  }
8227
8286
  async function migrateOneAspect(aspectYamlPath, aspectId, actions, warnings) {
@@ -8265,7 +8324,7 @@ async function migrateSecretsFile(yggRoot, _actions, warnings) {
8265
8324
  }
8266
8325
  var DRIFT_STATE_DIR2 = ".drift-state";
8267
8326
  async function scanDriftBaselineNodePaths(driftDir, relDir) {
8268
- const absDir = path16.join(driftDir, relDir);
8327
+ const absDir = path17.join(driftDir, relDir);
8269
8328
  let entries;
8270
8329
  try {
8271
8330
  entries = await readdir8(absDir, { withFileTypes: true });
@@ -8285,8 +8344,8 @@ async function scanDriftBaselineNodePaths(driftDir, relDir) {
8285
8344
  return results;
8286
8345
  }
8287
8346
  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`));
8347
+ const filePath = path17.join(driftDir, `${nodePath}.json`);
8348
+ const displayPath = toPosix(path17.join(DRIFT_STATE_DIR2, `${nodePath}.json`));
8290
8349
  let parsed;
8291
8350
  try {
8292
8351
  const content14 = await readFile17(filePath, "utf-8");
@@ -8330,7 +8389,7 @@ async function migrateOneDriftBaseline(yggRoot, driftDir, nodePath, actions, war
8330
8389
  actions.push(`${displayPath}: re-keyed drift-state baseline to typed format.`);
8331
8390
  }
8332
8391
  async function migrateDriftState(yggRoot, actions, warnings) {
8333
- const driftDir = path16.join(yggRoot, DRIFT_STATE_DIR2);
8392
+ const driftDir = path17.join(yggRoot, DRIFT_STATE_DIR2);
8334
8393
  const nodePaths = await scanDriftBaselineNodePaths(driftDir, "");
8335
8394
  for (const nodePath of nodePaths.sort()) {
8336
8395
  await migrateOneDriftBaseline(yggRoot, driftDir, nodePath, actions, warnings);
@@ -8381,34 +8440,34 @@ init_message_builder();
8381
8440
  init_debug_log();
8382
8441
  init_posix();
8383
8442
  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"))) {
8443
+ let dir = path18.dirname(fileURLToPath2(import.meta.url));
8444
+ while (dir !== path18.dirname(dir)) {
8445
+ if (existsSync2(path18.join(dir, "package.json"))) {
8387
8446
  return dir;
8388
8447
  }
8389
- dir = path17.dirname(dir);
8448
+ dir = path18.dirname(dir);
8390
8449
  }
8391
8450
  throw new Error("Could not locate package root (no package.json found walking up from init module).");
8392
8451
  }
8393
8452
  function getGraphSchemasDir() {
8394
- return path17.join(getPackageRoot(), "graph-schemas");
8453
+ return path18.join(getPackageRoot(), "graph-schemas");
8395
8454
  }
8396
8455
  async function getCliVersion() {
8397
- const pkgPath = path17.join(getPackageRoot(), "package.json");
8456
+ const pkgPath = path18.join(getPackageRoot(), "package.json");
8398
8457
  const pkg2 = JSON.parse(await readFile18(pkgPath, "utf-8"));
8399
8458
  return pkg2.version;
8400
8459
  }
8401
8460
  async function refreshSchemas(yggRoot) {
8402
- const schemasDir = path17.join(yggRoot, "schemas");
8461
+ const schemasDir = path18.join(yggRoot, "schemas");
8403
8462
  await mkdir3(schemasDir, { recursive: true });
8404
8463
  const graphSchemasDir = getGraphSchemasDir();
8405
8464
  try {
8406
8465
  const entries = await readdir9(graphSchemasDir, { withFileTypes: true });
8407
8466
  const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
8408
8467
  for (const file of schemaFiles) {
8409
- const srcPath = path17.join(graphSchemasDir, file);
8468
+ const srcPath = path18.join(graphSchemasDir, file);
8410
8469
  const content14 = await readFile18(srcPath, "utf-8");
8411
- await writeFile7(path17.join(schemasDir, file), content14, "utf-8");
8470
+ await writeFile7(path18.join(schemasDir, file), content14, "utf-8");
8412
8471
  }
8413
8472
  } catch (e) {
8414
8473
  debugWrite(`[init] refreshSchemas schema copy failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -8593,7 +8652,7 @@ async function runReviewerConfigFlow() {
8593
8652
  return { provider, model, apiKey: apiKey || void 0, endpoint };
8594
8653
  }
8595
8654
  async function writeReviewerConfig(yggRoot, config) {
8596
- const configPath = path17.join(yggRoot, "yg-config.yaml");
8655
+ const configPath = path18.join(yggRoot, "yg-config.yaml");
8597
8656
  let raw = {};
8598
8657
  try {
8599
8658
  const content14 = await readFile18(configPath, "utf-8");
@@ -8624,7 +8683,7 @@ async function writeReviewerConfig(yggRoot, config) {
8624
8683
  await writeFile7(configPath, yamlStringify(raw), "utf-8");
8625
8684
  }
8626
8685
  async function writeSecretsFile(yggRoot, provider, apiKey) {
8627
- const secretsPath = path17.join(yggRoot, "yg-secrets.yaml");
8686
+ const secretsPath = path18.join(yggRoot, "yg-secrets.yaml");
8628
8687
  let raw = {};
8629
8688
  try {
8630
8689
  const content14 = await readFile18(secretsPath, "utf-8");
@@ -8647,7 +8706,7 @@ async function writeSecretsFile(yggRoot, provider, apiKey) {
8647
8706
  await writeFile7(secretsPath, yamlStringify(raw), { encoding: "utf-8", mode: 384 });
8648
8707
  }
8649
8708
  async function freshInit(projectRoot) {
8650
- const yggRoot = path17.join(projectRoot, ".yggdrasil");
8709
+ const yggRoot = path18.join(projectRoot, ".yggdrasil");
8651
8710
  if (!isTTY()) {
8652
8711
  process.stderr.write(chalk2.red(`Error: ${buildIssueMessage({
8653
8712
  what: "yg init requires an interactive terminal.",
@@ -8677,19 +8736,19 @@ async function freshInit(projectRoot) {
8677
8736
  p.outro(chalk2.green("Yggdrasil initialized. Run yg check to get started."));
8678
8737
  }
8679
8738
  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");
8739
+ await mkdir3(path18.join(yggRoot, "model"), { recursive: true });
8740
+ await mkdir3(path18.join(yggRoot, "aspects"), { recursive: true });
8741
+ await mkdir3(path18.join(yggRoot, "flows"), { recursive: true });
8742
+ const schemasDir = path18.join(yggRoot, "schemas");
8684
8743
  await mkdir3(schemasDir, { recursive: true });
8685
8744
  const graphSchemasDir = getGraphSchemasDir();
8686
8745
  try {
8687
8746
  const entries = await readdir9(graphSchemasDir, { withFileTypes: true });
8688
8747
  const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
8689
8748
  for (const file of schemaFiles) {
8690
- const srcPath = path17.join(graphSchemasDir, file);
8749
+ const srcPath = path18.join(graphSchemasDir, file);
8691
8750
  const content14 = await readFile18(srcPath, "utf-8");
8692
- await writeFile7(path17.join(schemasDir, file), content14, "utf-8");
8751
+ await writeFile7(path18.join(schemasDir, file), content14, "utf-8");
8693
8752
  }
8694
8753
  } catch (err) {
8695
8754
  debugWrite(`[init] createYggdrasilStructure schema copy failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -8698,9 +8757,9 @@ async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
8698
8757
  `)
8699
8758
  );
8700
8759
  }
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");
8760
+ await writeFile7(path18.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
8761
+ await writeFile7(path18.join(yggRoot, "yg-architecture.yaml"), DEFAULT_ARCHITECTURE, "utf-8");
8762
+ await writeFile7(path18.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
8704
8763
  await installRulesForPlatform(projectRoot, platform);
8705
8764
  }
8706
8765
  async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
@@ -8709,7 +8768,7 @@ async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
8709
8768
  migrations: MIGRATIONS
8710
8769
  });
8711
8770
  await refreshSchemas(yggRoot);
8712
- const architecturePath = path17.join(yggRoot, "yg-architecture.yaml");
8771
+ const architecturePath = path18.join(yggRoot, "yg-architecture.yaml");
8713
8772
  try {
8714
8773
  await stat6(architecturePath);
8715
8774
  } catch (e) {
@@ -8721,7 +8780,7 @@ async function runVersionUpgrade2(projectRoot, yggRoot, platform) {
8721
8780
  return { rulesPath, migrationActions, migrationWarnings, withheld };
8722
8781
  }
8723
8782
  async function existingInit(projectRoot) {
8724
- const yggRoot = path17.join(projectRoot, ".yggdrasil");
8783
+ const yggRoot = path18.join(projectRoot, ".yggdrasil");
8725
8784
  if (!isTTY()) {
8726
8785
  process.stderr.write(chalk2.yellow(buildIssueMessage({
8727
8786
  what: ".yggdrasil/ already exists.",
@@ -8753,7 +8812,7 @@ async function existingInit(projectRoot) {
8753
8812
  p.log.info("2. Run yg approve on all nodes to establish baselines");
8754
8813
  p.outro(
8755
8814
  chalk2.green(
8756
- `Migrated from ${currentVersion} to ${landedVersion}. Rules installed: ${toPosixPath(path17.relative(projectRoot, result.rulesPath))}`
8815
+ `Migrated from ${currentVersion} to ${landedVersion}. Rules installed: ${toPosixPath(path18.relative(projectRoot, result.rulesPath))}`
8757
8816
  )
8758
8817
  );
8759
8818
  return;
@@ -8771,7 +8830,7 @@ async function existingInit(projectRoot) {
8771
8830
  case "upgrade": {
8772
8831
  const platform = await promptPlatform();
8773
8832
  const result = await runVersionUpgrade2(projectRoot, yggRoot, platform);
8774
- p.outro(chalk2.green(`Rules and schemas refreshed: ${toPosixPath(path17.relative(projectRoot, result.rulesPath))}`));
8833
+ p.outro(chalk2.green(`Rules and schemas refreshed: ${toPosixPath(path18.relative(projectRoot, result.rulesPath))}`));
8775
8834
  break;
8776
8835
  }
8777
8836
  case "reviewer": {
@@ -8786,7 +8845,7 @@ async function existingInit(projectRoot) {
8786
8845
  case "platform": {
8787
8846
  const platform = await promptPlatform();
8788
8847
  const rulesPath = await installRulesForPlatform(projectRoot, platform);
8789
- p.outro(chalk2.green(`Platform changed: ${toPosixPath(path17.relative(projectRoot, rulesPath))}`));
8848
+ p.outro(chalk2.green(`Platform changed: ${toPosixPath(path18.relative(projectRoot, rulesPath))}`));
8790
8849
  break;
8791
8850
  }
8792
8851
  }
@@ -8795,7 +8854,7 @@ function registerInitCommand(program2) {
8795
8854
  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
8855
  try {
8797
8856
  const projectRoot = process.cwd();
8798
- const yggRoot = path17.join(projectRoot, ".yggdrasil");
8857
+ const yggRoot = path18.join(projectRoot, ".yggdrasil");
8799
8858
  if (options.upgrade) {
8800
8859
  if (!options.platform) {
8801
8860
  process.stderr.write(
@@ -8875,7 +8934,7 @@ function registerInitCommand(program2) {
8875
8934
  );
8876
8935
  }
8877
8936
  process.stdout.write(
8878
- `Rules and schemas refreshed: ${toPosixPath(path17.relative(projectRoot, result.rulesPath))}
8937
+ `Rules and schemas refreshed: ${toPosixPath(path18.relative(projectRoot, result.rulesPath))}
8879
8938
  `
8880
8939
  );
8881
8940
  return;
@@ -8927,7 +8986,7 @@ init_paths();
8927
8986
  init_graph_fs();
8928
8987
  init_aspects();
8929
8988
  init_posix();
8930
- import path19 from "path";
8989
+ import path20 from "path";
8931
8990
 
8932
8991
  // src/core/graph/index.ts
8933
8992
  init_traversal();
@@ -8956,11 +9015,11 @@ function normPath(p2) {
8956
9015
  }
8957
9016
  function countDependents(graph, nodePath) {
8958
9017
  const paths = [];
8959
- for (const [path44, node] of graph.nodes) {
9018
+ for (const [path46, node] of graph.nodes) {
8960
9019
  const hasRelation = (node.meta.relations ?? []).some(
8961
9020
  (r) => r.target === nodePath && (STRUCTURAL_RELATION_TYPES2.has(r.type) || EVENT_RELATION_TYPES.has(r.type))
8962
9021
  );
8963
- if (hasRelation) paths.push(path44);
9022
+ if (hasRelation) paths.push(path46);
8964
9023
  }
8965
9024
  return { count: paths.length, paths };
8966
9025
  }
@@ -8982,7 +9041,7 @@ function buildNodeContextData(graph, nodePath) {
8982
9041
  name: aspectDef?.name ?? aspectId,
8983
9042
  description: aspectDef?.description ?? "",
8984
9043
  source,
8985
- verifiedAgainst: aspectDef?.reviewer?.type === "deterministic" ? `.yggdrasil/aspects/${aspectId}/check.mjs` : `.yggdrasil/aspects/${aspectId}/content.md`,
9044
+ 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
9045
  implies: aspectDef?.implies,
8987
9046
  status,
8988
9047
  ...refs && { references: refs }
@@ -9039,7 +9098,7 @@ function buildFileContextData(graph, filePath, ownerPath) {
9039
9098
  return {
9040
9099
  aspectId,
9041
9100
  aspectDescription: aspectDef?.description ?? aspectDef?.name ?? aspectId,
9042
- verifiedAgainst: aspectDef?.reviewer?.type === "deterministic" ? `.yggdrasil/aspects/${aspectId}/check.mjs` : `.yggdrasil/aspects/${aspectId}/content.md`,
9101
+ 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
9102
  status,
9044
9103
  ...refs && { references: refs }
9045
9104
  };
@@ -9304,7 +9363,7 @@ function issueMsg(data) {
9304
9363
  init_posix();
9305
9364
 
9306
9365
  // src/core/checks/architecture.ts
9307
- import path20 from "path";
9366
+ import path21 from "path";
9308
9367
 
9309
9368
  // src/core/file-when-evaluator.ts
9310
9369
  import { minimatch } from "minimatch";
@@ -9527,19 +9586,19 @@ function checkArchitectureParentCycles(graph) {
9527
9586
  const color = new Map(typeIds.map((id) => [id, WHITE]));
9528
9587
  const backEdges = /* @__PURE__ */ new Set();
9529
9588
  const recordedCycles = [];
9530
- function dfs(typeId, path44) {
9589
+ function dfs(typeId, path46) {
9531
9590
  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];
9591
+ const cycleStart = path46.indexOf(typeId);
9592
+ if (cycleStart !== -1) recordedCycles.push([...path46.slice(cycleStart), typeId]);
9593
+ const from = path46[path46.length - 1];
9535
9594
  if (from !== void 0) backEdges.add(`${from}->${typeId}`);
9536
9595
  return;
9537
9596
  }
9538
9597
  if (color.get(typeId) === BLACK) return;
9539
9598
  color.set(typeId, GRAY);
9540
- path44.push(typeId);
9541
- for (const parent of types[typeId]?.parents ?? []) dfs(parent, path44);
9542
- path44.pop();
9599
+ path46.push(typeId);
9600
+ for (const parent of types[typeId]?.parents ?? []) dfs(parent, path46);
9601
+ path46.pop();
9543
9602
  color.set(typeId, BLACK);
9544
9603
  }
9545
9604
  for (const id of typeIds) {
@@ -9642,13 +9701,13 @@ ${preview}${ellipsis}`,
9642
9701
  async function checkTypeWhenMismatch(graph, cache) {
9643
9702
  const issues = [];
9644
9703
  const unreadable = [];
9645
- const projectRoot = path20.dirname(graph.rootPath);
9704
+ const projectRoot = path21.dirname(graph.rootPath);
9646
9705
  for (const [nodePath, node] of graph.nodes) {
9647
9706
  const typeDef = graph.architecture.node_types[node.meta.type];
9648
9707
  if (typeDef === void 0 || typeDef.when === void 0) continue;
9649
9708
  const mapping = node.meta.mapping ?? [];
9650
9709
  for (const relPath of mapping) {
9651
- const absPath = path20.join(projectRoot, relPath);
9710
+ const absPath = path21.join(projectRoot, relPath);
9652
9711
  const result = await evaluateFileWhen(typeDef.when, {
9653
9712
  absPath,
9654
9713
  repoRelPath: relPath,
@@ -9876,7 +9935,7 @@ function checkDanglingAspectRefs(graph) {
9876
9935
  ...issueMsg({
9877
9936
  what: `Aspect '${aspectId}' is referenced by this node but not defined in aspects/.`,
9878
9937
  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.`
9938
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9880
9939
  })
9881
9940
  });
9882
9941
  }
@@ -9893,7 +9952,7 @@ function checkDanglingAspectRefs(graph) {
9893
9952
  ...issueMsg({
9894
9953
  what: `Aspect '${aspectId}' is referenced by port '${portName}' but not defined in aspects/.`,
9895
9954
  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.`
9955
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9897
9956
  })
9898
9957
  });
9899
9958
  }
@@ -9911,7 +9970,7 @@ function checkDanglingAspectRefs(graph) {
9911
9970
  ...issueMsg({
9912
9971
  what: `Aspect '${aspectId}' is referenced by architecture type '${typeId}' but not defined in aspects/.`,
9913
9972
  why: `Architecture declares a required aspect that does not exist.`,
9914
- next: `Create aspects/${aspectId}/ with yg-aspect.yaml and content.md.`
9973
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9915
9974
  })
9916
9975
  });
9917
9976
  }
@@ -9927,7 +9986,7 @@ function checkDanglingAspectRefs(graph) {
9927
9986
  ...issueMsg({
9928
9987
  what: `Aspect '${aspectId}' is referenced by flow '${flow.name}' but not defined in aspects/.`,
9929
9988
  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.`
9989
+ next: `Create the aspects/${aspectId} directory with yg-aspect.yaml and content.md.`
9931
9990
  })
9932
9991
  });
9933
9992
  }
@@ -10223,17 +10282,45 @@ init_graph();
10223
10282
  init_graph_fs();
10224
10283
  init_aspects();
10225
10284
  init_tier_selection();
10226
- import path21 from "path";
10285
+ import path22 from "path";
10227
10286
  init_posix();
10228
10287
  function checkAspectRuleSources(graph) {
10229
10288
  const issues = [];
10230
- const projectRoot = path21.dirname(graph.rootPath);
10289
+ const projectRoot = path22.dirname(graph.rootPath);
10231
10290
  for (const aspect of graph.aspects) {
10232
10291
  const reviewer = aspect.reviewer.type;
10292
+ const aspectDir = path22.join(projectRoot, ".yggdrasil", "aspects", aspect.id);
10293
+ const hasContentMd = fileExistsSync(path22.join(aspectDir, "content.md"));
10294
+ const hasCheckMjs = fileExistsSync(path22.join(aspectDir, "check.mjs"));
10295
+ if (reviewer === "aggregate") {
10296
+ if (hasContentMd || hasCheckMjs) {
10297
+ const present = [hasContentMd ? "content.md" : null, hasCheckMjs ? "check.mjs" : null].filter((f) => f !== null).join(" and ");
10298
+ issues.push({
10299
+ severity: "error",
10300
+ code: "aspect-unexpected-rule-source",
10301
+ rule: "aspect-rule-sources",
10302
+ ...issueMsg({
10303
+ what: `Aspect '${aspect.id}' is an aggregating aspect (no reviewer.type declared, only implies) but ships ${present}.`,
10304
+ why: `Aggregating aspects bundle implied aspects and have no own reviewer; a rule source here is never read.`,
10305
+ next: `Remove .yggdrasil/aspects/${aspect.id}/${present} to keep it aggregating, or declare reviewer.type explicitly to make it an LLM/deterministic aspect.`
10306
+ })
10307
+ });
10308
+ }
10309
+ if (!aspect.implies || aspect.implies.length === 0) {
10310
+ issues.push({
10311
+ severity: "error",
10312
+ code: "aspect-empty",
10313
+ rule: "aspect-rule-sources",
10314
+ ...issueMsg({
10315
+ what: `Aspect '${aspect.id}' has no content.md, no check.mjs, and no implies \u2014 it does nothing.`,
10316
+ 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.`,
10317
+ 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.`
10318
+ })
10319
+ });
10320
+ }
10321
+ continue;
10322
+ }
10233
10323
  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
10324
  if (hasContentMd && hasCheckMjs) {
10238
10325
  issues.push({
10239
10326
  severity: "error",
@@ -10361,7 +10448,7 @@ function formatBytes(n) {
10361
10448
  return `${Math.round(n / 1024)} KiB`;
10362
10449
  }
10363
10450
  async function checkAspectReferences(graph) {
10364
- const projectRoot = path21.dirname(graph.rootPath);
10451
+ const projectRoot = path22.dirname(graph.rootPath);
10365
10452
  const issues = [];
10366
10453
  for (const aspect of graph.aspects) {
10367
10454
  if (aspect.reviewer.type !== "llm") continue;
@@ -10395,7 +10482,7 @@ async function checkAspectReferences(graph) {
10395
10482
  const tierLabel = tierName != null ? `for tier '${tierName}'` : "for the resolved tier";
10396
10483
  let totalBytes = 0;
10397
10484
  for (const ref of aspect.references) {
10398
- const absPath = path21.join(projectRoot, ref.path);
10485
+ const absPath = path22.join(projectRoot, ref.path);
10399
10486
  const refPath = toPosixPath(ref.path);
10400
10487
  let stats;
10401
10488
  try {
@@ -10542,16 +10629,16 @@ init_hash();
10542
10629
  init_graph_fs();
10543
10630
  init_repo_scanner();
10544
10631
  init_aspects();
10545
- import path22 from "path";
10632
+ import path23 from "path";
10546
10633
  init_posix();
10547
10634
  async function checkFileMappingGitignored(graph) {
10548
- const projectRoot = path22.dirname(graph.rootPath);
10635
+ const projectRoot = path23.dirname(graph.rootPath);
10549
10636
  const tracked = new Set(await walkRepoFiles(projectRoot));
10550
10637
  const issues = [];
10551
10638
  for (const [nodePath, node] of graph.nodes) {
10552
10639
  const mapping = node.meta.mapping ?? [];
10553
10640
  for (const relPath of mapping) {
10554
- const absPath = path22.join(projectRoot, relPath);
10641
+ const absPath = path23.join(projectRoot, relPath);
10555
10642
  let st;
10556
10643
  try {
10557
10644
  st = await statPath(absPath);
@@ -10585,7 +10672,7 @@ async function checkStrictBackwardCoverage(graph, cache) {
10585
10672
  ([, def]) => def.enforce === "strict" && def.when !== void 0
10586
10673
  );
10587
10674
  if (strictTypes.length === 0) return { issues: [], unreadable: [] };
10588
- const projectRoot = path22.dirname(graph.rootPath);
10675
+ const projectRoot = path23.dirname(graph.rootPath);
10589
10676
  const fileToOwner = /* @__PURE__ */ new Map();
10590
10677
  for (const [nodePath, node] of graph.nodes) {
10591
10678
  for (const relPath of node.meta.mapping ?? []) {
@@ -10598,7 +10685,7 @@ async function checkStrictBackwardCoverage(graph, cache) {
10598
10685
  const overlapPairsSeen = /* @__PURE__ */ new Set();
10599
10686
  for (const rawRel of repoFiles) {
10600
10687
  const relPath = normalizePathForCompare(rawRel);
10601
- const absPath = path22.join(projectRoot, relPath);
10688
+ const absPath = path23.join(projectRoot, relPath);
10602
10689
  const matchingTypes = [];
10603
10690
  let fileSkipped = false;
10604
10691
  for (const [typeId2, def] of strictTypes) {
@@ -10749,11 +10836,11 @@ function checkMappingOverlap(graph) {
10749
10836
  }
10750
10837
  async function checkMappingPathsExist(graph) {
10751
10838
  const issues = [];
10752
- const projectRoot = path22.dirname(graph.rootPath);
10839
+ const projectRoot = path23.dirname(graph.rootPath);
10753
10840
  for (const [nodePath, node] of graph.nodes) {
10754
10841
  const mappingPaths = normalizeMappingPaths(node.meta.mapping).map(normalizePathForCompare);
10755
10842
  for (const mp of mappingPaths) {
10756
- const absPath = path22.join(projectRoot, mp);
10843
+ const absPath = path23.join(projectRoot, mp);
10757
10844
  try {
10758
10845
  await fileAccess(absPath);
10759
10846
  } catch {
@@ -10775,13 +10862,13 @@ async function checkMappingPathsExist(graph) {
10775
10862
  }
10776
10863
  function checkMappingEscapesRepo(graph) {
10777
10864
  const issues = [];
10778
- const projectRoot = path22.dirname(graph.rootPath);
10865
+ const projectRoot = path23.dirname(graph.rootPath);
10779
10866
  for (const [nodePath, node] of graph.nodes) {
10780
10867
  for (const raw of node.meta.mapping ?? []) {
10781
10868
  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("../")) {
10869
+ const resolved = path23.resolve(projectRoot, norm);
10870
+ const rel = normalizePathForCompare(path23.relative(projectRoot, resolved));
10871
+ if (path23.isAbsolute(norm) || rel === ".." || rel.startsWith("../")) {
10785
10872
  issues.push({
10786
10873
  severity: "error",
10787
10874
  code: "mapping-escapes-repo",
@@ -10830,7 +10917,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
10830
10917
  async function checkOversizedNodes(graph, cache) {
10831
10918
  const issues = [];
10832
10919
  const maxChars = graph.config.quality?.max_node_chars ?? 4e4;
10833
- const projectRoot = path22.dirname(graph.rootPath);
10920
+ const projectRoot = path23.dirname(graph.rootPath);
10834
10921
  const refsByAspect = /* @__PURE__ */ new Map();
10835
10922
  for (const aspect of graph.aspects) {
10836
10923
  if (aspect.references?.length) {
@@ -10838,8 +10925,8 @@ async function checkOversizedNodes(graph, cache) {
10838
10925
  }
10839
10926
  }
10840
10927
  async function charsOf(repoRelPath) {
10841
- if (BINARY_EXTENSIONS.has(path22.extname(repoRelPath).toLowerCase())) return 0;
10842
- const abs = path22.resolve(projectRoot, repoRelPath);
10928
+ if (BINARY_EXTENSIONS.has(path23.extname(repoRelPath).toLowerCase())) return 0;
10929
+ const abs = path23.resolve(projectRoot, repoRelPath);
10843
10930
  const res = await cache.read(abs);
10844
10931
  if (res.content !== void 0) return res.content.length;
10845
10932
  if (res.tooLarge) {
@@ -10886,7 +10973,7 @@ async function checkOversizedNodes(graph, cache) {
10886
10973
  }
10887
10974
  async function checkDirectoriesHaveNodeYaml(graph) {
10888
10975
  const issues = [];
10889
- const modelDir = path22.join(graph.rootPath, "model");
10976
+ const modelDir = path23.join(graph.rootPath, "model");
10890
10977
  async function scanDir(dirPath, segments) {
10891
10978
  const entries = await readSortedDir(dirPath);
10892
10979
  const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
@@ -10910,7 +10997,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
10910
10997
  for (const entry of entries) {
10911
10998
  if (!entry.isDirectory()) continue;
10912
10999
  if (entry.name.startsWith(".")) continue;
10913
- await scanDir(path22.join(dirPath, entry.name), [...segments, entry.name]);
11000
+ await scanDir(path23.join(dirPath, entry.name), [...segments, entry.name]);
10914
11001
  }
10915
11002
  }
10916
11003
  try {
@@ -10918,7 +11005,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
10918
11005
  for (const entry of rootEntries) {
10919
11006
  if (!entry.isDirectory()) continue;
10920
11007
  if (entry.name.startsWith(".")) continue;
10921
- await scanDir(path22.join(modelDir, entry.name), [entry.name]);
11008
+ await scanDir(path23.join(modelDir, entry.name), [entry.name]);
10922
11009
  }
10923
11010
  } catch {
10924
11011
  }
@@ -11366,7 +11453,7 @@ async function validate(graph, scope = "all") {
11366
11453
  }
11367
11454
 
11368
11455
  // src/cli/owner.ts
11369
- import path23 from "path";
11456
+ import path24 from "path";
11370
11457
  import { access as access2 } from "fs/promises";
11371
11458
  init_debug_log();
11372
11459
  init_message_builder();
@@ -11402,7 +11489,7 @@ function registerOwnerCommand(program2) {
11402
11489
  const repoRelative = resolveFileArg(repoRoot, options.file);
11403
11490
  const result = findOwner(graph, repoRoot, repoRelative);
11404
11491
  if (!result.nodePath) {
11405
- const absPath = path23.resolve(repoRoot, result.file);
11492
+ const absPath = path24.resolve(repoRoot, result.file);
11406
11493
  let exists = true;
11407
11494
  try {
11408
11495
  await access2(absPath);
@@ -11599,7 +11686,7 @@ ${errorList}`
11599
11686
 
11600
11687
  // src/cli/approve.ts
11601
11688
  import chalk4 from "chalk";
11602
- import path34 from "path";
11689
+ import path35 from "path";
11603
11690
  init_debug_log();
11604
11691
  init_approve();
11605
11692
  init_approve_reviewer();
@@ -11615,7 +11702,7 @@ init_paths();
11615
11702
  init_aspects();
11616
11703
  init_graph_fs();
11617
11704
  init_log_integrity();
11618
- import path33 from "path";
11705
+ import path34 from "path";
11619
11706
 
11620
11707
  // src/core/check-codes.ts
11621
11708
  var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
@@ -11645,8 +11732,10 @@ var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
11645
11732
  "when-unknown-port",
11646
11733
  "aspect-unexpected-rule-source",
11647
11734
  "aspect-missing-rule-source",
11735
+ "aspect-empty",
11648
11736
  "file-unreadable",
11649
11737
  "aspect-references-on-deterministic",
11738
+ "aspect-references-on-aggregate",
11650
11739
  "aspect-reference-broken",
11651
11740
  "aspect-reference-too-large",
11652
11741
  "aspect-references-total-too-large",
@@ -11655,7 +11744,13 @@ var STRUCTURAL_CODES = /* @__PURE__ */ new Set([
11655
11744
  "aspect-reference-escape",
11656
11745
  "aspect-reference-duplicate",
11657
11746
  "aspect-tier-unknown",
11658
- "mapping-escapes-repo"
11747
+ "mapping-escapes-repo",
11748
+ // Drift-state integrity: the recorded baseline hash for a node does not match
11749
+ // a recompute over its files, typed identity, and stored verdicts, yet no file
11750
+ // or identity change explains the divergence (the stored verdicts were tampered
11751
+ // or the baseline predates a hash-scheme change). Structural because it blocks
11752
+ // CI regardless of source-file drift — the baseline itself is untrustworthy.
11753
+ "baseline-integrity"
11659
11754
  ]);
11660
11755
  var COMPLETENESS_CODES = /* @__PURE__ */ new Set(["description-missing"]);
11661
11756
 
@@ -11663,7 +11758,7 @@ var COMPLETENESS_CODES = /* @__PURE__ */ new Set(["description-missing"]);
11663
11758
  init_log_format();
11664
11759
  init_posix();
11665
11760
  async function classifyDrift(graph) {
11666
- const projectRoot = path33.dirname(graph.rootPath);
11761
+ const projectRoot = path34.dirname(graph.rootPath);
11667
11762
  const issues = [];
11668
11763
  for (const [nodePath, node] of graph.nodes) {
11669
11764
  const mappingPaths = normalizeMappingPaths(node.meta.mapping);
@@ -11733,7 +11828,10 @@ ${mappingPaths.map((p2) => " " + p2).join("\n")}`,
11733
11828
  trackedFiles,
11734
11829
  storedFileData,
11735
11830
  excludePrefixes,
11736
- identity
11831
+ identity,
11832
+ storedEntry.aspectVerdicts,
11833
+ false
11834
+ // never reuse stored hashes by mtime in the check gate — always re-hash content
11737
11835
  );
11738
11836
  if (canonicalHash === storedEntry.hash) return;
11739
11837
  const resolveLayer = buildLayerResolver(trackedFiles);
@@ -11791,6 +11889,21 @@ ${mappingPaths.map((p2) => " " + p2).join("\n")}`,
11791
11889
  }
11792
11890
  }
11793
11891
  }
11892
+ if (directChanges.length === 0 && cascadeCauses.length === 0) {
11893
+ const baselineIntegrityMd = {
11894
+ what: `Recorded baseline hash for '${nodePath}' does not match a recompute over its files, identity, and verdicts.`,
11895
+ 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.",
11896
+ next: `yg approve --node ${nodePath} to re-establish the baseline, or restore it from git: git checkout HEAD -- .yggdrasil/.drift-state/${nodePath}.json`
11897
+ };
11898
+ issues.push({
11899
+ severity: "error",
11900
+ code: "baseline-integrity",
11901
+ rule: "baseline-integrity",
11902
+ messageData: baselineIntegrityMd,
11903
+ nodePath
11904
+ });
11905
+ return;
11906
+ }
11794
11907
  if (directChanges.length > 0) {
11795
11908
  const sourceFiles = directChanges.filter((f) => f.category === "source").map((f) => f.filePath);
11796
11909
  const sourceDriftMd = {
@@ -11842,7 +11955,7 @@ Verify source compliance, update if needed, then: yg approve --node ${nodePath}`
11842
11955
  async function classifyLogState(graph, projectRoot, issues) {
11843
11956
  for (const [nodePath] of graph.nodes) {
11844
11957
  const logRel = `.yggdrasil/model/${nodePath}/log.md`;
11845
- const logAbs = path33.join(projectRoot, logRel);
11958
+ const logAbs = path34.join(projectRoot, logRel);
11846
11959
  let logContent = null;
11847
11960
  try {
11848
11961
  logContent = await readTextFile(logAbs);
@@ -11896,8 +12009,8 @@ function scanUncoveredFiles(graph, gitTrackedFiles) {
11896
12009
  const paths = normalizeMappingPaths(node.meta.mapping);
11897
12010
  allMappings.push(...paths);
11898
12011
  }
11899
- const projectRoot = path33.dirname(graph.rootPath);
11900
- const yggPrefix = toPosixPath(path33.relative(projectRoot, graph.rootPath));
12012
+ const projectRoot = path34.dirname(graph.rootPath);
12013
+ const yggPrefix = toPosixPath(path34.relative(projectRoot, graph.rootPath));
11901
12014
  const uncovered = [];
11902
12015
  for (const file of gitTrackedFiles) {
11903
12016
  const normalized = toPosixPath(file.trim());
@@ -11965,8 +12078,8 @@ async function runCheck(graph, gitTrackedFiles) {
11965
12078
  let coveredFiles = 0;
11966
12079
  let totalFiles = 0;
11967
12080
  if (gitTrackedFiles !== null) {
11968
- const projectRoot = path33.dirname(graph.rootPath);
11969
- const yggPrefix = toPosixPath(path33.relative(projectRoot, graph.rootPath));
12081
+ const projectRoot = path34.dirname(graph.rootPath);
12082
+ const yggPrefix = toPosixPath(path34.relative(projectRoot, graph.rootPath));
11970
12083
  const sourceFiles = gitTrackedFiles.filter((f) => {
11971
12084
  const normalized = toPosixPath(f.trim());
11972
12085
  return !normalized.startsWith(yggPrefix + "/") && normalized !== yggPrefix;
@@ -11985,7 +12098,7 @@ async function runCheck(graph, gitTrackedFiles) {
11985
12098
  return n ? hasNonDraftEffectiveAspects(n, graph) : false;
11986
12099
  }
11987
12100
  );
11988
- const yggRelative = toPosixPath(path33.relative(path33.dirname(graph.rootPath), graph.rootPath));
12101
+ const yggRelative = toPosixPath(path34.relative(path34.dirname(graph.rootPath), graph.rootPath));
11989
12102
  const orphanWarnings = orphanedPaths.map((p2) => {
11990
12103
  const orphanMd = {
11991
12104
  what: `Drift state file exists for '${p2}' but node is no longer in the graph.`,
@@ -12015,7 +12128,7 @@ async function runCheck(graph, gitTrackedFiles) {
12015
12128
  const advisoryWarnings = allIssues.filter((i) => i.code === "aspect-violation-advisory").length;
12016
12129
  const draftSkipped = countDraftAspectsAcrossGraph(graph);
12017
12130
  return {
12018
- projectName: path33.basename(path33.dirname(graph.rootPath)),
12131
+ projectName: path34.basename(path34.dirname(graph.rootPath)),
12019
12132
  nodeCount: graph.nodes.size,
12020
12133
  nodeTypeCounts,
12021
12134
  aspectCount: graph.aspects.length,
@@ -12033,6 +12146,7 @@ function emitPerAspectIssues(node, graph, baseline, issues) {
12033
12146
  const storedVerdicts = baseline.aspectVerdicts;
12034
12147
  for (const [aspectId, status] of statuses) {
12035
12148
  if (status === "draft") continue;
12149
+ if (isAggregateAspect(graph, aspectId)) continue;
12036
12150
  const verdict = storedVerdicts[aspectId];
12037
12151
  if (!verdict) {
12038
12152
  const md = aspectNewlyActiveMessage({
@@ -12106,7 +12220,7 @@ function getChildMappingExclusions2(graph, nodePath) {
12106
12220
  async function allPathsMissing(projectRoot, mappingPaths) {
12107
12221
  for (const mp of mappingPaths) {
12108
12222
  try {
12109
- await fileAccess(path33.join(projectRoot, mp));
12223
+ await fileAccess(path34.join(projectRoot, mp));
12110
12224
  return false;
12111
12225
  } catch {
12112
12226
  }
@@ -12115,7 +12229,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
12115
12229
  }
12116
12230
  function groupCascadeByCause(cascadeErrors, graph) {
12117
12231
  const groups = /* @__PURE__ */ new Map();
12118
- const yggPrefix = graph ? toPosixPath(path33.relative(path33.dirname(graph.rootPath), graph.rootPath)) : ".yggdrasil";
12232
+ const yggPrefix = graph ? toPosixPath(path34.relative(path34.dirname(graph.rootPath), graph.rootPath)) : ".yggdrasil";
12119
12233
  const escPrefix = yggPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12120
12234
  for (const issue of cascadeErrors) {
12121
12235
  if (!issue.nodePath || !issue.cascadeCauses) continue;
@@ -12534,7 +12648,7 @@ async function runDryRunForNode(params) {
12534
12648
  const { buildPrompt: buildPrompt2 } = await Promise.resolve().then(() => (init_aspect_verifier(), aspect_verifier_exports));
12535
12649
  const aspects = resolveAspects(node, graph);
12536
12650
  const statuses = computeEffectiveAspectStatuses(node, graph);
12537
- const projectRoot = path34.dirname(graph.rootPath);
12651
+ const projectRoot = path35.dirname(graph.rootPath);
12538
12652
  const { trackedFiles } = collectTrackedFiles(node, graph);
12539
12653
  const { fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, void 0, []);
12540
12654
  const sourceFilePaths = Object.keys(fileHashes).filter((f) => {
@@ -12565,7 +12679,7 @@ async function runDryRunForNode(params) {
12565
12679
  }
12566
12680
  try {
12567
12681
  const structResult = await runStructureAspect({
12568
- aspectDir: path34.join(".yggdrasil/aspects", aspect.id),
12682
+ aspectDir: path35.join(".yggdrasil/aspects", aspect.id),
12569
12683
  aspectId: aspect.id,
12570
12684
  nodePath,
12571
12685
  graph,
@@ -12754,7 +12868,7 @@ function registerApproveCommand(program2) {
12754
12868
  }
12755
12869
  const graph = await loadGraphOrAbort(process.cwd());
12756
12870
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
12757
- const yggPrefix = toPosixPath(path34.relative(path34.dirname(graph.rootPath), graph.rootPath));
12871
+ const yggPrefix = toPosixPath(path35.relative(path35.dirname(graph.rootPath), graph.rootPath));
12758
12872
  if (options.dryRun && options.node) {
12759
12873
  let allFound = true;
12760
12874
  for (const rawPath of options.node) {
@@ -12883,11 +12997,11 @@ function registerTreeCommand(program2) {
12883
12997
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
12884
12998
  let roots;
12885
12999
  if (options.root?.trim()) {
12886
- const path44 = options.root.trim().replace(/\/$/, "");
12887
- const node = graph.nodes.get(path44);
13000
+ const path46 = options.root.trim().replace(/\/$/, "");
13001
+ const node = graph.nodes.get(path46);
12888
13002
  if (!node) {
12889
13003
  process.stderr.write(chalk5.red(buildIssueMessage({
12890
- what: `Node '${path44}' not found.`,
13004
+ what: `Node '${path46}' not found.`,
12891
13005
  why: `The --root path must be a valid node path in the graph.`,
12892
13006
  next: `Run yg tree (no --root) to list all nodes, then pick a valid path.`
12893
13007
  }) + "\n"));
@@ -12987,14 +13101,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
12987
13101
  }
12988
13102
  const chains = [];
12989
13103
  for (const node of transitiveOnly) {
12990
- const path44 = [];
13104
+ const path46 = [];
12991
13105
  let current = node;
12992
13106
  while (current) {
12993
- path44.unshift(current);
13107
+ path46.unshift(current);
12994
13108
  current = parent.get(current);
12995
13109
  }
12996
- if (path44.length >= 3) {
12997
- chains.push(path44.slice(1).map((p2) => `<- ${p2}`).join(" "));
13110
+ if (path46.length >= 3) {
13111
+ chains.push(path46.slice(1).map((p2) => `<- ${p2}`).join(" "));
12998
13112
  }
12999
13113
  }
13000
13114
  return chains.sort();
@@ -13026,14 +13140,14 @@ function collectIndirectDependents(graph, directlyAffected) {
13026
13140
  }
13027
13141
  for (const [node] of parent) {
13028
13142
  if (directSet.has(node)) continue;
13029
- const path44 = [node];
13143
+ const path46 = [node];
13030
13144
  let current = node;
13031
13145
  while (parent.has(current)) {
13032
13146
  current = parent.get(current);
13033
- path44.push(current);
13147
+ path46.push(current);
13034
13148
  }
13035
- const chain = path44.map((p2) => `<- ${p2}`).join(" ");
13036
- const depth = path44.length;
13149
+ const chain = path46.map((p2) => `<- ${p2}`).join(" ");
13150
+ const depth = path46.length;
13037
13151
  const existing = bestChain.get(node);
13038
13152
  if (!existing || depth < existing.depth) {
13039
13153
  bestChain.set(node, { chain, depth });
@@ -13773,7 +13887,7 @@ import chalk8 from "chalk";
13773
13887
  init_debug_log();
13774
13888
  init_message_builder();
13775
13889
  import { execFileSync } from "child_process";
13776
- import path35 from "path";
13890
+ import path36 from "path";
13777
13891
  function registerCheckCommand(program2) {
13778
13892
  program2.command("check").description("Unified graph gate \u2014 errors, drift, coverage, completeness").action(async () => {
13779
13893
  try {
@@ -13782,7 +13896,7 @@ function registerCheckCommand(program2) {
13782
13896
  initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
13783
13897
  let gitFiles = null;
13784
13898
  try {
13785
- const projectRoot = path35.dirname(graph.rootPath);
13899
+ const projectRoot = path36.dirname(graph.rootPath);
13786
13900
  const output = execFileSync("git", ["ls-files", "."], {
13787
13901
  cwd: projectRoot,
13788
13902
  encoding: "utf-8",
@@ -14010,7 +14124,7 @@ function sortByNodePath(issues) {
14010
14124
  }
14011
14125
 
14012
14126
  // src/cli/deterministic-test.ts
14013
- import path37 from "path";
14127
+ import path38 from "path";
14014
14128
  init_debug_log();
14015
14129
 
14016
14130
  // src/ast/runner.ts
@@ -14018,7 +14132,7 @@ init_loader_hook();
14018
14132
  init_parser();
14019
14133
  init_suppress();
14020
14134
  init_validate_check_module();
14021
- import path36 from "path";
14135
+ import path37 from "path";
14022
14136
  import { readFile as readFile20 } from "fs/promises";
14023
14137
  import { pathToFileURL as pathToFileURL3 } from "url";
14024
14138
  var AstRunnerError = class extends Error {
@@ -14034,7 +14148,7 @@ ${data.next}`);
14034
14148
  };
14035
14149
  async function runAstAspect(params) {
14036
14150
  ensureLoaderRegistered();
14037
- const checkPath = path36.resolve(params.projectRoot, params.aspectDir, "check.mjs");
14151
+ const checkPath = path37.resolve(params.projectRoot, params.aspectDir, "check.mjs");
14038
14152
  let mod;
14039
14153
  try {
14040
14154
  mod = await import(pathToFileURL3(checkPath).href);
@@ -14064,7 +14178,7 @@ async function runAstAspect(params) {
14064
14178
  sourceFiles.push({ path: f.path, content: cached.content, ast: cached.ast });
14065
14179
  continue;
14066
14180
  }
14067
- const content14 = await readFile20(path36.resolve(params.projectRoot, f.path), "utf-8");
14181
+ const content14 = await readFile20(path37.resolve(params.projectRoot, f.path), "utf-8");
14068
14182
  let ast;
14069
14183
  try {
14070
14184
  ast = await parseFile(f.path, content14);
@@ -14196,7 +14310,7 @@ function registerDeterministicTestCommand(program2) {
14196
14310
  process.exit(1);
14197
14311
  return;
14198
14312
  }
14199
- const aspectDir = path37.join(".yggdrasil", "aspects", aspect.id);
14313
+ const aspectDir = path38.join(".yggdrasil", "aspects", aspect.id);
14200
14314
  if (hasNode) {
14201
14315
  const nodePath = opts.node.trim().replace(/\/$/, "");
14202
14316
  const node = graph.nodes.get(nodePath);
@@ -14324,12 +14438,12 @@ function printStructureViolations(violations) {
14324
14438
  // src/cli/log.ts
14325
14439
  import chalk9 from "chalk";
14326
14440
  import { readFile as readFile22, stat as stat8 } from "fs/promises";
14327
- import path41 from "path";
14441
+ import path42 from "path";
14328
14442
  init_debug_log();
14329
14443
  init_message_builder();
14330
14444
 
14331
14445
  // src/core/log/log-add.ts
14332
- import path38 from "path";
14446
+ import path39 from "path";
14333
14447
 
14334
14448
  // src/utils/node-path-validator.ts
14335
14449
  init_posix();
@@ -14450,7 +14564,7 @@ async function logAdd(input) {
14450
14564
  }
14451
14565
  };
14452
14566
  }
14453
- const logPath2 = path38.join(graph.rootPath, "model", nodePath, "log.md");
14567
+ const logPath2 = path39.join(graph.rootPath, "model", nodePath, "log.md");
14454
14568
  const stats = await statLogFile(logPath2);
14455
14569
  if (stats !== null) {
14456
14570
  if (stats.isSymbolicLink) {
@@ -14519,7 +14633,7 @@ function reasonHasLevel2HeaderOutsideFence(reason) {
14519
14633
  }
14520
14634
 
14521
14635
  // src/core/log/log-read.ts
14522
- import path39 from "path";
14636
+ import path40 from "path";
14523
14637
  init_log_parser();
14524
14638
  init_log_format();
14525
14639
  init_posix();
@@ -14568,7 +14682,7 @@ async function logRead(input) {
14568
14682
  }
14569
14683
  };
14570
14684
  }
14571
- const logPath2 = path39.join(graph.rootPath, "model", nodePath, "log.md");
14685
+ const logPath2 = path40.join(graph.rootPath, "model", nodePath, "log.md");
14572
14686
  const content14 = await readLogSafe(logPath2);
14573
14687
  if (content14 === "") {
14574
14688
  return { ok: true, entries: [] };
@@ -14592,7 +14706,7 @@ async function logRead(input) {
14592
14706
 
14593
14707
  // src/core/log/log-merge-resolve.ts
14594
14708
  import { createHash as createHash5 } from "crypto";
14595
- import path40 from "path";
14709
+ import path41 from "path";
14596
14710
  init_log_parser();
14597
14711
 
14598
14712
  // src/utils/git-introspect.ts
@@ -14679,7 +14793,7 @@ async function logMergeResolve(input) {
14679
14793
  }
14680
14794
  };
14681
14795
  }
14682
- const logPath2 = path40.join(yggRoot, "model", nodePath, "log.md");
14796
+ const logPath2 = path41.join(yggRoot, "model", nodePath, "log.md");
14683
14797
  let currentLog;
14684
14798
  try {
14685
14799
  currentLog = await readTextFile(logPath2);
@@ -14885,7 +14999,7 @@ ${entry.body}`);
14885
14999
  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
15000
  try {
14887
15001
  const graph = await loadGraphOrAbort(process.cwd(), { tolerateInvalidConfig: true });
14888
- const repoRoot = path41.dirname(graph.rootPath);
15002
+ const repoRoot = path42.dirname(graph.rootPath);
14889
15003
  const nodePath = toPosix(opts.node.trim()).replace(/\/$/, "");
14890
15004
  const result = await logMergeResolve({ graph, nodePath, repoRoot });
14891
15005
  if (!result.ok) {
@@ -14912,15 +15026,15 @@ import chalk10 from "chalk";
14912
15026
  // src/io/find-index.ts
14913
15027
  init_debug_log();
14914
15028
  import { readFile as readFile23, lstat as lstat3 } from "fs/promises";
14915
- import path42 from "path";
15029
+ import path43 from "path";
14916
15030
  import MiniSearch from "minisearch";
14917
15031
  var MAX_BODY_BYTES = 1048576;
14918
15032
  async function buildIndex(graph) {
14919
- const projectRoot = path42.dirname(graph.rootPath);
15033
+ const projectRoot = path43.dirname(graph.rootPath);
14920
15034
  const docs = [];
14921
15035
  for (const [nodePath, node] of graph.nodes) {
14922
15036
  const displayPath = `model/${nodePath}/`;
14923
- const logPath2 = path42.join(graph.rootPath, "model", nodePath, "log.md");
15037
+ const logPath2 = path43.join(graph.rootPath, "model", nodePath, "log.md");
14924
15038
  let body = "";
14925
15039
  try {
14926
15040
  const st = await lstat3(logPath2);
@@ -14937,7 +15051,7 @@ This does not affect append-only integrity. No action required.
14937
15051
  }
14938
15052
  body = truncated;
14939
15053
  } else if (st.isSymbolicLink()) {
14940
- process.stderr.write(`Warning: skipping symlinked log.md at ${path42.relative(projectRoot, logPath2)}
15054
+ process.stderr.write(`Warning: skipping symlinked log.md at ${path43.relative(projectRoot, logPath2)}
14941
15055
  `);
14942
15056
  } else {
14943
15057
  }
@@ -15094,14 +15208,14 @@ import { existsSync as existsSync6 } from "fs";
15094
15208
  import { resolve as resolve5 } from "path";
15095
15209
 
15096
15210
  // src/core/type-classifier.ts
15097
- import path43 from "path";
15211
+ import path44 from "path";
15098
15212
  async function classifyFile(absPath, repoRelPath, graph, cache) {
15099
15213
  const matches = [];
15100
15214
  const partialScores = [];
15101
15215
  const ctx = {
15102
15216
  absPath,
15103
15217
  repoRelPath,
15104
- projectRoot: path43.dirname(graph.rootPath),
15218
+ projectRoot: path44.dirname(graph.rootPath),
15105
15219
  cache
15106
15220
  };
15107
15221
  for (const [typeId, def] of Object.entries(graph.architecture.node_types)) {
@@ -15389,7 +15503,7 @@ See: \`yg knowledge read aspect-status\`.
15389
15503
  `;
15390
15504
 
15391
15505
  // src/templates/knowledge/aspects-overview.ts
15392
- var summary2 = "What aspects are, when to create, LLM vs deterministic reviewer choice, cost model";
15506
+ var summary2 = "What aspects are, when to create, LLM vs deterministic vs aggregating reviewer choice, cost model";
15393
15507
  var content2 = `# Aspects overview
15394
15508
 
15395
15509
  Aspects are enforceable rules attached to nodes. A reviewer (LLM or
@@ -15428,12 +15542,33 @@ While the rule is still being authored or is unclear, give the aspect
15428
15542
  \`status: draft\` \u2014 a draft aspect is WIP, so the reviewer never runs on it
15429
15543
  and it costs zero.
15430
15544
 
15431
- ## LLM vs deterministic
15545
+ ## Three reviewer kinds
15546
+
15547
+ Three reviewer kinds exist: LLM, deterministic, and aggregating. The kind is
15548
+ **inferred** from which rule source file is present in the aspect directory:
15549
+ \`content.md\` \u2192 LLM; \`check.mjs\` \u2192 deterministic; neither file but \`implies:\`
15550
+ declared \u2192 aggregating. The \`reviewer:\` block in \`yg-aspect.yaml\` is optional;
15551
+ if present, an explicit \`reviewer.type\` must agree with the inferred kind.
15552
+
15553
+ ### Aggregating aspects
15554
+
15555
+ An aggregating aspect ships neither \`content.md\` nor \`check.mjs\`. It exists
15556
+ purely to bundle other aspects under one named attach point. When an aggregating
15557
+ aspect is effective on a node, all aspects in its \`implies:\` list are expanded
15558
+ and verified individually. The aggregate itself has no own reviewer and produces
15559
+ no own verdict. It never dispatches to an LLM and never runs \`check.mjs\`.
15432
15560
 
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.
15561
+ Use aggregating aspects to decompose a multi-rule contract: attach the aggregate
15562
+ once (per node, per flow, per architecture type) and let each implied child carry
15563
+ one concrete, independently-verdicted rule. An aspect with neither rule source
15564
+ and no \`implies:\` is rejected by the validator.
15565
+
15566
+ ### LLM and deterministic sweet spots
15567
+
15568
+ The deterministic reviewer runs \`check.mjs\` locally \u2014 it covers both per-file
15569
+ syntactic rules (single-file style) and cross-node graph-shape rules
15570
+ (graph-aware style), all in one reviewer. LLM and deterministic each have a
15571
+ distinct sweet spot.
15437
15572
 
15438
15573
  \`check.mjs\` runs in the main Node process with full privileges \u2014 there is no
15439
15574
  security sandbox. The graph-aware allow-list (below) is a read *discipline* that
@@ -15500,10 +15635,12 @@ To author a \`check.mjs\` (both single-file and graph-aware styles):
15500
15635
  ## Cost model
15501
15636
 
15502
15637
  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>\`.
15638
+ during \`yg approve\`, multiplied by the tier's consensus count. The reviewer
15639
+ always sends the full node in a single prompt \u2014 there is no chunking.
15640
+ Deterministic aspects run locally at zero LLM cost. Aggregating aspects have no
15641
+ own reviewer call. A node with 5 LLM aspects = at least 5 reviewer calls. An LLM
15642
+ aspect touching 20 nodes = at least 20 calls when you run
15643
+ \`yg approve --aspect <id>\`.
15507
15644
 
15508
15645
  Use \`yg impact --aspect <id>\` before creating or modifying a widely-used
15509
15646
  aspect to assess the re-approval cost.
@@ -15837,9 +15974,9 @@ The runner raises typed runtime errors when the contract is broken:
15837
15974
 
15838
15975
  ## Iterating over the files
15839
15976
 
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:
15977
+ A node's mapping may include non-parseable files (e.g. \`.md\`, \`.sh\`,
15978
+ \`.json\`). For those files \`file.ast\` is \`undefined\`. **Always guard
15979
+ before touching \`file.ast\`**:
15843
15980
 
15844
15981
  \`\`\`javascript
15845
15982
  import { walk, report, inFile, closest } from '@chrisdudek/yg/ast';
@@ -15847,6 +15984,7 @@ import { walk, report, inFile, closest } from '@chrisdudek/yg/ast';
15847
15984
  export function check(ctx) {
15848
15985
  const violations = [];
15849
15986
  for (const file of ctx.files) {
15987
+ if (!file.ast) continue; // skip non-parseable files (no tree-sitter AST)
15850
15988
  walk(file.ast.rootNode, node => {
15851
15989
  // ... inspect node, push report(file, node, ...) on a hit ...
15852
15990
  });
@@ -15855,6 +15993,10 @@ export function check(ctx) {
15855
15993
  }
15856
15994
  \`\`\`
15857
15995
 
15996
+ Content/regex checks that only use \`file.content\` (and never touch
15997
+ \`file.ast\`) do **not** need this guard \u2014 they should iterate all files
15998
+ including non-parseable ones.
15999
+
15858
16000
  If a rule should apply only to a subset of files, filter on \`file.path\`
15859
16001
  (for example with \`inFile(file, { glob: 'src/api/**' })\`) \u2014 there is no
15860
16002
  \`ctx.language\` and no per-language invocation today; every mapped file
@@ -15886,13 +16028,19 @@ Each \`node\` object from the AST exposes:
15886
16028
  ### Reading node-types.json
15887
16029
 
15888
16030
  To learn the grammar's node types and field names, inspect the
15889
- \`node-types.json\` shipped by the tree-sitter grammar package:
16031
+ \`node-types.json\` files shipped inside the installed package under
16032
+ \`dist/grammars/\`. Each file is named after its grammar:
15890
16033
 
15891
16034
  \`\`\`
15892
- node_modules/tree-sitter-typescript/typescript/node-types.json
15893
- node_modules/tree-sitter-javascript/node-types.json
16035
+ <yg install>/dist/grammars/tree-sitter-typescript.node-types.json
16036
+ <yg install>/dist/grammars/tree-sitter-tsx.node-types.json
16037
+ <yg install>/dist/grammars/tree-sitter-javascript.node-types.json
16038
+ <yg install>/dist/grammars/tree-sitter-python.node-types.json
15894
16039
  \`\`\`
15895
16040
 
16041
+ (and so on for every other shipped grammar \u2014 one \`.node-types.json\`
16042
+ per \`.wasm\` file in the same directory.)
16043
+
15896
16044
  Each entry lists its \`type\`, whether it is \`named\`, and the \`fields\`
15897
16045
  object whose keys are the field names usable with \`childForFieldName\`.
15898
16046
 
@@ -15904,6 +16052,7 @@ import { walk, report, inFile } from '@chrisdudek/yg/ast';
15904
16052
  export function check(ctx) {
15905
16053
  const violations = [];
15906
16054
  for (const file of ctx.files) {
16055
+ if (!file.ast) continue; // skip non-parseable files (no tree-sitter AST)
15907
16056
  if (!inFile(file, { glob: 'src/api/**' })) continue;
15908
16057
  walk(file.ast.rootNode, node => {
15909
16058
  if (node.type !== 'import_statement') return;
@@ -16746,8 +16895,13 @@ files, a typed \`identity\` block holding the node's upstream identity (its own
16746
16895
  aspect-relevant metadata, a per-aspect identity for every effective aspect, and
16747
16896
  per-dependency port-aspect hashes), and a per-aspect verdict map recording the
16748
16897
  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.
16898
+ computed over the real-file hashes, that typed identity, AND the per-aspect
16899
+ verdict map, so any upstream identity change cascades exactly like a source
16900
+ change \u2014 and a hand-edited verdict in \`.drift-state/*.json\` (e.g. flipping a
16901
+ committed \`refused\` to \`approved\`) changes the recomputed hash too. \`yg check\`
16902
+ blocks on any such divergence it cannot attribute to a file or identity change,
16903
+ reporting a \`baseline-integrity\` error; the fix is to re-approve the node (or
16904
+ restore the drift-state from git).
16751
16905
 
16752
16906
  Aspect \`status\` is deliberately NOT part of the identity: flipping an aspect
16753
16907
  between \`advisory\` and \`enforced\` does not drift the node (the recorded verdict
@@ -16886,7 +17040,6 @@ reviewer:
16886
17040
  model: qwen3 # Model identifier for this provider
16887
17041
  temperature: 0 # Sampling temperature (0 = deterministic)
16888
17042
  endpoint: http://localhost:11434 # Required for ollama and openai-compatible
16889
- max_tokens: auto # Response budget: 'auto' or positive integer
16890
17043
  # references: # optional caps on aspect reference files
16891
17044
  # max_bytes_per_file: 65536 # default: 64 KiB per reference file
16892
17045
  # max_total_bytes_per_aspect: 262144 # default: 256 KiB total per aspect
@@ -16961,8 +17114,6 @@ Provider-specific options passed to the LLM client:
16961
17114
  | \`model\` | string | Required. Provider-specific model identifier. |
16962
17115
  | \`temperature\` | number | Defaults to 0. Higher = more varied responses. |
16963
17116
  | \`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
17117
  | \`timeout\` | number | Timeout in seconds. Default 300. Applies to CLI providers only \u2014 non-CLI/API providers ignore it. |
16967
17118
 
16968
17119
  API keys do NOT live here \u2014 they belong in \`yg-secrets.yaml\` (api_key only).
@@ -17235,6 +17386,24 @@ Use \`--reason-file <path>\` instead of \`--reason\` to supply multi-line entry
17235
17386
  content from a file. On \`yg log read\`, \`--top\` and \`--all\` are mutually
17236
17387
  exclusive \u2014 you cannot combine them.
17237
17388
 
17389
+ ## yg suppressions
17390
+
17391
+ Read-only inventory of all active \`yg-suppress\` markers in the repository's
17392
+ source files. Lists each marker's aspect path, location, reason, and kind
17393
+ (single-line, bracket, or wildcard). Exits 0 always \u2014 it is a read-only
17394
+ inspection tool.
17395
+
17396
+ \`\`\`bash
17397
+ yg suppressions
17398
+ \`\`\`
17399
+
17400
+ Emits non-blocking warnings for:
17401
+ - **Unknown aspect-id** \u2014 the aspect path in the marker does not match any known aspect.
17402
+ - **Wildcard suppress** (\`*\`) \u2014 suppresses all aspects in range; any aspect added later is also silently waived.
17403
+ - **Unbounded range** \u2014 a \`yg-suppress-disable\` marker with no matching \`yg-suppress-enable\`; the suppression extends to end of file.
17404
+
17405
+ Use \`yg suppressions\` to audit accumulated waivers before a release or a new aspect rollout. It does not affect \`yg check\` or any baseline.
17406
+
17238
17407
  ## yg type-suggest
17239
17408
 
17240
17409
  Suggest which node_type a file fits based on architecture \`when\` predicates.
@@ -17910,13 +18079,176 @@ function registerKnowledgeCommand(program2) {
17910
18079
  });
17911
18080
  }
17912
18081
 
18082
+ // src/cli/suppressions.ts
18083
+ import chalk13 from "chalk";
18084
+ import { readFileSync as readFileSync5, existsSync as existsSync7 } from "fs";
18085
+ import { execFileSync as execFileSync2 } from "child_process";
18086
+ import path45 from "path";
18087
+ init_debug_log();
18088
+ init_suppress();
18089
+ init_message_builder();
18090
+ init_posix();
18091
+ function isBinaryContent(buf) {
18092
+ const checkLen = Math.min(buf.length, 8192);
18093
+ for (let i = 0; i < checkLen; i++) {
18094
+ if (buf[i] === 0) return true;
18095
+ }
18096
+ return false;
18097
+ }
18098
+ function isNoiseFile(relFile) {
18099
+ const p2 = toPosixPath(relFile);
18100
+ if (p2 === ".yggdrasil" || p2.startsWith(".yggdrasil/")) return true;
18101
+ if (p2.startsWith(".cursor/")) return true;
18102
+ if (p2.startsWith(".github/copilot")) return true;
18103
+ const base = p2.includes("/") ? p2.slice(p2.lastIndexOf("/") + 1) : p2;
18104
+ if (base === ".windsurfrules" || base === ".clinerules") return true;
18105
+ if (base === "log.md") return true;
18106
+ const lower = base.toLowerCase();
18107
+ if (lower.endsWith(".md") || lower.endsWith(".mdc") || lower.endsWith(".markdown") || lower.endsWith(".txt")) {
18108
+ return true;
18109
+ }
18110
+ return false;
18111
+ }
18112
+ function runSuppressionsScan(projectRoot, gitTrackedFiles, knownAspectIds) {
18113
+ const fileEntries = [];
18114
+ const warnings = [];
18115
+ let totalMarkers = 0;
18116
+ const openDisables = /* @__PURE__ */ new Map();
18117
+ for (const relFile of gitTrackedFiles) {
18118
+ if (isNoiseFile(relFile)) continue;
18119
+ const absFile = path45.join(projectRoot, relFile);
18120
+ if (!existsSync7(absFile)) continue;
18121
+ let buf;
18122
+ try {
18123
+ buf = readFileSync5(absFile);
18124
+ } catch (error) {
18125
+ debugWrite(`[suppressions] read fallback: ${relFile}: ${error instanceof Error ? error.message : String(error)}`);
18126
+ continue;
18127
+ }
18128
+ if (isBinaryContent(buf)) continue;
18129
+ const text2 = buf.toString("utf-8");
18130
+ const markers = scanSuppressionMarkers(text2);
18131
+ if (markers.length === 0) continue;
18132
+ fileEntries.push({ file: toPosixPath(relFile), markers });
18133
+ totalMarkers += markers.length;
18134
+ const disableStack = /* @__PURE__ */ new Map();
18135
+ for (const m of markers) {
18136
+ if (m.kind === "disable") {
18137
+ const stack = disableStack.get(m.aspectId) ?? [];
18138
+ stack.push(m.line);
18139
+ disableStack.set(m.aspectId, stack);
18140
+ } else if (m.kind === "enable") {
18141
+ const stack = disableStack.get(m.aspectId);
18142
+ if (stack && stack.length > 0) {
18143
+ stack.pop();
18144
+ if (stack.length === 0) disableStack.delete(m.aspectId);
18145
+ }
18146
+ }
18147
+ }
18148
+ if (disableStack.size > 0) {
18149
+ openDisables.set(toPosixPath(relFile), disableStack);
18150
+ }
18151
+ }
18152
+ const seenWildcard = /* @__PURE__ */ new Set();
18153
+ for (const { file, markers } of fileEntries) {
18154
+ for (const m of markers) {
18155
+ if (!m.wildcard && !knownAspectIds.has(m.aspectId)) {
18156
+ const msg = buildIssueMessage({
18157
+ what: `Unknown aspect id "${m.aspectId}" in suppress marker at ${file}:${m.line}.`,
18158
+ why: "The aspect does not exist in the graph. The suppression has no effect and likely refers to a renamed or deleted aspect.",
18159
+ next: `Run \`yg aspects\` to list defined aspect ids, then update or remove this marker.`
18160
+ });
18161
+ warnings.push(msg);
18162
+ }
18163
+ if (m.wildcard && !seenWildcard.has(`${file}:${m.line}`)) {
18164
+ seenWildcard.add(`${file}:${m.line}`);
18165
+ const msg = buildIssueMessage({
18166
+ what: `Wildcard suppression "*" at ${file}:${m.line} silences ALL aspects.`,
18167
+ 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.",
18168
+ next: `Replace "*" with the specific aspect id(s) you intend to suppress.`
18169
+ });
18170
+ warnings.push(msg);
18171
+ }
18172
+ }
18173
+ }
18174
+ for (const [file, disableMap] of openDisables) {
18175
+ for (const [aspectId, lines] of disableMap) {
18176
+ for (const lineNum of lines) {
18177
+ const msg = buildIssueMessage({
18178
+ what: `Unbounded yg-suppress-disable("${aspectId}") at ${file}:${lineNum} has no matching yg-suppress-enable.`,
18179
+ 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.",
18180
+ 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.`
18181
+ });
18182
+ warnings.push(msg);
18183
+ }
18184
+ }
18185
+ }
18186
+ return { fileEntries, totalMarkers, warnings };
18187
+ }
18188
+ function formatSuppressionsOutput(report) {
18189
+ const lines = [];
18190
+ if (report.fileEntries.length === 0) {
18191
+ lines.push("No active suppression markers found.");
18192
+ return lines.join("\n") + "\n";
18193
+ }
18194
+ lines.push("Active suppression markers:");
18195
+ lines.push("");
18196
+ for (const { file, markers } of report.fileEntries) {
18197
+ lines.push(` ${file}`);
18198
+ for (const m of markers) {
18199
+ const wildcardTag = m.wildcard ? chalk13.yellow(" [wildcard]") : "";
18200
+ const kindTag = m.kind === "single" ? "single" : m.kind === "disable" ? "disable" : "enable";
18201
+ const reasonPart = m.reason ? ` \u2014 ${m.reason}` : "";
18202
+ lines.push(` line ${m.line}: ${kindTag}(${m.aspectId})${wildcardTag}${reasonPart}`);
18203
+ }
18204
+ lines.push("");
18205
+ }
18206
+ const fileCount = report.fileEntries.length;
18207
+ lines.push(`Total: ${report.totalMarkers} marker${report.totalMarkers === 1 ? "" : "s"} across ${fileCount} file${fileCount === 1 ? "" : "s"}.`);
18208
+ if (report.warnings.length > 0) {
18209
+ lines.push("");
18210
+ lines.push(chalk13.yellow(`Warnings (${report.warnings.length}):`));
18211
+ for (const w of report.warnings) {
18212
+ const indented = w.split("\n").map((l) => ` ${l}`).join("\n");
18213
+ lines.push(chalk13.yellow(indented));
18214
+ }
18215
+ }
18216
+ return lines.join("\n") + "\n";
18217
+ }
18218
+ function registerSuppressionsCommand(program2) {
18219
+ program2.command("suppressions").description("Inventory active yg-suppress waivers and warn about footguns").action(async () => {
18220
+ try {
18221
+ const cwd = process.cwd();
18222
+ const graph = await loadGraphOrAbort(cwd);
18223
+ initDebugLog(graph.rootPath, graph.config.debug ?? false, appendToDebugLog);
18224
+ const projectRoot = path45.dirname(graph.rootPath);
18225
+ let gitFiles = [];
18226
+ try {
18227
+ const output = execFileSync2("git", ["ls-files", "."], {
18228
+ cwd: projectRoot,
18229
+ encoding: "utf-8",
18230
+ stdio: ["pipe", "pipe", "pipe"]
18231
+ });
18232
+ gitFiles = output.trim().split("\n").filter((f) => f.length > 0);
18233
+ } catch (error) {
18234
+ debugWrite(`[suppressions] git ls-files fallback: ${error instanceof Error ? error.message : String(error)}`);
18235
+ }
18236
+ const knownAspectIds = new Set(graph.aspects.map((a) => a.id));
18237
+ const report = runSuppressionsScan(projectRoot, gitFiles, knownAspectIds);
18238
+ process.stdout.write(formatSuppressionsOutput(report));
18239
+ } catch (error) {
18240
+ abortOnUnexpectedError(error, "scanning suppressions");
18241
+ }
18242
+ });
18243
+ }
18244
+
17913
18245
  // src/bin.ts
17914
- import { readFileSync as readFileSync5 } from "fs";
18246
+ import { readFileSync as readFileSync6 } from "fs";
17915
18247
  import { fileURLToPath as fileURLToPath5 } from "url";
17916
18248
  import { dirname as dirname2, join as join5 } from "path";
17917
18249
  var __filename3 = fileURLToPath5(import.meta.url);
17918
18250
  var __dirname3 = dirname2(__filename3);
17919
- var pkg = JSON.parse(readFileSync5(join5(__dirname3, "..", "package.json"), "utf-8"));
18251
+ var pkg = JSON.parse(readFileSync6(join5(__dirname3, "..", "package.json"), "utf-8"));
17920
18252
  var program = new Command2();
17921
18253
  program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
17922
18254
  registerInitCommand(program);
@@ -17933,6 +18265,7 @@ registerLogCommand(program);
17933
18265
  registerFindCommand(program);
17934
18266
  registerTypeSuggestCommand(program);
17935
18267
  registerKnowledgeCommand(program);
18268
+ registerSuppressionsCommand(program);
17936
18269
  process.on("unhandledRejection", (reason) => {
17937
18270
  const msg = reason instanceof Error ? reason.message : String(reason);
17938
18271
  process.stderr.write(`Error: ${msg}