@vzn/vx 0.0.573 → 0.0.575

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.573",
3
+ "version": "0.0.575",
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.573",
25
- "@vzn/vx-linux-arm64": "0.0.573",
26
- "@vzn/vx-darwin-x64": "0.0.573",
27
- "@vzn/vx-darwin-arm64": "0.0.573"
24
+ "@vzn/vx-linux-x64": "0.0.575",
25
+ "@vzn/vx-linux-arm64": "0.0.575",
26
+ "@vzn/vx-darwin-x64": "0.0.575",
27
+ "@vzn/vx-darwin-arm64": "0.0.575"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/why.ts CHANGED
@@ -106,7 +106,7 @@ export function parseWhyArgs(args: readonly string[]): WhyArgs {
106
106
  * every recorded id (the same rule `vx run` hints with) and the hint is
107
107
  * the runnable `project#task`; an anchored query is matched whole.
108
108
  */
109
- function suggest(query: string, ids: readonly string[]): string {
109
+ function suggest(query: string, ids: readonly string[], invoked: boolean): string {
110
110
  let hits: string[]
111
111
  if (query.includes('#')) {
112
112
  hits = nearMatches(query, ids)
@@ -120,9 +120,12 @@ function suggest(query: string, ids: readonly string[]): string {
120
120
  }
121
121
  if (hits.length > 0) return ` — did you mean ${hits.slice(0, 3).join(', ')}?`
122
122
  // No near name: say where to look, or that there is nothing yet.
123
- return ids.length === 0
124
- ? `; nothing has run here yet (vx run ${query}, then vx why)`
125
- : '; `vx last --list` shows what has run'
123
+ // A run of only group tasks records no task row: "nothing has run" was
124
+ // false there, and its advice re-ran the group to the same answer.
125
+ if (ids.length > 0) return '; `vx last --list` shows what has run'
126
+ return invoked
127
+ ? '; no recorded run executed a task (a group records none: ask about a task it depends on)'
128
+ : `; nothing has run here yet (vx run ${query}, then vx why)`
126
129
  }
127
130
 
128
131
  /**
@@ -151,8 +154,9 @@ async function resolveTarget(
151
154
  .query('SELECT DISTINCT project, task FROM runs ORDER BY project, task')
152
155
  .all() as Array<{ project: string; task: string }>
153
156
  const ids = pairs.map((p) => `${p.project}#${p.task}`)
157
+ const invoked = ids.length === 0 && db.query('SELECT 1 FROM invocations LIMIT 1').get() !== null
154
158
  if (target.includes('#')) {
155
- throw new UserError(`vx why: no recorded runs for "${target}"${suggest(target, ids)}`)
159
+ throw new UserError(`vx why: no recorded runs for "${target}"${suggest(target, ids, invoked)}`)
156
160
  }
157
161
  const matches = ids.filter((id) => id.endsWith(`#${target}`))
158
162
  if (matches.length === 1) return matches[0]!
@@ -164,7 +168,9 @@ async function resolveTarget(
164
168
  matches.map((m) => ` ${m}`).join('\n'),
165
169
  )
166
170
  }
167
- throw new UserError(`vx why: no recorded runs for task "${target}"${suggest(target, ids)}`)
171
+ throw new UserError(
172
+ `vx why: no recorded runs for task "${target}"${suggest(target, ids, invoked)}`,
173
+ )
168
174
  }
169
175
 
170
176
  const fmtWhen = (ms: number): string => new Date(ms).toISOString()
@@ -684,6 +684,10 @@ export function excludeDependencies(
684
684
  * 445). One rule, one place: the copy is gone.
685
685
  */
686
686
  export function outputsOverlap(rawA: string, rawB: string): boolean {
687
+ // A built path proves overlap only between globs as written: a literal's
688
+ // `/**` twin reads it as a directory, and `dist/types.d.ts` met
689
+ // `dist/**/*.js` at `dist/types.d.ts/x.js`, a file no one can write.
690
+ const bothGlobs = !isLiteralPattern(rawA) && !isLiteralPattern(rawB)
687
691
  for (const a of asTrees([rawA])) {
688
692
  for (const b of asTrees([rawB])) {
689
693
  if (isLiteralPattern(a) && isLiteralPattern(b)) {
@@ -692,7 +696,41 @@ export function outputsOverlap(rawA: string, rawB: string): boolean {
692
696
  if (taskGlob(b).match(a)) return true
693
697
  } else if (isLiteralPattern(b)) {
694
698
  if (taskGlob(a).match(b)) return true
695
- } else if (a === b || covers(a, b) || covers(b, a)) return true
699
+ } else if (a === b || covers(a, b) || covers(b, a) || (bothGlobs && witnessed(a, b)))
700
+ return true
701
+ }
702
+ }
703
+ return false
704
+ }
705
+
706
+ /** `staticPrefix` names the project root `.`; a path built under it starts bare. */
707
+ const rootless = (prefix: string): string => (prefix === '.' ? '' : prefix)
708
+
709
+ /**
710
+ * Do two globs both match a path built from them? Each candidate is a
711
+ * real path, so a hit proves the overlap and a miss decides nothing. The
712
+ * path sits under the deeper of the two literal prefixes and ends in
713
+ * either glob's last segment with its wildcards filled: a glob for every
714
+ * `.js` file under `dist` beside `dist/sth/**` meets at `dist/sth/x.js`,
715
+ * a pair `covers` cannot decide since neither is a subtree holding the
716
+ * other's prefix.
717
+ */
718
+ function witnessed(a: string, b: string): boolean {
719
+ const pa = rootless(staticPrefix(a))
720
+ const pb = rootless(staticPrefix(b))
721
+ const nested = (outer: string, inner: string): boolean =>
722
+ outer === '' || inner === outer || inner.startsWith(`${outer}/`)
723
+ if (!nested(pa, pb) && !nested(pb, pa)) return false
724
+ const deeper = pa.length >= pb.length ? pa : pb
725
+ const ga = taskGlob(a)
726
+ const gb = taskGlob(b)
727
+ for (const glob of [a, b]) {
728
+ const last = glob.slice(glob.lastIndexOf('/') + 1)
729
+ if (/[[\]{}()!+@]/.test(last)) continue
730
+ const leaf = last.replace(/\*+|\?/g, 'x')
731
+ for (const mid of ['', 'x/']) {
732
+ const candidate = deeper === '' ? `${mid}${leaf}` : `${deeper}/${mid}${leaf}`
733
+ if (ga.match(candidate) && gb.match(candidate)) return true
696
734
  }
697
735
  }
698
736
  return false
@@ -953,6 +991,19 @@ function overlapCandidates<T>(
953
991
  for (let y = x + 1; y < list.length; y++) pair(list[x]!, list[y]!)
954
992
  }
955
993
  }
994
+ // `witnessed`: two globs meet only when one's literal prefix is the
995
+ // other's or under it, so each looks up the globs filed at its prefix
996
+ // and at each ancestor.
997
+ const byPrefix = new Map<string, number[]>()
998
+ for (const [i, prefix] of prefixes) file(byPrefix, rootless(prefix), i)
999
+ for (const [i, raw] of prefixes) {
1000
+ const prefix = rootless(raw)
1001
+ for (const j of byPrefix.get(prefix) ?? []) pair(i, j)
1002
+ if (prefix !== '') for (const j of byPrefix.get('') ?? []) pair(i, j)
1003
+ for (let sep = prefix.indexOf('/'); sep !== -1; sep = prefix.indexOf('/', sep + 1)) {
1004
+ for (const j of byPrefix.get(prefix.slice(0, sep)) ?? []) pair(i, j)
1005
+ }
1006
+ }
956
1007
  for (const [i, prefix] of prefixes) {
957
1008
  for (const j of subtrees.get(prefix) ?? []) pair(i, j)
958
1009
  for (let sep = prefix.indexOf('/'); sep !== -1; sep = prefix.indexOf('/', sep + 1)) {