@vzn/vx 0.0.555 → 0.0.557

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.555",
3
+ "version": "0.0.557",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.555",
25
- "@vzn/vx-linux-arm64": "0.0.555",
26
- "@vzn/vx-darwin-x64": "0.0.555",
27
- "@vzn/vx-darwin-arm64": "0.0.555"
24
+ "@vzn/vx-linux-x64": "0.0.557",
25
+ "@vzn/vx-linux-arm64": "0.0.557",
26
+ "@vzn/vx-darwin-x64": "0.0.557",
27
+ "@vzn/vx-darwin-arm64": "0.0.557"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -692,10 +692,17 @@ export async function cleanOutputPaths(args: {
692
692
  await pruneEmptiedDirs(args.projectDir, await removeAll(files, args.projectDir))
693
693
  }
694
694
 
695
- /** A file's identity for the additive diff: what the hit path's fingerprint trusts too. */
695
+ /**
696
+ * A file's identity for the additive diff: what the hit path's
697
+ * `isOutputsCurrent` trusts too. Size and mtime alone missed a rewrite to
698
+ * bytes of the same length with the mtime stamped back; the inode and
699
+ * ctime are what no task sets (item 886).
700
+ */
696
701
  export interface OutputStamp {
697
702
  size: number
698
703
  mtimeMs: number
704
+ ino: number
705
+ ctimeMs: number
699
706
  }
700
707
 
701
708
  /**
@@ -724,7 +731,7 @@ function stampFiles(files: readonly string[]): Map<string, OutputStamp> {
724
731
  for (const f of files) {
725
732
  try {
726
733
  const st = lstatSync(f)
727
- out.set(f, { size: st.size, mtimeMs: st.mtimeMs })
734
+ out.set(f, { size: st.size, mtimeMs: st.mtimeMs, ino: st.ino, ctimeMs: st.ctimeMs })
728
735
  } catch {
729
736
  // Gone between the walk and the stat: not a file the task found.
730
737
  }
@@ -734,16 +741,20 @@ function stampFiles(files: readonly string[]): Map<string, OutputStamp> {
734
741
 
735
742
  /**
736
743
  * The files an additive task's run ADDED or CHANGED under its declared
737
- * outputs: every selected file that was not in `before`, or whose size or
738
- * mtime moved. Size + mtime is the proof the hit path already trusts for
739
- * a current tree, so no new trust is introduced. A file the run rewrote
740
- * with identical bytes counts as its own (the mtime moved), which is the
744
+ * outputs: every selected file that was not in `before`, or whose stamp
745
+ * (`OutputStamp`) moved — the proof the hit path trusts for a current
746
+ * tree, so no new trust is introduced. A file the run rewrote, even with
747
+ * identical bytes, counts as its own (its ctime moved), which is the
741
748
  * rewrite-in-place cost the design note records.
749
+ *
750
+ * Undefined when the run REMOVED a file it found: an artifact holds what a
751
+ * run wrote, never what it took away, and the upstream's restore puts the
752
+ * file back, so no entry reproduces that run and none is saved.
742
753
  */
743
754
  export async function ownOutputsSince(
744
755
  args: { projectDir: string; outputs: string[]; nestedProjectDirs: string[] },
745
756
  before: ReadonlyMap<string, OutputStamp>,
746
- ): Promise<string[]> {
757
+ ): Promise<string[] | undefined> {
747
758
  return changedSince(await resolveOutputs(args), before)
748
759
  }
749
760
 
@@ -751,15 +762,16 @@ export async function ownOutputsSince(
751
762
  export async function ownWorkspaceOutputsSince(
752
763
  args: { workspaceRoot: string; outputs: string[] },
753
764
  before: ReadonlyMap<string, OutputStamp>,
754
- ): Promise<string[]> {
765
+ ): Promise<string[] | undefined> {
755
766
  return changedSince(await resolveWorkspaceOutputs(args), before)
756
767
  }
757
768
 
758
769
  function changedSince(
759
770
  after: readonly string[],
760
771
  before: ReadonlyMap<string, OutputStamp>,
761
- ): string[] {
772
+ ): string[] | undefined {
762
773
  const own: string[] = []
774
+ let kept = 0
763
775
  for (const f of after) {
764
776
  const was = before.get(f)
765
777
  if (was === undefined) {
@@ -768,12 +780,19 @@ function changedSince(
768
780
  }
769
781
  try {
770
782
  const st = lstatSync(f)
771
- if (st.size !== was.size || st.mtimeMs !== was.mtimeMs) own.push(f)
783
+ kept++
784
+ if (
785
+ st.size !== was.size ||
786
+ st.mtimeMs !== was.mtimeMs ||
787
+ st.ino !== was.ino ||
788
+ st.ctimeMs !== was.ctimeMs
789
+ )
790
+ own.push(f)
772
791
  } catch {
773
- // Vanished since the walk: not an output.
792
+ // Vanished since the walk: removed, as one the walk missed is.
774
793
  }
775
794
  }
776
- return own
795
+ return kept < before.size ? undefined : own
777
796
  }
778
797
 
779
798
  /**
@@ -1213,6 +1213,7 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1213
1213
 
1214
1214
  const wallclockEndNs = process.hrtime.bigint() - args.runStartHrTimeNs
1215
1215
 
1216
+ let own: Awaited<ReturnType<typeof ownOutputs>>
1216
1217
  if (effectiveExitCode === 0 && willSave && deferralRequested) {
1217
1218
  // The outputs never landed here: no artifact, no rows. A partial local
1218
1219
  // record (a row with no artifact) is exactly the corrupt-entry shape
@@ -1234,18 +1235,14 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1234
1235
  },
1235
1236
  })
1236
1237
  }
1237
- } else if (effectiveExitCode === 0 && willSave && (await keyStillTrue())) {
1238
- const ownOutputFiles =
1239
- additive && stampedBefore !== undefined
1240
- ? await ownOutputsSince(cleanArgs, stampedBefore)
1241
- : undefined
1242
- const ownWsOutputFiles =
1243
- additive && wsStampedBefore !== undefined
1244
- ? await ownWorkspaceOutputsSince(wsCleanArgs, wsStampedBefore)
1245
- : undefined
1238
+ } else if (
1239
+ effectiveExitCode === 0 &&
1240
+ willSave &&
1241
+ (await keyStillTrue()) &&
1242
+ (own = await ownOutputs()) !== undefined
1243
+ ) {
1246
1244
  const { landed } = await saveMiss({
1247
- ...(ownOutputFiles !== undefined ? { ownOutputFiles } : {}),
1248
- ...(ownWsOutputFiles !== undefined ? { ownWsOutputFiles } : {}),
1245
+ ...own,
1249
1246
  node,
1250
1247
  hash,
1251
1248
  cache,
@@ -1302,6 +1299,28 @@ async function executeCachedTask(args: ExecuteArgs): Promise<TaskOutcome> {
1302
1299
  return true
1303
1300
  }
1304
1301
 
1302
+ /**
1303
+ * What an additive run saves as its own, or undefined when it removed a
1304
+ * file it found: no entry replays a removal (`ownOutputsSince`), so the
1305
+ * task saves nothing and runs again.
1306
+ */
1307
+ async function ownOutputs(): Promise<
1308
+ { ownOutputFiles?: string[]; ownWsOutputFiles?: string[] } | undefined
1309
+ > {
1310
+ const mine: { ownOutputFiles?: string[]; ownWsOutputFiles?: string[] } = {}
1311
+ if (stampedBefore !== undefined) {
1312
+ const files = await ownOutputsSince(cleanArgs, stampedBefore)
1313
+ if (files === undefined) return undefined
1314
+ mine.ownOutputFiles = files
1315
+ }
1316
+ if (wsStampedBefore !== undefined) {
1317
+ const files = await ownWorkspaceOutputsSince(wsCleanArgs, wsStampedBefore)
1318
+ if (files === undefined) return undefined
1319
+ mine.ownWsOutputFiles = files
1320
+ }
1321
+ return mine
1322
+ }
1323
+
1305
1324
  /**
1306
1325
  * The input that no longer holds what the key folded: `null` when the
1307
1326
  * key re-derived just before the command already differed (the file