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