@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2WDZPXVL.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-UK7RGJT6.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +362 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +21 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +73 -0
- package/server-managed/comparison.ts +41 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/locked-packages.ts +129 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +215 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +355 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +165 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +156 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +150 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +307 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { REVALIDATE_KINDS, revalidateKindOf } from '@preventive/report'
|
|
2
|
+
|
|
3
|
+
// Keep only the pass's fields, before gap-filling changes the survivor.
|
|
4
|
+
// In particular, a verdict borrowed from report B must never be attributed
|
|
5
|
+
// to an unstamped copy from report A when report C later contradicts it.
|
|
6
|
+
function fieldsOf(finding) {
|
|
7
|
+
return Object.fromEntries(Object.entries(finding).filter(([key, value]) =>
|
|
8
|
+
key.startsWith('revalidate') && value != null))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function recordRevalidationCopies(survivor, duplicate, reportName) {
|
|
12
|
+
const fields = fieldsOf(duplicate)
|
|
13
|
+
if (!survivor._revalidationCopies) {
|
|
14
|
+
const own = fieldsOf(survivor)
|
|
15
|
+
if (Object.keys(own).length === 0 && Object.keys(fields).length === 0) return []
|
|
16
|
+
survivor._revalidationCopies = Object.keys(own).length > 0
|
|
17
|
+
? [{ reportName: survivor._reportName ?? '', fields: own }]
|
|
18
|
+
: []
|
|
19
|
+
}
|
|
20
|
+
if (Object.keys(fields).length > 0) survivor._revalidationCopies.push({ reportName, fields })
|
|
21
|
+
return survivor._revalidationCopies
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Same comparisons as mergeDuplicateFields: missing values are gaps, stamps
|
|
25
|
+
// are normalized, and prose ignores surrounding whitespace. Group agreeing
|
|
26
|
+
// reports together, retaining every distinct answer rather than just a pair.
|
|
27
|
+
export function revalidationDifferences(copies) {
|
|
28
|
+
const byField = new Map()
|
|
29
|
+
const knownKinds = new Set(REVALIDATE_KINDS)
|
|
30
|
+
for (const { reportName, fields } of copies) {
|
|
31
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
32
|
+
if (value == null) continue
|
|
33
|
+
const normalized = key === 'revalidate'
|
|
34
|
+
? revalidateKindOf(fields) || String(value).trim().toLowerCase()
|
|
35
|
+
: String(value).trim()
|
|
36
|
+
if (key === 'revalidate' && !normalized) continue
|
|
37
|
+
if (key === 'revalidate' && !knownKinds.has(normalized)) continue
|
|
38
|
+
if (!byField.has(key)) byField.set(key, new Map())
|
|
39
|
+
const variants = byField.get(key)
|
|
40
|
+
if (!variants.has(normalized)) variants.set(normalized, new Set())
|
|
41
|
+
variants.get(normalized).add(reportName)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...byField].filter(([, variants]) => variants.size > 1).map(([field, variants]) => ({
|
|
45
|
+
field,
|
|
46
|
+
variants: [...variants].map(([value, reports]) => ({ value, reports: [...reports] })),
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Recover source-level row boundaries before hiding the App layer. App
|
|
2
|
+
// deduplication alone does not make the underlying code findings equivalent.
|
|
3
|
+
// Only split when every source ID is accounted for and every referenced input
|
|
4
|
+
// exists in this row. Missing input lists are empty, not missing evidence for
|
|
5
|
+
// an otherwise complete partition. Original findings and rows stay untouched.
|
|
6
|
+
export function splitRevalidationInputs(group, appFindings = null) {
|
|
7
|
+
const app = appFindings ?? group.filter((f) => f.isApp)
|
|
8
|
+
const source = group.filter((f) => !f.isApp)
|
|
9
|
+
if (app.length === 0 || source.length === 0) return [group]
|
|
10
|
+
// A surviving App finding means this is not a pure non-App projection;
|
|
11
|
+
// never move it into a source-only partition.
|
|
12
|
+
if (appFindings && group.some((f) => f.isApp)) return [group]
|
|
13
|
+
if (source.some((f) => typeof f.id !== 'string' || !f.id)) return [group]
|
|
14
|
+
|
|
15
|
+
const sourceIds = new Set(source.map((f) => f.id))
|
|
16
|
+
const inputsByApp = new Map()
|
|
17
|
+
const covered = new Set()
|
|
18
|
+
for (const f of app) {
|
|
19
|
+
const inputs = f.revalidateInputs === undefined ? [] : f.revalidateInputs
|
|
20
|
+
if (!Array.isArray(inputs)) return [group]
|
|
21
|
+
for (const id of inputs) {
|
|
22
|
+
if (!sourceIds.has(id)) return [group]
|
|
23
|
+
covered.add(id)
|
|
24
|
+
}
|
|
25
|
+
inputsByApp.set(f, inputs)
|
|
26
|
+
}
|
|
27
|
+
if (covered.size !== sourceIds.size) return [group]
|
|
28
|
+
|
|
29
|
+
const parent = new Map([...sourceIds].map((id) => [id, id]))
|
|
30
|
+
const find = (id) => {
|
|
31
|
+
let root = id
|
|
32
|
+
while (parent.get(root) !== root) root = parent.get(root)
|
|
33
|
+
while (parent.get(id) !== root) {
|
|
34
|
+
const next = parent.get(id)
|
|
35
|
+
parent.set(id, root)
|
|
36
|
+
id = next
|
|
37
|
+
}
|
|
38
|
+
return root
|
|
39
|
+
}
|
|
40
|
+
for (const inputs of inputsByApp.values()) {
|
|
41
|
+
if (inputs.length === 0) continue
|
|
42
|
+
const root = find(inputs[0])
|
|
43
|
+
for (const id of inputs) parent.set(find(id), root)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const components = new Map()
|
|
47
|
+
for (const f of group) {
|
|
48
|
+
const inputs = inputsByApp.get(f)
|
|
49
|
+
// Keep an App finding with its inputs until the caller projects the lens.
|
|
50
|
+
// App findings without inputs stand alone; hiding them then drops the row.
|
|
51
|
+
const key = inputs ? inputs.length > 0 ? find(inputs[0]) : f : find(f.id)
|
|
52
|
+
if (!components.has(key)) components.set(key, [])
|
|
53
|
+
components.get(key).push(f)
|
|
54
|
+
}
|
|
55
|
+
return components.size === 1 ? [group] : [...components.values()]
|
|
56
|
+
}
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
// `session.error` until `dismissError()`.
|
|
16
16
|
// `busy` — per-socket inflight cap dropped the save
|
|
17
17
|
// (transport backpressure). Recoverable; the
|
|
18
|
-
// client clears `pending
|
|
19
|
-
//
|
|
18
|
+
// client clears `pending`, re-arms `pendingSave`,
|
|
19
|
+
// and schedules a short timed retry — without the
|
|
20
|
+
// timer an idle client (no further edit /
|
|
21
|
+
// broadcast / reconnect) would strand its last
|
|
22
|
+
// edit unsynced indefinitely.
|
|
20
23
|
// `stale-base` — concurrent commit advanced the head past the
|
|
21
24
|
// client's claimed base. The server emits the
|
|
22
25
|
// typed frame AFTER a `workspace-state` catch-up
|
|
@@ -25,11 +28,21 @@
|
|
|
25
28
|
// and this frame's handler then early-returns
|
|
26
29
|
// on the missing pending. So `stale-base` is
|
|
27
30
|
// NOT in the recoverable set: in the happy path
|
|
28
|
-
// the recoverable branch is unreachable
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
31
|
+
// the recoverable branch is unreachable. When
|
|
32
|
+
// `pending` DOES survive to this frame — the
|
|
33
|
+
// catch-up was empty (chain gone: wiped /
|
|
34
|
+
// migrated DB) or didn't apply (chain rebuilt
|
|
35
|
+
// past the client's base) — the client answers
|
|
36
|
+
// with ONE full-state-push reset per recovery
|
|
37
|
+
// cycle (re-anchor at base=null; safe because
|
|
38
|
+
// the server's null-safe head CAS rejects a
|
|
39
|
+
// base=null root against any non-empty chain;
|
|
40
|
+
// see `staleResetAttempted` in triage-sync.ts).
|
|
41
|
+
// A repeat within the cycle marks the session
|
|
42
|
+
// errored (visible via UI + recoverable via
|
|
43
|
+
// `dismissError()`) rather than looping
|
|
44
|
+
// full-state saves against a relay that keeps
|
|
45
|
+
// rejecting them.
|
|
33
46
|
|
|
34
47
|
export type SaveErrorReason = 'too-large' | 'busy' | 'stale-base'
|
|
35
48
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const DEFAULT_SCAN_SERVER = 'http://127.0.0.1:3123/'
|
|
2
|
+
|
|
3
|
+
// Discovery is optional: malformed scan configuration must never prevent
|
|
4
|
+
// opening local data or recognizing the server's sync protocol.
|
|
5
|
+
export function normalizeScanServer(value: unknown): string | null {
|
|
6
|
+
if (typeof value !== 'string' || !value.trim()) return null
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value)
|
|
9
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) return null
|
|
10
|
+
url.pathname = url.pathname.replace(/\/+$/u, '') + '/'
|
|
11
|
+
return url.href
|
|
12
|
+
} catch { return null }
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Links as stampSecurityGroups reads them, shared by the managed server's
|
|
2
|
+
// permission filter and the view's security lens. A star per link is enough
|
|
3
|
+
// for transitive propagation, without a quadratic expansion for large links.
|
|
4
|
+
// `add` extends the lookup while a stamp is running: rows the finding index
|
|
5
|
+
// returns can carry links of their own.
|
|
6
|
+
export function securityLinks(links = []) {
|
|
7
|
+
const edges = new Map()
|
|
8
|
+
const connect = (a, b) => {
|
|
9
|
+
if (!edges.has(a)) edges.set(a, new Set())
|
|
10
|
+
edges.get(a).add(b)
|
|
11
|
+
}
|
|
12
|
+
const add = (ids) => {
|
|
13
|
+
for (const id of ids.slice(1)) { connect(ids[0], id); connect(id, ids[0]) }
|
|
14
|
+
}
|
|
15
|
+
for (const ids of links) add(ids)
|
|
16
|
+
return { add, linkedIds: (id) => edges.get(id) ?? [] }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// An App finding can name the source findings it revalidated only by ID, in
|
|
20
|
+
// another row or report. The App view counts those as represented by it, so
|
|
21
|
+
// they are one issue for security too: hiding one side alone would leave the
|
|
22
|
+
// App answer to a hidden security finding, or a source row without its App row.
|
|
23
|
+
// Only App findings: the view ignores inputs on any other finding
|
|
24
|
+
// (finding-filters.js confirmedCoverage, revalidation-input-groups.js).
|
|
25
|
+
export function revalidationLinks(findings) {
|
|
26
|
+
const links = []
|
|
27
|
+
for (const finding of findings) {
|
|
28
|
+
const inputs = finding?.revalidateInputs
|
|
29
|
+
if (finding?.isApp !== true || typeof finding.id !== 'string' || !Array.isArray(inputs)) continue
|
|
30
|
+
links.push([finding.id, ...inputs.filter((id) => typeof id === 'string' && id !== '')])
|
|
31
|
+
}
|
|
32
|
+
return links
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The sync-protocol mode a server advertises — shared by the server (which
|
|
2
|
+
// emits it) and the client (which parses + caches it) so a change to the
|
|
3
|
+
// shape surfaces as a compile error on BOTH sides, the same single-source
|
|
4
|
+
// discipline as ./save-error-reason.ts.
|
|
5
|
+
//
|
|
6
|
+
// Delivered to the client as the first `server-info` frame on a sync
|
|
7
|
+
// connection (the WS plane and the SSE+POST fallback share one send path).
|
|
8
|
+
// A client uses it to detect whether a deployment speaks the end-to-end
|
|
9
|
+
// (`e2e`, zero-knowledge) protocol, the trusted `managed` protocol, or both.
|
|
10
|
+
// Combined discovery modes put the default protocol first.
|
|
11
|
+
|
|
12
|
+
export type ServerMode = 'e2e' | 'managed'
|
|
13
|
+
|
|
14
|
+
// Managed-mode entry points the server advertises; null for an e2e server.
|
|
15
|
+
export interface ManagedServerInfo {
|
|
16
|
+
loginPath: string
|
|
17
|
+
cookieName: string
|
|
18
|
+
uploadChunkBytes?: number
|
|
19
|
+
uploadMaxBytes?: { reports: number; bundles: number }
|
|
20
|
+
allowShare?: boolean
|
|
21
|
+
}
|
|
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
|
+
// Where the user publishes dependency reviews to some team: an npm
|
|
30
|
+
// version's Review tab is theirs (server-managed/dependency-reviews.ts).
|
|
31
|
+
dependencyReview?: true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The `server-info` frame payload (the `type` discriminant is added at the
|
|
35
|
+
// send site). `managed` is null on e2e-only deployments.
|
|
36
|
+
export interface ServerInfo {
|
|
37
|
+
mode: ServerMode | 'managed+e2e' | 'e2e+managed'
|
|
38
|
+
managed: ManagedServerInfo | null
|
|
39
|
+
deepviewScanServer?: string
|
|
40
|
+
githubNewIssueLabels?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The mode-probe route. A client GETs this to learn a server's protocol up
|
|
44
|
+
// front — a managed server has no WS plane to carry the connect frame — and
|
|
45
|
+
// the response body is the same `ServerInfo` shape emitted as that frame.
|
|
46
|
+
export const CONFIG_PATH = '/api/config'
|
package/common/utf8.d.ts
CHANGED
|
@@ -11,3 +11,6 @@
|
|
|
11
11
|
// narrower declaration is safe.
|
|
12
12
|
export function encodeUtf8(str: string): Uint8Array<ArrayBuffer>
|
|
13
13
|
export function decodeUtf8(bytes: Uint8Array | ArrayBuffer | ArrayBufferView): string
|
|
14
|
+
// Bytes `str` takes in UTF-8, measured without encoding when no character
|
|
15
|
+
// is past \xFF. A lone surrogate counts as the U+FFFD it encodes to.
|
|
16
|
+
export function utf8ByteLength(str: string): number
|
package/common/utf8.js
CHANGED
|
@@ -43,6 +43,51 @@ export function encodeUtf8(str) {
|
|
|
43
43
|
return encoder.encode(str)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// ASCII characters in a row after which the count hands back to the
|
|
47
|
+
// native scan. A jump costs a call and a step does not, so it pays only
|
|
48
|
+
// across a real gap: at 1, alternating text (`aéaé…`) takes a jump per
|
|
49
|
+
// character, and past 2, text with an accent every few words steps
|
|
50
|
+
// through ASCII the scan crosses faster. Measured on 50 MB of each, 2 was
|
|
51
|
+
// never more than 1.5x the best of the thresholds tried, 1 to 32.
|
|
52
|
+
const ASCII_RUN = 2
|
|
53
|
+
|
|
54
|
+
// The number of bytes `str` takes in UTF-8, without encoding it where
|
|
55
|
+
// that can be helped. A string with no character past \xFF is Latin-1:
|
|
56
|
+
// ASCII takes a byte and \x80-\xFF two, so its size is its length plus
|
|
57
|
+
// its high characters. An engine stores such a string a byte per
|
|
58
|
+
// character, where no character past \xFF can be, so the first test is
|
|
59
|
+
// answered without reading it. Only a string with a wider character is
|
|
60
|
+
// encoded to be measured.
|
|
61
|
+
//
|
|
62
|
+
// The high characters are counted without collecting them: a match array
|
|
63
|
+
// takes an entry apiece, which for Latin-1-heavy text outweighs the
|
|
64
|
+
// encoding this avoids. A native scan jumps to the next one — for ASCII
|
|
65
|
+
// it finds none — and a loop counts on from there while they keep coming,
|
|
66
|
+
// handing back to the scan after a run of ASCII. Sparse text is read
|
|
67
|
+
// natively, dense text by the loop, and neither allocates.
|
|
68
|
+
//
|
|
69
|
+
// This sizes text for display, so unlike `encodeUtf8` it does not refuse
|
|
70
|
+
// a lone surrogate: it counts the U+FFFD that TextEncoder writes for one.
|
|
71
|
+
export function utf8ByteLength(str) {
|
|
72
|
+
if (typeof str !== 'string') {
|
|
73
|
+
throw new TypeError(`utf8ByteLength expects a string, got ${typeof str}`)
|
|
74
|
+
}
|
|
75
|
+
// Code units, not code points: a `u` regexp reads a two-byte string by
|
|
76
|
+
// code point, several times slower, and a surrogate is past \xFF either way.
|
|
77
|
+
// eslint-disable-next-line require-unicode-regexp
|
|
78
|
+
if (/[\u0100-\uFFFF]/.test(str)) return encoder.encode(str).byteLength
|
|
79
|
+
const high = /[\u0080-\u00FF]/gu
|
|
80
|
+
let bytes = str.length
|
|
81
|
+
while (high.test(str)) {
|
|
82
|
+
let i = high.lastIndex - 1
|
|
83
|
+
for (let ascii = 0; i < str.length && ascii < ASCII_RUN; i++) {
|
|
84
|
+
if (str.codePointAt(i) > 0x7F) { bytes++; ascii = 0 } else ascii++
|
|
85
|
+
}
|
|
86
|
+
high.lastIndex = i
|
|
87
|
+
}
|
|
88
|
+
return bytes
|
|
89
|
+
}
|
|
90
|
+
|
|
46
91
|
export function decodeUtf8(bytes) {
|
|
47
92
|
// Reject non-BufferSource so a missed destructure / optional field /
|
|
48
93
|
// misnamed property surfaces here instead of silently defaulting to
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { revalidateKindOf } from '@preventive/report'
|
|
2
|
+
import { canLockConfirmed } from './finding-filters.js'
|
|
3
|
+
import { mergeReportGroups } from './workspace-groups.js'
|
|
4
|
+
import { mergeLinkedWorkspaceGroups } from './linked-workspace-groups.js'
|
|
5
|
+
|
|
6
|
+
function appTabs(group) {
|
|
7
|
+
if (group.linkedTabs) return group.linkedTabs
|
|
8
|
+
const app = group.filter((f) => revalidateKindOf(f) === 'revalidation')
|
|
9
|
+
return app.length > 0 ? app : group
|
|
10
|
+
}
|
|
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
|
+
|
|
25
|
+
// Evaluate the entire workspace, independent of triage, filters, or the lens
|
|
26
|
+
// left active by another report. Use the same merge and link rules as the view.
|
|
27
|
+
// `countFindings` has a workspace that is not App count its rows too, after
|
|
28
|
+
// the same merging, for a caller that shows it collapsed (managed teams).
|
|
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
|
|
35
|
+
return {
|
|
36
|
+
appMode: true,
|
|
37
|
+
appFindings: linked.filter((group) => appTabs(group).some((f) => f.isApp)).length,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { revalidateKindOf } from '@preventive/report'
|
|
2
|
+
import { mergeReportDuplicateFields } from './finding-duplicates.js'
|
|
3
|
+
import { splitRevalidationInputs } from './revalidation-input-groups.js'
|
|
4
|
+
|
|
5
|
+
// Reports retain their original rows. Merge only this derived view, so two
|
|
6
|
+
// application findings that refer to the same source issue keep their own
|
|
7
|
+
// context, verdicts, and tabs. Code mode removes pass findings BEFORE deciding
|
|
8
|
+
// which rows may merge; imports from other analyzers remain App findings.
|
|
9
|
+
export function mergeReportGroups(reports, { showRevalidation = true, upstreamOnly = false, hideRuledOut = false, merges = [] } = {}) {
|
|
10
|
+
const rows = []
|
|
11
|
+
const ruledOutIds = new Set()
|
|
12
|
+
for (let reportIndex = 0; reportIndex < reports.length; reportIndex++) {
|
|
13
|
+
const report = reports[reportIndex]
|
|
14
|
+
for (const original of report.groups) {
|
|
15
|
+
for (const finding of original) {
|
|
16
|
+
if (finding.id && ['refuted', 'unreachable'].includes(revalidateKindOf(finding))) ruledOutIds.add(finding.id)
|
|
17
|
+
}
|
|
18
|
+
// Visibility precedes merging: a hidden answer from another app must
|
|
19
|
+
// neither cause a conflict nor gap-fill the answer the reader will see.
|
|
20
|
+
const kept = upstreamOnly
|
|
21
|
+
? original.filter((f) => f.isUpstream && revalidateKindOf(f) !== 'revalidation').map(withoutRevalidation)
|
|
22
|
+
: showRevalidation
|
|
23
|
+
? hideRuledOut
|
|
24
|
+
? original.filter((f) => !['refuted', 'unreachable'].includes(revalidateKindOf(f)))
|
|
25
|
+
: original
|
|
26
|
+
: original.filter((f) => revalidateKindOf(f) !== 'revalidation').map(withoutRevalidation)
|
|
27
|
+
const group = kept.length === original.length && kept.every((f, i) => f === original[i]) ? original : kept
|
|
28
|
+
if (group.length > 0) rows.push({ group, sourceGroup: original, reportIndex, reportName: report.fileName ?? '', hasApp: group.some((f) => f.isApp) })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const parent = rows.map((_, i) => i)
|
|
32
|
+
const hasApp = rows.map((r) => r.hasApp)
|
|
33
|
+
const find = (i) => {
|
|
34
|
+
let root = i
|
|
35
|
+
while (parent[root] !== root) root = parent[root]
|
|
36
|
+
while (parent[i] !== root) { const next = parent[i]; parent[i] = root; i = next }
|
|
37
|
+
return root
|
|
38
|
+
}
|
|
39
|
+
const union = (a, b) => {
|
|
40
|
+
a = find(a); b = find(b)
|
|
41
|
+
if (a === b) return
|
|
42
|
+
parent[b] = a
|
|
43
|
+
hasApp[a] ||= hasApp[b]
|
|
44
|
+
}
|
|
45
|
+
const byId = new Map()
|
|
46
|
+
for (let i = 0; i < rows.length; i++) {
|
|
47
|
+
for (const f of rows[i].group) {
|
|
48
|
+
if (!f.id) continue
|
|
49
|
+
if (!byId.has(f.id)) byId.set(f.id, [])
|
|
50
|
+
byId.get(f.id).push({ index: i, app: f.isApp })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// App identities and within-report overlaps establish their components first.
|
|
54
|
+
// Otherwise a source-only row could bridge two App rows through another report.
|
|
55
|
+
for (const occurrences of byId.values()) {
|
|
56
|
+
const ownReports = new Map()
|
|
57
|
+
const app = occurrences.find((entry) => entry.app)
|
|
58
|
+
for (const { index } of occurrences) {
|
|
59
|
+
if (app) union(app.index, index)
|
|
60
|
+
const reportIndex = rows[index].reportIndex
|
|
61
|
+
if (ownReports.has(reportIndex)) union(ownReports.get(reportIndex), index)
|
|
62
|
+
else ownReports.set(reportIndex, index)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const occurrences of byId.values()) {
|
|
66
|
+
let first = null
|
|
67
|
+
for (const { index } of occurrences) {
|
|
68
|
+
if (hasApp[find(index)]) continue
|
|
69
|
+
if (first === null) first = index
|
|
70
|
+
else union(first, index)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Explicit hints are retained for callers supplying already-deduplicated rows.
|
|
74
|
+
// Normal report loading no longer needs them: all original rows are retained.
|
|
75
|
+
for (const merge of merges) {
|
|
76
|
+
let first = null
|
|
77
|
+
for (const id of merge) {
|
|
78
|
+
for (const { index } of byId.get(id) ?? []) {
|
|
79
|
+
if (first === null) first = index
|
|
80
|
+
else union(first, index)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const components = new Map()
|
|
85
|
+
for (let i = 0; i < rows.length; i++) {
|
|
86
|
+
const root = find(i)
|
|
87
|
+
if (!components.has(root)) components.set(root, [])
|
|
88
|
+
components.get(root).push(rows[i])
|
|
89
|
+
}
|
|
90
|
+
const conflicts = new Map(), groups = []
|
|
91
|
+
for (const component of components.values()) {
|
|
92
|
+
const original = component[0].group
|
|
93
|
+
const ids = original.map((f) => f.id).filter(Boolean)
|
|
94
|
+
if (component.length === 1 && new Set(ids).size === ids.length && merges.length === 0) {
|
|
95
|
+
const app = reports.length > 1 && original.find((f) => f.isApp)
|
|
96
|
+
if (app) {
|
|
97
|
+
const group = [...original]
|
|
98
|
+
group.workspaceKey = app.id ?? String(app._id)
|
|
99
|
+
groups.push(...splitOutput(component, group, showRevalidation, upstreamOnly))
|
|
100
|
+
} else groups.push(...splitOutput(component, original, showRevalidation, upstreamOnly))
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const rowConflicts = new Map()
|
|
104
|
+
const copies = new Map()
|
|
105
|
+
const findings = []
|
|
106
|
+
for (const { group, reportName } of component) {
|
|
107
|
+
for (const f of group) {
|
|
108
|
+
if (f.id && copies.has(f.id)) {
|
|
109
|
+
mergeReportDuplicateFields(copies.get(f.id), f, reportName, rowConflicts)
|
|
110
|
+
} else {
|
|
111
|
+
// Only the derived copy receives gap fills and per-report variants.
|
|
112
|
+
const copy = { ...f, _reportName: f._reportName ?? reportName }
|
|
113
|
+
if (f.id) copies.set(f.id, copy)
|
|
114
|
+
findings.push(copy)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Combined rows carry the author's order; singleton load order does not.
|
|
119
|
+
const canonical = new Set()
|
|
120
|
+
for (const merge of merges) for (const id of merge) if (copies.has(id)) canonical.add(id)
|
|
121
|
+
for (const { group } of component) {
|
|
122
|
+
if (group.length > 1) for (const f of group) if (f.id) canonical.add(f.id)
|
|
123
|
+
}
|
|
124
|
+
const group = [...canonical].map((id) => copies.get(id))
|
|
125
|
+
for (const f of findings) if (!canonical.has(f.id)) group.push(f)
|
|
126
|
+
// Separate App rows can begin with the SAME source id. Give each row its
|
|
127
|
+
// App identity so Lit keys, selection, and popup close targets stay unique.
|
|
128
|
+
if (reports.length > 1) {
|
|
129
|
+
const app = group.find((f) => f.isApp)
|
|
130
|
+
if (app) group.workspaceKey = app.id ?? String(app._id)
|
|
131
|
+
}
|
|
132
|
+
groups.push(...splitOutput(component, group, showRevalidation, upstreamOnly))
|
|
133
|
+
for (const [id, conflict] of rowConflicts) {
|
|
134
|
+
const previous = conflicts.get(id)
|
|
135
|
+
conflicts.set(id, previous
|
|
136
|
+
? { finding: previous.finding, copies: [...previous.copies, ...conflict.copies] }
|
|
137
|
+
: conflict)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Object.defineProperty(groups, 'ruledOutIds', { value: ruledOutIds, configurable: true })
|
|
141
|
+
return { groups, conflicts }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function splitOutput(component, group, keepAppLayer, upstreamOnly) {
|
|
145
|
+
if (keepAppLayer) return [group]
|
|
146
|
+
if (upstreamOnly) {
|
|
147
|
+
// Upstream projection removes the App and own-source members before this
|
|
148
|
+
// function runs. Reconstruct the complete source component first so an
|
|
149
|
+
// App's inputs outside the upstream lens still determine its partition.
|
|
150
|
+
const complete = component.flatMap((row) => row.sourceGroup)
|
|
151
|
+
const partitions = splitRevalidationInputs(complete)
|
|
152
|
+
if (partitions.length === 1) return [group]
|
|
153
|
+
const projected = []
|
|
154
|
+
const covered = new Set()
|
|
155
|
+
for (const partition of partitions) {
|
|
156
|
+
const ids = new Set(partition.filter((f) => !f.isApp && f.id).map((f) => f.id))
|
|
157
|
+
const visible = group.filter((f) => ids.has(f.id))
|
|
158
|
+
if (visible.length > 0) {
|
|
159
|
+
projected.push(visible)
|
|
160
|
+
for (const finding of visible) covered.add(finding.id)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const groupIds = new Set(group.map((f) => f.id).filter(Boolean))
|
|
164
|
+
return covered.size === groupIds.size ? projected : [group]
|
|
165
|
+
}
|
|
166
|
+
const appFindings = component.flatMap((row) => row.sourceGroup.filter((f) => f.isApp))
|
|
167
|
+
return splitRevalidationInputs(group, appFindings)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function withoutRevalidation(finding) {
|
|
171
|
+
const fields = Object.keys(finding).filter((key) => key.startsWith('revalidate') || key === '_revalidationCopies')
|
|
172
|
+
if (fields.length === 0) return finding
|
|
173
|
+
const copy = { ...finding }
|
|
174
|
+
for (const field of fields) delete copy[field]
|
|
175
|
+
return copy
|
|
176
|
+
}
|