@preventive/triage 1.0.0-alpha.52 → 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,5 @@
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
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). 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
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
- 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 } = {}) {
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 shown = fold && entry.length > 1 && entry.some(isPass) ? entry.filter(isPass) : entry
47
- return shown.map(liteFinding)
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`, folded where nothing in the workspace contradicts the
56
- // revalidation pass, which opens a view of it in App mode
57
- // (ui/view/render.js configureReportRevalidation); `folded` where that left
58
- // 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).
59
68
  export function liteWorkspace(reports, links) {
60
- const fold = !managedTeamRevalidationConflicted(Object.assign(reports.map(({ filename, data }) => ({ filename, data })), { links }))
61
- const lite = reports.map(report => ({ ...report, data: liteReportData(report.data, { fold }) }))
62
- 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) }
63
75
  }
@@ -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
+ }