@preventive/triage 1.0.0-alpha.43 → 1.0.0-alpha.44

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 (57) hide show
  1. package/common/bundle-compare-diff.js +2 -167
  2. package/common/bundle-compare-versions.js +172 -0
  3. package/common/bundle-comparison.d.ts +8 -0
  4. package/common/bundle-comparison.js +62 -0
  5. package/common/managed/routes.js +3 -3
  6. package/common/server-info.ts +8 -0
  7. package/common/workspace-app.js +14 -1
  8. package/out/chunk-2ABLKCOY.js +46 -0
  9. package/out/chunk-5LSXOLUS.js +1 -0
  10. package/out/chunk-5RCM2J63.js +1 -0
  11. package/out/chunk-7B6HB4D7.js +1 -0
  12. package/out/{chunk-4F6JSF57.js → chunk-AJ4GTATZ.js} +1 -1
  13. package/out/chunk-CH4UN6WK.js +225 -0
  14. package/out/chunk-EDKLBQQN.js +17 -0
  15. package/out/{chunk-UXNUAIIC.js → chunk-F47DXCEV.js} +1 -1
  16. package/out/chunk-FK3DZEKL.js +1 -0
  17. package/out/chunk-G6AJWENO.js +1 -0
  18. package/out/chunk-GZWM7UQM.js +2 -0
  19. package/out/chunk-JCIIBLKR.js +1 -0
  20. package/out/chunk-LPCTJYNA.js +7 -0
  21. package/out/{chunk-LAAB5SZT.js → chunk-O4RJY5TP.js} +1 -1
  22. package/out/chunk-OD4LPN3H.js +1 -0
  23. package/out/{chunk-A7M4GV7M.js → chunk-QWDWTBM7.js} +1 -1
  24. package/out/chunk-QYWKC557.js +1 -0
  25. package/out/chunk-R75XOKUP.js +1 -0
  26. package/out/{chunk-RSHMVYNS.js → chunk-SWTLADW5.js} +1 -1
  27. package/out/chunk-T7OKH4VW.js +1 -0
  28. package/out/{chunk-ZMQQCGF6.js → chunk-UTR2LVME.js} +1 -1
  29. package/out/chunk-ZESLQCTF.js +6 -0
  30. package/out/client-managed-import.js +1 -1
  31. package/out/client-managed.js +20 -20
  32. package/out/client-sync.js +1 -1
  33. package/out/graph.js +1 -1
  34. package/out/index.html +14 -10
  35. package/out/terminal.js +1 -1
  36. package/out/view.css +1 -1
  37. package/out/view.js +48 -48
  38. package/package.json +7 -3
  39. package/risk/npm/assess.js +12 -7
  40. package/risk/npm/dependencies.js +11 -0
  41. package/server-managed/comparison.ts +41 -0
  42. package/server-managed/http.ts +99 -31
  43. package/server-managed/locked-packages.ts +11 -0
  44. package/server-managed/pretty-print.ts +14 -9
  45. package/server-managed/public-workspace.ts +26 -8
  46. package/server-managed/repository-risk.ts +11 -9
  47. package/server-managed/team-app.ts +38 -14
  48. package/out/chunk-7RU5EDRO.js +0 -226
  49. package/out/chunk-CLPKNNC7.js +0 -51
  50. package/out/chunk-NWZ4PVPN.js +0 -1
  51. package/out/chunk-OSIMGFUF.js +0 -1
  52. package/out/chunk-RFZQQXSG.js +0 -17
  53. package/out/chunk-VQJCXGIY.js +0 -7
  54. package/out/chunk-WZDTZE64.js +0 -1
  55. package/out/chunk-XOCAPDCM.js +0 -1
  56. package/out/chunk-YVZH5SHP.js +0 -1
  57. package/out/chunk-ZBBOBQ43.js +0 -1
@@ -353,170 +353,5 @@ export function computeBundleDiff(base, other, pkgOf, layout = treeLayout(base.k
353
353
  }
354
354
  }
355
355
 
