@preventive/triage 1.0.0-alpha.4 → 1.0.0-alpha.41

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 (312) 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 +2 -0
  6. package/client/dependency-paths.js +24 -0
  7. package/client/finding-link.js +305 -0
  8. package/client/linked-findings.d.ts +1 -0
  9. package/client/linked-findings.js +112 -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 +522 -0
  14. package/common/bundle-compare-inputs.d.ts +2 -0
  15. package/common/bundle-compare-inputs.js +65 -0
  16. package/common/bundle-metadata.d.ts +14 -0
  17. package/common/bundle-metadata.js +226 -0
  18. package/common/bundle-pkg-of.d.ts +1 -0
  19. package/common/bundle-pkg-of.js +90 -0
  20. package/common/bundle-reasons.d.ts +2 -0
  21. package/common/bundle-reasons.js +21 -0
  22. package/common/bundle-source-links.d.ts +6 -0
  23. package/common/bundle-source-links.js +216 -0
  24. package/common/bundle-sourcemap.d.ts +10 -0
  25. package/common/bundle-sourcemap.js +100 -0
  26. package/common/bundle-sources.d.ts +7 -0
  27. package/common/bundle-sources.js +310 -0
  28. package/common/bundle-stats.js +41 -0
  29. package/common/bundle-tabs.js +1 -0
  30. package/common/code-language.js +65 -0
  31. package/common/default-scan-models.ts +31 -0
  32. package/common/dependency-reviews.d.ts +23 -0
  33. package/common/dependency-reviews.js +81 -0
  34. package/common/finding-duplicates.js +58 -0
  35. package/common/finding-filters.js +274 -0
  36. package/common/finding-id.js +47 -0
  37. package/common/github-issue-labels.d.ts +1 -0
  38. package/common/github-issue-labels.js +11 -0
  39. package/common/github-pr.ts +56 -0
  40. package/common/gzip.js +59 -0
  41. package/common/line-diff.d.ts +8 -0
  42. package/common/line-diff.js +56 -0
  43. package/common/linked-workspace-groups.js +63 -0
  44. package/common/managed/comments.ts +34 -0
  45. package/common/managed/npm-packages.d.ts +5 -0
  46. package/common/managed/npm-packages.js +34 -0
  47. package/common/managed/permissions.ts +31 -0
  48. package/common/managed/report-content.ts +72 -0
  49. package/common/managed/report-filter.ts +108 -0
  50. package/common/managed/repository-alias.ts +65 -0
  51. package/common/managed/roles.ts +28 -0
  52. package/common/managed/routes.d.ts +2 -0
  53. package/common/managed/routes.js +254 -0
  54. package/common/managed/scan-models.ts +6 -0
  55. package/common/managed/team-app.d.ts +2 -0
  56. package/common/managed/team-app.js +31 -0
  57. package/common/managed/triage.ts +83 -0
  58. package/common/managed/upload-seal.ts +126 -0
  59. package/common/pretty-print.d.ts +5 -0
  60. package/common/pretty-print.js +24 -0
  61. package/common/revalidation-conflicts.js +48 -0
  62. package/common/revalidation-input-groups.js +56 -0
  63. package/common/save-error-reason.ts +20 -7
  64. package/common/scan-server.ts +13 -0
  65. package/common/server-info.ts +35 -0
  66. package/common/utf8.d.ts +3 -0
  67. package/common/utf8.js +45 -0
  68. package/common/workspace-app.js +23 -0
  69. package/common/workspace-groups.js +176 -0
  70. package/out/brotli-fallback.js +3 -3
  71. package/out/chunk-2AFQKRAM.js +51 -0
  72. package/out/chunk-2KBF5TZ7.js +2 -0
  73. package/out/chunk-2XUEZHIP.js +28 -0
  74. package/out/chunk-35VE6CDH.js +2 -0
  75. package/out/chunk-3BZ7JERB.js +1 -0
  76. package/out/chunk-4F6JSF57.js +3 -0
  77. package/out/chunk-4XCDFGDP.js +10 -0
  78. package/out/chunk-C4Z6ZBXV.js +10 -0
  79. package/out/chunk-CMQA4DM6.js +1 -0
  80. package/out/chunk-CVVJAXVY.js +2 -0
  81. package/out/chunk-DIJJNTRH.js +10 -0
  82. package/out/chunk-GJ4IUCNS.js +1 -0
  83. package/out/chunk-GNXOKIGP.js +80 -0
  84. package/out/chunk-GZ4MS3TE.js +13 -0
  85. package/out/chunk-IU3CCINR.js +3 -0
  86. package/out/chunk-K3X5EJBQ.js +2 -0
  87. package/out/chunk-KEX2ATEW.js +2 -0
  88. package/out/chunk-KVEUXIEU.js +1 -0
  89. package/out/chunk-KYCNJVB7.js +1 -0
  90. package/out/chunk-NRPC74AM.js +1 -0
  91. package/out/chunk-OOJM4CTU.js +1 -0
  92. package/out/chunk-PNHBZGDD.js +1 -0
  93. package/out/chunk-QZ3A376W.js +1 -0
  94. package/out/chunk-R5O6FAW4.js +2 -0
  95. package/out/chunk-RFZQQXSG.js +17 -0
  96. package/out/chunk-RSHMVYNS.js +1 -0
  97. package/out/chunk-TUII34K2.js +1 -0
  98. package/out/chunk-TY4RZD3Z.js +226 -0
  99. package/out/chunk-TZ5KB2Y5.js +10 -0
  100. package/out/chunk-U6OPEXIA.js +1 -0
  101. package/out/chunk-UNTRGE26.js +2 -0
  102. package/out/chunk-UXNUAIIC.js +33 -0
  103. package/out/chunk-UYAXVRVL.js +1 -0
  104. package/out/chunk-UZAZM2SX.js +2 -0
  105. package/out/chunk-V3ME6FI5.js +2 -0
  106. package/out/chunk-V5O7M2YT.js +1 -0
  107. package/out/chunk-VIHGO77Y.js +1 -0
  108. package/out/chunk-VQJCXGIY.js +7 -0
  109. package/out/chunk-W4J3MGLB.js +1 -0
  110. package/out/chunk-WGG7KQUH.js +1 -0
  111. package/out/chunk-WZDTZE64.js +1 -0
  112. package/out/chunk-XL35ZV6R.js +1 -0
  113. package/out/chunk-XYFXXB2G.js +10 -0
  114. package/out/chunk-YVZH5SHP.js +1 -0
  115. package/out/chunk-ZBBOBQ43.js +1 -0
  116. package/out/chunk-ZMQQCGF6.js +2 -0
  117. package/out/client-managed-import.js +4 -0
  118. package/out/client-managed.js +37 -0
  119. package/out/client-sync.js +10 -13
  120. package/out/github-issue-authorized.html +3 -0
  121. package/out/github-issue-authorized.js +1 -0
  122. package/out/graph.js +17 -4
  123. package/out/index.html +90 -8
  124. package/out/prism.js +6 -2
  125. package/out/stasis.svg +20 -0
  126. package/out/terminal.js +365 -39
  127. package/out/view.css +1 -1
  128. package/out/view.js +83 -117
  129. package/package.json +298 -59
  130. package/risk/files/characters.js +196 -0
  131. package/risk/files/hosts.js +205 -0
  132. package/risk/files/readability.js +171 -0
  133. package/risk/files/runs-code.js +595 -0
  134. package/risk/files/script.js +114 -0
  135. package/risk/files/stamps.js +46 -0
  136. package/risk/index.d.ts +156 -0
  137. package/risk/index.js +7 -0
  138. package/risk/npm/assess.js +223 -0
  139. package/risk/npm/combined.js +65 -0
  140. package/risk/npm/compare.js +228 -0
  141. package/risk/npm/dependencies.js +153 -0
  142. package/risk/npm/diff.js +43 -0
  143. package/risk/npm/downloads.js +84 -0
  144. package/risk/npm/files.js +34 -0
  145. package/risk/npm/held.js +60 -0
  146. package/risk/npm/install-scripts.js +34 -0
  147. package/risk/npm/malware.js +36 -0
  148. package/risk/npm/manifest.js +65 -0
  149. package/risk/npm/new-dependency.js +30 -0
  150. package/risk/npm/old-version.js +108 -0
  151. package/risk/npm/registry.js +282 -0
  152. package/risk/npm/release.js +85 -0
  153. package/risk/npm/score.js +59 -0
  154. package/risk/npm/specs.js +124 -0
  155. package/risk/npm/tarball.js +113 -0
  156. package/server-common/database-config.ts +16 -0
  157. package/server-common/initialize.ts +18 -0
  158. package/server-common/npm-advisories.ts +101 -0
  159. package/{server → server-common}/origin.ts +5 -5
  160. package/server-common/reap.ts +48 -0
  161. package/server-common/scan-config.ts +19 -0
  162. package/server-common/standalone.ts +29 -0
  163. package/server-common/storage-crypto.ts +182 -0
  164. package/server-common/storage-log.ts +38 -0
  165. package/server-common/vercel-blob.ts +103 -0
  166. package/server-e2e/app.ts +486 -0
  167. package/{server → server-e2e}/auth.ts +5 -1
  168. package/{server → server-e2e}/bus-receiver.ts +2 -1
  169. package/{server → server-e2e}/cli.js +9 -4
  170. package/{server → server-e2e}/config.ts +56 -35
  171. package/{server → server-e2e}/db-neon.ts +2 -2
  172. package/{server → server-e2e}/db-revision-sql.ts +1 -1
  173. package/{server → server-e2e}/db-stmt.ts +2 -2
  174. package/{server → server-e2e}/db.ts +7 -7
  175. package/{server → server-e2e}/http.ts +116 -18
  176. package/{server → server-e2e}/hub.ts +40 -9
  177. package/server-e2e/index.ts +17 -0
  178. package/server-e2e/lifecycle.ts +95 -0
  179. package/{server → server-e2e}/neon-driver.ts +2 -2
  180. package/{server → server-e2e}/npm-proxy.ts +12 -145
  181. package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
  182. package/{server → server-e2e}/objstore/blob.ts +1 -1
  183. package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
  184. package/{server → server-e2e}/objstore/handlers.ts +9 -8
  185. package/{server → server-e2e}/objstore/init.ts +35 -18
  186. package/{server → server-e2e}/objstore/reaper.ts +1 -1
  187. package/server-e2e/objstore/rest-deny.ts +28 -0
  188. package/server-e2e/objstore/rest-mint.ts +224 -0
  189. package/{server → server-e2e}/objstore/rest.ts +27 -26
  190. package/{server → server-e2e}/objstore/sign.ts +105 -0
  191. package/{server → server-e2e}/objstore/store-neon.ts +6 -10
  192. package/{server → server-e2e}/objstore/store.ts +7 -7
  193. package/{server → server-e2e}/objstore/tokens.ts +2 -2
  194. package/{server → server-e2e}/pubsub.ts +12 -9
  195. package/{server → server-e2e}/sign.ts +4 -4
  196. package/{server → server-e2e}/sse-server.ts +102 -68
  197. package/{server → server-e2e}/sse-session.ts +28 -8
  198. package/{server → server-e2e}/static.ts +31 -21
  199. package/server-e2e/sync-handlers.ts +408 -0
  200. package/{server → server-e2e}/ws-server.ts +20 -9
  201. package/server-managed/activity.ts +245 -0
  202. package/server-managed/avatar-store.ts +12 -0
  203. package/server-managed/blob-store.ts +17 -0
  204. package/server-managed/brotli.ts +10 -0
  205. package/server-managed/bundle-advisories.ts +120 -0
  206. package/server-managed/bundle-advisory-inventory.ts +77 -0
  207. package/server-managed/bundle-build-diagnostics.d.ts +10 -0
  208. package/server-managed/bundle-build-diagnostics.js +23 -0
  209. package/server-managed/bundle-build-leases.ts +41 -0
  210. package/server-managed/bundle-build-worker.js +111 -0
  211. package/server-managed/bundle-build.ts +198 -0
  212. package/server-managed/bundle-cache.ts +199 -0
  213. package/server-managed/bundle-catalog.ts +29 -0
  214. package/server-managed/bundle-commits.ts +140 -0
  215. package/server-managed/bundle-store.ts +24 -0
  216. package/server-managed/bundle-summary-cache.ts +130 -0
  217. package/server-managed/bundle.ts +120 -0
  218. package/server-managed/cache-storage.ts +19 -0
  219. package/server-managed/cli.js +21 -0
  220. package/server-managed/combined.ts +46 -0
  221. package/server-managed/comments.ts +151 -0
  222. package/server-managed/config.ts +158 -0
  223. package/server-managed/content-access.ts +15 -0
  224. package/server-managed/crypto.ts +25 -0
  225. package/server-managed/db-methods.ts +1725 -0
  226. package/server-managed/db-neon.ts +367 -0
  227. package/server-managed/db-schema.ts +227 -0
  228. package/server-managed/db-table-names.ts +22 -0
  229. package/server-managed/db.ts +132 -0
  230. package/server-managed/dependency-reviews.ts +248 -0
  231. package/server-managed/github-app.ts +391 -0
  232. package/server-managed/github-errors.ts +68 -0
  233. package/server-managed/github-issue-links.ts +127 -0
  234. package/server-managed/github-issue-oauth.ts +47 -0
  235. package/server-managed/github-issues.ts +152 -0
  236. package/server-managed/github-metadata.ts +213 -0
  237. package/server-managed/github-oauth.ts +235 -0
  238. package/server-managed/github-pulls.ts +227 -0
  239. package/server-managed/http-response.ts +50 -0
  240. package/server-managed/http.ts +3472 -0
  241. package/server-managed/import-triage.ts +51 -0
  242. package/server-managed/index.ts +187 -0
  243. package/server-managed/kept.ts +28 -0
  244. package/server-managed/link-reports.ts +158 -0
  245. package/server-managed/locked-packages.ts +85 -0
  246. package/server-managed/lru.ts +30 -0
  247. package/server-managed/managed-issues.ts +75 -0
  248. package/server-managed/management-catalog.ts +45 -0
  249. package/server-managed/management.ts +99 -0
  250. package/server-managed/npm-insights.ts +440 -0
  251. package/server-managed/npm-kept.ts +65 -0
  252. package/server-managed/npm-loads.ts +167 -0
  253. package/server-managed/npm-manifest.ts +115 -0
  254. package/server-managed/npm-packages.ts +378 -0
  255. package/server-managed/npm-provenance.ts +123 -0
  256. package/server-managed/npm-risk.ts +177 -0
  257. package/server-managed/object-storage-disk.ts +148 -0
  258. package/server-managed/object-storage-vercel.ts +67 -0
  259. package/server-managed/object-storage.ts +74 -0
  260. package/server-managed/package-entry-points.ts +51 -0
  261. package/server-managed/pretty-print.ts +352 -0
  262. package/server-managed/public-workspace.ts +220 -0
  263. package/server-managed/rename-diff-worker.js +26 -0
  264. package/server-managed/rename-diff.ts +270 -0
  265. package/server-managed/repo-path.ts +21 -0
  266. package/server-managed/report-migration.ts +35 -0
  267. package/server-managed/report-query.ts +10 -0
  268. package/server-managed/report-response.ts +16 -0
  269. package/server-managed/report-sources.ts +178 -0
  270. package/server-managed/report-store.ts +47 -0
  271. package/server-managed/repository-aliases.ts +91 -0
  272. package/server-managed/repository-browser.ts +153 -0
  273. package/server-managed/repository-discovery.ts +82 -0
  274. package/server-managed/repository-policy.ts +27 -0
  275. package/server-managed/repository-risk.ts +420 -0
  276. package/server-managed/revisions.ts +35 -0
  277. package/server-managed/session.ts +92 -0
  278. package/server-managed/slugs.ts +38 -0
  279. package/server-managed/solidity-entry-points.ts +69 -0
  280. package/server-managed/sql-postgres.ts +31 -0
  281. package/server-managed/sql.ts +76 -0
  282. package/server-managed/static.ts +28 -0
  283. package/server-managed/storage-command.ts +16 -0
  284. package/server-managed/storage-db.ts +175 -0
  285. package/server-managed/storage-encryption.ts +279 -0
  286. package/server-managed/storage-maintenance.ts +256 -0
  287. package/server-managed/storage-payload.ts +103 -0
  288. package/server-managed/storage-stores.ts +85 -0
  289. package/server-managed/storage.ts +78 -0
  290. package/server-managed/team-app.ts +94 -0
  291. package/server-managed/team-catalog.ts +7 -0
  292. package/server-managed/team-feed.ts +173 -0
  293. package/server-managed/team-npm-scopes.ts +77 -0
  294. package/server-managed/team-reports.ts +228 -0
  295. package/server-managed/triage-response.ts +16 -0
  296. package/server-managed/type-only-worker.js +19 -0
  297. package/server-managed/type-only.ts +103 -0
  298. package/server-managed/uploads.ts +86 -0
  299. package/server-managed/upstream-cache.ts +215 -0
  300. package/server-managed/view-as.ts +58 -0
  301. package/server-managed/workspace-shares.ts +166 -0
  302. package/server.ts +50 -0
  303. package/server/index.ts +0 -477
  304. package/server/lifecycle.ts +0 -204
  305. package/server/sync-handlers.ts +0 -320
  306. /package/{LICENSE → LICENSE-MIT} +0 -0
  307. /package/{server → server-e2e}/config.example.json +0 -0
  308. /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
  309. /package/{server → server-e2e}/objstore/fs.ts +0 -0
  310. /package/{server → server-e2e}/peer.ts +0 -0
  311. /package/{server → server-e2e}/util.ts +0 -0
  312. /package/{server → server-e2e}/validation.ts +0 -0
