@chrisdudek/yg 4.0.0 → 4.0.1

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 +178 -158
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -3891,6 +3891,158 @@ function registerOwnerCommand(program2) {
3891
3891
  });
3892
3892
  }
3893
3893
 
3894
+ // src/utils/hash.ts
3895
+ import { readFile as readFile14, readdir as readdir6, stat as stat5 } from "fs/promises";
3896
+ import path15 from "path";
3897
+ import { createHash } from "crypto";
3898
+ import { createRequire } from "module";
3899
+ var require2 = createRequire(import.meta.url);
3900
+ var ignoreFactory = require2("ignore");
3901
+ async function hashFile(filePath) {
3902
+ const content = await readFile14(filePath);
3903
+ return createHash("sha256").update(content).digest("hex");
3904
+ }
3905
+ async function loadRootGitignoreStack(projectRoot) {
3906
+ if (!projectRoot) return [];
3907
+ try {
3908
+ const content = await readFile14(path15.join(projectRoot, ".gitignore"), "utf-8");
3909
+ const matcher = ignoreFactory();
3910
+ matcher.add(content);
3911
+ return [{ basePath: projectRoot, matcher }];
3912
+ } catch {
3913
+ return [];
3914
+ }
3915
+ }
3916
+ function isIgnoredByStack(candidatePath, stack) {
3917
+ for (const { basePath, matcher } of stack) {
3918
+ const relativePath = path15.relative(basePath, candidatePath);
3919
+ if (relativePath === "" || relativePath.startsWith("..")) continue;
3920
+ if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
3921
+ }
3922
+ return false;
3923
+ }
3924
+ function hashString(content) {
3925
+ return createHash("sha256").update(content).digest("hex");
3926
+ }
3927
+ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes) {
3928
+ const fileHashes = {};
3929
+ const fileMtimes = {};
3930
+ const gitignoreStack = await loadRootGitignoreStack(projectRoot);
3931
+ const allFiles = [];
3932
+ for (const tf of trackedFiles) {
3933
+ if (tf.syntheticHash) {
3934
+ fileHashes[tf.path] = tf.syntheticHash;
3935
+ continue;
3936
+ }
3937
+ const absPath = path15.join(projectRoot, tf.path);
3938
+ try {
3939
+ const st = await stat5(absPath);
3940
+ if (st.isDirectory()) {
3941
+ const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
3942
+ projectRoot,
3943
+ gitignoreStack
3944
+ });
3945
+ for (const entry of dirEntries) {
3946
+ allFiles.push({
3947
+ relPath: path15.join(tf.path, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""),
3948
+ absPath: entry.absPath,
3949
+ mtimeMs: entry.mtimeMs
3950
+ });
3951
+ }
3952
+ } else {
3953
+ allFiles.push({ relPath: tf.path, absPath, mtimeMs: st.mtimeMs });
3954
+ }
3955
+ } catch {
3956
+ continue;
3957
+ }
3958
+ }
3959
+ const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => entry.relPath === prefix || entry.relPath.startsWith(prefix + "/"))) : allFiles;
3960
+ const dirty = [];
3961
+ for (const entry of filtered) {
3962
+ const storedMtime = storedFileData?.mtimes[entry.relPath];
3963
+ const storedHash = storedFileData?.hashes[entry.relPath];
3964
+ if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
3965
+ fileHashes[entry.relPath] = storedHash;
3966
+ } else {
3967
+ dirty.push(entry);
3968
+ }
3969
+ fileMtimes[entry.relPath] = entry.mtimeMs;
3970
+ }
3971
+ const BATCH_SIZE = 256;
3972
+ for (let i = 0; i < dirty.length; i += BATCH_SIZE) {
3973
+ const batch = dirty.slice(i, i + BATCH_SIZE);
3974
+ const hashes = await Promise.all(batch.map((e) => hashFile(e.absPath)));
3975
+ for (let j = 0; j < batch.length; j++) {
3976
+ fileHashes[batch[j].relPath] = hashes[j];
3977
+ }
3978
+ }
3979
+ const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
3980
+ const digest = sorted.map(([p2, h]) => `${p2}:${h}`).join("\n");
3981
+ const canonicalHash = hashString(digest);
3982
+ return { canonicalHash, fileHashes, fileMtimes };
3983
+ }
3984
+ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
3985
+ let stack = options.gitignoreStack ?? [];
3986
+ try {
3987
+ const localContent = await readFile14(path15.join(directoryPath, ".gitignore"), "utf-8");
3988
+ const localMatcher = ignoreFactory();
3989
+ localMatcher.add(localContent);
3990
+ stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
3991
+ } catch {
3992
+ }
3993
+ const entries = await readdir6(directoryPath, { withFileTypes: true });
3994
+ const dirs = [];
3995
+ const files = [];
3996
+ for (const entry of entries) {
3997
+ const absoluteChildPath = path15.join(directoryPath, entry.name);
3998
+ if (isIgnoredByStack(absoluteChildPath, stack)) continue;
3999
+ if (entry.isDirectory()) dirs.push(absoluteChildPath);
4000
+ else if (entry.isFile()) files.push(absoluteChildPath);
4001
+ }
4002
+ const [dirResults, fileStats] = await Promise.all([
4003
+ Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
4004
+ projectRoot: options.projectRoot,
4005
+ gitignoreStack: stack
4006
+ }))),
4007
+ Promise.all(files.map(async (f) => {
4008
+ const fileStat = await stat5(f);
4009
+ return {
4010
+ relPath: path15.relative(rootDirectoryPath, f).replace(/\\/g, "/").replace(/\/+$/, ""),
4011
+ absPath: f,
4012
+ mtimeMs: fileStat.mtimeMs
4013
+ };
4014
+ }))
4015
+ ]);
4016
+ const result = [];
4017
+ for (const nested of dirResults) result.push(...nested);
4018
+ result.push(...fileStats);
4019
+ return result;
4020
+ }
4021
+ async function expandMappingPaths(projectRoot, mappingPaths) {
4022
+ const gitignoreStack = await loadRootGitignoreStack(projectRoot);
4023
+ const result = [];
4024
+ for (const mp of mappingPaths) {
4025
+ const absPath = path15.join(projectRoot, mp);
4026
+ try {
4027
+ const st = await stat5(absPath);
4028
+ if (st.isDirectory()) {
4029
+ const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
4030
+ projectRoot,
4031
+ gitignoreStack
4032
+ });
4033
+ for (const entry of dirEntries) {
4034
+ result.push(path15.join(mp, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""));
4035
+ }
4036
+ } else {
4037
+ result.push(mp);
4038
+ }
4039
+ } catch {
4040
+ continue;
4041
+ }
4042
+ }
4043
+ return result;
4044
+ }
4045
+
3894
4046
  // src/cli/build-context.ts