356
- // The Overview's package rows: each package that differs, once, from its
357
- // sizes (computeBundleDiff's `packages`) and the versions each bundle
358
- // records for it (`bundlePackageVersions`). A package only one side has is
359
- // removed or added, with its size and versions there; one both have is
360
- // changed when its size or its versions moved, with both sides of each —
361
- // own source and workspace modules carry sizes alone.
362
- export function comparePackages(packages, baseVersions, otherVersions) {
363
- const sizes = new Map()
364
- for (const r of packages.onlyBase) sizes.set(r.pkg, { base: r.bytes, other: null })
365
- for (const r of packages.onlyOther) sizes.set(r.pkg, { base: null, other: r.bytes })
366
- for (const r of packages.changed) sizes.set(r.pkg, { base: r.baseBytes, other: r.otherBytes })
367
- for (const r of packages.unchanged ?? []) sizes.set(r.pkg, { base: r.bytes, other: r.bytes })
368
- const versionsOf = (map, pkg) => [...map.get(pkg) ?? []].toSorted(compareSemver)
369
- const moved = new Set([...packages.onlyBase, ...packages.onlyOther, ...packages.changed].map(r => r.pkg))
370
- for (const pkg of new Set([...baseVersions.keys(), ...otherVersions.keys()])) {
371
- if (!sameVersions(versionsOf(baseVersions, pkg), versionsOf(otherVersions, pkg))) moved.add(pkg)
372
- }
373
- const added = [], changed = [], removed = []
374
- for (const pkg of moved) {
375
- const size = sizes.get(pkg) ?? { base: null, other: null }
376
- const after = versionsOf(otherVersions, pkg), before = versionsOf(baseVersions, pkg)
377
- const onBase = size.base !== null || before.length > 0
378
- const onOther = size.other !== null || after.length > 0
379
- if (onBase && !onOther) removed.push({ pkg, bytes: size.base, versions: before })
380
- else if (onOther && !onBase) added.push({ pkg, bytes: size.other, versions: after })
381
- else {
382
- changed.push({
383
- pkg, baseBytes: size.base, otherBytes: size.other,
384
- delta: size.base !== null && size.other !== null ? size.other - size.base : null,
385
- baseVersions: before, otherVersions: after,
386
- direction: before.length > 0 && after.length > 0 && !sameVersions(before, after) ? versionDirection(before, after) : null,
387
- })
388
- }
389
- }
390
- const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
391
- return { removed: removed.toSorted(byPkg), added: added.toSorted(byPkg), changed: changed.toSorted(byPkg) }
392
- }
393
-
394
- // Split a version into its dotted-numeric core and its prerelease tail,
395
- // dropping any `+build` metadata (semver ignores it for precedence).
396
- // `1.2.3-rc.1+sha` → { core: '1.2.3', pre: 'rc.1' }.
397
- function splitVersion(v) {
398
- const s = String(v).trim()
399
- const noBuild = s.split('+', 1)[0]
400
- const dash = noBuild.indexOf('-')
401
- return dash === -1
402
- ? { core: noBuild, pre: '' }
403
- : { core: noBuild.slice(0, dash), pre: noBuild.slice(dash + 1) }
404
- }
405
-
406
- // Lightweight semver-ish comparison, enough to sort versions ascending
407
- // and label an update ↑/↓. Compares the dotted-numeric core part by
408
- // part (a missing part counts as 0); on an equal core a release ranks
409
- // above a prerelease (`1.0.0` > `1.0.0-rc.1`) and two prereleases
410
- // compare by their dot identifiers (numeric where both are numeric,
411
- // else lexical) per the semver precedence rules. NOT a full
412
- // implementation — a non-numeric core segment falls back to a string
413
- // compare of the whole version so ordering stays total and stable.
414
- export function compareSemver(a, b) {
415
- if (a === b) return 0
416
- const A = splitVersion(a)
417
- const B = splitVersion(b)
418
- const ap = A.core.split('.')
419
- const bp = B.core.split('.')
420
- for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
421
- const x = Number(ap[i] ?? 0)
422
- const y = Number(bp[i] ?? 0)
423
- if (Number.isNaN(x) || Number.isNaN(y)) return a < b ? -1 : 1
424
- if (x !== y) return x < y ? -1 : 1
425
- }
426
- if (A.pre === B.pre) return 0
427
- // A core with no prerelease is the higher (released) version.
428
- if (!A.pre) return 1
429
- if (!B.pre) return -1
430
- const ai = A.pre.split('.')
431
- const bi = B.pre.split('.')
432
- for (let i = 0; i < Math.max(ai.length, bi.length); i++) {
433
- const x = ai[i]
434
- const y = bi[i]
435
- if (x === undefined) return -1
436
- if (y === undefined) return 1
437
- if (x === y) continue
438
- const xNum = /^\d+$/u.test(x)
439
- const yNum = /^\d+$/u.test(y)
440
- // Numeric identifiers always rank below non-numeric ones (semver),
441
- // and compare numerically against each other.
442
- if (xNum && yNum) return Number(x) < Number(y) ? -1 : 1
443
- if (xNum !== yNum) return xNum ? -1 : 1
444
- return x < y ? -1 : 1
445
- }
446
- return 0
447
- }
448
-
449
- // True when two ascending-sorted version arrays hold the same members.
450
- function sameVersions(a, b) {
451
- if (a.length !== b.length) return false
452
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
453
- return true
454
- }
455
-
456
- // Direction of a version move, from the versions dropped and the versions
457
- // taken on: 'up' when everything dropped is older than everything taken
458
- // on (`1.0.0` → `2.0.0`), or, with only drops or only additions, than the
459
- // versions kept (`1.0.0, 2.0.0` → `2.0.0` drops the older copy, and a newer
460
- // copy added beside the old is up too); 'down' the other way round; and
461
- // 'changed' when the moves fall between (`1.0.0, 1.2.0, 2.0.0` → `1.0.0,
462
- // 2.0.0`).
463
- export function versionDirection(baseVersions, otherVersions) {
464
- const dropped = baseVersions.filter(v => !otherVersions.includes(v)).toSorted(compareSemver)
465
- const taken = otherVersions.filter(v => !baseVersions.includes(v)).toSorted(compareSemver)
466
- const kept = baseVersions.filter(v => otherVersions.includes(v)).toSorted(compareSemver)
467
- const [from, to] = dropped.length > 0 && taken.length > 0 ? [dropped, taken]
468
- : dropped.length > 0 ? [dropped, kept] : [kept, taken]
469
- if (from.length === 0 || to.length === 0) return 'changed'
470
- // Every version of `a` older than every version of `b`.
471
- const before = (a, b) => compareSemver(a.at(-1), b[0]) < 0
472
- return before(from, to) ? 'up' : before(to, from) ? 'down' : 'changed'
473
- }
474
-
475
- // Diff two `Map<packageName, Set<version>>` inventories (as
476
- // `bundlePackageVersions` returns for each bundle) into the dependency
477
- // version changes between them. Mirrors the file/package diff framing:
478
- // `base` is the open bundle, `other` the one compared against.
479
- //
480
- // {
481
- // updated: [{ pkg, baseVersions: [...], otherVersions: [...],
482
- // direction: 'up' | 'down' | 'changed' }],
483
- // added: [{ pkg, versions: [...] }], // dependency only in other
484
- // removed: [{ pkg, versions: [...] }], // dependency only in base
485
- // totals: { baseDeps, otherDeps }, // distinct package counts
486
- // }
487
- //
488
- // `updated` is the headline — a package present on BOTH sides whose set
489
- // of versions changed (the "what did this bump pull in?" answer);
490
- // packages whose versions are byte-for-byte the same are dropped.
491
- // Version arrays are sorted ascending; the three lists are sorted by
492
- // package name so the dependency list reads alphabetically.
493
- export function computeVersionUpdates(baseVersions, otherVersions) {
494
- const updated = []
495
- const added = []
496
- const removed = []
497
- const names = new Set([...baseVersions.keys(), ...otherVersions.keys()])
498
- for (const pkg of names) {
499
- const bSet = baseVersions.get(pkg)
500
- const oSet = otherVersions.get(pkg)
501
- if (bSet && oSet) {
502
- const b = [...bSet].toSorted(compareSemver)
503
- const o = [...oSet].toSorted(compareSemver)
504
- if (sameVersions(b, o)) continue
505
- updated.push({ pkg, baseVersions: b, otherVersions: o, direction: versionDirection(b, o) })
506
- } else if (bSet) {
507
- removed.push({ pkg, versions: [...bSet].toSorted(compareSemver) })
508
- } else {
509
- added.push({ pkg, versions: [...oSet].toSorted(compareSemver) })
510
- }
511
- }
512
- const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
513
- updated.sort(byPkg)
514
- added.sort(byPkg)
515
- removed.sort(byPkg)
516
- return {
517
- updated,
518
- added,
519
- removed,
520
- totals: { baseDeps: baseVersions.size, otherDeps: otherVersions.size },
521
- }
522
- }
356
+ // The versions' part (bundle-compare-versions.js), as one comparison reads it.
357
+ export { comparePackages, compareSemver, computeVersionUpdates, versionDirection } from './bundle-compare-versions.js'
@@ -0,0 +1,172 @@
1
+ // Dependency versions two bundles record, compared: sorting versions as
2
+ // semver orders them, which way a package moved, and the Overview's package
3
+ // rows from computeBundleDiff's sizes. No @preventive/compare here, so a view
4
+ // that only orders versions loads none of the comparison's pairing.
5
+
6
+ // The Overview's package rows: each package that differs, once, from its
7
+ // sizes (computeBundleDiff's `packages`) and the versions each bundle
8
+ // records for it (`bundlePackageVersions`). A package only one side has is
9
+ // removed or added, with its size and versions there; one both have is
10
+ // changed when its size or its versions moved, with both sides of each —
11
+ // own source and workspace modules carry sizes alone.
12
+ export function comparePackages(packages, baseVersions, otherVersions) {
13
+ const sizes = new Map()
14
+ for (const r of packages.onlyBase) sizes.set(r.pkg, { base: r.bytes, other: null })
15
+ for (const r of packages.onlyOther) sizes.set(r.pkg, { base: null, other: r.bytes })
16
+ for (const r of packages.changed) sizes.set(r.pkg, { base: r.baseBytes, other: r.otherBytes })
17
+ for (const r of packages.unchanged ?? []) sizes.set(r.pkg, { base: r.bytes, other: r.bytes })
18
+ const versionsOf = (map, pkg) => [...map.get(pkg) ?? []].toSorted(compareSemver)
19
+ const moved = new Set([...packages.onlyBase, ...packages.onlyOther, ...packages.changed].map(r => r.pkg))
20
+ for (const pkg of new Set([...baseVersions.keys(), ...otherVersions.keys()])) {
21
+ if (!sameVersions(versionsOf(baseVersions, pkg), versionsOf(otherVersions, pkg))) moved.add(pkg)
22
+ }
23
+ const added = [], changed = [], removed = []
24
+ for (const pkg of moved) {
25
+ const size = sizes.get(pkg) ?? { base: null, other: null }
26
+ const after = versionsOf(otherVersions, pkg), before = versionsOf(baseVersions, pkg)
27
+ const onBase = size.base !== null || before.length > 0
28
+ const onOther = size.other !== null || after.length > 0
29
+ if (onBase && !onOther) removed.push({ pkg, bytes: size.base, versions: before })
30
+ else if (onOther && !onBase) added.push({ pkg, bytes: size.other, versions: after })
31
+ else {
32
+ changed.push({
33
+ pkg, baseBytes: size.base, otherBytes: size.other,
34
+ delta: size.base !== null && size.other !== null ? size.other - size.base : null,
35
+ baseVersions: before, otherVersions: after,
36
+ direction: before.length > 0 && after.length > 0 && !sameVersions(before, after) ? versionDirection(before, after) : null,
37
+ })
38
+ }
39
+ }
40
+ const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
41
+ return { removed: removed.toSorted(byPkg), added: added.toSorted(byPkg), changed: changed.toSorted(byPkg) }
42
+ }
43
+
44
+ // Split a version into its dotted-numeric core and its prerelease tail,
45
+ // dropping any `+build` metadata (semver ignores it for precedence).
46
+ // `1.2.3-rc.1+sha` → { core: '1.2.3', pre: 'rc.1' }.
47
+ function splitVersion(v) {
48
+ const s = String(v).trim()
49
+ const noBuild = s.split('+', 1)[0]
50
+ const dash = noBuild.indexOf('-')
51
+ return dash === -1
52
+ ? { core: noBuild, pre: '' }
53
+ : { core: noBuild.slice(0, dash), pre: noBuild.slice(dash + 1) }
54
+ }
55
+
56
+ // Lightweight semver-ish comparison, enough to sort versions ascending
57
+ // and label an update ↑/↓. Compares the dotted-numeric core part by
58
+ // part (a missing part counts as 0); on an equal core a release ranks
59
+ // above a prerelease (`1.0.0` > `1.0.0-rc.1`) and two prereleases
60
+ // compare by their dot identifiers (numeric where both are numeric,
61
+ // else lexical) per the semver precedence rules. NOT a full
62
+ // implementation — a non-numeric core segment falls back to a string
63
+ // compare of the whole version so ordering stays total and stable.
64
+ export function compareSemver(a, b) {
65
+ if (a === b) return 0
66
+ const A = splitVersion(a)
67
+ const B = splitVersion(b)
68
+ const ap = A.core.split('.')
69
+ const bp = B.core.split('.')
70
+ for (let i = 0; i < Math.max(ap.length, bp.length); i++) {
71
+ const x = Number(ap[i] ?? 0)
72
+ const y = Number(bp[i] ?? 0)
73
+ if (Number.isNaN(x) || Number.isNaN(y)) return a < b ? -1 : 1
74
+ if (x !== y) return x < y ? -1 : 1
75
+ }
76
+ if (A.pre === B.pre) return 0
77
+ // A core with no prerelease is the higher (released) version.
78
+ if (!A.pre) return 1
79
+ if (!B.pre) return -1
80
+ const ai = A.pre.split('.')
81
+ const bi = B.pre.split('.')
82
+ for (let i = 0; i < Math.max(ai.length, bi.length); i++) {
83
+ const x = ai[i]
84
+ const y = bi[i]
85
+ if (x === undefined) return -1
86
+ if (y === undefined) return 1
87
+ if (x === y) continue
88
+ const xNum = /^\d+$/u.test(x)
89
+ const yNum = /^\d+$/u.test(y)
90
+ // Numeric identifiers always rank below non-numeric ones (semver),
91
+ // and compare numerically against each other.
92
+ if (xNum && yNum) return Number(x) < Number(y) ? -1 : 1
93
+ if (xNum !== yNum) return xNum ? -1 : 1
94
+ return x < y ? -1 : 1
95
+ }
96
+ return 0
97
+ }
98
+
99
+ // True when two ascending-sorted version arrays hold the same members.
100
+ function sameVersions(a, b) {
101
+ if (a.length !== b.length) return false
102
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
103
+ return true
104
+ }
105
+
106
+ // Direction of a version move, from the versions dropped and the versions
107
+ // taken on: 'up' when everything dropped is older than everything taken
108
+ // on (`1.0.0` → `2.0.0`), or, with only drops or only additions, than the
109
+ // versions kept (`1.0.0, 2.0.0` → `2.0.0` drops the older copy, and a newer
110
+ // copy added beside the old is up too); 'down' the other way round; and
111
+ // 'changed' when the moves fall between (`1.0.0, 1.2.0, 2.0.0` → `1.0.0,
112
+ // 2.0.0`).
113
+ export function versionDirection(baseVersions, otherVersions) {
114
+ const dropped = baseVersions.filter(v => !otherVersions.includes(v)).toSorted(compareSemver)
115
+ const taken = otherVersions.filter(v => !baseVersions.includes(v)).toSorted(compareSemver)
116
+ const kept = baseVersions.filter(v => otherVersions.includes(v)).toSorted(compareSemver)
117
+ const [from, to] = dropped.length > 0 && taken.length > 0 ? [dropped, taken]
118
+ : dropped.length > 0 ? [dropped, kept] : [kept, taken]
119
+ if (from.length === 0 || to.length === 0) return 'changed'
120
+ // Every version of `a` older than every version of `b`.
121
+ const before = (a, b) => compareSemver(a.at(-1), b[0]) < 0
122
+ return before(from, to) ? 'up' : before(to, from) ? 'down' : 'changed'
123
+ }
124
+
125
+ // Diff two `Map<packageName, Set<version>>` inventories (as
126
+ // `bundlePackageVersions` returns for each bundle) into the dependency
127
+ // version changes between them. Mirrors the file/package diff framing:
128
+ // `base` is the open bundle, `other` the one compared against.
129
+ //
130
+ // {
131
+ // updated: [{ pkg, baseVersions: [...], otherVersions: [...],
132
+ // direction: 'up' | 'down' | 'changed' }],
133
+ // added: [{ pkg, versions: [...] }], // dependency only in other
134
+ // removed: [{ pkg, versions: [...] }], // dependency only in base
135
+ // totals: { baseDeps, otherDeps }, // distinct package counts
136
+ // }
137
+ //
138
+ // `updated` is the headline — a package present on BOTH sides whose set
139
+ // of versions changed (the "what did this bump pull in?" answer);
140
+ // packages whose versions are byte-for-byte the same are dropped.
141
+ // Version arrays are sorted ascending; the three lists are sorted by
142
+ // package name so the dependency list reads alphabetically.
143
+ export function computeVersionUpdates(baseVersions, otherVersions) {
144
+ const updated = []
145
+ const added = []
146
+ const removed = []
147
+ const names = new Set([...baseVersions.keys(), ...otherVersions.keys()])
148
+ for (const pkg of names) {
149
+ const bSet = baseVersions.get(pkg)
150
+ const oSet = otherVersions.get(pkg)
151
+ if (bSet && oSet) {
152
+ const b = [...bSet].toSorted(compareSemver)
153
+ const o = [...oSet].toSorted(compareSemver)
154
+ if (sameVersions(b, o)) continue
155
+ updated.push({ pkg, baseVersions: b, otherVersions: o, direction: versionDirection(b, o) })
156
+ } else if (bSet) {
157
+ removed.push({ pkg, versions: [...bSet].toSorted(compareSemver) })
158
+ } else {
159
+ added.push({ pkg, versions: [...oSet].toSorted(compareSemver) })
160
+ }
161
+ }
162
+ const byPkg = (a, b) => a.pkg.localeCompare(b.pkg)
163
+ updated.sort(byPkg)
164
+ added.sort(byPkg)
165
+ removed.sort(byPkg)
166
+ return {
167
+ updated,
168
+ added,
169
+ removed,
170
+ totals: { baseDeps: baseVersions.size, otherDeps: otherVersions.size },
171
+ }
172
+ }
@@ -0,0 +1,8 @@
1
+ import type { Root } from '@preventive/compare'
2
+ import type { BundleDetails } from './bundle-metadata.js'
3
+ // A comparison as Compare and Review read it; `files.basePaths` maps every
4
+ // base file to how it reads as the other side lays its files out.
5
+ export interface Comparison extends Record<string, unknown> {
6
+ files: Record<string, unknown> & { root: Root | null; basePaths: Map<string, string> }
7
+ }
8
+ export function prepareComparison(base: BundleDetails, other: BundleDetails, scope?: string | null): Comparison
@@ -0,0 +1,62 @@
1
+ // One comparison model for Compare and Review: bytes, metadata, resolutions,
2
+ // dependency versions and line counts, its files paired by
3
+ // @preventive/compare (computeBundleDiff, treeLayout). Worked out by the
4
+ // server for two managed bundles or two versions of a package
5
+ // (server-managed/comparison.ts), and by the viewer only for bundles it opened
6
+ // itself, which no server holds (ui/view/bundle-comparison.js): a managed
7
+ // viewer loads none of it.
8
+ import { layoutPackageDirs, treeLayout } from '@preventive/compare'
9
+ import { bundlePkgOf } from './bundle-pkg-of.js'
10
+ import { bundlePackageDirs, bundlePackageVersions } from './bundle-sources.js'
11
+ import { bundleFileMeta, computeBundleDiff, computeResolutionDiff } from './bundle-compare-diff.js'
12
+ import { comparePackages, computeVersionUpdates } from './bundle-compare-versions.js'
13
+ import { bundleCompareFiles, bundleCompareResolutions } from './bundle-compare-inputs.js'
14
+ import { bundleLineCounts } from './bundle-metadata.js'
15
+
16
+ export function prepareComparison(base, other, scope = null) {
17
+ // Every file each side mounts, resources included: a diff of
18
+ // source alone would call two bundles identical when only their
19
+ // images differ, and total less than the Overview does.
20
+ // From the bundle picked to the open one.
21
+ const [from, to] = [base, other]
22
+ const baseSources = bundleCompareFiles(from, scope)
23
+ const otherSources = bundleCompareFiles(to, scope)
24
+ // Classify original paths and preserve recorded Stasis package
25
+ // boundaries from either side, as the open one lays its files out
26
+ // where their roots differ. Own source has one identity.
27
+ const layout = treeLayout(baseSources.keys(), otherSources.keys())
28
+ const packageDirs = layoutPackageDirs(bundlePackageDirs(from), bundlePackageDirs(to), layout)
29
+ const pkgOf = (p) => bundlePkgOf(p, { packageDir: packageDirs?.get(p) })
30
+ const diff = computeBundleDiff(baseSources, otherSources, pkgOf, layout, [bundleFileMeta(from), bundleFileMeta(to)])
31
+ // Each side's lines of code (bundleLineCounts), on its rows and in
32
+ // the totals; a file that isn't text has none.
33
+ const baseLines = bundleLineCounts(from), otherLines = bundleLineCounts(to)
34
+ const linesOf = (lines, paths) => { let total = 0; for (const path of paths) total += lines.get(path) ?? 0; return total }
35
+ Object.assign(diff.totals, { baseLines: linesOf(baseLines, baseSources.keys()), otherLines: linesOf(otherLines, otherSources.keys()) })
36
+ for (const row of diff.files.onlyBase) row.lines = baseLines.get(row.basePath ?? row.path) ?? 0
37
+ for (const row of diff.files.onlyOther) row.lines = otherLines.get(row.path) ?? 0
38
+ for (const row of diff.files.changed) Object.assign(row, { baseLines: baseLines.get(row.basePath ?? row.path) ?? 0, otherLines: otherLines.get(row.path) ?? 0 })
39
+ diff.resolutions = computeResolutionDiff(
40
+ bundleCompareResolutions(from, scope),
41
+ bundleCompareResolutions(to, scope),
42
+ new Map(diff.files.changed.filter(row => row.renamedFrom != null).map(row => [row.basePath, row.path])),
43
+ diff.files.basePaths,
44
+ diff.files.root,
45
+ )
46
+ // Dependency versions come from the stasis per-module `{ name,
47
+ // version }` metadata, not the path/byte walk above, so they're
48
+ // computed alongside and hung off the same memo: the package rows
49
+ // join them to the sizes, the summary counts them. Empty for
50
+ // sourcemap / v0 pairs (no version metadata), whose rows carry sizes
51
+ // alone.
52
+ // Keyed by install directory, as the sizes are (`bundlePkgOf`).
53
+ const versionKey = dir => bundlePkgOf('', { packageDir: dir })
54
+ const baseVersions = bundlePackageVersions(from, scope ? baseSources.keys() : null, versionKey)
55
+ const otherVersions = bundlePackageVersions(to, scope ? otherSources.keys() : null, versionKey)
56
+ diff.versionUpdates = computeVersionUpdates(baseVersions, otherVersions)
57
+ diff.packageRows = comparePackages(diff.packages, baseVersions, otherVersions)
58
+ // The reason scope its files are within, for what the server works out of
59
+ // them alike (rename-diff.js, type-only.js).
60
+ diff.scope = scope ?? ''
61
+ return diff
62
+ }
@@ -82,11 +82,11 @@ export function managedRoutePath(route) {
82
82
  }