@@ -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` and re-arms
19
- // `pendingSave` for the next natural trigger.
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; in the
29
- // pathological case where wire order flipped,
30
- // we'd rather mark the session errored (visible
31
- // via UI + recoverable via `dismissError()`)
32
- // than silently swallow a state divergence.
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,35 @@
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
+ // The `server-info` frame payload (the `type` discriminant is added at the
24
+ // send site). `managed` is null on e2e-only deployments.
25
+ export interface ServerInfo {
26
+ mode: ServerMode | 'managed+e2e' | 'e2e+managed'
27
+ managed: ManagedServerInfo | null
28
+ deepviewScanServer?: string
29
+ githubNewIssueLabels?: string
30
+ }
31
+
32
+ // The mode-probe route. A client GETs this to learn a server's protocol up
33
+ // front — a managed server has no WS plane to carry the connect frame — and
34
+ // the response body is the same `ServerInfo` shape emitted as that frame.
35
+ 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,23 @@
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
+ // Evaluate the entire workspace, independent of triage, filters, or the lens
13
+ // left active by another report. Use the same merge and link rules as the view.
14
+ export function workspaceAppMetadata(reports, duplicatesOf = () => []) {
15
+ const { groups, conflicts } = mergeReportGroups(reports, { hideRuledOut: true })
16
+ if (conflicts.size > 0) return { appMode: false }
17
+ const linked = mergeLinkedWorkspaceGroups(groups, duplicatesOf, appTabs)
18
+ if (!canLockConfirmed(linked, { tabs: appTabs, kindOf: revalidateKindOf, severityMode: 'corrected' })) return { appMode: false }
19
+ return {
20
+ appMode: true,
21
+ appFindings: linked.filter((group) => appTabs(group).some((f) => f.isApp)).length,
22
+ }
23
+ }
@@ -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
+ }