@chrisdudek/yg 4.0.0 → 4.0.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 +206 -208
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -2346,6 +2346,10 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
2346
2346
|
function projectRootFromGraph(yggRootPath) {
|
|
2347
2347
|
return path10.dirname(yggRootPath);
|
|
2348
2348
|
}
|
|
2349
|
+
function resolveFileArg(cwd, repoRoot, rawPath) {
|
|
2350
|
+
const absolute = path10.resolve(cwd, rawPath.trim());
|
|
2351
|
+
return path10.relative(repoRoot, absolute).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2352
|
+
}
|
|
2349
2353
|
|
|
2350
2354
|
// src/core/graph-loader.ts
|
|
2351
2355
|
function toModelPath(absolutePath, modelDir) {
|
|
@@ -2910,8 +2914,160 @@ function formatFileContext(data) {
|
|
|
2910
2914
|
}
|
|
2911
2915
|
|
|
2912
2916
|
// src/core/validator.ts
|
|
2913
|
-
import { readdir as
|
|
2917
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
2918
|
+
import path14 from "path";
|
|
2919
|
+
|
|
2920
|
+
// src/utils/hash.ts
|
|
2921
|
+
import { readFile as readFile14, readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
2914
2922
|
import path13 from "path";
|
|
2923
|
+
import { createHash } from "crypto";
|
|
2924
|
+
import { createRequire } from "module";
|
|
2925
|
+
var require2 = createRequire(import.meta.url);
|
|
2926
|
+
var ignoreFactory = require2("ignore");
|
|
2927
|
+
async function hashFile(filePath) {
|
|
2928
|
+
const content = await readFile14(filePath);
|
|
2929
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2930
|
+
}
|
|
2931
|
+
async function loadRootGitignoreStack(projectRoot) {
|
|
2932
|
+
if (!projectRoot) return [];
|
|
2933
|
+
try {
|
|
2934
|
+
const content = await readFile14(path13.join(projectRoot, ".gitignore"), "utf-8");
|
|
2935
|
+
const matcher = ignoreFactory();
|
|
2936
|
+
matcher.add(content);
|
|
2937
|
+
return [{ basePath: projectRoot, matcher }];
|
|
2938
|
+
} catch {
|
|
2939
|
+
return [];
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function isIgnoredByStack(candidatePath, stack) {
|
|
2943
|
+
for (const { basePath, matcher } of stack) {
|
|
2944
|
+
const relativePath = path13.relative(basePath, candidatePath);
|
|
2945
|
+
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
2946
|
+
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
2947
|
+
}
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
function hashString(content) {
|
|
2951
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2952
|
+
}
|
|
2953
|
+
async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes) {
|
|
2954
|
+
const fileHashes = {};
|
|
2955
|
+
const fileMtimes = {};
|
|
2956
|
+
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2957
|
+
const allFiles = [];
|
|
2958
|
+
for (const tf of trackedFiles) {
|
|
2959
|
+
if (tf.syntheticHash) {
|
|
2960
|
+
fileHashes[tf.path] = tf.syntheticHash;
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
const absPath = path13.join(projectRoot, tf.path);
|
|
2964
|
+
try {
|
|
2965
|
+
const st = await stat4(absPath);
|
|
2966
|
+
if (st.isDirectory()) {
|
|
2967
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2968
|
+
projectRoot,
|
|
2969
|
+
gitignoreStack
|
|
2970
|
+
});
|
|
2971
|
+
for (const entry of dirEntries) {
|
|
2972
|
+
allFiles.push({
|
|
2973
|
+
relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
2974
|
+
absPath: entry.absPath,
|
|
2975
|
+
mtimeMs: entry.mtimeMs
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
} else {
|
|
2979
|
+
allFiles.push({ relPath: tf.path, absPath, mtimeMs: st.mtimeMs });
|
|
2980
|
+
}
|
|
2981
|
+
} catch {
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => entry.relPath === prefix || entry.relPath.startsWith(prefix + "/"))) : allFiles;
|
|
2986
|
+
const dirty = [];
|
|
2987
|
+
for (const entry of filtered) {
|
|
2988
|
+
const storedMtime = storedFileData?.mtimes[entry.relPath];
|
|
2989
|
+
const storedHash = storedFileData?.hashes[entry.relPath];
|
|
2990
|
+
if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
|
|
2991
|
+
fileHashes[entry.relPath] = storedHash;
|
|
2992
|
+
} else {
|
|
2993
|
+
dirty.push(entry);
|
|
2994
|
+
}
|
|
2995
|
+
fileMtimes[entry.relPath] = entry.mtimeMs;
|
|
2996
|
+
}
|
|
2997
|
+
const BATCH_SIZE = 256;
|
|
2998
|
+
for (let i = 0; i < dirty.length; i += BATCH_SIZE) {
|
|
2999
|
+
const batch = dirty.slice(i, i + BATCH_SIZE);
|
|
3000
|
+
const hashes = await Promise.all(batch.map((e) => hashFile(e.absPath)));
|
|
3001
|
+
for (let j = 0; j < batch.length; j++) {
|
|
3002
|
+
fileHashes[batch[j].relPath] = hashes[j];
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
|
|
3006
|
+
const digest = sorted.map(([p2, h]) => `${p2}:${h}`).join("\n");
|
|
3007
|
+
const canonicalHash = hashString(digest);
|
|
3008
|
+
return { canonicalHash, fileHashes, fileMtimes };
|
|
3009
|
+
}
|
|
3010
|
+
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
3011
|
+
let stack = options.gitignoreStack ?? [];
|
|
3012
|
+
try {
|
|
3013
|
+
const localContent = await readFile14(path13.join(directoryPath, ".gitignore"), "utf-8");
|
|
3014
|
+
const localMatcher = ignoreFactory();
|
|
3015
|
+
localMatcher.add(localContent);
|
|
3016
|
+
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
3017
|
+
} catch {
|
|
3018
|
+
}
|
|
3019
|
+
const entries = await readdir5(directoryPath, { withFileTypes: true });
|
|
3020
|
+
const dirs = [];
|
|
3021
|
+
const files = [];
|
|
3022
|
+
for (const entry of entries) {
|
|
3023
|
+
const absoluteChildPath = path13.join(directoryPath, entry.name);
|
|
3024
|
+
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
3025
|
+
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
3026
|
+
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
3027
|
+
}
|
|
3028
|
+
const [dirResults, fileStats] = await Promise.all([
|
|
3029
|
+
Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
|
|
3030
|
+
projectRoot: options.projectRoot,
|
|
3031
|
+
gitignoreStack: stack
|
|
3032
|
+
}))),
|
|
3033
|
+
Promise.all(files.map(async (f) => {
|
|
3034
|
+
const fileStat = await stat4(f);
|
|
3035
|
+
return {
|
|
3036
|
+
relPath: path13.relative(rootDirectoryPath, f).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
3037
|
+
absPath: f,
|
|
3038
|
+
mtimeMs: fileStat.mtimeMs
|
|
3039
|
+
};
|
|
3040
|
+
}))
|
|
3041
|
+
]);
|
|
3042
|
+
const result = [];
|
|
3043
|
+
for (const nested of dirResults) result.push(...nested);
|
|
3044
|
+
result.push(...fileStats);
|
|
3045
|
+
return result;
|
|
3046
|
+
}
|
|
3047
|
+
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
3048
|
+
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
3049
|
+
const result = [];
|
|
3050
|
+
for (const mp of mappingPaths) {
|
|
3051
|
+
const absPath = path13.join(projectRoot, mp);
|
|
3052
|
+
try {
|
|
3053
|
+
const st = await stat4(absPath);
|
|
3054
|
+
if (st.isDirectory()) {
|
|
3055
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
3056
|
+
projectRoot,
|
|
3057
|
+
gitignoreStack
|
|
3058
|
+
});
|
|
3059
|
+
for (const entry of dirEntries) {
|
|
3060
|
+
result.push(path13.join(mp, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""));
|
|
3061
|
+
}
|
|
3062
|
+
} else {
|
|
3063
|
+
result.push(mp);
|
|
3064
|
+
}
|
|
3065
|
+
} catch {
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
return result;
|
|
3070
|
+
}
|
|
2915
3071
|
|
|
2916
3072
|
// src/formatters/message-builder.ts
|
|
2917
3073
|
function buildIssueMessage(msg) {
|
|
@@ -2972,10 +3128,11 @@ async function validate(graph, scope = "all") {
|
|
|
2972
3128
|
issues.push(...checkOrphanedAspects(graph));
|
|
2973
3129
|
let filtered = issues;
|
|
2974
3130
|
let nodesScanned = graph.nodes.size;
|
|
2975
|
-
|
|
2976
|
-
|
|
3131
|
+
const normalizedScope = scope.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3132
|
+
if (normalizedScope !== "all" && normalizedScope) {
|
|
3133
|
+
if (!graph.nodes.has(normalizedScope)) {
|
|
2977
3134
|
const parseError = (graph.nodeParseErrors ?? []).find(
|
|
2978
|
-
(e) => e.nodePath ===
|
|
3135
|
+
(e) => e.nodePath === normalizedScope || normalizedScope.startsWith(e.nodePath + "/")
|
|
2979
3136
|
);
|
|
2980
3137
|
if (parseError) {
|
|
2981
3138
|
return {
|
|
@@ -2990,13 +3147,13 @@ async function validate(graph, scope = "all") {
|
|
|
2990
3147
|
};
|
|
2991
3148
|
}
|
|
2992
3149
|
return {
|
|
2993
|
-
issues: [{ severity: "error", rule: "invalid-scope", message: buildIssueMessage({ what: `Node not found: ${
|
|
3150
|
+
issues: [{ severity: "error", rule: "invalid-scope", message: buildIssueMessage({ what: `Node not found: ${normalizedScope}`, why: "Validation scope references a node that does not exist in the graph.", next: "Check the node path and try again." }) }],
|
|
2994
3151
|
nodesScanned: 0
|
|
2995
3152
|
};
|
|
2996
3153
|
}
|
|
2997
|
-
const scopePrefix =
|
|
2998
|
-
filtered = issues.filter((i) => !i.nodePath || i.nodePath ===
|
|
2999
|
-
nodesScanned = [...graph.nodes.keys()].filter((p2) => p2 ===
|
|
3154
|
+
const scopePrefix = normalizedScope + "/";
|
|
3155
|
+
filtered = issues.filter((i) => !i.nodePath || i.nodePath === normalizedScope || i.nodePath.startsWith(scopePrefix));
|
|
3156
|
+
nodesScanned = [...graph.nodes.keys()].filter((p2) => p2 === normalizedScope || p2.startsWith(scopePrefix)).length;
|
|
3000
3157
|
}
|
|
3001
3158
|
return { issues: filtered, nodesScanned };
|
|
3002
3159
|
}
|
|
@@ -3313,12 +3470,12 @@ function checkMappingOverlap(graph) {
|
|
|
3313
3470
|
}
|
|
3314
3471
|
async function checkMappingPathsExist(graph) {
|
|
3315
3472
|
const issues = [];
|
|
3316
|
-
const projectRoot =
|
|
3473
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3317
3474
|
const { access: access3 } = await import("fs/promises");
|
|
3318
3475
|
for (const [nodePath, node] of graph.nodes) {
|
|
3319
3476
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
3320
3477
|
for (const mp of mappingPaths) {
|
|
3321
|
-
const absPath =
|
|
3478
|
+
const absPath = path14.join(projectRoot, mp);
|
|
3322
3479
|
try {
|
|
3323
3480
|
await access3(absPath);
|
|
3324
3481
|
} catch {
|
|
@@ -3362,13 +3519,13 @@ function checkBrokenFlowRefs(graph) {
|
|
|
3362
3519
|
async function checkWideNodes(graph) {
|
|
3363
3520
|
const issues = [];
|
|
3364
3521
|
const maxFiles = graph.config.quality?.max_mapping_source_files ?? 10;
|
|
3365
|
-
const projectRoot =
|
|
3522
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3366
3523
|
for (const [nodePath, node] of graph.nodes) {
|
|
3367
3524
|
const effectiveAspects = computeEffectiveAspects(node, graph);
|
|
3368
3525
|
if (effectiveAspects.size === 0) continue;
|
|
3369
3526
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
3370
3527
|
if (mappingPaths.length === 0) continue;
|
|
3371
|
-
const sourceFiles = await
|
|
3528
|
+
const sourceFiles = await expandMappingPaths(projectRoot, mappingPaths);
|
|
3372
3529
|
if (sourceFiles.length <= maxFiles) continue;
|
|
3373
3530
|
issues.push({
|
|
3374
3531
|
severity: "warning",
|
|
@@ -3483,9 +3640,9 @@ function checkSchemas(graph) {
|
|
|
3483
3640
|
}
|
|
3484
3641
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
3485
3642
|
const issues = [];
|
|
3486
|
-
const modelDir =
|
|
3643
|
+
const modelDir = path14.join(graph.rootPath, "model");
|
|
3487
3644
|
async function scanDir(dirPath, segments) {
|
|
3488
|
-
const entries = (await
|
|
3645
|
+
const entries = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3489
3646
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
3490
3647
|
const hasFiles = entries.some((e) => e.isFile());
|
|
3491
3648
|
const graphPath = segments.join("/");
|
|
@@ -3507,47 +3664,20 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
3507
3664
|
for (const entry of entries) {
|
|
3508
3665
|
if (!entry.isDirectory()) continue;
|
|
3509
3666
|
if (entry.name.startsWith(".")) continue;
|
|
3510
|
-
await scanDir(
|
|
3667
|
+
await scanDir(path14.join(dirPath, entry.name), [...segments, entry.name]);
|
|
3511
3668
|
}
|
|
3512
3669
|
}
|
|
3513
3670
|
try {
|
|
3514
|
-
const rootEntries = (await
|
|
3671
|
+
const rootEntries = (await readdir6(modelDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3515
3672
|
for (const entry of rootEntries) {
|
|
3516
3673
|
if (!entry.isDirectory()) continue;
|
|
3517
3674
|
if (entry.name.startsWith(".")) continue;
|
|
3518
|
-
await scanDir(
|
|
3675
|
+
await scanDir(path14.join(modelDir, entry.name), [entry.name]);
|
|
3519
3676
|
}
|
|
3520
3677
|
} catch {
|
|
3521
3678
|
}
|
|
3522
3679
|
return issues;
|
|
3523
3680
|
}
|
|
3524
|
-
async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
3525
|
-
const files = [];
|
|
3526
|
-
async function collectFiles(absPath) {
|
|
3527
|
-
try {
|
|
3528
|
-
const s = await stat4(absPath);
|
|
3529
|
-
if (s.isFile()) {
|
|
3530
|
-
files.push(absPath);
|
|
3531
|
-
} else if (s.isDirectory()) {
|
|
3532
|
-
const entries = await readdir5(absPath, { withFileTypes: true });
|
|
3533
|
-
for (const entry of entries) {
|
|
3534
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3535
|
-
const entryPath = path13.join(absPath, entry.name);
|
|
3536
|
-
if (entry.isFile()) {
|
|
3537
|
-
files.push(entryPath);
|
|
3538
|
-
} else if (entry.isDirectory()) {
|
|
3539
|
-
await collectFiles(entryPath);
|
|
3540
|
-
}
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
} catch {
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
for (const mp of mappingPaths) {
|
|
3547
|
-
await collectFiles(path13.join(projectRoot, mp));
|
|
3548
|
-
}
|
|
3549
|
-
return files;
|
|
3550
|
-
}
|
|
3551
3681
|
function checkMissingDescriptions(graph) {
|
|
3552
3682
|
const issues = [];
|
|
3553
3683
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -3815,7 +3945,7 @@ function checkOrphanedAspects(graph) {
|
|
|
3815
3945
|
}
|
|
3816
3946
|
|
|
3817
3947
|
// src/cli/owner.ts
|
|
3818
|
-
import
|
|
3948
|
+
import path15 from "path";
|
|
3819
3949
|
import { access } from "fs/promises";
|
|
3820
3950
|
import chalk2 from "chalk";
|
|
3821
3951
|
function normalizeForMatch(inputPath) {
|
|
@@ -3846,12 +3976,10 @@ function registerOwnerCommand(program2) {
|
|
|
3846
3976
|
const graph = await loadGraph(cwd);
|
|
3847
3977
|
initDebugLog(graph.rootPath, graph.config.debug ?? false);
|
|
3848
3978
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3849
|
-
const
|
|
3850
|
-
const absolute = path14.resolve(cwd, rawPath);
|
|
3851
|
-
const repoRelative = path14.relative(repoRoot, absolute).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3979
|
+
const repoRelative = resolveFileArg(cwd, repoRoot, options.file);
|
|
3852
3980
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3853
3981
|
if (!result.nodePath) {
|
|
3854
|
-
const absPath =
|
|
3982
|
+
const absPath = path15.resolve(repoRoot, result.file);
|
|
3855
3983
|
let exists = true;
|
|
3856
3984
|
try {
|
|
3857
3985
|
await access(absPath);
|
|
@@ -3956,7 +4084,8 @@ function registerBuildCommand(program2) {
|
|
|
3956
4084
|
let resolvedFilePath;
|
|
3957
4085
|
if (options.file) {
|
|
3958
4086
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3959
|
-
const
|
|
4087
|
+
const repoRelative = resolveFileArg(process.cwd(), repoRoot, options.file);
|
|
4088
|
+
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3960
4089
|
if (!result.nodePath) {
|
|
3961
4090
|
const candidates = findCandidateNodes(graph, result.file);
|
|
3962
4091
|
if (candidates.length > 0) {
|
|
@@ -4024,17 +4153,15 @@ ${errorList}`
|
|
|
4024
4153
|
process.stdout.write(formatFileContext(data));
|
|
4025
4154
|
} else {
|
|
4026
4155
|
const data = buildNodeContextData(graph, nodePath);
|
|
4156
|
+
const projectRoot = projectRootFromGraph(graph.rootPath);
|
|
4157
|
+
data.sourceFiles = await expandMappingPaths(projectRoot, data.sourceFiles);
|
|
4027
4158
|
process.stdout.write(formatNodeContext(data));
|
|
4028
4159
|
}
|
|
4029
4160
|
} catch (error) {
|
|
4030
4161
|
const err = error;
|
|
4031
4162
|
if (err.code === "ENOENT") {
|
|
4032
4163
|
process.stderr.write(
|
|
4033
|
-
chalk3.red(
|
|
4034
|
-
what: "No .yggdrasil/ directory found.",
|
|
4035
|
-
why: "Yggdrasil is not initialized in this project.",
|
|
4036
|
-
next: "Run 'yg init' to initialize."
|
|
4037
|
-
}) + "\n")
|
|
4164
|
+
chalk3.red("Error: No .yggdrasil/ directory found. Run 'yg init' first.\n")
|
|
4038
4165
|
);
|
|
4039
4166
|
} else {
|
|
4040
4167
|
process.stderr.write(chalk3.red(`Error: ${error.message}
|
|
@@ -4049,31 +4176,30 @@ ${errorList}`
|
|
|
4049
4176
|
// src/cli/approve.ts
|
|
4050
4177
|
import chalk4 from "chalk";
|
|
4051
4178
|
import path20 from "path";
|
|
4052
|
-
import { readFile as readFile18 } from "fs/promises";
|
|
4053
4179
|
|
|
4054
4180
|
// src/io/drift-state-store.ts
|
|
4055
|
-
import { readFile as
|
|
4056
|
-
import
|
|
4181
|
+
import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir7, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
4182
|
+
import path16 from "path";
|
|
4057
4183
|
var DRIFT_STATE_DIR = ".drift-state";
|
|
4058
4184
|
function nodeStatePath(yggRoot, nodePath) {
|
|
4059
|
-
return
|
|
4185
|
+
return path16.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
|
|
4060
4186
|
}
|
|
4061
4187
|
async function scanJsonFiles(dir, baseDir) {
|
|
4062
4188
|
const results = [];
|
|
4063
4189
|
let entries;
|
|
4064
4190
|
try {
|
|
4065
|
-
entries = await
|
|
4191
|
+
entries = await readdir7(dir, { withFileTypes: true });
|
|
4066
4192
|
} catch (err) {
|
|
4067
4193
|
debugWrite(`[drift-state-store] scanJsonFiles readdir: ${err.message}`);
|
|
4068
4194
|
return results;
|
|
4069
4195
|
}
|
|
4070
4196
|
for (const entry of entries) {
|
|
4071
|
-
const fullPath =
|
|
4197
|
+
const fullPath = path16.join(dir, entry.name);
|
|
4072
4198
|
if (entry.isDirectory()) {
|
|
4073
4199
|
const nested = await scanJsonFiles(fullPath, baseDir);
|
|
4074
4200
|
results.push(...nested);
|
|
4075
4201
|
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
4076
|
-
const relPath =
|
|
4202
|
+
const relPath = path16.relative(baseDir, fullPath);
|
|
4077
4203
|
const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
|
|
4078
4204
|
results.push(nodePath);
|
|
4079
4205
|
}
|
|
@@ -4081,13 +4207,13 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
4081
4207
|
return results;
|
|
4082
4208
|
}
|
|
4083
4209
|
async function removeEmptyParents(filePath, stopDir) {
|
|
4084
|
-
let dir =
|
|
4210
|
+
let dir = path16.dirname(filePath);
|
|
4085
4211
|
while (dir !== stopDir && dir.startsWith(stopDir)) {
|
|
4086
4212
|
try {
|
|
4087
|
-
const entries = await
|
|
4213
|
+
const entries = await readdir7(dir);
|
|
4088
4214
|
if (entries.length === 0) {
|
|
4089
4215
|
await rm2(dir, { recursive: true });
|
|
4090
|
-
dir =
|
|
4216
|
+
dir = path16.dirname(dir);
|
|
4091
4217
|
} else {
|
|
4092
4218
|
break;
|
|
4093
4219
|
}
|
|
@@ -4100,7 +4226,7 @@ async function removeEmptyParents(filePath, stopDir) {
|
|
|
4100
4226
|
async function readNodeDriftState(yggRoot, nodePath) {
|
|
4101
4227
|
try {
|
|
4102
4228
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
4103
|
-
const content = await
|
|
4229
|
+
const content = await readFile15(filePath, "utf-8");
|
|
4104
4230
|
const parsed = JSON.parse(content);
|
|
4105
4231
|
return parsed;
|
|
4106
4232
|
} catch (err) {
|
|
@@ -4110,12 +4236,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
|
|
|
4110
4236
|
}
|
|
4111
4237
|
async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
4112
4238
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
4113
|
-
await mkdir3(
|
|
4239
|
+
await mkdir3(path16.dirname(filePath), { recursive: true });
|
|
4114
4240
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
4115
4241
|
await writeFile5(filePath, content, "utf-8");
|
|
4116
4242
|
}
|
|
4117
4243
|
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
4118
|
-
const driftDir =
|
|
4244
|
+
const driftDir = path16.join(yggRoot, DRIFT_STATE_DIR);
|
|
4119
4245
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
4120
4246
|
const removed = [];
|
|
4121
4247
|
for (const nodePath of allNodePaths) {
|
|
@@ -4129,7 +4255,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
|
4129
4255
|
return removed.sort();
|
|
4130
4256
|
}
|
|
4131
4257
|
async function readDriftState(yggRoot) {
|
|
4132
|
-
const driftPath =
|
|
4258
|
+
const driftPath = path16.join(yggRoot, DRIFT_STATE_DIR);
|
|
4133
4259
|
let driftStat;
|
|
4134
4260
|
try {
|
|
4135
4261
|
driftStat = await stat5(driftPath);
|
|
@@ -4152,134 +4278,6 @@ async function readDriftState(yggRoot) {
|
|
|
4152
4278
|
return state;
|
|
4153
4279
|
}
|
|
4154
4280
|
|
|
4155
|
-
// src/utils/hash.ts
|
|
4156
|
-
import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
4157
|
-
import path16 from "path";
|
|
4158
|
-
import { createHash } from "crypto";
|
|
4159
|
-
import { createRequire } from "module";
|
|
4160
|
-
var require2 = createRequire(import.meta.url);
|
|
4161
|
-
var ignoreFactory = require2("ignore");
|
|
4162
|
-
async function hashFile(filePath) {
|
|
4163
|
-
const content = await readFile15(filePath);
|
|
4164
|
-
return createHash("sha256").update(content).digest("hex");
|
|
4165
|
-
}
|
|
4166
|
-
async function loadRootGitignoreStack(projectRoot) {
|
|
4167
|
-
if (!projectRoot) return [];
|
|
4168
|
-
try {
|
|
4169
|
-
const content = await readFile15(path16.join(projectRoot, ".gitignore"), "utf-8");
|
|
4170
|
-
const matcher = ignoreFactory();
|
|
4171
|
-
matcher.add(content);
|
|
4172
|
-
return [{ basePath: projectRoot, matcher }];
|
|
4173
|
-
} catch {
|
|
4174
|
-
return [];
|
|
4175
|
-
}
|
|
4176
|
-
}
|
|
4177
|
-
function isIgnoredByStack(candidatePath, stack) {
|
|
4178
|
-
for (const { basePath, matcher } of stack) {
|
|
4179
|
-
const relativePath = path16.relative(basePath, candidatePath);
|
|
4180
|
-
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
4181
|
-
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
4182
|
-
}
|
|
4183
|
-
return false;
|
|
4184
|
-
}
|
|
4185
|
-
function hashString(content) {
|
|
4186
|
-
return createHash("sha256").update(content).digest("hex");
|
|
4187
|
-
}
|
|
4188
|
-
async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes) {
|
|
4189
|
-
const fileHashes = {};
|
|
4190
|
-
const fileMtimes = {};
|
|
4191
|
-
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
4192
|
-
const allFiles = [];
|
|
4193
|
-
for (const tf of trackedFiles) {
|
|
4194
|
-
if (tf.syntheticHash) {
|
|
4195
|
-
fileHashes[tf.path] = tf.syntheticHash;
|
|
4196
|
-
continue;
|
|
4197
|
-
}
|
|
4198
|
-
const absPath = path16.join(projectRoot, tf.path);
|
|
4199
|
-
try {
|
|
4200
|
-
const st = await stat6(absPath);
|
|
4201
|
-
if (st.isDirectory()) {
|
|
4202
|
-
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
4203
|
-
projectRoot,
|
|
4204
|
-
gitignoreStack
|
|
4205
|
-
});
|
|
4206
|
-
for (const entry of dirEntries) {
|
|
4207
|
-
allFiles.push({
|
|
4208
|
-
relPath: path16.join(tf.path, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
4209
|
-
absPath: entry.absPath,
|
|
4210
|
-
mtimeMs: entry.mtimeMs
|
|
4211
|
-
});
|
|
4212
|
-
}
|
|
4213
|
-
} else {
|
|
4214
|
-
allFiles.push({ relPath: tf.path, absPath, mtimeMs: st.mtimeMs });
|
|
4215
|
-
}
|
|
4216
|
-
} catch {
|
|
4217
|
-
continue;
|
|
4218
|
-
}
|
|
4219
|
-
}
|
|
4220
|
-
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => entry.relPath === prefix || entry.relPath.startsWith(prefix + "/"))) : allFiles;
|
|
4221
|
-
const dirty = [];
|
|
4222
|
-
for (const entry of filtered) {
|
|
4223
|
-
const storedMtime = storedFileData?.mtimes[entry.relPath];
|
|
4224
|
-
const storedHash = storedFileData?.hashes[entry.relPath];
|
|
4225
|
-
if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
|
|
4226
|
-
fileHashes[entry.relPath] = storedHash;
|
|
4227
|
-
} else {
|
|
4228
|
-
dirty.push(entry);
|
|
4229
|
-
}
|
|
4230
|
-
fileMtimes[entry.relPath] = entry.mtimeMs;
|
|
4231
|
-
}
|
|
4232
|
-
const BATCH_SIZE = 256;
|
|
4233
|
-
for (let i = 0; i < dirty.length; i += BATCH_SIZE) {
|
|
4234
|
-
const batch = dirty.slice(i, i + BATCH_SIZE);
|
|
4235
|
-
const hashes = await Promise.all(batch.map((e) => hashFile(e.absPath)));
|
|
4236
|
-
for (let j = 0; j < batch.length; j++) {
|
|
4237
|
-
fileHashes[batch[j].relPath] = hashes[j];
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
|
|
4241
|
-
const digest = sorted.map(([p2, h]) => `${p2}:${h}`).join("\n");
|
|
4242
|
-
const canonicalHash = hashString(digest);
|
|
4243
|
-
return { canonicalHash, fileHashes, fileMtimes };
|
|
4244
|
-
}
|
|
4245
|
-
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
4246
|
-
let stack = options.gitignoreStack ?? [];
|
|
4247
|
-
try {
|
|
4248
|
-
const localContent = await readFile15(path16.join(directoryPath, ".gitignore"), "utf-8");
|
|
4249
|
-
const localMatcher = ignoreFactory();
|
|
4250
|
-
localMatcher.add(localContent);
|
|
4251
|
-
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
4252
|
-
} catch {
|
|
4253
|
-
}
|
|
4254
|
-
const entries = await readdir7(directoryPath, { withFileTypes: true });
|
|
4255
|
-
const dirs = [];
|
|
4256
|
-
const files = [];
|
|
4257
|
-
for (const entry of entries) {
|
|
4258
|
-
const absoluteChildPath = path16.join(directoryPath, entry.name);
|
|
4259
|
-
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
4260
|
-
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
4261
|
-
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
4262
|
-
}
|
|
4263
|
-
const [dirResults, fileStats] = await Promise.all([
|
|
4264
|
-
Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
|
|
4265
|
-
projectRoot: options.projectRoot,
|
|
4266
|
-
gitignoreStack: stack
|
|
4267
|
-
}))),
|
|
4268
|
-
Promise.all(files.map(async (f) => {
|
|
4269
|
-
const fileStat = await stat6(f);
|
|
4270
|
-
return {
|
|
4271
|
-
relPath: path16.relative(rootDirectoryPath, f).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
4272
|
-
absPath: f,
|
|
4273
|
-
mtimeMs: fileStat.mtimeMs
|
|
4274
|
-
};
|
|
4275
|
-
}))
|
|
4276
|
-
]);
|
|
4277
|
-
const result = [];
|
|
4278
|
-
for (const nested of dirResults) result.push(...nested);
|
|
4279
|
-
result.push(...fileStats);
|
|
4280
|
-
return result;
|
|
4281
|
-
}
|
|
4282
|
-
|
|
4283
4281
|
// src/core/context-files.ts
|
|
4284
4282
|
import path17 from "path";
|
|
4285
4283
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -5760,14 +5758,13 @@ function registerApproveCommand(program2) {
|
|
|
5760
5758
|
}
|
|
5761
5759
|
const aspects = resolveAspects(node2, graph);
|
|
5762
5760
|
const projectRoot = path20.dirname(graph.rootPath);
|
|
5763
|
-
const
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
}
|
|
5761
|
+
const trackedFiles = collectTrackedFiles(node2, graph);
|
|
5762
|
+
const { fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, void 0, []);
|
|
5763
|
+
const sourceFilePaths = Object.keys(fileHashes).filter((f) => {
|
|
5764
|
+
const normalized = f.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5765
|
+
return !normalized.startsWith(yggPrefix);
|
|
5766
|
+
});
|
|
5767
|
+
const sourceFiles = await loadSourceFiles(sourceFilePaths, projectRoot);
|
|
5771
5768
|
process.stdout.write(chalk4.bold(`
|
|
5772
5769
|
--- Dry run: ${nodePath2} ---
|
|
5773
5770
|
|
|
@@ -6218,7 +6215,8 @@ function registerImpactCommand(program2) {
|
|
|
6218
6215
|
initDebugLog(graph.rootPath, graph.config.debug ?? false);
|
|
6219
6216
|
if (options.file) {
|
|
6220
6217
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
6221
|
-
const
|
|
6218
|
+
const repoRelative = resolveFileArg(process.cwd(), repoRoot, options.file);
|
|
6219
|
+
const result = findOwner(graph, repoRoot, repoRelative);
|
|
6222
6220
|
if (!result.nodePath) {
|
|
6223
6221
|
process.stderr.write(chalk6.red(`${result.file} -> no graph coverage
|
|
6224
6222
|
`));
|
|
@@ -6531,7 +6529,7 @@ function registerFlowsCommand(program2) {
|
|
|
6531
6529
|
|
|
6532
6530
|
// src/cli/check.ts
|
|
6533
6531
|
import chalk9 from "chalk";
|
|
6534
|
-
import {
|
|
6532
|
+
import { execFileSync } from "child_process";
|
|
6535
6533
|
import path21 from "path";
|
|
6536
6534
|
function registerCheckCommand(program2) {
|
|
6537
6535
|
program2.command("check").description("Unified graph gate \u2014 errors, drift, coverage, completeness").action(async () => {
|
|
@@ -6542,7 +6540,7 @@ function registerCheckCommand(program2) {
|
|
|
6542
6540
|
let gitFiles = null;
|
|
6543
6541
|
try {
|
|
6544
6542
|
const projectRoot = path21.dirname(graph.rootPath);
|
|
6545
|
-
const output =
|
|
6543
|
+
const output = execFileSync("git", ["ls-files", "."], {
|
|
6546
6544
|
cwd: projectRoot,
|
|
6547
6545
|
encoding: "utf-8",
|
|
6548
6546
|
stdio: ["pipe", "pipe", "pipe"]
|