83
83
 
84
84
  // The npm viewer: its lookup page, or a package version's Overview, Code,
85
- // Compare or Treemap tab, `/npm/<name>[@<version or dist-tag>][/<tab>…]`. Code names
85
+ // Search, Compare or Treemap tab, `/npm/<name>[@<version or dist-tag>][/<tab>…]`. Code names
86
86
  // its file by number and its lines in the fragment, as a bundle's Code
87
87
  // does: `/code/<file>#L4`. Compare names the version compared with, then
88
88
  // its mode past the Overview: `/compare/<version>/code` or `/diff`.
89
- export const NPM_TABS = new Set(['overview', 'code', 'compare', 'review', 'treemap'])
89
+ export const NPM_TABS = new Set(['overview', 'code', 'search', 'compare', 'review', 'treemap'])
90
90
  function npmPackagePath(route) {
91
91
  if (route.packageName == null) return '/npm'
92
92
  const tab = route.bundleTab ?? 'overview'
@@ -114,7 +114,7 @@ function parseNpmRoute(path, hash) {
114
114
  return { view: 'npm', packageName: review[1], packageSpec: review[2] ?? null, bundleTab: 'review',
115
115
  ...(review[3] ? { reviewPackage: versionOnly ? review[1] : review[3], reviewSpec: versionOnly ? review[3] : review[4] ?? null } : {}) }
116
116
  }
117
- const match = /^\/npm(?:\/((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?(?:\/(code|compare|treemap)(?:\/([^/]+)(?:\/([a-z]+))?)?)?)?$/u.exec(path)
117
+ const match = /^\/npm(?:\/((?:@[^/@]+\/)?[^/@]+)(?:@([^/]+))?(?:\/(code|search|compare|treemap)(?:\/([^/]+)(?:\/([a-z]+))?)?)?)?$/u.exec(path)
118
118
  if (!match) return undefined
119
119
  if (match[1] == null) return { view: 'npm' }
120
120
  if (!isNpmPackageName(match[1]) || (match[2] != null && !isNpmPackageSpec(match[2]))) return null
@@ -20,6 +20,14 @@ export interface ManagedServerInfo {
20
20
  allowShare?: boolean
21
21
  }
22
22
 
23
+ // GET /api/auth/session's answer. A managed server's GET /api/config also
24
+ // carries it as `session`, null when signed out, so a client can skip asking.
25
+ export interface ManagedSessionInfo {
26
+ user: { id: string; login: string; name: string | null; role: string }
27
+ csrfToken: string
28
+ viewer?: { id: string; login: string; name: string | null }
29
+ }
30
+
23
31
  // The `server-info` frame payload (the `type` discriminant is added at the
24
32
  // send site). `managed` is null on e2e-only deployments.
25
33
  export interface ServerInfo {
@@ -9,13 +9,26 @@ function appTabs(group) {
9
9
  return app.length > 0 ? app : group
10
10
  }
11
11
 
12
+ // Findings imported from another analyzer are App findings by construction,
13
+ // and Confirmed counts one its report holds no pass for as shown. A set of
14
+ // nothing but those is already its own App view, with no pass to fold anything
15
+ // under. The Confirmed lock asks for a pass as well (it has no outcome control
16
+ // to offer without one), so this is asked on its own: a team whose permissions
17
+ // leave only other analyzers' findings opens as App, as it does unrestricted.
18
+ // Such a view opens with no outcome filter, but keeps the confidence range of
19
+ // imports that score themselves (DeepSec's do).
20
+ function onlyUnjudgedImports(groups) {
21
+ const visible = groups.map(appTabs).filter((g) => g.length > 0)
22
+ return visible.length > 0 && visible.every((g) => g.every((f) => f.isApp && f._source && !f._sourcePass && revalidateKindOf(f) === ''))
23
+ }
24
+
12
25
  // Evaluate the entire workspace, independent of triage, filters, or the lens
13
26
  // left active by another report. Use the same merge and link rules as the view.
14
27
  export function workspaceAppMetadata(reports, duplicatesOf = () => []) {
15
28
  const { groups, conflicts } = mergeReportGroups(reports, { hideRuledOut: true })
16
29
  if (conflicts.size > 0) return { appMode: false }
17
30
  const linked = mergeLinkedWorkspaceGroups(groups, duplicatesOf, appTabs)
18
- if (!canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' })) return { appMode: false }
31
+ if (!onlyUnjudgedImports(linked) && !canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' })) return { appMode: false }
19
32
  return {
20
33
  appMode: true,
21
34
  appFindings: linked.filter((group) => appTabs(group).some((f) => f.isApp)).length,