@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51

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.
Files changed (368) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/README.md +16 -0
  3. package/api/reap.ts +17 -0
  4. package/cli.js +6 -0
  5. package/client/dependency-paths.d.ts +3 -0
  6. package/client/dependency-paths.js +36 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +115 -0
  10. package/common/bundle-commit.d.ts +1 -0
  11. package/common/bundle-commit.js +4 -0
  12. package/common/bundle-compare-diff.d.ts +3 -0
  13. package/common/bundle-compare-diff.js +357 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-compare-versions.js +172 -0
  17. package/common/bundle-comparison.d.ts +8 -0
  18. package/common/bundle-comparison.js +62 -0
  19. package/common/bundle-metadata.d.ts +15 -0
  20. package/common/bundle-metadata.js +226 -0
  21. package/common/bundle-pkg-of.d.ts +1 -0
  22. package/common/bundle-pkg-of.js +90 -0
  23. package/common/bundle-reasons.d.ts +2 -0
  24. package/common/bundle-reasons.js +21 -0
  25. package/common/bundle-source-links.d.ts +6 -0
  26. package/common/bundle-source-links.js +216 -0
  27. package/common/bundle-sourcemap.d.ts +10 -0
  28. package/common/bundle-sourcemap.js +100 -0
  29. package/common/bundle-sources.d.ts +7 -0
  30. package/common/bundle-sources.js +333 -0
  31. package/common/bundle-stats.js +41 -0
  32. package/common/bundle-tabs.js +1 -0
  33. package/common/code-language.js +65 -0
  34. package/common/default-scan-models.ts +31 -0
  35. package/common/dependency-reviews.d.ts +18 -0
  36. package/common/dependency-reviews.js +64 -0
  37. package/common/finding-duplicates.js +58 -0
  38. package/common/finding-filters.js +274 -0
  39. package/common/finding-id.js +47 -0
  40. package/common/github-issue-labels.d.ts +1 -0
  41. package/common/github-issue-labels.js +11 -0
  42. package/common/github-pr.ts +56 -0
  43. package/common/gzip.js +59 -0
  44. package/common/line-diff.d.ts +8 -0
  45. package/common/line-diff.js +57 -0
  46. package/common/linked-workspace-groups.js +63 -0
  47. package/common/managed/agent-tasks.ts +292 -0
  48. package/common/managed/comments.ts +34 -0
  49. package/common/managed/finding-detail.d.ts +3 -0
  50. package/common/managed/finding-detail.js +66 -0
  51. package/common/managed/github-agents.ts +114 -0
  52. package/common/managed/github-logins.ts +49 -0
  53. package/common/managed/npm-packages.d.ts +7 -0
  54. package/common/managed/npm-packages.js +51 -0
  55. package/common/managed/permissions.ts +31 -0
  56. package/common/managed/report-content.ts +72 -0
  57. package/common/managed/report-filter.ts +109 -0
  58. package/common/managed/repository-alias.ts +65 -0
  59. package/common/managed/roles.ts +35 -0
  60. package/common/managed/routes.d.ts +2 -0
  61. package/common/managed/routes.js +359 -0
  62. package/common/managed/scan-models.ts +6 -0
  63. package/common/managed/team-app.d.ts +6 -0
  64. package/common/managed/team-app.js +47 -0
  65. package/common/managed/triage.ts +83 -0
  66. package/common/managed/upload-seal.ts +126 -0
  67. package/common/pretty-print.d.ts +5 -0
  68. package/common/pretty-print.js +24 -0
  69. package/common/revalidation-conflicts.js +48 -0
  70. package/common/revalidation-input-groups.js +56 -0
  71. package/common/save-error-reason.ts +20 -7
  72. package/common/scan-server.ts +13 -0
  73. package/common/security-links.d.ts +5 -0
  74. package/common/security-links.js +33 -0
  75. package/common/server-info.ts +46 -0
  76. package/common/utf8.d.ts +3 -0
  77. package/common/utf8.js +45 -0
  78. package/common/workspace-app.js +39 -0
  79. package/common/workspace-groups.js +176 -0
  80. package/out/activity.js +4 -0
  81. package/out/brotli-fallback.js +3 -3
  82. package/out/chunk-2K6UN3PC.js +3 -0
  83. package/out/chunk-2XUEZHIP.js +28 -0
  84. package/out/chunk-2ZDZWLFY.js +1 -0
  85. package/out/chunk-4HI2MSAV.js +17 -0
  86. package/out/chunk-6LNQTKAV.js +52 -0
  87. package/out/chunk-7BKKYVWR.js +7 -0
  88. package/out/chunk-7C7DWL7I.js +1 -0
  89. package/out/chunk-7JBSSRD7.js +3 -0
  90. package/out/chunk-7VAWINQF.js +6 -0
  91. package/out/chunk-C4Z6ZBXV.js +10 -0
  92. package/out/chunk-FMXWXDR4.js +36 -0
  93. package/out/chunk-GDPCLSOS.js +2 -0
  94. package/out/chunk-GO7FT4OD.js +1 -0
  95. package/out/chunk-HT2SSA3V.js +1 -0
  96. package/out/chunk-IPAH4LKF.js +232 -0
  97. package/out/chunk-IYPY47MC.js +1 -0
  98. package/out/chunk-IZ7POWI3.js +18 -0
  99. package/out/chunk-JCIIBLKR.js +1 -0
  100. package/out/chunk-KOGNQDSJ.js +1 -0
  101. package/out/chunk-NRPC74AM.js +1 -0
  102. package/out/chunk-OOJM4CTU.js +1 -0
  103. package/out/chunk-PBZPYIG7.js +1 -0
  104. package/out/chunk-PNHBZGDD.js +1 -0
  105. package/out/chunk-PWFUOLZK.js +17 -0
  106. package/out/chunk-PXK7EU4V.js +1 -0
  107. package/out/chunk-QB4CBOX2.js +1 -0
  108. package/out/chunk-R3NZFXVR.js +1 -0
  109. package/out/chunk-R5O6FAW4.js +2 -0
  110. package/out/chunk-SXV2T5C4.js +80 -0
  111. package/out/chunk-TQJDYHVE.js +2 -0
  112. package/out/chunk-TUII34K2.js +1 -0
  113. package/out/chunk-VDRXHIHN.js +3 -0
  114. package/out/chunk-VFWMQ3LG.js +2 -0
  115. package/out/chunk-VH4XMZIW.js +2 -0
  116. package/out/chunk-WCPJDAYA.js +10 -0
  117. package/out/chunk-WFYLMF6Z.js +10 -0
  118. package/out/chunk-WRAJVD35.js +1 -0
  119. package/out/chunk-WYLQWYLA.js +1 -0
  120. package/out/client-managed-import.js +4 -0
  121. package/out/client-managed.js +33 -0
  122. package/out/client-sync.js +11 -17
  123. package/out/compare.js +35 -0
  124. package/out/dialogs.js +55 -0
  125. package/out/github-issue-authorized.html +3 -0
  126. package/out/github-issue-authorized.js +1 -0
  127. package/out/graph.js +17 -4
  128. package/out/index.html +90 -8
  129. package/out/prism.js +14 -10
  130. package/out/scan.js +2 -0
  131. package/out/stasis.svg +20 -0
  132. package/out/terminal.js +366 -39
  133. package/out/view.css +1 -1
  134. package/out/view.js +19 -144
  135. package/package.json +364 -59
  136. package/risk/actions/findings.js +161 -0
  137. package/risk/actions/resolve.js +219 -0
  138. package/risk/actions/scripts/commands.js +182 -0
  139. package/risk/actions/scripts/grade.js +113 -0
  140. package/risk/actions/scripts/packages.js +123 -0
  141. package/risk/actions/scripts/shell.js +169 -0
  142. package/risk/actions/uses.js +76 -0
  143. package/risk/actions/workflow.js +110 -0
  144. package/risk/files/characters.js +196 -0
  145. package/risk/files/hosts.js +205 -0
  146. package/risk/files/readability.js +177 -0
  147. package/risk/files/runs-code.js +615 -0
  148. package/risk/files/script.js +118 -0
  149. package/risk/files/stamps.js +47 -0
  150. package/risk/index.d.ts +254 -0
  151. package/risk/index.js +11 -0
  152. package/risk/npm/assess.js +285 -0
  153. package/risk/npm/combined.js +68 -0
  154. package/risk/npm/compare.js +257 -0
  155. package/risk/npm/dependencies.js +172 -0
  156. package/risk/npm/diff.js +79 -0
  157. package/risk/npm/downloads.js +84 -0
  158. package/risk/npm/files.js +445 -0
  159. package/risk/npm/held.js +63 -0
  160. package/risk/npm/install-scripts.js +34 -0
  161. package/risk/npm/malware.js +36 -0
  162. package/risk/npm/manifest.js +65 -0
  163. package/risk/npm/new-dependency.js +60 -0
  164. package/risk/npm/old-version.js +108 -0
  165. package/risk/npm/registry.js +388 -0
  166. package/risk/npm/release.js +85 -0
  167. package/risk/npm/score.js +59 -0
  168. package/risk/npm/specs.js +124 -0
  169. package/risk/npm/tarball.js +96 -0
  170. package/server-common/database-config.ts +16 -0
  171. package/server-common/initialize.ts +18 -0
  172. package/server-common/npm-advisories.ts +101 -0
  173. package/{server → server-common}/origin.ts +5 -5
  174. package/server-common/reap.ts +48 -0
  175. package/server-common/scan-config.ts +19 -0
  176. package/server-common/standalone.ts +29 -0
  177. package/server-common/storage-crypto.ts +182 -0
  178. package/server-common/storage-log.ts +38 -0
  179. package/server-common/vercel-blob.ts +103 -0
  180. package/server-e2e/app.ts +487 -0
  181. package/{server → server-e2e}/bus-receiver.ts +2 -1
  182. package/{server → server-e2e}/cli.js +9 -4
  183. package/{server → server-e2e}/config.ts +50 -42
  184. package/{server → server-e2e}/db-neon.ts +2 -2
  185. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  186. package/{server → server-e2e}/db-stmt.ts +2 -2
  187. package/{server → server-e2e}/db.ts +7 -7
  188. package/{server → server-e2e}/http.ts +44 -13
  189. package/{server → server-e2e}/hub.ts +40 -9
  190. package/server-e2e/index.ts +17 -0
  191. package/server-e2e/lifecycle.ts +95 -0
  192. package/{server → server-e2e}/neon-driver.ts +2 -2
  193. package/{server → server-e2e}/npm-proxy.ts +12 -145
  194. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  195. package/{server → server-e2e}/objstore/blob.ts +1 -1
  196. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  197. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  198. package/{server → server-e2e}/objstore/init.ts +19 -27
  199. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  200. package/server-e2e/objstore/rest-deny.ts +28 -0
  201. package/server-e2e/objstore/rest-mint.ts +224 -0
  202. package/{server → server-e2e}/objstore/rest.ts +3 -3
  203. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  204. package/{server → server-e2e}/objstore/store.ts +7 -7
  205. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  206. package/{server → server-e2e}/pubsub.ts +12 -9
  207. package/{server → server-e2e}/sign.ts +4 -4
  208. package/{server → server-e2e}/sse-server.ts +60 -33
  209. package/{server → server-e2e}/static.ts +31 -21
  210. package/{server → server-e2e}/sync-handlers.ts +64 -30
  211. package/{server → server-e2e}/ws-server.ts +20 -9
  212. package/server-managed/activity.ts +258 -0
  213. package/server-managed/agent-merges.ts +109 -0
  214. package/server-managed/app-agents.ts +23 -0
  215. package/server-managed/avatar-store.ts +13 -0
  216. package/server-managed/blob-store.ts +17 -0
  217. package/server-managed/brotli.ts +10 -0
  218. package/server-managed/bundle-advisories.ts +120 -0
  219. package/server-managed/bundle-advisory-inventory.ts +77 -0
  220. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  221. package/server-managed/bundle-build-diagnostics.js +23 -0
  222. package/server-managed/bundle-build-leases.ts +41 -0
  223. package/server-managed/bundle-build-worker.js +115 -0
  224. package/server-managed/bundle-build.ts +200 -0
  225. package/server-managed/bundle-cache.ts +211 -0
  226. package/server-managed/bundle-catalog.ts +29 -0
  227. package/server-managed/bundle-commits.ts +140 -0
  228. package/server-managed/bundle-store.ts +24 -0
  229. package/server-managed/bundle-summary-cache.ts +130 -0
  230. package/server-managed/bundle.ts +119 -0
  231. package/server-managed/cache-storage.ts +20 -0
  232. package/server-managed/cli.js +21 -0
  233. package/server-managed/combined.ts +47 -0
  234. package/server-managed/comments.ts +151 -0
  235. package/server-managed/commit-coauthors.ts +106 -0
  236. package/server-managed/commit-lockfiles.ts +76 -0
  237. package/server-managed/comparison.ts +52 -0
  238. package/server-managed/config.ts +164 -0
  239. package/server-managed/content-access.ts +15 -0
  240. package/server-managed/crypto.ts +25 -0
  241. package/server-managed/db-methods.ts +1923 -0
  242. package/server-managed/db-neon.ts +468 -0
  243. package/server-managed/db-schema.ts +238 -0
  244. package/server-managed/db-table-names.ts +22 -0
  245. package/server-managed/db.ts +143 -0
  246. package/server-managed/dependabot-access.ts +60 -0
  247. package/server-managed/dependabot-alerts.ts +185 -0
  248. package/server-managed/dependency-review-comments.ts +98 -0
  249. package/server-managed/dependency-reviews.ts +364 -0
  250. package/server-managed/github-accounts.ts +60 -0
  251. package/server-managed/github-actions.ts +71 -0
  252. package/server-managed/github-app.ts +376 -0
  253. package/server-managed/github-errors.ts +70 -0
  254. package/server-managed/github-graphql.ts +39 -0
  255. package/server-managed/github-issue-links.ts +140 -0
  256. package/server-managed/github-issue-oauth.ts +47 -0
  257. package/server-managed/github-issues.ts +152 -0
  258. package/server-managed/github-link-oauth.ts +69 -0
  259. package/server-managed/github-links.ts +204 -0
  260. package/server-managed/github-metadata.ts +213 -0
  261. package/server-managed/github-oauth.ts +238 -0
  262. package/server-managed/github-pulls.ts +227 -0
  263. package/server-managed/github-requests.ts +136 -0
  264. package/server-managed/github-users.ts +30 -0
  265. package/server-managed/http-response.ts +76 -0
  266. package/server-managed/http.ts +5503 -0
  267. package/server-managed/import-triage.ts +51 -0
  268. package/server-managed/index.ts +192 -0
  269. package/server-managed/kept-briefly.ts +37 -0
  270. package/server-managed/kept.ts +28 -0
  271. package/server-managed/link-reports.ts +162 -0
  272. package/server-managed/listed-activity.ts +39 -0
  273. package/server-managed/locked-packages.ts +152 -0
  274. package/server-managed/lockfile-scope.ts +49 -0
  275. package/server-managed/lru.ts +30 -0
  276. package/server-managed/managed-issues.ts +78 -0
  277. package/server-managed/management-catalog.ts +45 -0
  278. package/server-managed/management.ts +99 -0
  279. package/server-managed/npm-delta-worker.js +23 -0
  280. package/server-managed/npm-delta.ts +194 -0
  281. package/server-managed/npm-insights.ts +449 -0
  282. package/server-managed/npm-kept.ts +225 -0
  283. package/server-managed/npm-loads.ts +264 -0
  284. package/server-managed/npm-manifest.ts +115 -0
  285. package/server-managed/npm-packages.ts +454 -0
  286. package/server-managed/npm-patch.ts +325 -0
  287. package/server-managed/npm-provenance.ts +123 -0
  288. package/server-managed/npm-risk.ts +285 -0
  289. package/server-managed/object-storage-disk.ts +148 -0
  290. package/server-managed/object-storage-vercel.ts +67 -0
  291. package/server-managed/object-storage.ts +75 -0
  292. package/server-managed/package-entry-points.ts +51 -0
  293. package/server-managed/pretty-print.ts +434 -0
  294. package/server-managed/public-workspace.ts +238 -0
  295. package/server-managed/pull-comment-reviews.ts +161 -0
  296. package/server-managed/pull-dependencies.ts +351 -0
  297. package/server-managed/pull-dependency-reviews.ts +334 -0
  298. package/server-managed/pull-reviews.ts +205 -0
  299. package/server-managed/queue.ts +39 -0
  300. package/server-managed/release-commits.ts +84 -0
  301. package/server-managed/rename-diff-worker.js +26 -0
  302. package/server-managed/rename-diff.ts +279 -0
  303. package/server-managed/repo-path.ts +21 -0
  304. package/server-managed/report-migration.ts +35 -0
  305. package/server-managed/report-query.ts +10 -0
  306. package/server-managed/report-response.ts +16 -0
  307. package/server-managed/report-sources.ts +178 -0
  308. package/server-managed/report-store.ts +85 -0
  309. package/server-managed/repository-actions.ts +218 -0
  310. package/server-managed/repository-agents.ts +448 -0
  311. package/server-managed/repository-aliases.ts +91 -0
  312. package/server-managed/repository-browser.ts +294 -0
  313. package/server-managed/repository-changes.ts +284 -0
  314. package/server-managed/repository-code.ts +215 -0
  315. package/server-managed/repository-dependency-reviews.ts +166 -0
  316. package/server-managed/repository-discovery.ts +82 -0
  317. package/server-managed/repository-history.ts +432 -0
  318. package/server-managed/repository-issues.ts +195 -0
  319. package/server-managed/repository-policy.ts +27 -0
  320. package/server-managed/repository-pulls.ts +212 -0
  321. package/server-managed/repository-pulse.ts +460 -0
  322. package/server-managed/repository-risk-assess.ts +105 -0
  323. package/server-managed/repository-risk-audit.ts +79 -0
  324. package/server-managed/repository-risk-read.ts +170 -0
  325. package/server-managed/repository-risk.ts +491 -0
  326. package/server-managed/repository-squashes.ts +168 -0
  327. package/server-managed/request-timing.ts +63 -0
  328. package/server-managed/reviewed-dependencies.ts +149 -0
  329. package/server-managed/revisions.ts +35 -0
  330. package/server-managed/scan-downloads.ts +87 -0
  331. package/server-managed/session.ts +132 -0
  332. package/server-managed/slugs.ts +38 -0
  333. package/server-managed/solidity-entry-points.ts +71 -0
  334. package/server-managed/sql-postgres.ts +34 -0
  335. package/server-managed/sql.ts +88 -0
  336. package/server-managed/static.ts +28 -0
  337. package/server-managed/storage-command.ts +16 -0
  338. package/server-managed/storage-db.ts +183 -0
  339. package/server-managed/storage-encryption.ts +290 -0
  340. package/server-managed/storage-maintenance.ts +259 -0
  341. package/server-managed/storage-payload.ts +121 -0
  342. package/server-managed/storage-stores.ts +91 -0
  343. package/server-managed/storage.ts +82 -0
  344. package/server-managed/team-app.ts +134 -0
  345. package/server-managed/team-catalog.ts +7 -0
  346. package/server-managed/team-feed.ts +180 -0
  347. package/server-managed/team-npm-scopes.ts +106 -0
  348. package/server-managed/team-reports.ts +319 -0
  349. package/server-managed/triage-response.ts +16 -0
  350. package/server-managed/type-only-worker.js +19 -0
  351. package/server-managed/type-only.ts +105 -0
  352. package/server-managed/uploads.ts +106 -0
  353. package/server-managed/upstream-cache.ts +243 -0
  354. package/server-managed/view-as.ts +58 -0
  355. package/server-managed/workspace-shares.ts +169 -0
  356. package/server.ts +70 -0
  357. package/server/index.ts +0 -480
  358. package/server/lifecycle.ts +0 -208
  359. /package/{LICENSE → LICENSE-MIT} +0 -0
  360. /package/{server → server-e2e}/auth.ts +0 -0
  361. /package/{server → server-e2e}/config.example.json +0 -0
  362. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  363. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  364. /package/{server → server-e2e}/objstore/sign.ts +0 -0
  365. /package/{server → server-e2e}/peer.ts +0 -0
  366. /package/{server → server-e2e}/sse-session.ts +0 -0
  367. /package/{server → server-e2e}/util.ts +0 -0
  368. /package/{server → server-e2e}/validation.ts +0 -0
