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