@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.
@@ -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[])[]): { reports: T[]; folded: boolean }
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). A team's
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.length > NAME_CHARS ? `${title.slice(0, NAME_CHARS - 1)}…` : 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
- export function liteReportData(data, { fold = false } = {}) {
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 shown = fold && entry.length > 1 && entry.some(isPass) ? entry.filter(isPass) : entry
50
- return shown.map(liteFinding)
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`, folded where nothing in the workspace contradicts the
59
- // revalidation pass, which opens a view of it in App mode
60
- // (ui/view/render.js configureReportRevalidation); `folded` where that left
61
- // rows out.
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 fold = !managedTeamRevalidationConflicted(Object.assign(reports.map(({ filename, data }) => ({ filename, data })), { links }))
64
- const lite = reports.map(report => ({ ...report, data: liteReportData(report.data, { fold }) }))
65
- return { reports: lite, folded: fold && lite.some((report, index) => rows(report.data) < rows(reports[index].data)) }
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,2 @@
1
+ export function findingValueIndex(): { values: unknown[]; index<T>(data: T): T }
2
+ export function expandFindingValues<T>(data: T, values: unknown): T
@@ -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,2 @@
1
+ export function liteName(name: string): string
2
+ export function readLiteReportData<T>(data: T): T
@@ -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
  }
@@ -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 { groups, conflicts } = mergeReportGroups(reports, { hideRuledOut: true })
31
- const linked = mergeLinkedWorkspaceGroups(groups, duplicatesOf, appTabs)
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
+ }