@@ -0,0 +1,115 @@
1
+ // The "links" file — a small JSON document that says which findings
2
+ // are the SAME finding, reported twice:
3
+ //
4
+ // [["a", "b"], ["c", "d", "e"]]
5
+ // Entries may also be objects such as {"id":"a"}, for existing exports.
6
+ //
7
+ // One inner array per link. Every finding named in it is a duplicate
8
+ // of every other one in that array, and the file says nothing else:
9
+ // no severity, no title, no file, no prose. That is the whole format.
10
+ //
11
+ // It is NOT a report, and this module deliberately sits outside
12
+ // `@preventive/report` because of it. A report carries findings; a links file
13
+ // carries only ids of findings that must already live in reports the
14
+ // reader holds. So it never reaches `ingestReport` / `state.reports`,
15
+ // it has its own view (`ui/view/render-links.js`) that points at the
16
+ // findings. It also supplies the "Duplicates:" row on finding cards
17
+ // and groups explicitly linked rows in workspace App view.
18
+ //
19
+ // Pure — no storage, no DOM, no app state. `linked-findings-index.js`
20
+ // beside this file is the OPFS-wide store built on top; everything
21
+ // here is text in, data out, so the recognition rules are testable on
22
+ // their own.
23
+
24
+ import { isLinkableFindingId } from './finding-link.js'
25
+
26
+ // The `source` marker a links file is filed under — the same slot a
27
+ // report's producer ('deepsec', 'piolium', …) occupies in the counts
28
+ // cache, so `getKind(name)` answers "what kind of file is this" for
29
+ // links and reports alike and the sidebar can bucket both from one
30
+ // lookup. No report format can collide with it: this value comes from
31
+ // here, never from a document's own content.
32
+ export const LINKS_KIND = 'links'
33
+
34
+ // Read a string ID or an existing object entry — `{"id": "…"}`. Extra fields are allowed and ignored: an exporter that
35
+ // writes the title or the report alongside the id is writing a
36
+ // superset of this format, not a different one.
37
+ function entryId(entry) {
38
+ if (typeof entry === 'string') return entry.length > 0 ? entry : null
39
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null
40
+ const { id } = entry
41
+ return typeof id === 'string' && id.length > 0 ? id : null
42
+ }
43
+
44
+ // Recognise `content` as a links file and normalise it.
45
+ //
46
+ // Returns `{ groups, skipped }` — `groups` is one `string[]` of finding
47
+ // ids per link, and `skipped` counts ids the app could never follow (a
48
+ // session-local numeric id, a control character, something absurdly
49
+ // long — `isLinkableFindingId` draws the same line the per-finding Link
50
+ // button draws). Returns null when the text isn't this format at all.
51
+ //
52
+ // Recognition is on SHAPE, and it is strict, because this parser runs
53
+ // against every dropped file before the report readers get their turn:
54
+ // a top-level array, holding only arrays, holding string IDs or objects with a
55
+ // string `id`. Nothing else this app reads is a bare JSON array, so
56
+ // nothing else can be mistaken for one — and an empty top-level array
57
+ // is refused rather than claimed, since `[]` is every empty JSON list
58
+ // in the world, not distinctively a links file.
59
+ //
60
+ // Normalisation drops what carries no information: ids repeated inside
61
+ // one link, ids that can't be linked, and then any link left naming
62
+ // fewer than two findings — a "link" to a single finding links nothing.
63
+ // A file whose every link normalises away is still a links file; it
64
+ // just has no links, which its view says plainly.
65
+ export function parseLinkedFindings(content) {
66
+ // Only text opening an array can parse to one, so a report — which
67
+ // opens an object — is turned away without being parsed twice.
68
+ if (typeof content === 'string' && !/^\s*\[/u.test(content)) return null
69
+ let data
70
+ try { data = JSON.parse(content) } catch { return null }
71
+ if (!Array.isArray(data) || data.length === 0) return null
72
+ const groups = []
73
+ let skipped = 0
74
+ for (const raw of data) {
75
+ if (!Array.isArray(raw)) return null
76
+ const ids = new Set()
77
+ for (const entry of raw) {
78
+ const id = entryId(entry)
79
+ if (id === null) return null
80
+ if (isLinkableFindingId(id)) ids.add(id)
81
+ else skipped++
82
+ }
83
+ if (ids.size > 1) groups.push([...ids])
84
+ }
85
+ return { groups, skipped }
86
+ }
87
+
88
+ // How many findings the links in `groups` name, counted once each — a
89
+ // finding named by two links is one linked finding. What the sidebar
90
+ // badge and the view's header report alongside the link count.
91
+ export function countLinkedIds(groups) {
92
+ const ids = new Set()
93
+ for (const group of groups) for (const id of group) ids.add(id)
94
+ return ids.size
95
+ }
96
+
97
+ // Fold links into `index` (a `Map<id, Set<id>>`): every id in a link
98
+ // gains every OTHER id in it as a duplicate. Called once per links
99
+ // file, so an id linked by two files ends up with the union of what
100
+ // both said about it.
101
+ //
102
+ // Union, NOT transitive closure. If one file links a↔b and another
103
+ // links b↔c, then b has two duplicates and a has one — a and c were
104
+ // never said to be the same finding, and inferring it would put a
105
+ // claim in the reader's card that no file they dropped ever made.
106
+ export function collectDuplicates(groups, index) {
107
+ for (const group of groups) {
108
+ for (const id of group) {
109
+ let set = index.get(id)
110
+ if (!set) index.set(id, set = new Set())
111
+ for (const other of group) if (other !== id) set.add(other)
112
+ }
113
+ }
114
+ return index
115
+ }
@@ -0,0 +1 @@
1
+ export function bundleCommitHash(value: unknown): string | null
@@ -0,0 +1,4 @@
1
+ // Match the full Git object IDs used by bundle origin links.
2
+ export function bundleCommitHash(value) {
3
+ return typeof value === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value) ? value : null
4
+ }
@@ -0,0 +1,3 @@
1
+ export function computeBundleDiff(base: Map<string, unknown>, other: Map<string, unknown>, pkgOf: (path: string) => string, layout?: import('@preventive/compare').Layout): {
2
+ files: { onlyBase: { path: string; basePath?: string }[]; onlyOther: { path: string }[]; changed: { path: string; basePath?: string; renamedFrom?: string }[] }
3
+ }
@@ -0,0 +1,357 @@
1
+ // Shared bundle comparison — no Lit, no DOM, no `state`. Given two
2
+ // `Map<path, content>` file maps (the shape `bundleFilesAsMap` returns
3
+ // for either a sourcemap or a stasis bundle: every file the terminal
4
+ // mounts, source or resource, with no directory captures) plus a
5
+ // `pkgOf(path)` bucketing function, it computes a structural diff:
6
+ // which files exist only in one side, which exist in both but changed,
7
+ // per-package size deltas, and the roll-up totals.
8
+ //
9
+ // Kept free of Lit, the DOM and the OPFS parse pipeline on purpose, so
10
+ // `<bundle-compare>` (the Compare slide in the bundles view) and its
11
+ // unit test can both consume the same logic — the test exercises this
12
+ // module directly. Its imports are the file sizer the Overview uses and
13
+ // @preventive/compare's renames and layout, all pure. The component is a
14
+ // thin rendering shell around `computeBundleDiff`.
15
+ //
16
+ // `base` is the currently-open bundle (the tab you're viewing);
17
+ // `other` is the bundle picked to compare against. The result names
18
+ // the two sides `onlyBase` / `onlyOther` rather than added / removed
19
+ // so the UI can label them with each bundle's actual name — "added"
20
+ // is ambiguous without knowing which side is newer.
21
+
22
+ import { compareTrees, treeLayout } from '@preventive/compare'
23
+ import { bundleFileByteLength } from './bundle-sources.js'
24
+ import { resolutionKey } from './bundle-compare-inputs.js'
25
+
26
+ // Resolutions compare independently from file bytes: identical files can be
27
+ // wired together differently. Inputs are keyed by importer, specifier,
28
+ // conditions (including import attributes), and platform. Only existing
29
+ // resolutions whose targets changed are reported; additions/removals are omitted.
30
+ // `renames` (base path → other path, see renamesOf) names base files as
31
+ // the other bundle does: a renamed importer's resolutions pair with its new
32
+ // path's, reported under it, and a target renamed is the same target.
33
+ // `basePaths` and `root` (computeBundleDiff's `files`) name the rest where
34
+ // the bundles' roots differ, a base target shown as the other bundle would
35
+ // lay it out; a path no file has (an importer the bundle didn't capture, a
36
+ // builtin) may read either as it is or through the root.
37
+ export function computeResolutionDiff(base, other, renames = new Map(), basePaths = new Map(), root = null) {
38
+ const names = path => {
39
+ const known = renames.get(path) ?? basePaths.get(path)
40
+ return known === undefined ? [...new Set([path, through(root, path) ?? path])] : [known]
41
+ }
42
+ const changed = []
43
+ for (const before of base.values()) {
44
+ const after = names(before.parent).map(parent => other.get(resolutionKey(parent, before.specifier, before.conditions, before.platform))).find(Boolean)
45
+ if (after && !names(before.target).includes(after.target)) {
46
+ const { target: otherTarget, ...identity } = after
47
+ changed.push({ ...identity, baseTarget: basePaths.get(before.target) ?? before.target, otherTarget })
48
+ }
49
+ }
50
+ changed.sort((a, b) => a.key.localeCompare(b.key))
51
+ return { changed, totalChanges: changed.length }
52
+ }
53
+
54
+ // A file's size, by the measure the Overview and Treemap use: text by the
55
+ // UTF-8 it encodes to, a base64 resource by the bytes it decodes to. A
56
+ // file with no size to give (a base64 spelling that does not decode)
57
+ // counts as zero bytes, but is still compared.
58
+ function byteLen(content) {
59
+ return bundleFileByteLength(content) ?? 0
60
+ }
61
+
62
+ // Whether two sides hold the same file. Text compares as text; a base64
63
+ // resource arrives as a fresh `{ format: 'base64', data }` on each side,
64
+ // so it compares by its spelling — identity would call every image
65
+ // changed. A file that is text on one side and base64 on the other has
66
+ // changed. An npm package's binary file compares by its digest.
67
+ function sameContent(a, b) {
68
+ return a === b || (a?.format === 'base64' && b?.format === 'base64' && a.data === b.data)
69
+ || (a?.format === 'digest' && b?.format === 'digest' && a.digest === b.digest)
70
+ }
71
+
72
+ // What a stasis bundle records of a file beside its bytes: the format it
73
+ // loads it in (a resource is carried as data, the rest run as code), whether
74
+ // it carries an execute bit, and whether the bundle starts from it. A
75
+ // function of the file's path; null for another kind of bundle, which
76
+ // records none of it.
77
+ export function bundleFileMeta(details) {
78
+ if (details?.kind !== 'stasis' || !details.bundle) return null
79
+ const { formats, executable, entries } = details.bundle
80
+ return path => ({ format: formats?.get(path), executable: executable?.has(path) ?? false, entry: entries?.has(path) ?? false })
81
+ }
82
+
83
+ // How a file's record changed from one stasis bundle to the other, as
84
+ // `{ field, from, to }` (bundleFileMeta's fields): none where either side
85
+ // records nothing. A format left unsaid is one too: stasis runs such a
86
+ // file as code, so a resource whose format is dropped runs.
87
+ function metaChanges(before, after) {
88
+ if (!before || !after) return []
89
+ return ['format', 'executable', 'entry'].filter(field => before[field] !== after[field]).map(field => ({ field, from: before[field], to: after[field] }))
90
+ }
91
+
92
+ // Comparator: largest absolute delta first, then path/label ascending
93
+ // so equal-magnitude rows stay stably ordered. Used for the changed
94
+ // file + package lists, where the biggest mover is the most
95
+ // interesting row.
96
+ function byAbsDeltaThenKey(key) {
97
+ return (a, b) => {
98
+ const d = Math.abs(b.delta) - Math.abs(a.delta)
99
+ // `d || …` falls through to the tiebreak only when the magnitudes
100
+ // match (d === 0); a non-zero d is returned as-is.
101
+ return d || a[key].localeCompare(b[key])
102
+ }
103
+ }
104
+
105
+ // Largest byte size first, then key ascending — for the only-in-one-
106
+ // side lists, where there's no delta to rank by.
107
+ function byBytesThenKey(key) {
108
+ return (a, b) => {
109
+ const d = b.bytes - a.bytes
110
+ return d || a[key].localeCompare(b[key])
111
+ }
112
+ }
113
+
114
+ // A file's content as compareTrees reads it to find renames, each kind tagged
115
+ // apart: text as it is (a lone surrogate, which no UTF-8 holds, as U+FFFD); a
116
+ // resource by its spelling or its digest; and one with none, by its place in
117
+ // its list alone, so that only its path pairs it.
118
+ function treeData(content, at) {
119
+ if (typeof content === 'string') return `\0text\0${content.toWellFormed()}`
120
+ if (content?.format === 'base64') return `\0base64\0${content.data}`
121
+ return content?.format === 'digest' ? `\0digest\0${content.digest}` : `\0none\0${at}`
122
+ }
123
+
124
+ // git's diff.renameLimit for a package's renames: past this many removed files
125
+ // by added ones, squared, no pair is scored, as that would hold the page for
126
+ // seconds, and only identical content and paths pair them.
127
+ const RENAME_LIMIT = 250
128
+
129
+ // The files of `onlyBase` renamed to one of `onlyOther`, both by path as the
130
+ // other bundle lays them out, within a package: `Map<base path, other path>`,
131
+ // as @preventive/compare's compareTrees finds them, by content as git diff -M
132
+ // does and where their paths say so. Whether a renamed file changed is
133
+ // sameContent's to say, not this reading's. A path no UTF-8 holds is left out.
134
+ function renamesOf(onlyBase, onlyOther, base, other, pkgOf) {
135
+ const entries = (rows, files, side) => rows.filter(({ path }) => path !== '' && path.isWellFormed())
136
+ .map(({ path }, at) => ({ name: path, data: treeData(files.get(path), `${side}${at}`) }))
137
+ const options = { paths: 'keys', renameGroup: pkgOf, renameLimit: RENAME_LIMIT, lines: false }
138
+ const changes = compareTrees(entries(onlyBase, base, 'a'), entries(onlyOther, other, 'b'), options)
139
+ return new Map(changes.filter(({ status }) => status === 'renamed').map(change => [change.old.path, change.new.path]))
140
+ }
141
+
142
+ // A base path through `root` as treeLayout reads it, or null for one outside
143
+ // a base root.
144
+ function through(root, path) {
145
+ if (!root) return path
146
+ if (root.base === '') return `${root.other}${path}`
147
+ return path.startsWith(root.base) ? path.slice(root.base.length) : null
148
+ }
149
+
150
+ // Per-package accumulator factory. Tracks each side's byte total plus
151
+ // per-bucket file counts so a package is flagged `changed` whenever
152
+ // ANY of its files moved — not only when the byte total happens to
153
+ // differ (two versions can shuffle content while landing on the same
154
+ // size).
155
+ function emptyPkgAcc() {
156
+ return {
157
+ baseBytes: 0,
158
+ otherBytes: 0,
159
+ onlyBaseFiles: 0,
160
+ onlyOtherFiles: 0,
161
+ changedFiles: 0,
162
+ }
163
+ }
164
+
165
+ // Compare two bundle source maps. Returns:
166
+ // {
167
+ // totals: { baseFiles, baseBytes, otherFiles, otherBytes,
168
+ // onlyBaseFiles, onlyBaseBytes, onlyOtherFiles,
169
+ // onlyOtherBytes, changedFiles, changedDelta,
170
+ // unchangedFiles, fileDelta, byteDelta, identical },
171
+ // files: {
172
+ // onlyBase: [{ path, bytes }],
173
+ // onlyOther: [{ path, bytes }],
174
+ // changed: [{ path, baseBytes, otherBytes, delta, basePath?, modified?, meta? }],
175
+ // },
176
+ // packages: {
177
+ // onlyBase: [{ pkg, bytes }],
178
+ // onlyOther: [{ pkg, bytes }],
179
+ // changed: [{ pkg, baseBytes, otherBytes, delta }],
180
+ // },
181
+ // }
182
+ //
183
+ // `delta` is always `other − base` (positive = the compared bundle is
184
+ // larger). A file renamed (see renamesOf) is changed, not
185
+ // removed and added: its row's `path` is the other side's, `renamedFrom`
186
+ // the base's, `modified` whether its contents changed too, and it counts in
187
+ // `renamedFiles` as well, its contents moved or not.
188
+ // Where one bundle's files sit under a directory of the other's (`root`, see
189
+ // treeLayout), the base's are compared as the other lays them out: a row's
190
+ // `path` (and `renamedFrom`) reads that way, and `basePath` names the file
191
+ // in the base bundle wherever that differs from `path`. `files.basePaths`
192
+ // maps every base file to how it reads. `layout` (treeLayout) may come
193
+ // from the caller, to key `pkgOf` by the same paths: it is asked of files
194
+ // as the other bundle lays them out.
195
+ // `meta`, each side's bundleFileMeta where it has one, compares what two
196
+ // stasis bundles record of a file beside its bytes: a row whose record
197
+ // changed carries how (`meta`, metaChanges), and a file whose record alone
198
+ // changed is changed too, `modified: false`, its bytes the same.
199
+ // `identical` is true when the two bundles carry the same files with
200
+ // byte-identical content, resources included, each recorded alike.
201
+ export function computeBundleDiff(base, other, pkgOf, layout = treeLayout(base.keys(), other.keys()), meta = []) {
202
+ const { root, paths: basePaths } = layout
203
+ // The base's files as the other lays them out, and back.
204
+ const baseFile = new Map([...basePaths].map(([from, to]) => [to, from]))
205
+ base = new Map([...base].map(([path, content]) => [basePaths.get(path), content]))
206
+ const atBase = path => baseFile.get(path) === path ? {} : { basePath: baseFile.get(path) }
207
+ // How a file's record changed, by its base path and its path, as a row's fields.
208
+ const recorded = (from, to) => {
209
+ const changes = metaChanges(meta[0]?.(from), meta[1]?.(to))
210
+ return changes.length > 0 ? { meta: changes } : {}
211
+ }
212
+ const onlyBase = []
213
+ const onlyOther = []
214
+ const changed = []
215
+ let baseBytes = 0
216
+ let otherBytes = 0
217
+ let onlyBaseBytes = 0
218
+ let onlyOtherBytes = 0
219
+ let changedDelta = 0
220
+ let unchangedFiles = 0
221
+ const pkgs = new Map()
222
+ const pkgAcc = (path) => {
223
+ const key = pkgOf(path)
224
+ let acc = pkgs.get(key)
225
+ if (!acc) { acc = emptyPkgAcc(); pkgs.set(key, acc) }
226
+ return acc
227
+ }
228
+
229
+ const allPaths = new Set([...base.keys(), ...other.keys()])
230
+ for (const path of allPaths) {
231
+ const inBase = base.has(path)
232
+ const inOther = other.has(path)
233
+ const acc = pkgAcc(path)
234
+ if (inBase && inOther) {
235
+ const bC = base.get(path)
236
+ const oC = other.get(path)
237
+ const same = sameContent(bC, oC)
238
+ const record = recorded(baseFile.get(path), path)
239
+ const bB = byteLen(bC)
240
+ // Identical content shares the byte count, so only measure the
241
+ // other side when the files actually differ.
242
+ const oB = same ? bB : byteLen(oC)
243
+ baseBytes += bB
244
+ otherBytes += oB
245
+ acc.baseBytes += bB
246
+ acc.otherBytes += oB
247
+ if (same && !record.meta) {
248
+ unchangedFiles++
249
+ } else {
250
+ const delta = oB - bB
251
+ changed.push({ path, ...atBase(path), baseBytes: bB, otherBytes: oB, delta, ...record, ...same ? { modified: false } : {} })
252
+ changedDelta += delta
253
+ acc.changedFiles++
254
+ }
255
+ } else if (inBase) {
256
+ const bB = byteLen(base.get(path))
257
+ baseBytes += bB
258
+ onlyBaseBytes += bB
259
+ acc.baseBytes += bB
260
+ acc.onlyBaseFiles++
261
+ onlyBase.push({ path, ...atBase(path), bytes: bB })
262
+ } else {
263
+ const oB = byteLen(other.get(path))
264
+ otherBytes += oB
265
+ onlyOtherBytes += oB
266
+ acc.otherBytes += oB
267
+ acc.onlyOtherFiles++
268
+ onlyOther.push({ path, bytes: oB })
269
+ }
270
+ }
271
+
272
+ const renames = renamesOf(onlyBase, onlyOther, base, other, pkgOf)
273
+ if (renames.size > 0) {
274
+ const renamedTo = new Set(renames.values())
275
+ for (const [from, to] of renames) {
276
+ const bC = base.get(from), oC = other.get(to)
277
+ const same = sameContent(bC, oC)
278
+ const bB = byteLen(bC), oB = same ? bB : byteLen(oC)
279
+ onlyBaseBytes -= bB
280
+ onlyOtherBytes -= oB
281
+ changed.push({ path: to, renamedFrom: from, basePath: baseFile.get(from), baseBytes: bB, otherBytes: oB, delta: oB - bB, modified: !same, ...recorded(baseFile.get(from), to) })
282
+ changedDelta += oB - bB
283
+ // Both in one package (its key is in the rename's).
284
+ const acc = pkgAcc(from)
285
+ acc.onlyBaseFiles--
286
+ acc.onlyOtherFiles--
287
+ acc.changedFiles++
288
+ }
289
+ onlyBase.splice(0, onlyBase.length, ...onlyBase.filter(row => !renames.has(row.path)))
290
+ onlyOther.splice(0, onlyOther.length, ...onlyOther.filter(row => !renamedTo.has(row.path)))
291
+ }
292
+
293
+ // Classify each package from its accumulator: present on exactly one
294
+ // side → onlyBase / onlyOther; present on both with any moved file →
295
+ // changed; otherwise unchanged (dropped — the lists surface only
296
+ // what differs).
297
+ const pkgOnlyBase = []
298
+ const pkgOnlyOther = []
299
+ const pkgChanged = []
300
+ const pkgUnchanged = []
301
+ for (const [pkg, acc] of pkgs) {
302
+ // Present on a side if any of its files contributed there — its
303
+ // byte total, an only-this-side file, or a changed file (which
304
+ // exists on both). A changed file alone is enough to count as
305
+ // present even when that side's bytes net to zero.
306
+ const onBase = acc.baseBytes > 0 || acc.onlyBaseFiles > 0 || acc.changedFiles > 0
307
+ const onOther = acc.otherBytes > 0 || acc.onlyOtherFiles > 0 || acc.changedFiles > 0
308
+ const movedFiles = acc.onlyBaseFiles + acc.onlyOtherFiles + acc.changedFiles
309
+ if (onBase && !onOther) {
310
+ pkgOnlyBase.push({ pkg, bytes: acc.baseBytes })
311
+ } else if (onOther && !onBase) {
312
+ pkgOnlyOther.push({ pkg, bytes: acc.otherBytes })
313
+ } else if (movedFiles > 0 || acc.baseBytes !== acc.otherBytes) {
314
+ pkgChanged.push({
315
+ pkg,
316
+ baseBytes: acc.baseBytes,
317
+ otherBytes: acc.otherBytes,
318
+ delta: acc.otherBytes - acc.baseBytes,
319
+ })
320
+ } else {
321
+ pkgUnchanged.push({ pkg, bytes: acc.baseBytes })
322
+ }
323
+ }
324
+
325
+ onlyBase.sort(byBytesThenKey('path'))
326
+ onlyOther.sort(byBytesThenKey('path'))
327
+ changed.sort(byAbsDeltaThenKey('path'))
328
+ pkgOnlyBase.sort(byBytesThenKey('pkg'))
329
+ pkgOnlyOther.sort(byBytesThenKey('pkg'))
330
+ pkgChanged.sort(byAbsDeltaThenKey('pkg'))
331
+
332
+ return {
333
+ totals: {
334
+ baseFiles: base.size,
335
+ baseBytes,
336
+ otherFiles: other.size,
337
+ otherBytes,
338
+ onlyBaseFiles: onlyBase.length,
339
+ onlyBaseBytes,
340
+ onlyOtherFiles: onlyOther.length,
341
+ onlyOtherBytes,
342
+ changedFiles: changed.length,
343
+ renamedFiles: renames.size,
344
+ changedDelta,
345
+ unchangedFiles,
346
+ fileDelta: other.size - base.size,
347
+ byteDelta: otherBytes - baseBytes,
348
+ identical: onlyBase.length === 0 && onlyOther.length === 0 && changed.length === 0,
349
+ },
350
+ files: { onlyBase, onlyOther, changed, root, basePaths },
351
+ // `unchanged` lends a version-only change its (equal) sizes.
352
+ packages: { onlyBase: pkgOnlyBase, onlyOther: pkgOnlyOther, changed: pkgChanged, unchanged: pkgUnchanged },
353
+ }
354
+ }
355
+
356
+ // The versions' part (bundle-compare-versions.js), as one comparison reads it.
357
+ export { comparePackages, compareSemver, computeVersionUpdates, versionDirection } from './bundle-compare-versions.js'
@@ -0,0 +1,2 @@
1
+ import type { BundleDetails } from './bundle-metadata.js'
2
+ export function bundleCompareFiles(details: BundleDetails, scope?: string | null): Map<string, unknown>
@@ -0,0 +1,65 @@
1
+ import { bundleFilesAsMap } from './bundle-sources.js'
2
+
3
+ // Use the union of named trees. A tree absent from one stasis bundle contains
4
+ // no files there; falling back to all files would invent additions/removals.
5
+ // A bundle that names no trees (a sourcemap) is compared whole under any of
6
+ // them. Unlike a single-bundle graph, retain a scope that includes every file
7
+ // on one side.
8
+ export function bundleCompareScopes(...details) {
9
+ const names = new Set()
10
+ for (const item of details) {
11
+ const files = bundleFilesAsMap(item)
12
+ for (const [name, paths] of Object.entries(item?.bundle?.reason ?? {})) {
13
+ if (Array.isArray(paths) && paths.some(path => files.has(path))) names.add(name)
14
+ }
15
+ }
16
+ // A scope is only useful as a filter if it changes the files compared on
17
+ // some side. Keep every scope when at least one does, otherwise hide the
18
+ // selector, as single-bundle views do (see common/bundle-reasons.js).
19
+ const narrows = name => details.some(item => {
20
+ if (item?.kind !== 'stasis') return false
21
+ const files = bundleFilesAsMap(item)
22
+ const paths = item?.bundle?.reason?.[name]
23
+ return new Set(Array.isArray(paths) ? paths.filter(path => files.has(path)) : []).size < files.size
24
+ })
25
+ if (![...names].some(narrows)) return []
26
+ return [...names].toSorted().map(name => ({ id: `reason:${name}`, label: name }))
27
+ }
28
+
29
+ export function bundleCompareFiles(details, scope = '') {
30
+ const files = bundleFilesAsMap(details)
31
+ if (!scope || details?.kind !== 'stasis') return files
32
+ const paths = details?.bundle?.reason?.[scope.replace(/^reason:/u, '')]
33
+ const selected = new Set(Array.isArray(paths) ? paths : [])
34
+ return new Map([...files].filter(([path]) => selected.has(path)))
35
+ }
36
+
37
+ // A resolution's identity: its importer, specifier, conditions (import
38
+ // attributes included), and Metro platform.
39
+ export function resolutionKey(parent, specifier, conditions, platform) {
40
+ return JSON.stringify([parent, specifier, conditions, platform])
41
+ }
42
+
43
+ // Keep the full resolution identity: collapsing to graph edges would lose
44
+ // specifier, condition/import-attribute, and Metro platform changes.
45
+ export function bundleCompareResolutions(details, scope = '') {
46
+ const result = new Map()
47
+ if (details?.kind !== 'stasis' || !details.bundle) return result
48
+ const files = scope ? bundleFilesAsMap(details) : null
49
+ const selected = scope ? bundleCompareFiles(details, scope) : null
50
+ for (const [conditions, byParent] of details.bundle.imports) {
51
+ for (const [parent, specifiers] of byParent) {
52
+ for (const [specifier, resolved] of specifiers) {
53
+ const targets = typeof resolved === 'string' ? [[null, resolved]] : resolved
54
+ for (const [platform, target] of targets) {
55
+ // Include imports from scoped files, plus imports into the scope
56
+ // from uncaptured parents (e.g. an app in a dependencies-only bundle).
57
+ if (selected && !selected.has(parent) && (files.has(parent) || !selected.has(target))) continue
58
+ const key = resolutionKey(parent, specifier, conditions, platform)
59
+ result.set(key, { key, parent, specifier, conditions, platform, target })
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return result
65
+ }