@preventive/triage 1.0.0-alpha.52 → 1.0.0-alpha.54
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/common/bundle-compare-diff.js +28 -9
- package/common/bundle-compare-inputs.d.ts +1 -0
- package/common/bundle-compare-inputs.js +91 -15
- package/common/bundle-comparison.js +14 -7
- package/common/default-scan-models.ts +3 -0
- package/common/dependency-reviews.d.ts +1 -1
- package/common/dependency-reviews.js +1 -1
- package/common/managed/finding-detail.d.ts +4 -2
- package/common/managed/finding-detail.js +27 -15
- package/common/managed/team-app.d.ts +9 -0
- package/common/managed/team-app.js +59 -6
- package/common/workspace-app.js +38 -6
- package/out/activity.js +4 -4
- package/out/chunk-4EMM6UCD.js +1 -0
- package/out/chunk-4ISO5F7Y.js +7 -0
- package/out/chunk-4XIH7YTX.js +1 -0
- package/out/chunk-6W5BMATD.js +1 -0
- package/out/{chunk-WFYLMF6Z.js → chunk-74AEKRES.js} +1 -1
- package/out/chunk-DEVAORP2.js +1 -0
- package/out/{chunk-KOGNQDSJ.js → chunk-DTM47KGS.js} +1 -1
- package/out/chunk-E3LFSUDJ.js +9 -0
- package/out/chunk-FHIVVVUA.js +6 -0
- package/out/chunk-G3LY2FV4.js +1 -0
- package/out/chunk-HAZKDK4M.js +4 -0
- package/out/chunk-HOB3YPS5.js +1 -0
- package/out/chunk-I47TYPXO.js +52 -0
- package/out/chunk-J7A5OSZE.js +244 -0
- package/out/chunk-JFL7WLRV.js +1 -0
- package/out/chunk-JMG5DXII.js +2 -0
- package/out/chunk-JOXLZXMI.js +1 -0
- package/out/chunk-L4CTEMRG.js +2 -0
- package/out/chunk-M37X4LCF.js +14 -0
- package/out/chunk-M3Y6SLC4.js +1 -0
- package/out/chunk-NXATZJQ2.js +1 -0
- package/out/{chunk-MYGJBYYJ.js → chunk-ORIP5INC.js} +1 -1
- package/out/chunk-PAD2JHZT.js +1 -0
- package/out/chunk-QMVOEBWC.js +13 -0
- package/out/{chunk-SXV2T5C4.js → chunk-QRKFPLA4.js} +1 -1
- package/out/chunk-RRBIJL2B.js +1 -0
- package/out/{chunk-GDPCLSOS.js → chunk-SYXT3SFP.js} +1 -1
- package/out/chunk-TOXMUOIW.js +5 -0
- package/out/chunk-UBOCLLF2.js +1 -0
- package/out/chunk-UCP3JRE2.js +1 -0
- package/out/{chunk-NCN3M5EP.js → chunk-VC72ZO3P.js} +2 -2
- package/out/chunk-VRX2APNA.js +9 -0
- package/out/{chunk-IMM274IW.js → chunk-VZMELPVL.js} +4 -3
- package/out/{chunk-CHRBUGVN.js → chunk-WHVXTJHK.js} +1 -1
- package/out/chunk-WMFDQS2L.js +11 -0
- package/out/chunk-WNSVBOSO.js +41 -0
- package/out/chunk-WREU7JAG.js +1 -0
- package/out/chunk-Y5ZHFNBD.js +2 -0
- package/out/chunk-YF4M5OCI.js +1 -0
- package/out/chunk-Z667U2UJ.js +1 -0
- package/out/chunk-ZQHLECTI.js +1 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +27 -26
- package/out/compare.js +14 -14
- package/out/dialogs.js +36 -51
- package/out/graph.js +17 -15
- package/out/index.html +26 -10
- package/out/prism.js +1 -1
- package/out/scan.js +2 -2
- package/out/terminal.js +232 -232
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +12 -7
- package/risk/actions/resolve.js +36 -5
- package/risk/index.d.ts +20 -2
- package/risk/npm/malware.js +6 -4
- package/risk/npm/registry.js +8 -6
- package/server-managed/bundle-advisories.ts +56 -74
- package/server-managed/bundle-build-worker.js +61 -4
- package/server-managed/bundle-build.ts +13 -4
- package/server-managed/bundle-dependency-reviews.ts +74 -0
- package/server-managed/bundle-validation.ts +32 -0
- package/server-managed/db-methods.ts +51 -16
- package/server-managed/db-neon.ts +18 -2
- package/server-managed/db.ts +11 -2
- package/server-managed/github-issue-links.ts +151 -47
- package/server-managed/github-pulls.ts +34 -15
- package/server-managed/history-windows.ts +172 -0
- package/server-managed/http.ts +227 -12
- package/server-managed/index.ts +13 -1
- package/server-managed/managed-issues.ts +29 -15
- package/server-managed/npm-insights.ts +4 -3
- package/server-managed/npm-kept.ts +4 -0
- package/server-managed/package-entry-points.ts +37 -9
- package/server-managed/pretty-print.ts +7 -5
- package/server-managed/public-workspace.ts +2 -2
- package/server-managed/repository-actions.ts +7 -4
- package/server-managed/repository-browser.ts +11 -4
- package/server-managed/repository-history.ts +2 -2
- package/server-managed/repository-pulse.ts +11 -2
- package/server-managed/repository-risk-audit.ts +4 -5
- package/server-managed/repository-risk.ts +4 -4
- package/server-managed/sql.ts +2 -1
- package/server-managed/stasis-config.ts +46 -0
- package/server-managed/team-app-summaries.ts +54 -0
- package/server-managed/team-app.ts +52 -31
- package/server-managed/team-reports.ts +34 -13
- package/server-managed/upstream-cache.ts +41 -32
- package/server-managed/workspace-shares.ts +3 -1
- package/out/chunk-2ZDZWLFY.js +0 -1
- package/out/chunk-7VAWINQF.js +0 -6
- package/out/chunk-FMXWXDR4.js +0 -36
- package/out/chunk-IZ7POWI3.js +0 -18
- package/out/chunk-LJ7XRXFU.js +0 -52
- package/out/chunk-OGAYMEER.js +0 -232
- package/out/chunk-QB4CBOX2.js +0 -1
- package/out/chunk-TQJDYHVE.js +0 -2
- package/out/chunk-VH4XMZIW.js +0 -2
- package/out/chunk-WYLQWYLA.js +0 -1
|
@@ -25,8 +25,12 @@ import { resolutionKey } from './bundle-compare-inputs.js'
|
|
|
25
25
|
|
|
26
26
|
// Resolutions compare independently from file bytes: identical files can be
|
|
27
27
|
// wired together differently. Inputs are keyed by importer, specifier,
|
|
28
|
-
// conditions (including import attributes), and platform.
|
|
29
|
-
//
|
|
28
|
+
// conditions (including import attributes), and platform. Resolutions whose
|
|
29
|
+
// targets changed are reported (`changed`, repointed); one recorded on a side
|
|
30
|
+
// alone (`onlyBase`, `onlyOther`) only where its importer holds the same
|
|
31
|
+
// contents on both sides (`identical`, of its path as the other bundle lays
|
|
32
|
+
// it out), as a file whose code changed gains and drops imports with it.
|
|
33
|
+
// Each row names its target on either side, null on the side without one.
|
|
30
34
|
// `renames` (base path → other path, see renamesOf) names base files as
|
|
31
35
|
// the other bundle does: a renamed importer's resolutions pair with its new
|
|
32
36
|
// path's, reported under it, and a target renamed is the same target.
|
|
@@ -34,21 +38,36 @@ import { resolutionKey } from './bundle-compare-inputs.js'
|
|
|
34
38
|
// the bundles' roots differ, a base target shown as the other bundle would
|
|
35
39
|
// lay it out; a path no file has (an importer the bundle didn't capture, a
|
|
36
40
|
// builtin) may read either as it is or through the root.
|
|
37
|
-
export function computeResolutionDiff(base, other, renames = new Map(), basePaths = new Map(), root = null) {
|
|
41
|
+
export function computeResolutionDiff(base, other, renames = new Map(), basePaths = new Map(), root = null, identical = () => false) {
|
|
38
42
|
const names = path => {
|
|
39
43
|
const known = renames.get(path) ?? basePaths.get(path)
|
|
40
44
|
return known === undefined ? [...new Set([path, through(root, path) ?? path])] : [known]
|
|
41
45
|
}
|
|
42
|
-
const changed = []
|
|
46
|
+
const changed = [], onlyBase = [], onlyOther = []
|
|
47
|
+
const paired = new Set()
|
|
43
48
|
for (const before of base.values()) {
|
|
44
49
|
const after = names(before.parent).map(parent => other.get(resolutionKey(parent, before.specifier, before.conditions, before.platform))).find(Boolean)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const baseTarget = basePaths.get(before.target) ?? before.target
|
|
51
|
+
if (after) {
|
|
52
|
+
paired.add(after.key)
|
|
53
|
+
if (!names(before.target).includes(after.target)) {
|
|
54
|
+
const { target: otherTarget, ...identity } = after
|
|
55
|
+
changed.push({ ...identity, baseTarget, otherTarget })
|
|
56
|
+
}
|
|
57
|
+
continue
|
|
48
58
|
}
|
|
59
|
+
const parent = names(before.parent).find(identical)
|
|
60
|
+
if (parent === undefined) continue
|
|
61
|
+
const { specifier, conditions, platform } = before
|
|
62
|
+
onlyBase.push({ key: resolutionKey(parent, specifier, conditions, platform), parent, specifier, conditions, platform, baseTarget, otherTarget: null })
|
|
63
|
+
}
|
|
64
|
+
for (const after of other.values()) {
|
|
65
|
+
if (paired.has(after.key) || !identical(after.parent)) continue
|
|
66
|
+
const { target: otherTarget, ...identity } = after
|
|
67
|
+
onlyOther.push({ ...identity, baseTarget: null, otherTarget })
|
|
49
68
|
}
|
|
50
|
-
changed.sort((a, b) => a.key.localeCompare(b.key))
|
|
51
|
-
return { changed, totalChanges: changed.length }
|
|
69
|
+
for (const rows of [changed, onlyBase, onlyOther]) rows.sort((a, b) => a.key.localeCompare(b.key))
|
|
70
|
+
return { changed, onlyBase, onlyOther, totalChanges: changed.length + onlyBase.length + onlyOther.length }
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
// A file's size, by the measure the Overview and Treemap use: text by the
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { BundleDetails } from './bundle-metadata.js'
|
|
2
2
|
export function bundleCompareFiles(details: BundleDetails, scope?: string | null): Map<string, unknown>
|
|
3
|
+
export function bundleCompareSideScopes(base: BundleDetails, other: BundleDetails, scope?: string | null): [string, string]
|
|
@@ -1,31 +1,107 @@
|
|
|
1
1
|
import { bundleFilesAsMap } from './bundle-sources.js'
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
// A side's files, and the named trees holding any of them, with those they hold.
|
|
4
|
+
function namedTrees(details) {
|
|
5
|
+
const files = bundleFilesAsMap(details), trees = new Map()
|
|
6
|
+
for (const [name, paths] of Object.entries(details?.bundle?.reason ?? {})) {
|
|
7
|
+
const present = new Set(Array.isArray(paths) ? paths.filter(path => files.has(path)) : [])
|
|
8
|
+
if (present.size > 0) trees.set(name, present)
|
|
9
|
+
}
|
|
10
|
+
return { stasis: details?.kind === 'stasis', files, trees }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Trees two sides name apart for the same files: a side naming the second of
|
|
14
|
+
// a pair and not the first, the other the first and not the second, has it
|
|
15
|
+
// compared as the first. A static build names the Metro bundle's `bundle:metro`
|
|
16
|
+
// and the rest `bundle`, as a capture names them `metro` and `run`: `bundle` is
|
|
17
|
+
// `run` only where `bundle:metro` is `metro`, on the same side.
|
|
18
|
+
const ALIASES = [['extra', 'add'], ['metro', 'bundle:metro']]
|
|
19
|
+
function aliased(sides) {
|
|
20
|
+
if (sides.length !== 2) return sides
|
|
21
|
+
const apart = (name, alias) => sides.find((side, i) => side.trees.has(alias) && !side.trees.has(name)
|
|
22
|
+
&& sides[1 - i].trees.has(name) && !sides[1 - i].trees.has(alias))
|
|
23
|
+
const renames = sides.map(() => new Map())
|
|
24
|
+
for (const [name, alias] of ALIASES) {
|
|
25
|
+
const side = apart(name, alias)
|
|
26
|
+
if (side) renames[sides.indexOf(side)].set(alias, name)
|
|
27
|
+
}
|
|
28
|
+
const built = apart('metro', 'bundle:metro')
|
|
29
|
+
if (built && apart('run', 'bundle') === built) renames[sides.indexOf(built)].set('bundle', 'run')
|
|
30
|
+
// `actual`: the name a side gives a tree it is compared as another by.
|
|
31
|
+
return sides.map((side, i) => renames[i].size === 0 ? side : { ...side,
|
|
32
|
+
trees: new Map([...side.trees].map(([name, paths]) => [renames[i].get(name) ?? name, paths])),
|
|
33
|
+
actual: new Map([...renames[i]].map(([alias, name]) => [name, alias])) })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The one tree of a stasis side naming every file it has (its own selector
|
|
37
|
+
// hidden, as common/bundle-reasons.js hides it), where no other side names it:
|
|
38
|
+
// such a side is compared within that tree under any of the others' trees.
|
|
39
|
+
function soleTree(side, sides) {
|
|
40
|
+
if (!side.stasis || side.trees.size !== 1) return null
|
|
41
|
+
const [[name, paths]] = side.trees
|
|
42
|
+
return paths.size === side.files.size && !sides.some(other => other !== side && other.trees.has(name)) ? name : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A side naming no tree, or one alone holding all its files.
|
|
46
|
+
const trivial = side => side.trees.size === 0 || (side.trees.size === 1 && [...side.trees.values()][0].size === side.files.size)
|
|
47
|
+
|
|
48
|
+
// A tree of each side the other names none of, compared with each other, as
|
|
49
|
+
// one scope: the same whichever side is the base, as a swap keeps it.
|
|
50
|
+
const PAIR = 'reasons:'
|
|
51
|
+
const pairScope = (first, second) => `${PAIR}${JSON.stringify([first, second].toSorted())}`
|
|
52
|
+
function pairOf(scope) {
|
|
53
|
+
if (!scope.startsWith(PAIR)) return null
|
|
54
|
+
try {
|
|
55
|
+
const pair = JSON.parse(scope.slice(PAIR.length))
|
|
56
|
+
return Array.isArray(pair) && pair.length === 2 && pair.every(name => typeof name === 'string') ? pair : null
|
|
57
|
+
} catch { return null }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Use the union of named trees, each as its side is compared (aliased). A tree
|
|
61
|
+
// absent from one stasis bundle contains no files there; falling back to all
|
|
62
|
+
// files would invent additions/removals. A bundle that names no trees (a
|
|
63
|
+
// sourcemap) is compared whole under any of them, as is one whose sole tree
|
|
64
|
+
// holds all its files where the other names none of it (soleTree), its tree
|
|
65
|
+
// left out. Where neither side is trivial and each names a tree the other
|
|
66
|
+
// doesn't, the trees both name are offered, and each of the first side's own
|
|
67
|
+
// paired with each of the second's (pairScope), labelled from the first to the
|
|
68
|
+
// second. Unlike a single-bundle graph, retain a scope that includes every file
|
|
7
69
|
// on one side.
|
|
8
70
|
export function bundleCompareScopes(...details) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
71
|
+
const sides = aliased(details.map(namedTrees))
|
|
72
|
+
if (sides.length === 2 && !sides.some(trivial)) {
|
|
73
|
+
const [first, second] = sides.map(side => [...side.trees.keys()].toSorted())
|
|
74
|
+
const shared = first.filter(name => second.includes(name))
|
|
75
|
+
const [firstOwn, secondOwn] = [first, second].map(names => names.filter(name => !shared.includes(name)))
|
|
76
|
+
if (firstOwn.length > 0 && secondOwn.length > 0) {
|
|
77
|
+
return [...shared.map(name => ({ id: `reason:${name}`, label: name })),
|
|
78
|
+
...firstOwn.flatMap(from => secondOwn.map(to => ({ id: pairScope(from, to), label: `${from} → ${to}` })))]
|
|
14
79
|
}
|
|
15
80
|
}
|
|
81
|
+
const scoped = sides.filter(side => soleTree(side, sides) === null)
|
|
82
|
+
const names = new Set(scoped.flatMap(side => [...side.trees.keys()]))
|
|
16
83
|
// A scope is only useful as a filter if it changes the files compared on
|
|
17
84
|
// some side. Keep every scope when at least one does, otherwise hide the
|
|
18
85
|
// selector, as single-bundle views do (see common/bundle-reasons.js).
|
|
19
|
-
const narrows = name =>
|
|
20
|
-
if (item?.kind !== 'stasis') return false
|
|
21
|
-
const files = bundleFilesAsMap(item)
|
|
22
|
-
const paths = item?.bundle?.reason?.[name]
|
|
23
|
-
return new Set(Array.isArray(paths) ? paths.filter(path => files.has(path)) : []).size < files.size
|
|
24
|
-
})
|
|
86
|
+
const narrows = name => scoped.some(side => side.stasis && (side.trees.get(name)?.size ?? 0) < side.files.size)
|
|
25
87
|
if (![...names].some(narrows)) return []
|
|
26
88
|
return [...names].toSorted().map(name => ({ id: `reason:${name}`, label: name }))
|
|
27
89
|
}
|
|
28
90
|
|
|
91
|
+
// Each side's scope in a comparison within `scope`: within the tree of a pair
|
|
92
|
+
// (pairScope) it names; a side whose sole tree holds all its files, the other
|
|
93
|
+
// naming none of it (soleTree), within that tree; each within the tree it
|
|
94
|
+
// names as it is compared (aliased).
|
|
95
|
+
export function bundleCompareSideScopes(base, other, scope = '') {
|
|
96
|
+
if (!scope) return ['', '']
|
|
97
|
+
const pair = pairOf(scope), sides = aliased([base, other].map(namedTrees))
|
|
98
|
+
const named = scope.startsWith('reason:') ? scope.slice('reason:'.length) : null
|
|
99
|
+
return sides.map(side => {
|
|
100
|
+
const name = pair ? pair.find(tree => side.trees.has(tree)) : soleTree(side, sides) ?? named
|
|
101
|
+
return name == null ? scope : `reason:${side.actual?.get(name) ?? name}`
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
29
105
|
export function bundleCompareFiles(details, scope = '') {
|
|
30
106
|
const files = bundleFilesAsMap(details)
|
|
31
107
|
if (!scope || details?.kind !== 'stasis') return files
|
|
@@ -10,7 +10,7 @@ import { bundlePkgOf } from './bundle-pkg-of.js'
|
|
|
10
10
|
import { bundlePackageDirs, bundlePackageVersions } from './bundle-sources.js'
|
|
11
11
|
import { bundleFileMeta, computeBundleDiff, computeResolutionDiff } from './bundle-compare-diff.js'
|
|
12
12
|
import { comparePackages, computeVersionUpdates } from './bundle-compare-versions.js'
|
|
13
|
-
import { bundleCompareFiles, bundleCompareResolutions } from './bundle-compare-inputs.js'
|
|
13
|
+
import { bundleCompareFiles, bundleCompareResolutions, bundleCompareSideScopes } from './bundle-compare-inputs.js'
|
|
14
14
|
import { bundleLineCounts } from './bundle-metadata.js'
|
|
15
15
|
|
|
16
16
|
export function prepareComparison(base, other, scope = null) {
|
|
@@ -19,8 +19,10 @@ export function prepareComparison(base, other, scope = null) {
|
|
|
19
19
|
// images differ, and total less than the Overview does.
|
|
20
20
|
// From the bundle picked to the open one.
|
|
21
21
|
const [from, to] = [base, other]
|
|
22
|
-
|
|
23
|
-
const
|
|
22
|
+
// Each side within its scope (bundleCompareSideScopes).
|
|
23
|
+
const [baseScope, otherScope] = bundleCompareSideScopes(from, to, scope)
|
|
24
|
+
const baseSources = bundleCompareFiles(from, baseScope)
|
|
25
|
+
const otherSources = bundleCompareFiles(to, otherScope)
|
|
24
26
|
// Classify original paths and preserve recorded Stasis package
|
|
25
27
|
// boundaries from either side, as the open one lays its files out
|
|
26
28
|
// where their roots differ. Own source has one identity.
|
|
@@ -36,12 +38,17 @@ export function prepareComparison(base, other, scope = null) {
|
|
|
36
38
|
for (const row of diff.files.onlyBase) row.lines = baseLines.get(row.basePath ?? row.path) ?? 0
|
|
37
39
|
for (const row of diff.files.onlyOther) row.lines = otherLines.get(row.path) ?? 0
|
|
38
40
|
for (const row of diff.files.changed) Object.assign(row, { baseLines: baseLines.get(row.basePath ?? row.path) ?? 0, otherLines: otherLines.get(row.path) ?? 0 })
|
|
41
|
+
// A file both sides hold with the same contents, renamed or recorded
|
|
42
|
+
// otherwise or not: an import resolution one side records alone is told
|
|
43
|
+
// only of these.
|
|
44
|
+
const differs = new Set([...diff.files.onlyOther, ...diff.files.changed.filter(row => row.modified !== false)].map(row => row.path))
|
|
39
45
|
diff.resolutions = computeResolutionDiff(
|
|
40
|
-
bundleCompareResolutions(from,
|
|
41
|
-
bundleCompareResolutions(to,
|
|
46
|
+
bundleCompareResolutions(from, baseScope),
|
|
47
|
+
bundleCompareResolutions(to, otherScope),
|
|
42
48
|
new Map(diff.files.changed.filter(row => row.renamedFrom != null).map(row => [row.basePath, row.path])),
|
|
43
49
|
diff.files.basePaths,
|
|
44
50
|
diff.files.root,
|
|
51
|
+
path => otherSources.has(path) && !differs.has(path),
|
|
45
52
|
)
|
|
46
53
|
// Dependency versions come from the stasis per-module `{ name,
|
|
47
54
|
// version }` metadata, not the path/byte walk above, so they're
|
|
@@ -51,8 +58,8 @@ export function prepareComparison(base, other, scope = null) {
|
|
|
51
58
|
// alone.
|
|
52
59
|
// Keyed by install directory, as the sizes are (`bundlePkgOf`).
|
|
53
60
|
const versionKey = dir => bundlePkgOf('', { packageDir: dir })
|
|
54
|
-
const baseVersions = bundlePackageVersions(from,
|
|
55
|
-
const otherVersions = bundlePackageVersions(to,
|
|
61
|
+
const baseVersions = bundlePackageVersions(from, baseScope ? baseSources.keys() : null, versionKey)
|
|
62
|
+
const otherVersions = bundlePackageVersions(to, otherScope ? otherSources.keys() : null, versionKey)
|
|
56
63
|
diff.versionUpdates = computeVersionUpdates(baseVersions, otherVersions)
|
|
57
64
|
diff.packageRows = comparePackages(diff.packages, baseVersions, otherVersions)
|
|
58
65
|
// The reason scope its files are within, for what the server works out of
|
|
@@ -10,9 +10,12 @@ export function defaultScanModels(provider: string | null = null) {
|
|
|
10
10
|
'openai/gpt-6-astra-pro',
|
|
11
11
|
'openai/gpt-6.1-sol',
|
|
12
12
|
'openai/gpt-6.1-sol-pro',
|
|
13
|
+
'openai/gpt-6.1-terra',
|
|
14
|
+
'openai/gpt-6.1-terra-pro',
|
|
13
15
|
'openai/gpt-6-luna',
|
|
14
16
|
'openai/gpt-6-luna-pro',
|
|
15
17
|
'moonshotai/kimi-k3',
|
|
18
|
+
'moonshotai/kimi-k3.1-pro',
|
|
16
19
|
'google/gemini-4-argon',
|
|
17
20
|
'google/gemma-4-31b-it',
|
|
18
21
|
'x-ai/grok-4.7',
|
|
@@ -12,7 +12,7 @@ export function allowedReviewVerdicts(entries: { file: string; status: ReviewVer
|
|
|
12
12
|
interface ReviewNpmVersion { version: string; private?: boolean; manifest?: Record<string, unknown> }
|
|
13
13
|
export function npmTakenDown(entry: { npm: ReviewNpmVersion }): boolean
|
|
14
14
|
export function npmReviewVerdictLimit(npm: ReviewNpmVersion | null | undefined,
|
|
15
|
-
advisoryList?: { versions: string[]; advisories: { id?: string; title?: string; severity?: string; cwe?: string[]; affected: number[] }[] } | null,
|
|
15
|
+
advisoryList?: { versions: string[]; advisories: { id?: string; title?: string; severity?: string; cwe?: string[]; malware?: boolean; affected: number[] }[] } | null,
|
|
16
16
|
socket?: { alerts: { type: string }[] } | null, risk?: { findings: { id: string }[] } | null): ReviewVerdict
|
|
17
17
|
export function needsPackageJsonReview(entries: { file: string; status: ReviewVerdict | null }[], files: string[], nonTextFiles?: string[] | null): boolean
|
|
18
18
|
export function parseReviewNotes(comment: string): { text: string; bullet: boolean }[]
|
|
@@ -47,7 +47,7 @@ export function npmReviewVerdictLimit(npm, advisoryList, socket, risk) {
|
|
|
47
47
|
const advisories = index < 0 ? [] : advisoryList.advisories.filter(advisory => advisory.affected.includes(index))
|
|
48
48
|
const alerts = npm.private ? [] : socket?.alerts ?? []
|
|
49
49
|
if (risk?.findings?.some(finding => finding.id === 'malware') || npmTakenDown({ npm }) || alerts.some(alert => alert.type === 'malware')
|
|
50
|
-
|| advisories.some(advisory => npmMalwareAdvisory(advisory) || advisory.id?.startsWith('MAL-') || /^Malicious package\b/iu.test(advisory.title ?? ''))) return 'reject'
|
|
50
|
+
|| advisories.some(advisory => advisory.malware === true || npmMalwareAdvisory(advisory) || advisory.id?.startsWith('MAL-') || /^Malicious package\b/iu.test(advisory.title ?? ''))) return 'reject'
|
|
51
51
|
if (advisories.some(advisory => advisory.severity === 'critical') || alerts.some(alert => alert.type === 'criticalCVE')) return 'request-changes'
|
|
52
52
|
return 'accept'
|
|
53
53
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export const FINDING_DETAIL_FIELDS: readonly string[]
|
|
2
|
-
export function liteReportData(data: unknown, options?: { fold?: boolean }): unknown
|
|
3
|
-
export function liteWorkspace<T extends { filename: string; data: unknown }>(reports: readonly T[], links: readonly (readonly string[])[]): {
|
|
2
|
+
export function liteReportData(data: unknown, options?: { fold?: boolean; shown?: ReadonlySet<string> | null }): unknown
|
|
3
|
+
export function liteWorkspace<T extends { filename: string; data: unknown }>(reports: readonly T[], links: readonly (readonly string[])[], view?: import('./team-app.js').ManagedTeamView): {
|
|
4
|
+
reports: T[]; folded: false | 'rows' | 'app'; lens: { drop: boolean; detail: boolean; upstream: boolean } | null
|
|
5
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findingTitle, reportEntries, revalidateKindOf } from '@preventive/report'
|
|
2
|
-
import {
|
|
2
|
+
import { managedTeamView } from './team-app.js'
|
|
3
3
|
import { liteName } from './lite-name.js'
|
|
4
4
|
|
|
5
5
|
// A finding's detail: what its popup, the views besides the board and an
|
|
@@ -8,7 +8,8 @@ import { liteName } from './lite-name.js'
|
|
|
8
8
|
// and an export tell (write-md-finding.js PLAIN_FIELDS, CODE_FIELDS, but for
|
|
9
9
|
// `priority`, which the board sorts and filters by), and the run meta its
|
|
10
10
|
// card's run line and a report's header read (its `model` and `type`, which
|
|
11
|
-
// the board's filters read, stay)
|
|
11
|
+
// the board's filters read, stay), and the link the report gave its location
|
|
12
|
+
// (`location`), which its card does not draw. A team's
|
|
12
13
|
// reports are first sent without it (server-managed/team-reports.ts), the
|
|
13
14
|
// board drawn from them, and their findings filled in from the whole reports
|
|
14
15
|
// (ui/view/managed-details.js).
|
|
@@ -16,7 +17,7 @@ export const FINDING_DETAIL_FIELDS = Object.freeze([
|
|
|
16
17
|
'description', 'evidence', 'impact', 'reproduction', 'recommendation', 'confidenceReason', 'revalidateVerdict', 'revalidateRecommendation',
|
|
17
18
|
'fileHash', 'exportName', 'methodName',
|
|
18
19
|
'severityReason', 'commitHash', 'auditedCommit', 'detectedAt', 'committedAt', 'dateCreated', 'status', 'branch', 'pocStatus', 'parent', 'slug', 'reportPath',
|
|
19
|
-
'category', 'discoveredIn', 'effort', 'exportsMode',
|
|
20
|
+
'category', 'discoveredIn', 'effort', 'exportsMode', 'location',
|
|
20
21
|
])
|
|
21
22
|
const DETAIL = new Set(FINDING_DETAIL_FIELDS)
|
|
22
23
|
// A finding without its detail. One without a title of its own is named by its
|
|
@@ -37,14 +38,20 @@ const isPass = finding => revalidateKindOf(finding) === 'revalidation'
|
|
|
37
38
|
// App mode shows of an entry holding the revalidation pass's row that row
|
|
38
39
|
// alone (ui/view/group.js foldedTabs): with `fold`, such an entry is sent as
|
|
39
40
|
// the pass's rows, the rows beneath them left for the whole reports
|
|
40
|
-
// (ui/view/managed-details.js).
|
|
41
|
-
|
|
41
|
+
// (ui/view/managed-details.js). With `shown`, the IDs of the findings one
|
|
42
|
+
// whose App view locks Confirmed shows (common/workspace-app.js
|
|
43
|
+
// workspaceAppRows), those alone are sent.
|
|
44
|
+
export function liteReportData(data, { fold = false, shown = null } = {}) {
|
|
42
45
|
const key = Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
|
|
43
46
|
if (!key) return data
|
|
47
|
+
if (shown) {
|
|
48
|
+
const kept = entry => (Array.isArray(entry) ? entry : [entry]).filter(finding => shown.has(finding?.id))
|
|
49
|
+
return { ...data, [key]: data[key].map(kept).filter(entry => entry.length > 0).map(entry => entry.map(liteFinding)) }
|
|
50
|
+
}
|
|
44
51
|
const entries = data[key].map(entry => {
|
|
45
52
|
if (!Array.isArray(entry)) return liteFinding(entry)
|
|
46
|
-
const
|
|
47
|
-
return
|
|
53
|
+
const folded = fold && entry.length > 1 && entry.some(isPass) ? entry.filter(isPass) : entry
|
|
54
|
+
return folded.map(liteFinding)
|
|
48
55
|
})
|
|
49
56
|
return { ...data, [key]: entries }
|
|
50
57
|
}
|
|
@@ -52,12 +59,17 @@ export function liteReportData(data, { fold = false } = {}) {
|
|
|
52
59
|
const rows = data => (reportEntries(data) ?? []).flat().length
|
|
53
60
|
|
|
54
61
|
// A team's reports as its board is first sent them (server-managed/team-reports.ts),
|
|
55
|
-
// each `liteReportData
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// each `liteReportData`: where the workspace's App view locks Confirmed, only
|
|
63
|
+
// the rows that view shows (`folded: 'app'`); else, where nothing in it
|
|
64
|
+
// contradicts the revalidation pass, which opens a view of it in App mode
|
|
65
|
+
// (ui/view/render.js configureReportRevalidation), its entries folded under
|
|
66
|
+
// their App rows (`folded: 'rows'`). Folded, the `lens` the rows left out
|
|
67
|
+
// tell (team-app.js managedTeamLens). All of it read from `view`, the
|
|
68
|
+
// workspace's (team-app.js managedTeamView), where its builder has one.
|
|
69
|
+
export function liteWorkspace(reports, links, view = managedTeamView(Object.assign(reports.map(({ filename, data }) => ({ filename, data })), { links }))) {
|
|
70
|
+
const shown = view.rows
|
|
71
|
+
const fold = shown !== null || !view.conflicted()
|
|
72
|
+
const lite = reports.map(report => ({ ...report, data: liteReportData(report.data, { fold, shown }) }))
|
|
73
|
+
if (!fold || !lite.some((report, index) => rows(report.data) < rows(reports[index].data))) return { reports: lite, folded: false, lens: null }
|
|
74
|
+
return { reports: lite, folded: shown ? 'app' : 'rows', lens: view.lens() }
|
|
63
75
|
}
|
|
@@ -4,3 +4,12 @@ export type TeamAppMetadata = { appMode: true; appFindings: number } | { appMode
|
|
|
4
4
|
export function managedTeamAppMetadata(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] },
|
|
5
5
|
options?: { countFindings?: boolean }): TeamAppMetadata
|
|
6
6
|
export function managedTeamRevalidationConflicted(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] }): boolean
|
|
7
|
+
export function managedTeamAppRows(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] }): Set<string> | null
|
|
8
|
+
export function managedTeamLens(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] }): { drop: boolean; detail: boolean; upstream: boolean }
|
|
9
|
+
export interface ManagedTeamView {
|
|
10
|
+
app: TeamAppMetadata
|
|
11
|
+
rows: Set<string> | null
|
|
12
|
+
conflicted(): boolean
|
|
13
|
+
lens(): { drop: boolean; detail: boolean; upstream: boolean }
|
|
14
|
+
}
|
|
15
|
+
export function managedTeamView(workspace: readonly { filename: string; data: unknown }[] & { links?: readonly (readonly string[])[] }): ManagedTeamView
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { inheritReportMeta, isAppFinding, reportEntries } from '@preventive/report'
|
|
1
|
+
import { inheritReportMeta, isAppFinding, reportEntries, revalidateKindOf } from '@preventive/report'
|
|
2
2
|
import { collectDuplicates } from '../../client/linked-findings.js'
|
|
3
|
+
import { dependencyDirectory, isDependencyFile } from '../../client/dependency-paths.js'
|
|
3
4
|
import { hasRevalidateStamp } from '../finding-filters.js'
|
|
4
|
-
import { workspaceAppMetadata } from '../workspace-app.js'
|
|
5
|
+
import { workspaceApp, workspaceAppMetadata, workspaceAppRows } from '../workspace-app.js'
|
|
5
6
|
import { mergeReportGroups } from '../workspace-groups.js'
|
|
6
7
|
|
|
7
8
|
// The same classification as local workspaces, using only the complete,
|
|
@@ -22,11 +23,63 @@ export function managedTeamRevalidationConflicted(workspace) {
|
|
|
22
23
|
return mergeReportGroups(viewReports(workspace).reports).conflicts.size > 0
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
// The IDs of the findings the workspace's basic App view can show
|
|
27
|
+
// (common/workspace-app.js workspaceAppRows), or null.
|
|
28
|
+
export function managedTeamAppRows(workspace) {
|
|
29
|
+
const { reports, duplicates } = viewReports(workspace)
|
|
30
|
+
return workspaceAppRows(reports, id => [...duplicates.get(id) ?? []])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// What a view of the whole workspace offers beside the App view
|
|
34
|
+
// (ui/view/render.js), told by all its rows: whether the revalidation layer
|
|
35
|
+
// can come off (`drop`), whether its detail stop has rows to unfold
|
|
36
|
+
// (`detail`), and whether an upstream lens has ground (`upstream`).
|
|
37
|
+
export function managedTeamLens(workspace) {
|
|
38
|
+
const { reports } = viewReports(workspace)
|
|
39
|
+
return lensOf(reports, mergeReportGroups(reports).groups)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// One reading of a team's published workspace for all that is asked of it as
|
|
43
|
+
// its reports are built (server-managed/team-reports.ts), each worked out of
|
|
44
|
+
// the same rows: its classification (managedTeamAppMetadata, its findings
|
|
45
|
+
// counted, and of a team not App, how many of its reports open in App mode on
|
|
46
|
+
// their own), the rows its App view shows (managedTeamAppRows), and, asked
|
|
47
|
+
// only where wanted, whether copies contradict the pass
|
|
48
|
+
// (managedTeamRevalidationConflicted) and what its rows offer the toolbar
|
|
49
|
+
// (managedTeamLens).
|
|
50
|
+
export function managedTeamView(workspace) {
|
|
51
|
+
const { reports, duplicates, linked } = viewReports(workspace)
|
|
52
|
+
const { metadata, rows } = workspaceApp(reports, id => [...duplicates.get(id) ?? []], { countFindings: true })
|
|
53
|
+
// Each report alone, with the links the workspace has, not its link reports'.
|
|
54
|
+
const alone = id => [...linked.get(id) ?? []]
|
|
55
|
+
const app = metadata.appMode ? metadata : { ...metadata, appReports: reports.filter(report => workspaceAppMetadata([report], alone).appMode).length }
|
|
56
|
+
let merged = null
|
|
57
|
+
const plain = () => merged ??= mergeReportGroups(reports)
|
|
58
|
+
return { app, rows, conflicted: () => plain().conflicts.size > 0, lens: () => lensOf(reports, plain().groups) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const RULED_OUT = new Set(['refuted', 'unreachable'])
|
|
62
|
+
|
|
63
|
+
function lensOf(reports, groups) {
|
|
64
|
+
const rows = reports.flatMap(report => report.groups.flat())
|
|
65
|
+
const directory = dependencyDirectory(reports)
|
|
66
|
+
const detail = groups.some(group => {
|
|
67
|
+
const kinds = group.map(revalidateKindOf)
|
|
68
|
+
return kinds.includes('partial') || kinds.includes('revalidation') && kinds.some(kind => kind !== 'revalidation' && !RULED_OUT.has(kind))
|
|
69
|
+
})
|
|
70
|
+
return {
|
|
71
|
+
drop: rows.some(hasRevalidateStamp),
|
|
72
|
+
detail,
|
|
73
|
+
upstream: rows.some(f => f.isUpstream === true || f.isApp === false && !('isUpstream' in f) && isDependencyFile(f.file, directory)),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
25
77
|
// The workspace's reports as a view holds them (ui/view/ingest.js), with the
|
|
26
|
-
// duplicates its links name.
|
|
78
|
+
// duplicates its links name, and those its own `links` name alone (`linked`).
|
|
27
79
|
function viewReports(workspace) {
|
|
28
|
-
const duplicates = new Map(), reports = []
|
|
80
|
+
const duplicates = new Map(), linked = new Map(), reports = []
|
|
29
81
|
collectDuplicates(workspace.links ?? [], duplicates)
|
|
82
|
+
collectDuplicates(workspace.links ?? [], linked)
|
|
30
83
|
for (const { filename, data } of workspace) {
|
|
31
84
|
if (data.source === 'links' && Array.isArray(data.links)) {
|
|
32
85
|
collectDuplicates(data.links, duplicates)
|
|
@@ -41,7 +94,7 @@ function viewReports(workspace) {
|
|
|
41
94
|
finding._sourcePass = judged.has(finding._source)
|
|
42
95
|
return finding
|
|
43
96
|
}))
|
|
44
|
-
reports.push({ fileName: filename, groups })
|
|
97
|
+
reports.push({ fileName: filename, groups, treeDependencies: data.treeDependencies ?? null })
|
|
45
98
|
}
|
|
46
|
-
return { reports, duplicates }
|
|
99
|
+
return { reports, duplicates, linked }
|
|
47
100
|
}
|
package/common/workspace-app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { revalidateKindOf } from '@preventive/report'
|
|
2
|
-
import { canLockConfirmed } from './finding-filters.js'
|
|
2
|
+
import { activeRevalidateKinds, canLockConfirmed } from './finding-filters.js'
|
|
3
3
|
import { mergeReportGroups } from './workspace-groups.js'
|
|
4
4
|
import { mergeLinkedWorkspaceGroups } from './linked-workspace-groups.js'
|
|
5
5
|
|
|
@@ -27,13 +27,45 @@ function onlyUnjudgedImports(groups) {
|
|
|
27
27
|
// `countFindings` has a workspace that is not App count its rows too, after
|
|
28
28
|
// the same merging, for a caller that shows it collapsed (managed teams).
|
|
29
29
|
export function workspaceAppMetadata(reports, duplicatesOf = () => [], { countFindings = false } = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
return metadataOf(appView(reports, duplicatesOf), countFindings)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The IDs of the findings a workspace's basic App view can show: of each row
|
|
34
|
+
// its locked Confirmed shows (ui/view/filters.js filterRevalidateKind), the
|
|
35
|
+
// App tabs, the rest folded under them; none the pass ruled out. Null where it
|
|
36
|
+
// has no such view, or one with no outcome to lock, which shows every row.
|
|
37
|
+
export function workspaceAppRows(reports, duplicatesOf = () => []) {
|
|
38
|
+
return rowsOf(appView(reports, duplicatesOf))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Both, of one merge of its rows.
|
|
42
|
+
export function workspaceApp(reports, duplicatesOf = () => [], { countFindings = false } = {}) {
|
|
43
|
+
const view = appView(reports, duplicatesOf)
|
|
44
|
+
return { metadata: metadataOf(view, countFindings), rows: rowsOf(view) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function metadataOf({ linked, app }, countFindings) {
|
|
48
|
+
if (!app) return countFindings ? { appMode: false, findings: linked.length } : { appMode: false }
|
|
35
49
|
return {
|
|
36
50
|
appMode: true,
|
|
37
51
|
appFindings: linked.filter((group) => appTabs(group).some((f) => f.isApp)).length,
|
|
38
52
|
}
|
|
39
53
|
}
|
|
54
|
+
|
|
55
|
+
function rowsOf({ linked, app }) {
|
|
56
|
+
if (!app || onlyUnjudgedImports(linked)) return null
|
|
57
|
+
const kinds = activeRevalidateKinds('confirmed', '')
|
|
58
|
+
const confirmed = (f) => kinds.includes(revalidateKindOf(f) || (f._source && !f._sourcePass ? 'revalidation' : ''))
|
|
59
|
+
const ids = new Set()
|
|
60
|
+
for (const group of linked) if (group.some(confirmed)) for (const f of appTabs(group)) ids.add(f.id)
|
|
61
|
+
return ids
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A workspace's rows as its basic App view merges them, and whether it has one.
|
|
65
|
+
function appView(reports, duplicatesOf) {
|
|
66
|
+
const { groups, conflicts } = mergeReportGroups(reports, { hideRuledOut: true })
|
|
67
|
+
const linked = mergeLinkedWorkspaceGroups(groups, duplicatesOf, appTabs)
|
|
68
|
+
const app = conflicts.size === 0
|
|
69
|
+
&& (onlyUnjudgedImports(linked) || canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' }))
|
|
70
|
+
return { linked, app }
|
|
71
|
+
}
|