@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.
Files changed (2) hide show
  1. package/dist/bin.js +206 -208
  2. 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 readdir5, stat as stat4 } from "fs/promises";
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
- if (scope !== "all" && scope.trim()) {
2976
- if (!graph.nodes.has(scope)) {
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 === scope || scope.startsWith(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: ${scope}`, why: "Validation scope references a node that does not exist in the graph.", next: "Check the node path and try again." }) }],
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 = scope + "/";
2998
- filtered = issues.filter((i) => !i.nodePath || i.nodePath === scope || i.nodePath.startsWith(scopePrefix));
2999
- nodesScanned = [...graph.nodes.keys()].filter((p2) => p2 === scope || p2.startsWith(scopePrefix)).length;
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 = path13.dirname(graph.rootPath);
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 = path13.join(projectRoot, mp);
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 = path13.dirname(graph.rootPath);
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 expandMappingToFiles(projectRoot, mappingPaths);
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 = path13.join(graph.rootPath, "model");
3643
+ const modelDir = path14.join(graph.rootPath, "model");
3487
3644
  async function scanDir(dirPath, segments) {
3488
- const entries = (await readdir5(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
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(path13.join(dirPath, entry.name), [...segments, entry.name]);
3667
+ await scanDir(path14.join(dirPath, entry.name), [...segments, entry.name]);
3511
3668
  }
3512
3669
  }
3513
3670
  try {
3514
- const rootEntries = (await readdir5(modelDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
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(path13.join(modelDir, entry.name), [entry.name]);
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 path14 from "path";
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 rawPath = options.file.trim();
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 = path14.resolve(repoRoot, result.file);
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 result = findOwner(graph, repoRoot, options.file.trim());
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(buildIssueMessage({
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 readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
4056
- import path15 from "path";
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 path15.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
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 readdir6(dir, { withFileTypes: true });
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 = path15.join(dir, entry.name);
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 = path15.relative(baseDir, fullPath);
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 = path15.dirname(filePath);
4210
+ let dir = path16.dirname(filePath);
4085
4211
  while (dir !== stopDir && dir.startsWith(stopDir)) {
4086
4212
  try {
4087
- const entries = await readdir6(dir);
4213
+ const entries = await readdir7(dir);
4088
4214
  if (entries.length === 0) {
4089
4215
  await rm2(dir, { recursive: true });
4090
- dir = path15.dirname(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 readFile14(filePath, "utf-8");
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(path15.dirname(filePath), { recursive: true });
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 = path15.join(yggRoot, DRIFT_STATE_DIR);
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 = path15.join(yggRoot, DRIFT_STATE_DIR);
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 sourceFiles = [];
5764
- for (const mp of node2.meta.mapping ?? []) {
5765
- try {
5766
- const content = await readFile18(path20.join(projectRoot, mp), "utf-8");
5767
- sourceFiles.push({ path: mp, content });
5768
- } catch {
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 result = findOwner(graph, repoRoot, options.file.trim());
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 { execSync } from "child_process";
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 = execSync("git ls-files .", {
6543
+ const output = execFileSync("git", ["ls-files", "."], {
6546
6544
  cwd: projectRoot,
6547
6545
  encoding: "utf-8",
6548
6546
  stdio: ["pipe", "pipe", "pipe"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrisdudek/yg",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "Continuous architecture enforcement for AI-assisted development. Aspects, review, enforcement.",
5
5
  "type": "module",
6
6
  "bin": {