@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.
- 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 +3 -0
- package/common/managed/finding-detail.js +66 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +7 -0
- package/common/managed/npm-packages.js +51 -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 +359 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +6 -0
- package/common/managed/team-app.js +47 -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-2K6UN3PC.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-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7JBSSRD7.js +3 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FMXWXDR4.js +36 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -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-KOGNQDSJ.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-VDRXHIHN.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-WRAJVD35.js +1 -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 +364 -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 +23 -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 +76 -0
- package/server-managed/comparison.ts +52 -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 +468 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +143 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +98 -0
- package/server-managed/dependency-reviews.ts +364 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +376 -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 +136 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5503 -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/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +152 -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 +225 -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-patch.ts +325 -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 +351 -0
- package/server-managed/pull-dependency-reviews.ts +334 -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 +448 -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 +166 -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 +212 -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 +149 -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 +319 -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,56 @@
|
|
|
1
|
+
const MAX_PULL_REQUEST_URL = 2048
|
|
2
|
+
|
|
3
|
+
export interface PullRequestRef { repo: string; number: number }
|
|
4
|
+
export type GithubFixRef = PullRequestRef & { kind: 'pull' | 'issue' }
|
|
5
|
+
export type GithubFixStatus = 'open' | 'draft' | 'closed' | 'merged'
|
|
6
|
+
export type GithubIssueClosedReason = 'completed' | 'not_planned' | 'duplicate' | 'unknown'
|
|
7
|
+
export type GithubFixResult = { url: string } & (
|
|
8
|
+
{ title: string; description: string | null; status: GithubFixStatus; stateReason: GithubIssueClosedReason | null } | { error: 'unavailable' }
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Unknown closure reasons must never imply that an issue was completed.
|
|
12
|
+
export function githubIssueClosedReason(value: unknown): GithubIssueClosedReason {
|
|
13
|
+
return value === 'completed' || value === 'not_planned' || value === 'duplicate' ? value : 'unknown'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isGithubRepoName(value: string): boolean {
|
|
17
|
+
const [owner, repo, extra] = value.split('/')
|
|
18
|
+
return extra === undefined && owner !== undefined && repo !== undefined
|
|
19
|
+
&& owner.length <= 39 && /^[a-zA-Z\d](?:-?[a-zA-Z\d])*$/u.test(owner)
|
|
20
|
+
&& repo.length <= 100 && repo !== '.' && repo !== '..' && /^[a-zA-Z\d._-]+$/u.test(repo)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Only identify the repository and PR number. This never supplies an API URL:
|
|
24
|
+
// the server must match a team repository and use its stored full name.
|
|
25
|
+
function parseGithubNumberedUrl(value: unknown, path: RegExp): PullRequestRef | null {
|
|
26
|
+
if (typeof value !== 'string' || value.length > MAX_PULL_REQUEST_URL) return null
|
|
27
|
+
let url: URL
|
|
28
|
+
try { url = new URL(value) } catch { return null }
|
|
29
|
+
if (url.href !== value || url.origin !== 'https://github.com' || url.username || url.password) return null
|
|
30
|
+
const match = path.exec(url.pathname)
|
|
31
|
+
if (!match) return null
|
|
32
|
+
const repo = `${match[1]}/${match[2]}`
|
|
33
|
+
const number = Number(match[3])
|
|
34
|
+
if (!isGithubRepoName(repo) || !Number.isSafeInteger(number) || number <= 0) return null
|
|
35
|
+
return { repo, number }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseGithubPrUrl(value: unknown): PullRequestRef | null {
|
|
39
|
+
return parseGithubNumberedUrl(value, /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/(?:files|commits|checks))?\/?$/u)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseGithubIssueUrl(value: unknown): PullRequestRef | null {
|
|
43
|
+
return parseGithubNumberedUrl(value, /^\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/u)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseGithubFixUrl(value: unknown): GithubFixRef | null {
|
|
47
|
+
const pr = parseGithubPrUrl(value)
|
|
48
|
+
if (pr) return { ...pr, kind: 'pull' }
|
|
49
|
+
const issue = parseGithubIssueUrl(value)
|
|
50
|
+
return issue ? { ...issue, kind: 'issue' } : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Browser-only deduplication key; never used to construct a server API call.
|
|
54
|
+
export function githubFixKey(ref: GithubFixRef): string {
|
|
55
|
+
return `${ref.repo.toLowerCase()}#${ref.kind}:${ref.number}`
|
|
56
|
+
}
|
package/common/gzip.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Gzip helpers built on the platform CompressionStream /
|
|
2
|
+
// DecompressionStream. Centralised so the half-dozen call sites
|
|
3
|
+
// (storage, triage / workspace export + import, sync-crypto) share
|
|
4
|
+
// one implementation of the Blob → stream → Response plumbing instead
|
|
5
|
+
// of hand-copying it. Bytes in / bytes out is the primitive; the text
|
|
6
|
+
// helpers run the bytes through `common/utf8.js` so the same
|
|
7
|
+
// fatal-on-invalid / BOM-preserving decode rules apply everywhere.
|
|
8
|
+
//
|
|
9
|
+
// `deflate` (raw, no gzip framing) is intentionally NOT covered here —
|
|
10
|
+
// `triage.js` uses it for its own at-rest blob and must stay distinct
|
|
11
|
+
// from the gzip-magic sniffing the bundles / import paths rely on.
|
|
12
|
+
|
|
13
|
+
import { decodeUtf8, encodeUtf8 } from './utf8.js'
|
|
14
|
+
|
|
15
|
+
export async function gzipBytes(bytes) {
|
|
16
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream('gzip'))
|
|
17
|
+
return new Uint8Array(await new Response(stream).arrayBuffer())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `maxBytes` (optional) caps the DECOMPRESSED size: gzip expands up to
|
|
21
|
+
// ~1032:1, so a small hostile payload can balloon to GiBs and OOM the
|
|
22
|
+
// process before any content validation runs. Callers decompressing
|
|
23
|
+
// peer-controlled bytes (sync-crypto's inbound changesets) pass a cap;
|
|
24
|
+
// the read then aborts with a RangeError the moment the budget is crossed,
|
|
25
|
+
// instead of materialising the full expansion. Uncapped callers keep
|
|
26
|
+
// the one-shot Response read.
|
|
27
|
+
export async function gunzipBytes(bytes, { maxBytes } = {}) {
|
|
28
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip'))
|
|
29
|
+
if (maxBytes == null) return new Uint8Array(await new Response(stream).arrayBuffer())
|
|
30
|
+
const reader = stream.getReader()
|
|
31
|
+
const chunks = []
|
|
32
|
+
let total = 0
|
|
33
|
+
for (;;) {
|
|
34
|
+
const { value, done } = await reader.read()
|
|
35
|
+
if (done) break
|
|
36
|
+
total += value.length
|
|
37
|
+
if (total > maxBytes) {
|
|
38
|
+
// Cancel so the DecompressionStream stops inflating the rest.
|
|
39
|
+
try { await reader.cancel() } catch {}
|
|
40
|
+
throw new RangeError(`gunzipBytes: decompressed size exceeds ${maxBytes}-byte cap`)
|
|
41
|
+
}
|
|
42
|
+
chunks.push(value)
|
|
43
|
+
}
|
|
44
|
+
const out = new Uint8Array(total)
|
|
45
|
+
let offset = 0
|
|
46
|
+
for (const chunk of chunks) {
|
|
47
|
+
out.set(chunk, offset)
|
|
48
|
+
offset += chunk.length
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function gzipText(text) {
|
|
54
|
+
return await gzipBytes(encodeUtf8(text))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function gunzipToText(bytes) {
|
|
58
|
+
return decodeUtf8(await gunzipBytes(bytes))
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Type declarations for `common/line-diff.js`, for `.ts` callers.
|
|
2
|
+
|
|
3
|
+
// Lines `a[a0..a1)` replaced by `b[b0..b1)`.
|
|
4
|
+
export interface ChangeBlock { a0: number; a1: number; b0: number; b1: number }
|
|
5
|
+
|
|
6
|
+
export function textLines(text: string): string[]
|
|
7
|
+
export function changeBlocks(before: string, after: string, ignoreWhitespace?: boolean): ChangeBlock[]
|
|
8
|
+
export function blocksHold(before: string, after: string, blocks: unknown, ignoreWhitespace?: boolean): boolean
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The change blocks between two texts' lines, found by @preventive/diff: what
|
|
2
|
+
// the viewer's Compare diffs (ui/view/bundle-compare-code-model.js); and the
|
|
3
|
+
// server's check, apart from @preventive/compare's own, that its diff with
|
|
4
|
+
// renamed names hidden takes the old side to the new (server-managed/rename-diff.ts).
|
|
5
|
+
//
|
|
6
|
+
// Lines are numbered the way the diff numbers them: from zero, a final
|
|
7
|
+
// newline ending the last line rather than starting an empty one.
|
|
8
|
+
import { changeSet } from '@preventive/diff'
|
|
9
|
+
|
|
10
|
+
export function textLines(text) {
|
|
11
|
+
if (text === '') return []
|
|
12
|
+
const lines = text.split('\n')
|
|
13
|
+
if (lines.at(-1) === '') lines.pop()
|
|
14
|
+
return lines
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Whether a text's last line ends in a newline. One added or dropped there is
|
|
18
|
+
// a change of that line, as diff has it (the viewer marks it on that line's
|
|
19
|
+
// row), but for diff -b, which ignores it as it does other trailing blanks.
|
|
20
|
+
const endsLine = text => text === '' || text.endsWith('\n')
|
|
21
|
+
|
|
22
|
+
// A line as diff -b reads it (@preventive/diff's): a run of the C locale's
|
|
23
|
+
// blanks as one space, and those that end it as none.
|
|
24
|
+
const spacing = line => line.replace(/[ \t\n\v\f\r]+$/u, '').replaceAll(/[ \t\n\v\f\r]+/gu, ' ')
|
|
25
|
+
|
|
26
|
+
// Each `a[a0..a1)` replaced by `b[b0..b1)`. `ignoreWhitespace` is diff -b:
|
|
27
|
+
// lines differing only in how much whitespace they have pair up as unchanged,
|
|
28
|
+
// whitespace added or removed between other characters still a change. Those
|
|
29
|
+
// a unified diff with no context shows (horizon 0), found without printing
|
|
30
|
+
// one and reading it back.
|
|
31
|
+
export function changeBlocks(before, after, ignoreWhitespace = false) {
|
|
32
|
+
return changeSet(before, after, { horizon: 0, whitespace: ignoreWhitespace ? 'change' : 'none' }).map(({ a0, a1, b0, b1 }) => ({ a0, a1, b0, b1 }))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Whether `blocks` take `before`'s lines to `after`'s: each in order and apart
|
|
36
|
+
// from the last, within both texts, and every line between them alike on both
|
|
37
|
+
// sides (alike but for how much whitespace they have where `ignoreWhitespace`,
|
|
38
|
+
// as diff -b has them), the last line in a block where a newline was added or
|
|
39
|
+
// dropped after it, so that a diff drawn from them hides no change. A check on
|
|
40
|
+
// what changeBlocks found, or on one found apart from the texts drawn.
|
|
41
|
+
export function blocksHold(before, after, blocks, ignoreWhitespace = false) {
|
|
42
|
+
if (!Array.isArray(blocks)) return false
|
|
43
|
+
const a = textLines(before), b = textLines(after)
|
|
44
|
+
const alike = ignoreWhitespace ? (x, y) => spacing(x) === spacing(y) : (x, y) => x === y
|
|
45
|
+
let i = 0, j = 0
|
|
46
|
+
for (const block of [...blocks, { a0: a.length, a1: a.length, b0: b.length, b1: b.length, last: true }]) {
|
|
47
|
+
const { a0, a1, b0, b1 } = block ?? {}
|
|
48
|
+
if (![a0, a1, b0, b1].every(Number.isInteger) || a0 < i || b0 < j || a1 < a0 || b1 < b0 || a1 > a.length || b1 > b.length) return false
|
|
49
|
+
// A block changes a line, and as many lines are alike before it on each side.
|
|
50
|
+
if ((!block.last && a1 === a0 && b1 === b0) || a0 - i !== b0 - j) return false
|
|
51
|
+
if (block.last && a0 > i && !ignoreWhitespace && endsLine(before) !== endsLine(after)) return false
|
|
52
|
+
for (; i < a0; i++, j++) if (!alike(a[i], b[j])) return false
|
|
53
|
+
i = a1
|
|
54
|
+
j = b1
|
|
55
|
+
}
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Explicit links combine already-resolved workspace App rows. This is a view
|
|
2
|
+
// grouping, not report deduplication: each report's revalidation answer has
|
|
3
|
+
// already been checked in its own row, and links never create report conflicts.
|
|
4
|
+
//
|
|
5
|
+
// `visibleTabs` decides which tabs may BRIDGE two rows; `retainedTabs` (the
|
|
6
|
+
// same projection unless the caller says otherwise) decides which tabs the
|
|
7
|
+
// combined row keeps. They part when a display choice shows a row more of
|
|
8
|
+
// itself without meaning to re-draw the workspace's row boundaries.
|
|
9
|
+
export function mergeLinkedWorkspaceGroups(groups, duplicatesOf, visibleTabs, retainedTabs = visibleTabs) {
|
|
10
|
+
const parent = groups.map((_, i) => i)
|
|
11
|
+
const find = (i) => {
|
|
12
|
+
while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i] }
|
|
13
|
+
return i
|
|
14
|
+
}
|
|
15
|
+
const tabs = groups.map(visibleTabs)
|
|
16
|
+
const byId = new Map()
|
|
17
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
18
|
+
for (const f of tabs[i]) {
|
|
19
|
+
if (!f.id) continue
|
|
20
|
+
if (!byId.has(f.id)) byId.set(f.id, [])
|
|
21
|
+
byId.get(f.id).push(i)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const [id, rows] of byId) {
|
|
25
|
+
for (const other of duplicatesOf(id)) {
|
|
26
|
+
if (other === id) continue
|
|
27
|
+
for (const a of rows) {
|
|
28
|
+
for (const b of byId.get(other) ?? []) {
|
|
29
|
+
const left = find(a), right = find(b)
|
|
30
|
+
if (left !== right) parent[right] = left
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const components = new Map()
|
|
36
|
+
for (let i = 0; i < groups.length; i++) {
|
|
37
|
+
const root = find(i)
|
|
38
|
+
if (!components.has(root)) components.set(root, [])
|
|
39
|
+
components.get(root).push(i)
|
|
40
|
+
}
|
|
41
|
+
if (components.size === groups.length) return groups
|
|
42
|
+
const key = (f) => f.id ?? String(f._id)
|
|
43
|
+
const merged = [...components.values()].map((indices) => {
|
|
44
|
+
if (indices.length === 1) return groups[indices[0]]
|
|
45
|
+
// Retain the tabs visible in each original row. An imported App finding
|
|
46
|
+
// or an unjudged source row must not disappear when linked to a pass row.
|
|
47
|
+
const visible = new Map()
|
|
48
|
+
for (const i of indices) for (const f of retainedTabs(groups[i])) if (!visible.has(key(f))) visible.set(key(f), f)
|
|
49
|
+
const members = new Map()
|
|
50
|
+
for (const i of indices) {
|
|
51
|
+
for (const f of groups[i]) {
|
|
52
|
+
if (!members.has(key(f))) members.set(key(f), visible.get(key(f)) ?? f)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const group = [...members.values()]
|
|
56
|
+
const first = groups[indices[0]]
|
|
57
|
+
group.workspaceKey = first.workspaceKey ?? key(first[0])
|
|
58
|
+
group.linkedTabs = [...visible.values()]
|
|
59
|
+
return group
|
|
60
|
+
})
|
|
61
|
+
Object.defineProperty(merged, 'ruledOutIds', { value: groups.ruledOutIds, configurable: true })
|
|
62
|
+
return merged
|
|
63
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Agents' sessions and their tasks, reconstructed from what each
|
|
2
|
+
// leaves on GitHub (the code viewer's Agents tab, repository-agents.js): a
|
|
3
|
+
// task is a pull request it opened, or a commit it pushed to the default
|
|
4
|
+
// branch without one; a session holds as many as name it, with the branches
|
|
5
|
+
// they are on and the issues they work on. The server reads each artifact's
|
|
6
|
+
// own texts, never others' comments on it (a pull request's body and its
|
|
7
|
+
// head and merge commits' messages, where an agent links its session; a
|
|
8
|
+
// commit's message; a branch's tip commit) for the marks an agent leaves
|
|
9
|
+
// (agentSignals), sending those alone; agentSessions then groups them:
|
|
10
|
+
// those naming one session, a pull request with its branch, a branch with
|
|
11
|
+
// the session its commits name, an issue with the pull requests closing it
|
|
12
|
+
// or a branch named for it. Without a session named, a branch's pull
|
|
13
|
+
// requests are one session, and a commit its own.
|
|
14
|
+
//
|
|
15
|
+
// The agents are Claude and Codex, and those the server knows by their GitHub
|
|
16
|
+
// Apps or accounts (server-managed/app-agents.ts APP_AGENTS): each told by its
|
|
17
|
+
// fingerprint, a line of its texts, by the account its work is authored by
|
|
18
|
+
// (github-agents.ts githubAgent), or by its branches' prefix, the server's to
|
|
19
|
+
// read, and named to the client
|
|
20
|
+
// by its id, name and avatar (AppAgent). Such an agent names no session:
|
|
21
|
+
// a pull request of its is a session alone, with its branch; Claude's or
|
|
22
|
+
// Codex's marks on its work show those as its tools. A pull request labelled
|
|
23
|
+
// for an agent (labelAgents) is that agent's too, where its own texts and
|
|
24
|
+
// branch name none: the mark one who ran it gives work it left unmarked.
|
|
25
|
+
import { githubAgent } from './github-agents.ts'
|
|
26
|
+
|
|
27
|
+
// 'claude', 'codex', or an app agent's id.
|
|
28
|
+
export type Agent = string
|
|
29
|
+
|
|
30
|
+
// An agent known by its GitHub App or account, as the server names it to the
|
|
31
|
+
// client: its id, its name, and its avatar, its mark (/api/avatar/<avatar>:
|
|
32
|
+
// `app-<id>` a GitHub App's, `user-<id>` a GitHub account's), where the app
|
|
33
|
+
// draws no mark of its own for it (Copilot's, ui/view/repository-tiles.js).
|
|
34
|
+
export interface AppAgent { id: string; name: string; avatar?: string }
|
|
35
|
+
// How the server tells one, beside the accounts its work is authored by (its
|
|
36
|
+
// id githubAgent's): its fingerprint, a line of an artifact's texts starting
|
|
37
|
+
// with it; its branches' prefix.
|
|
38
|
+
export interface AppAgentMarks { id: string; fingerprint?: string; prefix: string }
|
|
39
|
+
// An artifact's author, as GitHub names its account: its login and id.
|
|
40
|
+
export interface AgentAuthor { login: string; id: number }
|
|
41
|
+
|
|
42
|
+
// What an artifact's texts say of the agents that made it: the Claude Code
|
|
43
|
+
// sessions and Codex tasks they link, whether they carry Claude's marks,
|
|
44
|
+
// its co-author trailer or its "Generated with" line, and Codex's, its
|
|
45
|
+
// co-author trailer, and the app agents whose fingerprints they carry, or
|
|
46
|
+
// whose branch it is.
|
|
47
|
+
export interface AgentSignals { sessions: string[]; tasks: string[]; claude: boolean; codex: boolean; agents: string[] }
|
|
48
|
+
|
|
49
|
+
// Each by its author's login, and their account's numeric id (`authorId`)
|
|
50
|
+
// where GitHub names it.
|
|
51
|
+
export interface AgentPull { number: number; title: string; state: 'open' | 'merged' | 'closed'; draft: boolean; author: string | null; authorId?: number | null
|
|
52
|
+
// Its branch, and the owner of the fork it is in, null where it is in the
|
|
53
|
+
// repository itself.
|
|
54
|
+
createdAt: number | null; updatedAt: number | null; head: string | null; fork: string | null; additions: number; deletions: number; commits: number
|
|
55
|
+
closes: number[]; signals: AgentSignals }
|
|
56
|
+
export interface AgentIssue { number: number; title: string; state: 'open' | 'closed'; author: string | null; authorId?: number | null; createdAt: number | null; updatedAt: number | null }
|
|
57
|
+
export interface AgentBranch { name: string; at: number | null; author: string | null; authorId?: number | null; signals: AgentSignals }
|
|
58
|
+
// A commit to the default branch no pull request merged.
|
|
59
|
+
export interface AgentCommit { oid: string; title: string; at: number | null; author: string | null; authorId?: number | null; additions: number; deletions: number
|
|
60
|
+
signals: AgentSignals }
|
|
61
|
+
|
|
62
|
+
export interface AgentSession {
|
|
63
|
+
id: string
|
|
64
|
+
agents: Agent[]
|
|
65
|
+
// Where to open it: a Claude Code session or a Codex task.
|
|
66
|
+
sessions: string[]
|
|
67
|
+
tasks: string[]
|
|
68
|
+
title: string
|
|
69
|
+
authors: string[]
|
|
70
|
+
startedAt: number | null
|
|
71
|
+
updatedAt: number | null
|
|
72
|
+
state: 'open' | 'merged' | 'closed' | 'branch'
|
|
73
|
+
// Its tasks: the pull requests it opened, and the commits it pushed to
|
|
74
|
+
// the default branch, the newest first.
|
|
75
|
+
pulls: Pick<AgentPull, 'number' | 'title' | 'state' | 'draft'>[]
|
|
76
|
+
commits: Pick<AgentCommit, 'oid' | 'title'>[]
|
|
77
|
+
issues: Pick<AgentIssue, 'number' | 'title' | 'state'>[]
|
|
78
|
+
branches: string[]
|
|
79
|
+
// The commits its pull requests hold, and their lines and its commits'.
|
|
80
|
+
commitCount: number
|
|
81
|
+
additions: number
|
|
82
|
+
deletions: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const SESSION = /https:\/\/claude\.ai\/code\/(session_[A-Za-z\d]{6,64})\b/gu
|
|
86
|
+
const CODEX_TASK = /https:\/\/chatgpt\.com\/codex\/tasks\/(task_[a-z]_[A-Za-z\d]{6,64})\b/gu
|
|
87
|
+
// Claude's marks on its commits and pull requests: its co-author trailer,
|
|
88
|
+
// its "Generated with" line, its session trailer.
|
|
89
|
+
const CLAUDE_MARK = /^co-authored-by: claude\b|generated with \[claude code\]|^claude-session:/imu
|
|
90
|
+
// Codex's: its co-author trailer, by its address, as Codex names itself
|
|
91
|
+
// co-author of the commits it makes as whoever ran it; a trailer of a text's
|
|
92
|
+
// trailers (trailerBlock), not a line before them that shows one.
|
|
93
|
+
const CODEX_MARK = /^co-authored-by:[ \t]*[^<\n]*<codex@openai\.com>[ \t]*$/imu
|
|
94
|
+
const MAX_LINKS = 8
|
|
95
|
+
|
|
96
|
+
// A message's trailers, as git reads them (`git interpret-trailers`): its
|
|
97
|
+
// last paragraph, past its subject's, where each of its lines is a trailer
|
|
98
|
+
// (`Key: value`) or goes on with one (indented), or, where it holds git's
|
|
99
|
+
// own `Signed-off-by:`, a quarter of them are; none where it isn't, as
|
|
100
|
+
// lines a description or example holds, fenced or not, are not.
|
|
101
|
+
const TRAILER_LINE = /^[A-Za-z\d-]+[ \t]*:/u
|
|
102
|
+
export function trailerBlock(text: string): string {
|
|
103
|
+
const paragraphs = text.replaceAll('\r\n', '\n').trim().split(/\n[ \t]*\n/u)
|
|
104
|
+
const block = paragraphs.length > 1 ? paragraphs.at(-1)! : ''
|
|
105
|
+
const lines = block.split('\n')
|
|
106
|
+
const trailers = lines.filter(line => TRAILER_LINE.test(line)).length
|
|
107
|
+
const others = lines.filter((line, at) => !TRAILER_LINE.test(line) && !(at > 0 && /^[ \t]/u.test(line))).length
|
|
108
|
+
return others === 0 || (lines.some(line => /^signed-off-by[ \t]*:/iu.test(line)) && trailers * 3 >= others) ? block : ''
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The agents of those whose ids are `ids` that `labels` name (a pull
|
|
112
|
+
// request's): a label an agent's id, regardless of case, alone or after
|
|
113
|
+
// `agent:` or `agent/` (`Codex`, `agent: claude`).
|
|
114
|
+
const LABEL = /^(?:agent[:/] ?)?([a-z][a-z\d-]*)$/u
|
|
115
|
+
export function labelAgents(labels: unknown[], ids: readonly string[]): Agent[] {
|
|
116
|
+
return [...new Set(labels.flatMap(label => {
|
|
117
|
+
const id = typeof label === 'string' ? LABEL.exec(label.trim().toLowerCase())?.[1] : undefined
|
|
118
|
+
return id !== undefined && ids.includes(id) ? [id] : []
|
|
119
|
+
}))]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The sessions and tasks `texts` link, whether they carry Claude's marks and
|
|
123
|
+
// Codex's, and the agents of `apps` whose fingerprints start a line of them
|
|
124
|
+
// (not one a text quotes, as one describing it may), whose account `author`
|
|
125
|
+
// is (githubAgent), or whose prefix the branch `branch` is under;
|
|
126
|
+
// Claude, Codex or the agents of `apps` its `labels` name (labelAgents).
|
|
127
|
+
export function agentSignals(texts: unknown[], apps: readonly AppAgentMarks[] = [], branch: string | null = null,
|
|
128
|
+
author: AgentAuthor | null = null, labels: unknown[] = []): AgentSignals {
|
|
129
|
+
const strings = texts.filter((text): text is string => typeof text === 'string')
|
|
130
|
+
const found = (pattern: RegExp) => [...new Set(strings.flatMap(text => [...text.matchAll(pattern)].map(match => match[1]!)))].slice(0, MAX_LINKS)
|
|
131
|
+
const lines = strings.flatMap(text => text.split('\n').map(line => line.trimStart()))
|
|
132
|
+
const authored = author ? githubAgent(author.login, author.id) : null
|
|
133
|
+
const labelled = labelAgents(labels, ['claude', 'codex', ...apps.map(app => app.id)])
|
|
134
|
+
return { sessions: found(SESSION), tasks: found(CODEX_TASK), claude: labelled.includes('claude') || strings.some(text => CLAUDE_MARK.test(text)),
|
|
135
|
+
codex: labelled.includes('codex') || strings.some(text => CODEX_MARK.test(trailerBlock(text))),
|
|
136
|
+
agents: apps.filter(app => (app.fingerprint && lines.some(line => line.startsWith(app.fingerprint!)))
|
|
137
|
+
|| authored === app.id || branch?.startsWith(app.prefix) || labelled.includes(app.id)).map(app => app.id) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The agent a branch's name says made it, and the issue or pull request the
|
|
141
|
+
// Claude GitHub App names its branch for (`claude/issue-12-…`).
|
|
142
|
+
export function agentBranch(name: string | null): { agent: Agent | null; issue: number | null; pull: number | null } {
|
|
143
|
+
const agent = name?.startsWith('claude/') ? 'claude' : name?.startsWith('codex/') ? 'codex' : null
|
|
144
|
+
const named = agent === 'claude' ? /^claude\/(issue|pr)-([1-9]\d{0,9})-/u.exec(name!) : null
|
|
145
|
+
return { agent, issue: named?.[1] === 'issue' ? Number(named[2]) : null, pull: named?.[1] === 'pr' ? Number(named[2]) : null }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A pull request's branch, as GitHub names one in a fork: `owner:branch`;
|
|
149
|
+
// two forks' of one name, or a fork's and the repository's, are two.
|
|
150
|
+
const headName = (pull: AgentPull) => pull.head && pull.fork ? `${pull.fork}:${pull.head}` : pull.head
|
|
151
|
+
|
|
152
|
+
const signalAgents = (signals: AgentSignals): Agent[] => [...signals.agents, ...signals.sessions.length > 0 || signals.claude ? ['claude'] : [],
|
|
153
|
+
...signals.tasks.length > 0 || signals.codex ? ['codex'] : []]
|
|
154
|
+
const TOOLS = new Set(['claude', 'codex'])
|
|
155
|
+
|
|
156
|
+
type Artifacts = { pulls?: AgentPull[]; commits?: AgentCommit[]; issues?: AgentIssue[]; branches?: AgentBranch[] }
|
|
157
|
+
type Group = { agents: Set<Agent>; keys: string[] }
|
|
158
|
+
|
|
159
|
+
// The artifacts joined (`pull:<n>`, `commit:<oid>`, `branch:<name>`,
|
|
160
|
+
// `session:<id>`, `task:<id>`), each set with the agents whose marks are on
|
|
161
|
+
// them: by the sessions and tasks they link and the branches pull requests
|
|
162
|
+
// are from. An app agent's join none by what they link, and a pull request
|
|
163
|
+
// of its its branch alone, where no pull request newer is from it, an app
|
|
164
|
+
// agent's or another's: one a session.
|
|
165
|
+
function artifactGroups({ pulls = [], commits = [], branches = [] }: Artifacts): Group[] {
|
|
166
|
+
const parent = new Map<string, string>()
|
|
167
|
+
const find = (key: string): string => {
|
|
168
|
+
let root = key
|
|
169
|
+
while (parent.has(root) && parent.get(root) !== root) root = parent.get(root)!
|
|
170
|
+
parent.set(key, root)
|
|
171
|
+
return root
|
|
172
|
+
}
|
|
173
|
+
const join = (a: string, b: string) => {
|
|
174
|
+
const [x, y] = [find(a), find(b)]
|
|
175
|
+
if (x !== y) parent.set(y, x)
|
|
176
|
+
}
|
|
177
|
+
const agentsOf = new Map<string, Set<Agent>>()
|
|
178
|
+
const mark = (key: string, agents: Agent[]) => { for (const agent of agents) agentsOf.set(key, new Set([...agentsOf.get(key) ?? [], agent])) }
|
|
179
|
+
const linked = (key: string, signals: AgentSignals) => {
|
|
180
|
+
find(key)
|
|
181
|
+
if (signals.agents.length === 0) for (const session of signals.sessions) join(key, `session:${session}`)
|
|
182
|
+
if (signals.agents.length === 0) for (const task of signals.tasks) join(key, `task:${task}`)
|
|
183
|
+
mark(key, signalAgents(signals))
|
|
184
|
+
}
|
|
185
|
+
const branchNames = new Set(branches.map(branch => branch.name))
|
|
186
|
+
for (const branch of branches) {
|
|
187
|
+
const key = `branch:${branch.name}`
|
|
188
|
+
linked(key, branch.signals)
|
|
189
|
+
const { agent } = agentBranch(branch.name)
|
|
190
|
+
if (agent) mark(key, [agent])
|
|
191
|
+
}
|
|
192
|
+
// The newest pull request read from each branch, by its number, as GitHub
|
|
193
|
+
// numbers them as they are opened: they are read as they were updated.
|
|
194
|
+
const newest = new Map<string, number>()
|
|
195
|
+
for (const pull of pulls) {
|
|
196
|
+
const head = headName(pull)
|
|
197
|
+
if (head !== null && pull.number > (newest.get(head) ?? 0)) newest.set(head, pull.number)
|
|
198
|
+
}
|
|
199
|
+
for (const pull of pulls) {
|
|
200
|
+
const key = `pull:${pull.number}`
|
|
201
|
+
linked(key, pull.signals)
|
|
202
|
+
const { agent, pull: named } = agentBranch(pull.head)
|
|
203
|
+
if (agent) mark(key, [agent])
|
|
204
|
+
// Its branch, an agent's or its own, read or not; a fork's its own.
|
|
205
|
+
const head = headName(pull)
|
|
206
|
+
if (pull.signals.agents.length > 0) {
|
|
207
|
+
if (head !== null && newest.get(head) === pull.number) join(key, `branch:${head}`)
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
if (head && (agent || signalAgents(pull.signals).length > 0 || branchNames.has(head))) join(key, `branch:${head}`)
|
|
211
|
+
if (named) join(key, `pull:${named}`)
|
|
212
|
+
}
|
|
213
|
+
for (const commit of commits) linked(`commit:${commit.oid}`, commit.signals)
|
|
214
|
+
|
|
215
|
+
const groups = new Map<string, Group>()
|
|
216
|
+
for (const key of parent.keys()) {
|
|
217
|
+
const root = find(key)
|
|
218
|
+
const group = groups.get(root) ?? { agents: new Set(), keys: [] }
|
|
219
|
+
group.keys.push(key)
|
|
220
|
+
for (const agent of agentsOf.get(key) ?? []) group.agents.add(agent)
|
|
221
|
+
groups.set(root, group)
|
|
222
|
+
}
|
|
223
|
+
return [...groups.values()]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const byTime = <T>(time: (item: T) => number | null) => (a: T, b: T) => (time(a) ?? 0) - (time(b) ?? 0)
|
|
227
|
+
|
|
228
|
+
// The session a group of artifacts is, its tasks and what they touched; null
|
|
229
|
+
// for one no agent left marks on, or holding none read.
|
|
230
|
+
function sessionOf({ agents, keys }: Group, { pulls, commits, issues, branches }: {
|
|
231
|
+
pulls: Map<number, AgentPull>; commits: Map<string, AgentCommit>; issues: Map<number, AgentIssue>; branches: Map<string, AgentBranch> }): AgentSession | null {
|
|
232
|
+
if (agents.size === 0) return null
|
|
233
|
+
const of = (prefix: string) => keys.filter(key => key.startsWith(prefix)).map(key => key.slice(prefix.length))
|
|
234
|
+
const taskPulls = of('pull:').flatMap(number => pulls.get(Number(number)) ?? []).toSorted(byTime(pull => pull.createdAt))
|
|
235
|
+
const taskCommits = of('commit:').flatMap(oid => commits.get(oid) ?? []).toSorted(byTime(commit => commit.at))
|
|
236
|
+
const names = [...new Set([...of('branch:'), ...taskPulls.flatMap(pull => headName(pull) ?? [])])]
|
|
237
|
+
// The issues its pull requests close, and those its branches are named
|
|
238
|
+
// for, by a pull request's own name for its branch, a fork's too.
|
|
239
|
+
const issueNumbers = new Set([...taskPulls.flatMap(pull => pull.closes),
|
|
240
|
+
...taskPulls.flatMap(pull => agentBranch(pull.head).issue ?? []), ...of('branch:').flatMap(name => agentBranch(name).issue ?? [])])
|
|
241
|
+
const taskIssues = [...issueNumbers].flatMap(number => issues.get(number) ?? []).toSorted(byTime(issue => issue.createdAt))
|
|
242
|
+
const taskBranches = names.flatMap(name => branches.get(name) ?? [])
|
|
243
|
+
const items = [...taskPulls, ...taskCommits, ...taskIssues, ...taskBranches]
|
|
244
|
+
if (items.length === 0) return null
|
|
245
|
+
const [codexTasks, sessions] = [of('task:'), of('session:')]
|
|
246
|
+
const started = items.flatMap(item => ('createdAt' in item ? item.createdAt : item.at) ?? [])
|
|
247
|
+
const updated = items.flatMap(item => ('updatedAt' in item ? item.updatedAt : item.at) ?? [])
|
|
248
|
+
const states = new Set(taskPulls.map(pull => pull.state))
|
|
249
|
+
// Its first task names it: the earliest of its pull requests and commits.
|
|
250
|
+
const first = [...taskPulls.map(pull => ({ at: pull.createdAt, title: pull.title })), ...taskCommits.map(commit => ({ at: commit.at, title: commit.title }))]
|
|
251
|
+
.toSorted(byTime(task => task.at))[0]
|
|
252
|
+
const landed = [...taskPulls, ...taskCommits]
|
|
253
|
+
return {
|
|
254
|
+
id: sessions[0] ?? codexTasks[0] ?? (taskPulls[0] ? `pull-${taskPulls[0].number}` : taskCommits[0]?.oid ?? names[0] ?? ''),
|
|
255
|
+
// An app agent's first, the one whose session it is, then the tools it
|
|
256
|
+
// used, Claude's or Codex's.
|
|
257
|
+
agents: [...[...agents].filter(agent => !TOOLS.has(agent)), ...['claude', 'codex'].filter(agent => agents.has(agent))],
|
|
258
|
+
sessions, tasks: codexTasks,
|
|
259
|
+
title: first?.title ?? taskIssues[0]?.title ?? names[0] ?? '',
|
|
260
|
+
authors: [...new Set(items.flatMap(item => item.author ?? []))],
|
|
261
|
+
startedAt: started.length > 0 ? Math.min(...started) : null,
|
|
262
|
+
updatedAt: updated.length > 0 ? Math.max(...updated) : null,
|
|
263
|
+
state: states.has('open') ? 'open' : states.has('merged') || taskCommits.length > 0 ? 'merged' : states.has('closed') ? 'closed' : 'branch',
|
|
264
|
+
pulls: taskPulls.map(({ number, title, state, draft }) => ({ number, title, state, draft })).toReversed(),
|
|
265
|
+
commits: taskCommits.map(({ oid, title }) => ({ oid, title })).toReversed(),
|
|
266
|
+
issues: taskIssues.map(({ number, title, state }) => ({ number, title, state })),
|
|
267
|
+
branches: names,
|
|
268
|
+
commitCount: taskPulls.reduce((sum, pull) => sum + pull.commits, 0) + taskCommits.length,
|
|
269
|
+
additions: landed.reduce((sum, item) => sum + item.additions, 0),
|
|
270
|
+
deletions: landed.reduce((sum, item) => sum + item.deletions, 0),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// The sessions `pulls`, `commits`, `issues` and `branches` make, the most
|
|
275
|
+
// recently updated first: each a set of them, joined by the sessions and
|
|
276
|
+
// tasks they link and the branches pull requests are from, with the issues
|
|
277
|
+
// a pull request closes or a branch is named for.
|
|
278
|
+
// Artifacts no agent left marks on belong to none. Where pull requests
|
|
279
|
+
// updated before `branchesSince` are yet to be read, a session of branches
|
|
280
|
+
// alone older than that waits for them: its pull request may be among them.
|
|
281
|
+
export function agentSessions(artifacts: Artifacts, { branchesSince = null }: { branchesSince?: number | null } = {}): AgentSession[] {
|
|
282
|
+
const lookups = {
|
|
283
|
+
pulls: new Map((artifacts.pulls ?? []).map(pull => [pull.number, pull])),
|
|
284
|
+
commits: new Map((artifacts.commits ?? []).map(commit => [commit.oid, commit])),
|
|
285
|
+
issues: new Map((artifacts.issues ?? []).map(issue => [issue.number, issue])),
|
|
286
|
+
branches: new Map((artifacts.branches ?? []).map(branch => [branch.name, branch])),
|
|
287
|
+
}
|
|
288
|
+
const waiting = (session: AgentSession) => branchesSince !== null && session.pulls.length === 0 && session.commits.length === 0
|
|
289
|
+
&& (session.updatedAt ?? 0) < branchesSince
|
|
290
|
+
return artifactGroups(artifacts).flatMap(group => sessionOf(group, lookups) ?? []).filter(session => !waiting(session))
|
|
291
|
+
.toSorted(byTime(session => session.updatedAt)).toReversed()
|
|
292
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Managed comments are independent records, shared by reports containing the
|
|
2
|
+
// same finding. Imported and legacy comments may lack attribution or dates.
|
|
3
|
+
export interface ManagedComment {
|
|
4
|
+
id: string
|
|
5
|
+
findingId: string
|
|
6
|
+
body: string
|
|
7
|
+
authorId: string | null
|
|
8
|
+
authorLogin: string | null
|
|
9
|
+
createdAt: number | null
|
|
10
|
+
updatedAt: number | null
|
|
11
|
+
version: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MAX_COMMENT_TEXT = 10_000
|
|
15
|
+
|
|
16
|
+
// Callers also enforce write access to the report containing the finding.
|
|
17
|
+
// Admins can remove comments with no linked user, but cannot claim authorship.
|
|
18
|
+
export function canDeleteComment(comment: Pick<ManagedComment, 'authorId'>, actor: { id: string; role: string } | null | undefined): boolean {
|
|
19
|
+
return actor != null && (comment.authorId === actor.id || (comment.authorId == null && actor.role === 'admin'))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function compareManagedComments(a: ManagedComment, b: ManagedComment): number {
|
|
23
|
+
if (a.createdAt == null && b.createdAt != null) return -1
|
|
24
|
+
if (a.createdAt != null && b.createdAt == null) return 1
|
|
25
|
+
const byDate = (a.createdAt ?? 0) - (b.createdAt ?? 0)
|
|
26
|
+
if (byDate !== 0) return byDate
|
|
27
|
+
if (a.id === b.id) return 0
|
|
28
|
+
return a.id < b.id ? -1 : 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function parseCommentBody(value: unknown): string | null {
|
|
32
|
+
if (typeof value !== 'string' || value.length > MAX_COMMENT_TEXT) return null
|
|
33
|
+
return value.trim() || null
|
|
34
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const FINDING_DETAIL_FIELDS: readonly string[]
|
|
2
|
+
export function liteReportData(data: unknown, options?: { fold?: boolean }): unknown
|
|
3
|
+
export function liteWorkspace<T extends { filename: string; data: unknown }>(reports: readonly T[], links: readonly (readonly string[])[]): { reports: T[]; folded: boolean }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { findingTitle, reportEntries, revalidateKindOf } from '@preventive/report'
|
|
2
|
+
import { managedTeamRevalidationConflicted } from './team-app.js'
|
|
3
|
+
|
|
4
|
+
// A finding's detail: what its popup, the views besides the board and an
|
|
5
|
+
// export read, and the board does not. Its narrative and evidence, the hash
|
|
6
|
+
// its Code button finds a bundle by, the export it names, the facts its card
|
|
7
|
+
// and an export tell (write-md-finding.js PLAIN_FIELDS, CODE_FIELDS, but for
|
|
8
|
+
// `priority`, which the board sorts and filters by), and the run meta its
|
|
9
|
+
// card's run line and a report's header read (its `model` and `type`, which
|
|
10
|
+
// the board's filters read, stay). A team's
|
|
11
|
+
// reports are first sent without it (server-managed/team-reports.ts), the
|
|
12
|
+
// board drawn from them, and their findings filled in from the whole reports
|
|
13
|
+
// (ui/view/managed-details.js).
|
|
14
|
+
export const FINDING_DETAIL_FIELDS = Object.freeze([
|
|
15
|
+
'description', 'evidence', 'impact', 'reproduction', 'recommendation', 'confidenceReason', 'revalidateVerdict', 'revalidateRecommendation',
|
|
16
|
+
'fileHash', 'exportName', 'methodName',
|
|
17
|
+
'commitHash', 'auditedCommit', 'detectedAt', 'committedAt', 'dateCreated', 'status', 'branch', 'pocStatus', 'parent', 'slug', 'reportPath',
|
|
18
|
+
'category', 'discoveredIn', 'effort', 'exportsMode',
|
|
19
|
+
])
|
|
20
|
+
const DETAIL = new Set(FINDING_DETAIL_FIELDS)
|
|
21
|
+
// As much of a name as a heading takes (@preventive/report write-md-finding.js
|
|
22
|
+
// HEADING_MAX), about a board card's three lines.
|
|
23
|
+
const NAME_CHARS = 120
|
|
24
|
+
|
|
25
|
+
// A finding without its detail. One without a title of its own is named by its
|
|
26
|
+
// description's first line (findingTitle), which it keeps as its description,
|
|
27
|
+
// as much of it as a card shows.
|
|
28
|
+
function liteFinding(finding) {
|
|
29
|
+
if (!finding || typeof finding !== 'object' || Array.isArray(finding)) return finding
|
|
30
|
+
const lite = Object.fromEntries(Object.entries(finding).filter(([key]) => !DETAIL.has(key)))
|
|
31
|
+
const titled = typeof finding.title === 'string' && finding.title.trim() !== ''
|
|
32
|
+
const title = titled ? '' : findingTitle(finding)
|
|
33
|
+
if (title) lite.description = title.length > NAME_CHARS ? `${title.slice(0, NAME_CHARS - 1)}…` : title
|
|
34
|
+
return lite
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isPass = finding => revalidateKindOf(finding) === 'revalidation'
|
|
38
|
+
|
|
39
|
+
// A report's data, its findings without their detail. A workspace opening in
|
|
40
|
+
// App mode shows of an entry holding the revalidation pass's row that row
|
|
41
|
+
// alone (ui/view/group.js foldedTabs): with `fold`, such an entry is sent as
|
|
42
|
+
// the pass's rows, the rows beneath them left for the whole reports
|
|
43
|
+
// (ui/view/managed-details.js).
|
|
44
|
+
export function liteReportData(data, { fold = false } = {}) {
|
|
45
|
+
const key = Array.isArray(data?.findings) ? 'findings' : Array.isArray(data?.groups) ? 'groups' : null
|
|
46
|
+
if (!key) return data
|
|
47
|
+
const entries = data[key].map(entry => {
|
|
48
|
+
if (!Array.isArray(entry)) return liteFinding(entry)
|
|
49
|
+
const shown = fold && entry.length > 1 && entry.some(isPass) ? entry.filter(isPass) : entry
|
|
50
|
+
return shown.map(liteFinding)
|
|
51
|
+
})
|
|
52
|
+
return { ...data, [key]: entries }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rows = data => (reportEntries(data) ?? []).flat().length
|
|
56
|
+
|
|
57
|
+
// A team's reports as its board is first sent them (server-managed/team-reports.ts),
|
|
58
|
+
// each `liteReportData`, folded where nothing in the workspace contradicts the
|
|
59
|
+
// revalidation pass, which opens a view of it in App mode
|
|
60
|
+
// (ui/view/render.js configureReportRevalidation); `folded` where that left
|
|
61
|
+
// rows out.
|
|
62
|
+
export function liteWorkspace(reports, links) {
|
|
63
|
+
const fold = !managedTeamRevalidationConflicted(Object.assign(reports.map(({ filename, data }) => ({ filename, data })), { links }))
|
|
64
|
+
const lite = reports.map(report => ({ ...report, data: liteReportData(report.data, { fold }) }))
|
|
65
|
+
return { reports: lite, folded: fold && lite.some((report, index) => rows(report.data) < rows(reports[index].data)) }
|
|
66
|
+
}
|