3895
4047
  function findCandidateNodes(graph, unmappedFile) {
3896
4048
  const dir = unmappedFile.replace(/\/[^/]+$/, "");
@@ -4024,17 +4176,15 @@ ${errorList}`
4024
4176
  process.stdout.write(formatFileContext(data));
4025
4177
  } else {
4026
4178
  const data = buildNodeContextData(graph, nodePath);
4179
+ const projectRoot = projectRootFromGraph(graph.rootPath);
4180
+ data.sourceFiles = await expandMappingPaths(projectRoot, data.sourceFiles);
4027
4181
  process.stdout.write(formatNodeContext(data));
4028
4182
  }
4029
4183
  } catch (error) {
4030
4184
  const err = error;
4031
4185
  if (err.code === "ENOENT") {
4032
4186
  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")
4187
+ chalk3.red("Error: No .yggdrasil/ directory found. Run 'yg init' first.\n")
4038
4188
  );
4039
4189
  } else {
4040
4190
  process.stderr.write(chalk3.red(`Error: ${error.message}
@@ -4049,31 +4199,30 @@ ${errorList}`
4049
4199
  // src/cli/approve.ts
4050
4200
  import chalk4 from "chalk";
4051
4201
  import path20 from "path";
4052
- import { readFile as readFile18 } from "fs/promises";
4053
4202
 
4054
4203
  // 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";
4204
+ import { readFile as readFile15, writeFile as writeFile5, stat as stat6, readdir as readdir7, mkdir as mkdir3, rm as rm2 } from "fs/promises";
4205
+ import path16 from "path";
4057
4206
  var DRIFT_STATE_DIR = ".drift-state";
4058
4207
  function nodeStatePath(yggRoot, nodePath) {
4059
- return path15.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
4208
+ return path16.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
4060
4209
  }
4061
4210
  async function scanJsonFiles(dir, baseDir) {
4062
4211
  const results = [];
4063
4212
  let entries;
4064
4213
  try {
4065
- entries = await readdir6(dir, { withFileTypes: true });
4214
+ entries = await readdir7(dir, { withFileTypes: true });
4066
4215
  } catch (err) {
4067
4216
  debugWrite(`[drift-state-store] scanJsonFiles readdir: ${err.message}`);
4068
4217
  return results;
4069
4218
  }
4070
4219
  for (const entry of entries) {
4071
- const fullPath = path15.join(dir, entry.name);
4220
+ const fullPath = path16.join(dir, entry.name);
4072
4221
  if (entry.isDirectory()) {
4073
4222
  const nested = await scanJsonFiles(fullPath, baseDir);
4074
4223
  results.push(...nested);
4075
4224
  } else if (entry.isFile() && entry.name.endsWith(".json")) {
4076
- const relPath = path15.relative(baseDir, fullPath);
4225
+ const relPath = path16.relative(baseDir, fullPath);
4077
4226
  const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
4078
4227
  results.push(nodePath);
4079
4228
  }
@@ -4081,13 +4230,13 @@ async function scanJsonFiles(dir, baseDir) {
4081
4230
  return results;
4082
4231
  }
4083
4232
  async function removeEmptyParents(filePath, stopDir) {
4084
- let dir = path15.dirname(filePath);
4233
+ let dir = path16.dirname(filePath);
4085
4234
  while (dir !== stopDir && dir.startsWith(stopDir)) {
4086
4235
  try {
4087
- const entries = await readdir6(dir);
4236
+ const entries = await readdir7(dir);
4088
4237
  if (entries.length === 0) {
4089
4238
  await rm2(dir, { recursive: true });
4090
- dir = path15.dirname(dir);
4239
+ dir = path16.dirname(dir);
4091
4240
  } else {
4092
4241
  break;
4093
4242
  }
@@ -4100,7 +4249,7 @@ async function removeEmptyParents(filePath, stopDir) {
4100
4249
  async function readNodeDriftState(yggRoot, nodePath) {
4101
4250
  try {
4102
4251
  const filePath = nodeStatePath(yggRoot, nodePath);
4103
- const content = await readFile14(filePath, "utf-8");
4252
+ const content = await readFile15(filePath, "utf-8");
4104
4253
  const parsed = JSON.parse(content);
4105
4254
  return parsed;
4106
4255
  } catch (err) {
@@ -4110,12 +4259,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
4110
4259
  }
4111
4260
  async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
4112
4261
  const filePath = nodeStatePath(yggRoot, nodePath);
4113
- await mkdir3(path15.dirname(filePath), { recursive: true });
4262
+ await mkdir3(path16.dirname(filePath), { recursive: true });
4114
4263
  const content = JSON.stringify(nodeState, null, 2) + "\n";
4115
4264
  await writeFile5(filePath, content, "utf-8");
4116
4265
  }
4117
4266
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
4118
- const driftDir = path15.join(yggRoot, DRIFT_STATE_DIR);
4267
+ const driftDir = path16.join(yggRoot, DRIFT_STATE_DIR);
4119
4268
  const allNodePaths = await scanJsonFiles(driftDir, driftDir);
4120
4269
  const removed = [];
4121
4270
  for (const nodePath of allNodePaths) {
@@ -4129,10 +4278,10 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
4129
4278
  return removed.sort();
4130
4279
  }
4131
4280
  async function readDriftState(yggRoot) {
4132
- const driftPath = path15.join(yggRoot, DRIFT_STATE_DIR);
4281
+ const driftPath = path16.join(yggRoot, DRIFT_STATE_DIR);
4133
4282
  let driftStat;
4134
4283
  try {
4135
- driftStat = await stat5(driftPath);
4284
+ driftStat = await stat6(driftPath);
4136
4285
  } catch (err) {
4137
4286
  debugWrite(`[drift-state-store] readDriftState stat: ${err.message}`);
4138
4287
  return {};
@@ -4152,134 +4301,6 @@ async function readDriftState(yggRoot) {
4152
4301
  return state;
4153
4302
  }
4154
4303
 
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
4304
  // src/core/context-files.ts
4284
4305
  import path17 from "path";
4285
4306
  import { createHash as createHash2 } from "crypto";
@@ -5760,14 +5781,13 @@ function registerApproveCommand(program2) {
5760
5781
  }
5761
5782
  const aspects = resolveAspects(node2, graph);
5762
5783
  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
- }
5784
+ const trackedFiles = collectTrackedFiles(node2, graph);
5785
+ const { fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, void 0, []);
5786
+ const sourceFilePaths = Object.keys(fileHashes).filter((f) => {
5787
+ const normalized = f.replace(/\\/g, "/").replace(/\/+$/, "");
5788
+ return !normalized.startsWith(yggPrefix);
5789
+ });
5790
+ const sourceFiles = await loadSourceFiles(sourceFilePaths, projectRoot);
5771
5791
  process.stdout.write(chalk4.bold(`
5772
5792
  --- Dry run: ${nodePath2} ---
5773
5793
 
@@ -6531,7 +6551,7 @@ function registerFlowsCommand(program2) {
6531
6551
 
6532
6552
  // src/cli/check.ts
6533
6553
  import chalk9 from "chalk";
6534
- import { execSync } from "child_process";
6554
+ import { execFileSync } from "child_process";
6535
6555
  import path21 from "path";
6536
6556
  function registerCheckCommand(program2) {
6537
6557
  program2.command("check").description("Unified graph gate \u2014 errors, drift, coverage, completeness").action(async () => {
@@ -6542,7 +6562,7 @@ function registerCheckCommand(program2) {
6542
6562
  let gitFiles = null;
6543
6563
  try {
6544
6564
  const projectRoot = path21.dirname(graph.rootPath);
6545
- const output = execSync("git ls-files .", {
6565
+ const output = execFileSync("git", ["ls-files", "."], {
6546
6566
  cwd: projectRoot,
6547
6567
  encoding: "utf-8",
6548
6568
  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.1",
4
4
  "description": "Continuous architecture enforcement for AI-assisted development. Aspects, review, enforcement.",
5
5
  "type": "module",
6
6
  "bin": {