@riddledc/riddle-proof 0.5.4 → 0.5.6

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.
@@ -2,9 +2,11 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { execSync } from "node:child_process";
3
3
  import {
4
4
  existsSync,
5
+ copyFileSync,
5
6
  lstatSync,
6
7
  mkdirSync,
7
8
  readFileSync,
9
+ renameSync,
8
10
  rmSync,
9
11
  symlinkSync,
10
12
  unlinkSync,
@@ -276,6 +278,7 @@ export function ensureWorktree({
276
278
  }
277
279
 
278
280
  const DEPS_MANIFEST = ".workspace-core-deps.json";
281
+ const DEPS_INPUT_FILES = ["package.json", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock"];
279
282
 
280
283
  function depsManifestPath(projectDir) {
281
284
  return path.join(projectDir, "node_modules", DEPS_MANIFEST);
@@ -285,7 +288,7 @@ export function computeDependencyFingerprint(projectDir) {
285
288
  const packageJson = path.join(projectDir, "package.json");
286
289
  if (!existsSync(packageJson)) return "";
287
290
  const digest = createHash("sha256");
288
- for (const name of ["package.json", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock"]) {
291
+ for (const name of DEPS_INPUT_FILES) {
289
292
  const filePath = path.join(projectDir, name);
290
293
  if (!existsSync(filePath)) continue;
291
294
  digest.update(name);
@@ -318,6 +321,93 @@ function writeDepsManifest(projectDir, fingerprint, installCmd) {
318
321
  writeFileSync(manifestPath, JSON.stringify({ fingerprint, install_cmd: installCmd }, null, 2));
319
322
  }
320
323
 
324
+ function dependencyCacheRoot(projectDir) {
325
+ if (process.env.RIDDLE_PROOF_DISABLE_DEPS_CACHE === "1") return "";
326
+ const configured = (process.env.RIDDLE_PROOF_DEPS_CACHE_ROOT || "").trim();
327
+ if (configured) return path.resolve(configured);
328
+
329
+ const resolved = path.resolve(projectDir);
330
+ const worktreeMarker = `${path.sep}.riddle-proof-worktrees${path.sep}`;
331
+ const worktreeIndex = resolved.indexOf(worktreeMarker);
332
+ if (worktreeIndex >= 0) {
333
+ return path.join(resolved.slice(0, worktreeIndex), ".riddle-proof-deps-cache");
334
+ }
335
+ return path.join(path.dirname(resolved), ".riddle-proof-deps-cache");
336
+ }
337
+
338
+ function dependencyCacheKey(fingerprint, installCmd) {
339
+ const nodeMajor = (process.versions.node || "").split(".")[0] || "unknown";
340
+ return [
341
+ sanitizeFragment(installCmd, "install"),
342
+ process.platform,
343
+ process.arch,
344
+ `node${sanitizeFragment(nodeMajor, "unknown")}`,
345
+ fingerprint.slice(0, 24),
346
+ ].join("-");
347
+ }
348
+
349
+ function copyDependencyInputs(sourceDir, targetDir) {
350
+ mkdirSync(targetDir, { recursive: true });
351
+ for (const name of DEPS_INPUT_FILES) {
352
+ const sourcePath = path.join(sourceDir, name);
353
+ if (existsSync(sourcePath)) {
354
+ copyFileSync(sourcePath, path.join(targetDir, name));
355
+ }
356
+ }
357
+ }
358
+
359
+ function linkNodeModules(projectDir, sourceModules) {
360
+ const projectModules = path.join(projectDir, "node_modules");
361
+ removePath(projectModules);
362
+ symlinkSync(sourceModules, projectModules, "dir");
363
+ }
364
+
365
+ function tryEnsureCachedDeps({ projectDir, fingerprint, installCmd }) {
366
+ const cacheRoot = dependencyCacheRoot(projectDir);
367
+ if (!cacheRoot) return "";
368
+
369
+ const cacheDir = path.join(cacheRoot, dependencyCacheKey(fingerprint, installCmd));
370
+ const cacheModules = path.join(cacheDir, "node_modules");
371
+ const cacheManifest = readDepsManifest(cacheDir);
372
+ if (cacheManifest.fingerprint === fingerprint && cacheManifest.install_cmd === installCmd && existsSync(cacheModules)) {
373
+ linkNodeModules(projectDir, cacheModules);
374
+ return `reused_cache:${cacheDir}`;
375
+ }
376
+
377
+ const tempCacheDir = `${cacheDir}.tmp-${process.pid}-${randomUUID().slice(0, 8)}`;
378
+ try {
379
+ mkdirSync(path.dirname(cacheDir), { recursive: true });
380
+ removePath(tempCacheDir);
381
+ copyDependencyInputs(projectDir, tempCacheDir);
382
+
383
+ const installResult = runSafe(`${installCmd} 2>&1 | tail -5`, tempCacheDir, dependencyInstallTimeoutMs());
384
+ if (!installResult.ok) {
385
+ removePath(tempCacheDir);
386
+ return "";
387
+ }
388
+ writeDepsManifest(tempCacheDir, fingerprint, installCmd);
389
+
390
+ if (!existsSync(cacheDir)) {
391
+ try {
392
+ renameSync(tempCacheDir, cacheDir);
393
+ } catch {
394
+ removePath(tempCacheDir);
395
+ }
396
+ } else {
397
+ removePath(tempCacheDir);
398
+ }
399
+
400
+ const finalManifest = readDepsManifest(cacheDir);
401
+ if (finalManifest.fingerprint === fingerprint && finalManifest.install_cmd === installCmd && existsSync(cacheModules)) {
402
+ linkNodeModules(projectDir, cacheModules);
403
+ return `cached:${installCmd}`;
404
+ }
405
+ } catch {
406
+ removePath(tempCacheDir);
407
+ }
408
+ return "";
409
+ }
410
+
321
411
  function dependencyInstallTimeoutMs() {
322
412
  const parsed = Number.parseInt(process.env.RIDDLE_PROOF_INSTALL_TIMEOUT_MS || "", 10);
323
413
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 600000;
@@ -337,15 +427,17 @@ export function ensureDeps({ projectDir, reuseFrom = "" } = {}) {
337
427
  const sourceManifest = readDepsManifest(reuseFrom);
338
428
  const sourceModules = path.join(reuseFrom, "node_modules");
339
429
  if (sourceFingerprint === fingerprint && sourceManifest.fingerprint === fingerprint && existsSync(sourceModules)) {
340
- const projectModules = path.join(projectDir, "node_modules");
341
- removePath(projectModules);
342
- symlinkSync(sourceModules, projectModules);
430
+ linkNodeModules(projectDir, sourceModules);
343
431
  return `reused_from:${reuseFrom}`;
344
432
  }
345
433
  }
346
434
 
347
435
  const installCmd = detectInstallCommand(projectDir);
348
436
  if (!installCmd) return "no_install_command";
437
+ const cachedStatus = tryEnsureCachedDeps({ projectDir, fingerprint, installCmd });
438
+ if (cachedStatus) return cachedStatus;
439
+
440
+ removePath(path.join(projectDir, "node_modules"));
349
441
  const installResult = runSafe(`${installCmd} 2>&1 | tail -5`, projectDir, dependencyInstallTimeoutMs());
350
442
  if (!installResult.ok) {
351
443
  throw new Error(`dependency install failed in ${projectDir}: ${installResult.output.slice(0, 300)}`);
@@ -385,6 +477,9 @@ async function main() {
385
477
  case "ensure-deps":
386
478
  ok({ status: ensureDeps(payload) });
387
479
  return;
480
+ case "dependency-fingerprint":
481
+ ok({ fingerprint: computeDependencyFingerprint(payload.projectDir) });
482
+ return;
388
483
  default:
389
484
  throw new Error(`Unsupported command: ${command}`);
390
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -77,6 +77,13 @@ def ensure_deps(project_dir, reuse_from=''):
77
77
  return result.get('status', '')
78
78
 
79
79
 
80
+ def dependency_fingerprint(project_dir):
81
+ if not project_dir:
82
+ return ''
83
+ result = workspace_core('dependency-fingerprint', {'projectDir': project_dir}, timeout=30)
84
+ return result.get('fingerprint', '') or ''
85
+
86
+
80
87
  def record_setup_phase(phase, status='running', summary=''):
81
88
  global s
82
89
  ts = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
@@ -131,6 +138,24 @@ def ensure_deps_phase(phase, project_dir, reuse_from='', summary=''):
131
138
  return status
132
139
 
133
140
 
141
+ def dependencies_match(left_dir, right_dir):
142
+ left = dependency_fingerprint(left_dir)
143
+ right = dependency_fingerprint(right_dir)
144
+ return bool(left and right and left == right)
145
+
146
+
147
+ def compatible_reuse_source(source_dir, target_dirs):
148
+ if not source_dir:
149
+ return ''
150
+ source = dependency_fingerprint(source_dir)
151
+ if not source:
152
+ return ''
153
+ for target_dir in target_dirs:
154
+ if target_dir and dependency_fingerprint(target_dir) == source:
155
+ return source_dir
156
+ return ''
157
+
158
+
134
159
  def resolve_worktree_root(repo_dir):
135
160
  configured = (s.get('worktree_root') or os.environ.get('RIDDLE_PROOF_WORKTREE_ROOT') or '').strip()
136
161
  if configured:
@@ -401,17 +426,36 @@ print('After worktree: ' + AFTER_DIR + ' (' + AFTER_WORKTREE_BRANCH + ' -> ' + b
401
426
  apply_repo_profile(AFTER_DIR)
402
427
  save_state(s)
403
428
 
429
+ target_dependency_dirs = [AFTER_DIR]
430
+ if reference in ('before', 'both'):
431
+ target_dependency_dirs.append(BEFORE_DIR)
432
+
404
433
  reuse_source = repo_dir if os.path.exists(os.path.join(repo_dir, 'package.json')) else ''
405
- shared_status = ensure_deps_phase('shared_deps', reuse_source, summary='Ensuring shared repository dependencies.') if reuse_source else ''
406
- if shared_status:
407
- print('Shared deps status: ' + shared_status)
434
+ shared_reuse_source = compatible_reuse_source(reuse_source, target_dependency_dirs)
435
+ shared_status = ''
436
+ if shared_reuse_source:
437
+ shared_status = ensure_deps_phase('shared_deps', shared_reuse_source, summary='Ensuring shared repository dependencies.')
438
+ if shared_status:
439
+ print('Shared deps status: ' + shared_status)
440
+ elif reuse_source:
441
+ record_setup_phase(
442
+ 'shared_deps',
443
+ 'completed',
444
+ 'skipped: active workspace dependencies differ from proof worktrees',
445
+ )
446
+ print('Shared deps skipped: active workspace dependencies differ from proof worktrees')
408
447
 
409
448
  before_dep_status = ''
410
449
  if reference in ('before', 'both'):
411
- before_dep_status = ensure_deps_phase('before_deps', BEFORE_DIR, reuse_from=reuse_source, summary='Ensuring before-worktree dependencies.')
450
+ before_dep_status = ensure_deps_phase('before_deps', BEFORE_DIR, reuse_from=shared_reuse_source, summary='Ensuring before-worktree dependencies.')
412
451
  print('Before deps status: ' + before_dep_status)
413
452
 
414
- after_dep_status = ensure_deps_phase('after_deps', AFTER_DIR, reuse_from=reuse_source, summary='Ensuring after-worktree dependencies.')
453
+ after_reuse_source = ''
454
+ if before_dep_status and dependencies_match(BEFORE_DIR, AFTER_DIR):
455
+ after_reuse_source = BEFORE_DIR
456
+ elif shared_reuse_source:
457
+ after_reuse_source = shared_reuse_source
458
+ after_dep_status = ensure_deps_phase('after_deps', AFTER_DIR, reuse_from=after_reuse_source, summary='Ensuring after-worktree dependencies.')
415
459
  print('After deps status: ' + after_dep_status)
416
460
 
417
461
  # Patch Next.js config in after worktree if needed