@preventive/triage 1.0.0-alpha.51 → 1.0.0-alpha.53
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/managed/finding-detail.d.ts +4 -2
- package/common/managed/finding-detail.js +29 -20
- package/common/managed/finding-values.d.ts +2 -0
- package/common/managed/finding-values.js +44 -0
- package/common/managed/lite-name.d.ts +2 -0
- package/common/managed/lite-name.js +20 -0
- package/common/managed/team-app.d.ts +2 -0
- package/common/managed/team-app.js +32 -3
- package/common/workspace-app.js +26 -6
- package/out/activity.js +4 -4
- package/out/chunk-JEFZTVNX.js +3 -0
- package/out/chunk-LJ7XRXFU.js +52 -0
- package/out/{chunk-PBZPYIG7.js → chunk-MYGJBYYJ.js} +1 -1
- package/out/{chunk-7JBSSRD7.js → chunk-NE2UGKRJ.js} +1 -1
- package/out/chunk-TSIFIFBX.js +232 -0
- package/out/{chunk-WRAJVD35.js → chunk-YA4ETCFU.js} +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +5 -5
- package/out/dialogs.js +41 -41
- package/out/index.html +3 -3
- package/out/scan.js +2 -2
- package/out/view.js +11 -11
- package/package.json +7 -1
- package/server-managed/github-requests.ts +54 -24
- package/server-managed/history-windows.ts +172 -0
- package/server-managed/http.ts +192 -62
- package/server-managed/npm-kept.ts +4 -0
- package/server-managed/repository-history.ts +2 -2
- package/server-managed/repository-pulse.ts +15 -4
- package/server-managed/repository-stamp.ts +41 -0
- package/server-managed/team-app.ts +3 -1
- package/server-managed/team-reports.ts +21 -13
- package/out/chunk-6LNQTKAV.js +0 -52
- package/out/chunk-IPAH4LKF.js +0 -232
- package/out/chunk-VDRXHIHN.js +0 -3
|
@@ -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[])[]): {
|
|
4
|
+
reports: T[]; folded: false | 'rows' | 'app'; lens: { drop: boolean; detail: boolean; upstream: boolean } | null
|
|
5
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findingTitle, reportEntries, revalidateKindOf } from '@preventive/report'
|
|
2
|
-
import { managedTeamRevalidationConflicted } from './team-app.js'
|
|
2
|
+
import { managedTeamAppRows, managedTeamLens, managedTeamRevalidationConflicted } from './team-app.js'
|
|
3
|
+
import { liteName } from './lite-name.js'
|
|
3
4
|
|
|
4
5
|
// A finding's detail: what its popup, the views besides the board and an
|
|
5
6
|
// export read, and the board does not. Its narrative and evidence, the hash
|
|
@@ -7,21 +8,18 @@ import { managedTeamRevalidationConflicted } from './team-app.js'
|
|
|
7
8
|
// and an export tell (write-md-finding.js PLAIN_FIELDS, CODE_FIELDS, but for
|
|
8
9
|
// `priority`, which the board sorts and filters by), and the run meta its
|
|
9
10
|
// card's run line and a report's header read (its `model` and `type`, which
|
|
10
|
-
// 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
|
|
11
13
|
// reports are first sent without it (server-managed/team-reports.ts), the
|
|
12
14
|
// board drawn from them, and their findings filled in from the whole reports
|
|
13
15
|
// (ui/view/managed-details.js).
|
|
14
16
|
export const FINDING_DETAIL_FIELDS = Object.freeze([
|
|
15
17
|
'description', 'evidence', 'impact', 'reproduction', 'recommendation', 'confidenceReason', 'revalidateVerdict', 'revalidateRecommendation',
|
|
16
18
|
'fileHash', 'exportName', 'methodName',
|
|
17
|
-
'commitHash', 'auditedCommit', 'detectedAt', 'committedAt', 'dateCreated', 'status', 'branch', 'pocStatus', 'parent', 'slug', 'reportPath',
|
|
18
|
-
'category', 'discoveredIn', 'effort', 'exportsMode',
|
|
19
|
+
'severityReason', 'commitHash', 'auditedCommit', 'detectedAt', 'committedAt', 'dateCreated', 'status', 'branch', 'pocStatus', 'parent', 'slug', 'reportPath',
|
|
20
|
+
'category', 'discoveredIn', 'effort', 'exportsMode', 'location',
|
|
19
21
|
])
|
|
20
22
|
const DETAIL = new Set(FINDING_DETAIL_FIELDS)
|
|
21
|
-
// As much of a name as a heading takes (@preventive/report write-md-finding.js
|
|
22
|
-
// HEADING_MAX), about a board card's three lines.
|
|
23
|
-
const NAME_CHARS = 120
|
|
24
|
-
|
|
25
23
|
// A finding without its detail. One without a title of its own is named by its
|
|
26
24
|
// description's first line (findingTitle), which it keeps as its description,
|
|
27
25
|
// as much of it as a card shows.
|
|
@@ -30,7 +28,7 @@ function liteFinding(finding) {
|
|
|
30
28
|
const lite = Object.fromEntries(Object.entries(finding).filter(([key]) => !DETAIL.has(key)))
|
|
31
29
|
const titled = typeof finding.title === 'string' && finding.title.trim() !== ''
|
|
32
30
|
const title = titled ? '' : findingTitle(finding)
|
|
33
|
-
if (title) lite.description = title
|
|
31
|
+
if (title) lite.description = liteName(title)
|
|
34
32
|
return lite
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -40,14 +38,20 @@ const isPass = finding => revalidateKindOf(finding) === 'revalidation'
|
|
|
40
38
|
// App mode shows of an entry holding the revalidation pass's row that row
|
|
41
39
|
// alone (ui/view/group.js foldedTabs): with `fold`, such an entry is sent as
|
|
42
40
|
// the pass's rows, the rows beneath them left for the whole reports
|
|
43
|
-
// (ui/view/managed-details.js).
|
|
44
|
-
|
|
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 } = {}) {
|
|
45
45
|
const key = Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
|
|
46
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
|
+
}
|
|
47
51
|
const entries = data[key].map(entry => {
|
|
48
52
|
if (!Array.isArray(entry)) return liteFinding(entry)
|
|
49
|
-
const
|
|
50
|
-
return
|
|
53
|
+
const folded = fold && entry.length > 1 && entry.some(isPass) ? entry.filter(isPass) : entry
|
|
54
|
+
return folded.map(liteFinding)
|
|
51
55
|
})
|
|
52
56
|
return { ...data, [key]: entries }
|
|
53
57
|
}
|
|
@@ -55,12 +59,17 @@ export function liteReportData(data, { fold = false } = {}) {
|
|
|
55
59
|
const rows = data => (reportEntries(data) ?? []).flat().length
|
|
56
60
|
|
|
57
61
|
// A team's reports as its board is first sent them (server-managed/team-reports.ts),
|
|
58
|
-
// each `liteReportData
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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).
|
|
62
68
|
export function liteWorkspace(reports, links) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
69
|
+
const view = Object.assign(reports.map(({ filename, data }) => ({ filename, data })), { links })
|
|
70
|
+
const shown = managedTeamAppRows(view)
|
|
71
|
+
const fold = shown !== null || !managedTeamRevalidationConflicted(view)
|
|
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: managedTeamLens(view) }
|
|
66
75
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// A finding's `package` and `repo`: the same few across a team's many
|
|
2
|
+
// findings. A team's reports are sent with each once, in the response's
|
|
3
|
+
// `values`, and each finding's as its index there
|
|
4
|
+
// (server-managed/team-reports.ts), put back as they are read
|
|
5
|
+
// (client/managed/session.js).
|
|
6
|
+
const FIELDS = ['package', 'repo']
|
|
7
|
+
|
|
8
|
+
const entriesKey = data => Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
|
|
9
|
+
|
|
10
|
+
// `values`, and `index(data)`: a report's data with its findings' values as
|
|
11
|
+
// their index in it, one value at one index across every report it is given.
|
|
12
|
+
export function findingValueIndex() {
|
|
13
|
+
const indexes = new Map(), values = []
|
|
14
|
+
const indexOf = value => {
|
|
15
|
+
const key = JSON.stringify(value)
|
|
16
|
+
if (!indexes.has(key)) indexes.set(key, values.push(value) - 1)
|
|
17
|
+
return indexes.get(key)
|
|
18
|
+
}
|
|
19
|
+
const indexed = finding => {
|
|
20
|
+
if (!finding || typeof finding !== 'object' || !FIELDS.some(field => finding[field] != null)) return finding
|
|
21
|
+
const copy = { ...finding }
|
|
22
|
+
for (const field of FIELDS) if (finding[field] != null) copy[field] = indexOf(finding[field])
|
|
23
|
+
return copy
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
values,
|
|
27
|
+
index(data) {
|
|
28
|
+
const key = entriesKey(data)
|
|
29
|
+
return key ? { ...data, [key]: data[key].map(entry => Array.isArray(entry) ? entry.map(indexed) : indexed(entry)) } : data
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A report's data, as read, its findings' values put in place of their index
|
|
35
|
+
// in a response's `values`; one sent without any as it is.
|
|
36
|
+
export function expandFindingValues(data, values) {
|
|
37
|
+
const key = entriesKey(data)
|
|
38
|
+
if (!key || !Array.isArray(values)) return data
|
|
39
|
+
for (const finding of data[key].flat()) {
|
|
40
|
+
if (!finding || typeof finding !== 'object') continue
|
|
41
|
+
for (const field of FIELDS) if (Number.isInteger(finding[field])) finding[field] = values[finding[field]]
|
|
42
|
+
}
|
|
43
|
+
return data
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { reportEntries } from '@preventive/report'
|
|
2
|
+
|
|
3
|
+
// As much of a name as a heading takes (@preventive/report write-md-finding.js
|
|
4
|
+
// HEADING_MAX), about a board card's three lines. A board's copy of a team's
|
|
5
|
+
// reports (finding-detail.js) cuts one longer to end in `CUT`, an ellipsis
|
|
6
|
+
// once read.
|
|
7
|
+
const CUT = '...', NAME_CHARS = 120
|
|
8
|
+
|
|
9
|
+
export const liteName = name => name.length > NAME_CHARS ? `${name.slice(0, NAME_CHARS - CUT.length)}${CUT}` : name
|
|
10
|
+
|
|
11
|
+
// A report's data from its board's copy, as read: a name cut ends in an
|
|
12
|
+
// ellipsis. One as long as a cut leaves it and ending in `CUT` is one; one
|
|
13
|
+
// shorter ends as it does.
|
|
14
|
+
export function readLiteReportData(data) {
|
|
15
|
+
for (const finding of (reportEntries(data) ?? []).flat()) {
|
|
16
|
+
const name = finding?.description
|
|
17
|
+
if (typeof name === 'string' && name.length === NAME_CHARS && name.endsWith(CUT)) finding.description = `${name.slice(0, -CUT.length)}…`
|
|
18
|
+
}
|
|
19
|
+
return data
|
|
20
|
+
}
|
|
@@ -4,3 +4,5 @@ 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 }
|
|
@@ -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 { 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,6 +23,34 @@ 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
|
+
const RULED_OUT = new Set(['refuted', 'unreachable'])
|
|
34
|
+
|
|
35
|
+
// What a view of the whole workspace offers beside the App view
|
|
36
|
+
// (ui/view/render.js), told by all its rows: whether the revalidation layer
|
|
37
|
+
// can come off (`drop`), whether its detail stop has rows to unfold
|
|
38
|
+
// (`detail`), and whether an upstream lens has ground (`upstream`).
|
|
39
|
+
export function managedTeamLens(workspace) {
|
|
40
|
+
const { reports } = viewReports(workspace)
|
|
41
|
+
const rows = reports.flatMap(report => report.groups.flat())
|
|
42
|
+
const directory = dependencyDirectory(reports)
|
|
43
|
+
const detail = mergeReportGroups(reports).groups.some(group => {
|
|
44
|
+
const kinds = group.map(revalidateKindOf)
|
|
45
|
+
return kinds.includes('partial') || kinds.includes('revalidation') && kinds.some(kind => kind !== 'revalidation' && !RULED_OUT.has(kind))
|
|
46
|
+
})
|
|
47
|
+
return {
|
|
48
|
+
drop: rows.some(hasRevalidateStamp),
|
|
49
|
+
detail,
|
|
50
|
+
upstream: rows.some(f => f.isUpstream === true || f.isApp === false && !('isUpstream' in f) && isDependencyFile(f.file, directory)),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
// The workspace's reports as a view holds them (ui/view/ingest.js), with the
|
|
26
55
|
// duplicates its links name.
|
|
27
56
|
function viewReports(workspace) {
|
|
@@ -41,7 +70,7 @@ function viewReports(workspace) {
|
|
|
41
70
|
finding._sourcePass = judged.has(finding._source)
|
|
42
71
|
return finding
|
|
43
72
|
}))
|
|
44
|
-
reports.push({ fileName: filename, groups })
|
|
73
|
+
reports.push({ fileName: filename, groups, treeDependencies: data.treeDependencies ?? null })
|
|
45
74
|
}
|
|
46
75
|
return { reports, duplicates }
|
|
47
76
|
}
|
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,33 @@ 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
|
-
const {
|
|
31
|
-
|
|
32
|
-
const notApp = countFindings ? { appMode: false, findings: linked.length } : { appMode: false }
|
|
33
|
-
if (conflicts.size > 0) return notApp
|
|
34
|
-
if (!onlyUnjudgedImports(linked) && !canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' })) return notApp
|
|
30
|
+
const { linked, app } = appView(reports, duplicatesOf)
|
|
31
|
+
if (!app) return countFindings ? { appMode: false, findings: linked.length } : { appMode: false }
|
|
35
32
|
return {
|
|
36
33
|
appMode: true,
|
|
37
34
|
appFindings: linked.filter((group) => appTabs(group).some((f) => f.isApp)).length,
|
|
38
35
|
}
|
|
39
36
|
}
|
|
37
|
+
|
|
38
|
+
// The IDs of the findings a workspace's basic App view can show: of each row
|
|
39
|
+
// its locked Confirmed shows (ui/view/filters.js filterRevalidateKind), the
|
|
40
|
+
// App tabs, the rest folded under them; none the pass ruled out. Null where it
|
|
41
|
+
// has no such view, or one with no outcome to lock, which shows every row.
|
|
42
|
+
export function workspaceAppRows(reports, duplicatesOf = () => []) {
|
|
43
|
+
const { linked, app } = appView(reports, duplicatesOf)
|
|
44
|
+
if (!app || onlyUnjudgedImports(linked)) return null
|
|
45
|
+
const kinds = activeRevalidateKinds('confirmed', '')
|
|
46
|
+
const confirmed = (f) => kinds.includes(revalidateKindOf(f) || (f._source && !f._sourcePass ? 'revalidation' : ''))
|
|
47
|
+
const ids = new Set()
|
|
48
|
+
for (const group of linked) if (group.some(confirmed)) for (const f of appTabs(group)) ids.add(f.id)
|
|
49
|
+
return ids
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A workspace's rows as its basic App view merges them, and whether it has one.
|
|
53
|
+
function appView(reports, duplicatesOf) {
|
|
54
|
+
const { groups, conflicts } = mergeReportGroups(reports, { hideRuledOut: true })
|
|
55
|
+
const linked = mergeLinkedWorkspaceGroups(groups, duplicatesOf, appTabs)
|
|
56
|
+
const app = conflicts.size === 0
|
|
57
|
+
&& (onlyUnjudgedImports(linked) || canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' }))
|
|
58
|
+
return { linked, app }
|
|
59
|
+
}
|