monkeypatchlist 0.0.0-stage → 0.1.0
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 +202 -0
- package/NOTICE +61 -0
- package/README.md +183 -2
- package/catalog/catalog.json +552211 -0
- package/catalog/catalog.schema.json +787 -0
- package/catalog/changelog.json +3 -0
- package/dist/bin/monkeypatchlist.d.ts +2 -0
- package/dist/bin/monkeypatchlist.js +3 -0
- package/dist/catalog.d.ts +561 -0
- package/dist/catalog.js +58 -0
- package/dist/cli.d.ts +84 -0
- package/dist/cli.js +470 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lockfile-input.d.ts +9 -0
- package/dist/lockfile-input.js +91 -0
- package/dist/packages/analysis/binding.d.ts +2 -0
- package/dist/packages/analysis/binding.js +5 -0
- package/dist/packages/analysis/index.d.ts +10 -0
- package/dist/packages/analysis/index.js +13 -0
- package/dist/packages/analysis/job.d.ts +33 -0
- package/dist/packages/analysis/job.js +85 -0
- package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
- package/dist/packages/analysis/lib/absence-tags.js +340 -0
- package/dist/packages/analysis/lib/ast.d.ts +28 -0
- package/dist/packages/analysis/lib/ast.js +64 -0
- package/dist/packages/analysis/lib/binding.d.ts +36 -0
- package/dist/packages/analysis/lib/binding.js +66 -0
- package/dist/packages/analysis/lib/budget.d.ts +80 -0
- package/dist/packages/analysis/lib/budget.js +31 -0
- package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
- package/dist/packages/analysis/lib/catch-arms.js +351 -0
- package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
- package/dist/packages/analysis/lib/chunk-loads.js +882 -0
- package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
- package/dist/packages/analysis/lib/chunk-registry.js +334 -0
- package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
- package/dist/packages/analysis/lib/code-routes.js +921 -0
- package/dist/packages/analysis/lib/collisions.d.ts +22 -0
- package/dist/packages/analysis/lib/collisions.js +103 -0
- package/dist/packages/analysis/lib/definitions.d.ts +76 -0
- package/dist/packages/analysis/lib/definitions.js +782 -0
- package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
- package/dist/packages/analysis/lib/early-exits.js +161 -0
- package/dist/packages/analysis/lib/effect.d.ts +138 -0
- package/dist/packages/analysis/lib/effect.js +800 -0
- package/dist/packages/analysis/lib/global-object.d.ts +581 -0
- package/dist/packages/analysis/lib/global-object.js +8240 -0
- package/dist/packages/analysis/lib/keys.d.ts +33 -0
- package/dist/packages/analysis/lib/keys.js +82 -0
- package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
- package/dist/packages/analysis/lib/line-starts.js +79 -0
- package/dist/packages/analysis/lib/link-group.d.ts +51 -0
- package/dist/packages/analysis/lib/link-group.js +487 -0
- package/dist/packages/analysis/lib/module-links.d.ts +117 -0
- package/dist/packages/analysis/lib/module-links.js +1060 -0
- package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
- package/dist/packages/analysis/lib/nested-code.js +200 -0
- package/dist/packages/analysis/lib/numeric.d.ts +16 -0
- package/dist/packages/analysis/lib/numeric.js +216 -0
- package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
- package/dist/packages/analysis/lib/once-flags.js +190 -0
- package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
- package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
- package/dist/packages/analysis/lib/parse.d.ts +10 -0
- package/dist/packages/analysis/lib/parse.js +39 -0
- package/dist/packages/analysis/lib/partial.d.ts +60 -0
- package/dist/packages/analysis/lib/partial.js +518 -0
- package/dist/packages/analysis/lib/print.d.ts +3 -0
- package/dist/packages/analysis/lib/print.js +150 -0
- package/dist/packages/analysis/lib/reachability.d.ts +137 -0
- package/dist/packages/analysis/lib/reachability.js +1295 -0
- package/dist/packages/analysis/lib/resolver.d.ts +13 -0
- package/dist/packages/analysis/lib/resolver.js +2886 -0
- package/dist/packages/analysis/lib/restores.d.ts +42 -0
- package/dist/packages/analysis/lib/restores.js +482 -0
- package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
- package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
- package/dist/packages/analysis/lib/scope.d.ts +110 -0
- package/dist/packages/analysis/lib/scope.js +443 -0
- package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
- package/dist/packages/analysis/lib/script-loads.js +359 -0
- package/dist/packages/analysis/lib/summaries.d.ts +177 -0
- package/dist/packages/analysis/lib/summaries.js +498 -0
- package/dist/packages/analysis/lib/types.d.ts +632 -0
- package/dist/packages/analysis/lib/types.js +128 -0
- package/dist/packages/analysis/lib/wrap.d.ts +34 -0
- package/dist/packages/analysis/lib/wrap.js +407 -0
- package/dist/packages/analysis/lib/writes.d.ts +358 -0
- package/dist/packages/analysis/lib/writes.js +1612 -0
- package/dist/packages/analysis/visit-order.d.ts +3 -0
- package/dist/packages/analysis/visit-order.js +23 -0
- package/dist/packages/attribution/index.d.ts +233 -0
- package/dist/packages/attribution/index.js +444 -0
- package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
- package/dist/packages/attribution/lib/containing-package.js +304 -0
- package/dist/packages/attribution/lib/first-party.d.ts +37 -0
- package/dist/packages/attribution/lib/first-party.js +153 -0
- package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
- package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
- package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
- package/dist/packages/attribution/lib/lockfile.js +330 -0
- package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
- package/dist/packages/attribution/lib/path-rules.js +84 -0
- package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
- package/dist/packages/attribution/lib/pnpm.js +133 -0
- package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
- package/dist/packages/attribution/lib/remote-url.js +24 -0
- package/dist/packages/attribution/lib/source-map.d.ts +51 -0
- package/dist/packages/attribution/lib/source-map.js +125 -0
- package/dist/packages/attribution/lib/yaml.d.ts +9 -0
- package/dist/packages/attribution/lib/yaml.js +277 -0
- package/dist/packages/attribution/lib/yarn.d.ts +7 -0
- package/dist/packages/attribution/lib/yarn.js +227 -0
- package/dist/packages/baseline/index.d.ts +45 -0
- package/dist/packages/baseline/index.js +41 -0
- package/dist/packages/baseline/lib/error.d.ts +15 -0
- package/dist/packages/baseline/lib/error.js +25 -0
- package/dist/packages/baseline/lib/format.d.ts +49 -0
- package/dist/packages/baseline/lib/format.js +22 -0
- package/dist/packages/baseline/lib/migrations.d.ts +19 -0
- package/dist/packages/baseline/lib/migrations.js +7 -0
- package/dist/packages/baseline/lib/previous.d.ts +23 -0
- package/dist/packages/baseline/lib/previous.js +64 -0
- package/dist/packages/baseline/lib/read.d.ts +39 -0
- package/dist/packages/baseline/lib/read.js +219 -0
- package/dist/packages/baseline/lib/report.d.ts +25 -0
- package/dist/packages/baseline/lib/report.js +104 -0
- package/dist/packages/baseline/lib/skew.d.ts +51 -0
- package/dist/packages/baseline/lib/skew.js +75 -0
- package/dist/packages/compare/index.d.ts +120 -0
- package/dist/packages/compare/index.js +274 -0
- package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
- package/dist/packages/compare/lib/catalog-notes.js +224 -0
- package/dist/packages/completeness/index.d.ts +246 -0
- package/dist/packages/completeness/index.js +249 -0
- package/dist/packages/confidence/index.d.ts +54 -0
- package/dist/packages/confidence/index.js +71 -0
- package/dist/packages/config/index.d.ts +107 -0
- package/dist/packages/config/index.js +95 -0
- package/dist/packages/config/lib/command-line.d.ts +23 -0
- package/dist/packages/config/lib/command-line.js +239 -0
- package/dist/packages/config/lib/config-file.d.ts +11 -0
- package/dist/packages/config/lib/config-file.js +119 -0
- package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
- package/dist/packages/config/lib/scan-config-id.js +54 -0
- package/dist/packages/config/lib/schema.d.ts +2 -0
- package/dist/packages/config/lib/schema.js +31 -0
- package/dist/packages/config/lib/types.d.ts +64 -0
- package/dist/packages/config/lib/types.js +6 -0
- package/dist/packages/config/lib/urls.d.ts +6 -0
- package/dist/packages/config/lib/urls.js +9 -0
- package/dist/packages/discovery/index.d.ts +277 -0
- package/dist/packages/discovery/index.js +1950 -0
- package/dist/packages/discovery/lib/import-map.d.ts +68 -0
- package/dist/packages/discovery/lib/import-map.js +180 -0
- package/dist/packages/discovery/lib/requests.d.ts +58 -0
- package/dist/packages/discovery/lib/requests.js +591 -0
- package/dist/packages/discovery/lib/scopes.d.ts +6 -0
- package/dist/packages/discovery/lib/scopes.js +286 -0
- package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
- package/dist/packages/discovery/lib/source-maps.js +209 -0
- package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
- package/dist/packages/discovery/lib/swept-code.js +107 -0
- package/dist/packages/discovery/lib/syntax.d.ts +17 -0
- package/dist/packages/discovery/lib/syntax.js +39 -0
- package/dist/packages/findings/index.d.ts +106 -0
- package/dist/packages/findings/index.js +254 -0
- package/dist/packages/findings/lib/scope.d.ts +38 -0
- package/dist/packages/findings/lib/scope.js +110 -0
- package/dist/packages/parallel/index.d.ts +2 -0
- package/dist/packages/parallel/index.js +5 -0
- package/dist/packages/parallel/lib/protocol.d.ts +15 -0
- package/dist/packages/parallel/lib/protocol.js +2 -0
- package/dist/packages/parallel/lib/runner.d.ts +36 -0
- package/dist/packages/parallel/lib/runner.js +96 -0
- package/dist/packages/parallel/lib/schedule.d.ts +16 -0
- package/dist/packages/parallel/lib/schedule.js +52 -0
- package/dist/packages/parallel/lib/worker.d.ts +1 -0
- package/dist/packages/parallel/lib/worker.js +18 -0
- package/dist/packages/policy/index.d.ts +36 -0
- package/dist/packages/policy/index.js +22 -0
- package/dist/packages/report/explain.d.ts +26 -0
- package/dist/packages/report/explain.js +99 -0
- package/dist/packages/report/index.d.ts +210 -0
- package/dist/packages/report/index.js +388 -0
- package/dist/packages/report/terminal.d.ts +11 -0
- package/dist/packages/report/terminal.js +331 -0
- package/dist/packages/source-store/fixture.d.ts +22 -0
- package/dist/packages/source-store/fixture.js +58 -0
- package/dist/packages/source-store/index.d.ts +278 -0
- package/dist/packages/source-store/index.js +322 -0
- package/dist/packages/source-store/lib/addresses.d.ts +6 -0
- package/dist/packages/source-store/lib/addresses.js +91 -0
- package/dist/packages/source-store/lib/connection.d.ts +15 -0
- package/dist/packages/source-store/lib/connection.js +135 -0
- package/dist/packages/source-store/lib/content-types.d.ts +16 -0
- package/dist/packages/source-store/lib/content-types.js +75 -0
- package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
- package/dist/packages/source-store/lib/data-urls.js +85 -0
- package/dist/packages/source-store/lib/deadline.d.ts +18 -0
- package/dist/packages/source-store/lib/deadline.js +44 -0
- package/dist/packages/source-store/lib/exchange.d.ts +29 -0
- package/dist/packages/source-store/lib/exchange.js +112 -0
- package/dist/packages/source-store/lib/integrity.d.ts +19 -0
- package/dist/packages/source-store/lib/integrity.js +86 -0
- package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
- package/dist/packages/source-store/lib/outcomes.js +22 -0
- package/dist/packages/source-store/lib/proxy.d.ts +11 -0
- package/dist/packages/source-store/lib/proxy.js +53 -0
- package/dist/packages/source-store/lib/session.d.ts +12 -0
- package/dist/packages/source-store/lib/session.js +139 -0
- package/dist/packages/source-store/lib/urls.d.ts +6 -0
- package/dist/packages/source-store/lib/urls.js +15 -0
- package/dist/packages/source-store/resolve.d.ts +59 -0
- package/dist/packages/source-store/resolve.js +45 -0
- package/dist/scan.d.ts +65 -0
- package/dist/scan.js +592 -0
- package/package.json +95 -4
- package/schema/config.schema.json +133 -0
- package/schema/report.schema.json +1905 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The protected surface (§12 Matching): which targets the protected-surface list covers, in each realm.
|
|
2
|
+
/** The suffix of a `prototype-change` target's name: `<holder>.[[Prototype]]`. */
|
|
3
|
+
const prototypeSuffix = '.[[Prototype]]';
|
|
4
|
+
/**
|
|
5
|
+
* The protected surface of a catalog (§12 Matching). Entries are keyed on (interface, member), independent of realm
|
|
6
|
+
* and holder. A target is covered when:
|
|
7
|
+
* - it is a catalog target an entry names: a member entry's member, in every realm and on each interface that
|
|
8
|
+
* includes its mixin, and its worker-realm equivalent; a whole entry's global binding and the static, prototype and
|
|
9
|
+
* namespace members the catalog lists for it, without `onX`
|
|
10
|
+
* - it shadows one of those, on whatever holder it lands (`history.pushState`, `XMLHttpRequest.prototype.toString`)
|
|
11
|
+
* - it is an `add` of a named member, whatever its key, to a whole entry's constructor, prototype or namespace
|
|
12
|
+
* - it is a `prototype-change` of a whole entry's prototype or namespace (never of its constructor), or of a covered
|
|
13
|
+
* singleton
|
|
14
|
+
* - it is a wildcard over native members (`<holder>.*`) of a holder a covered target sits on, or of a covered singleton;
|
|
15
|
+
* one a candidate set narrows when a member it matches is a covered target
|
|
16
|
+
*
|
|
17
|
+
* A singleton is covered when its interface or namespace is a whole entry (`history`, `localStorage`, `crypto.subtle`,
|
|
18
|
+
* `navigator.serviceWorker`), or when an entry names a member of its interface (`navigator`, through
|
|
19
|
+
* `Navigator.sendBeacon`). A pattern wildcard of non-native adds (`<holder>.<prefix>*`) is never covered.
|
|
20
|
+
*/
|
|
21
|
+
export function protectedSurface(catalog) {
|
|
22
|
+
const byRealm = new Map();
|
|
23
|
+
const coveredIn = (realm) => {
|
|
24
|
+
let covered = byRealm.get(realm);
|
|
25
|
+
if (covered === undefined) {
|
|
26
|
+
covered = cover(catalog, realm);
|
|
27
|
+
byRealm.set(realm, covered);
|
|
28
|
+
}
|
|
29
|
+
return covered;
|
|
30
|
+
};
|
|
31
|
+
return ({ realm, name, shadows, matches }, effect) => {
|
|
32
|
+
const covered = coveredIn(realm);
|
|
33
|
+
if (name.endsWith('*')) {
|
|
34
|
+
if (!name.endsWith('.*'))
|
|
35
|
+
return false;
|
|
36
|
+
if (matches !== undefined)
|
|
37
|
+
return matches.some((match) => covered.targets.has(match));
|
|
38
|
+
const holder = name.slice(0, -'.*'.length);
|
|
39
|
+
return covered.singletons.get(holder) ?? covered.holdersOfTargets.has(holder);
|
|
40
|
+
}
|
|
41
|
+
if (covered.targets.has(name))
|
|
42
|
+
return true;
|
|
43
|
+
if (shadows !== undefined && covered.targets.has(shadows))
|
|
44
|
+
return true;
|
|
45
|
+
if (effect === 'add') {
|
|
46
|
+
const holder = holderOf(name, catalog.holders[realm]);
|
|
47
|
+
return holder !== undefined && covered.addHolders.has(holder);
|
|
48
|
+
}
|
|
49
|
+
if (effect === 'prototype-change' && name.endsWith(prototypeSuffix)) {
|
|
50
|
+
return covered.prototypeHolders.has(name.slice(0, -prototypeSuffix.length));
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function cover(catalog, realm) {
|
|
56
|
+
const targets = new Set();
|
|
57
|
+
const whole = new Set();
|
|
58
|
+
for (const entries of Object.values(catalog.protectedSurface)) {
|
|
59
|
+
for (const entry of entries ?? []) {
|
|
60
|
+
for (const target of entry.targets[realm])
|
|
61
|
+
targets.add(target);
|
|
62
|
+
if (entry.member === undefined)
|
|
63
|
+
whole.add(entry.interface);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const holders = catalog.holders[realm];
|
|
67
|
+
const catalogTargets = catalog.targets[realm];
|
|
68
|
+
const holdersOfTargets = new Set();
|
|
69
|
+
const coveredOwners = new Set();
|
|
70
|
+
for (const target of targets) {
|
|
71
|
+
const holder = holderOf(target, holders);
|
|
72
|
+
if (holder !== undefined)
|
|
73
|
+
holdersOfTargets.add(holder);
|
|
74
|
+
coveredOwners.add(catalogTargets[target].owner);
|
|
75
|
+
}
|
|
76
|
+
const addHolders = new Set();
|
|
77
|
+
const prototypeHolders = new Set();
|
|
78
|
+
for (const [name, { kind, owner }] of Object.entries(holders)) {
|
|
79
|
+
if (!whole.has(owner))
|
|
80
|
+
continue;
|
|
81
|
+
if (kind === 'constructor' || kind === 'prototype' || kind === 'namespace')
|
|
82
|
+
addHolders.add(name);
|
|
83
|
+
if (kind === 'prototype' || kind === 'namespace')
|
|
84
|
+
prototypeHolders.add(name);
|
|
85
|
+
}
|
|
86
|
+
const singletons = new Map();
|
|
87
|
+
for (const [path, singleton] of Object.entries(catalog.singletons[realm])) {
|
|
88
|
+
const owner = 'interface' in singleton ? singleton.interface : singleton.namespace;
|
|
89
|
+
const covered = whole.has(owner) || coveredOwners.has(owner);
|
|
90
|
+
singletons.set(path, covered);
|
|
91
|
+
if (covered)
|
|
92
|
+
prototypeHolders.add(path);
|
|
93
|
+
}
|
|
94
|
+
return { targets, holdersOfTargets, addHolders, prototypeHolders, singletons };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The holder a target is on (§4 Property keys): the part before a member written in JSON-quoted brackets
|
|
98
|
+
* (`Array["prototype.map"]`), and otherwise the longest holder the name continues with a dot, since a canonical numeric
|
|
99
|
+
* member keeps the dot form (`Array.prototype.1.5`).
|
|
100
|
+
*/
|
|
101
|
+
export function holderOf(name, holders) {
|
|
102
|
+
const bracket = name.indexOf('["');
|
|
103
|
+
if (bracket >= 0)
|
|
104
|
+
return Object.hasOwn(holders, name.slice(0, bracket)) ? name.slice(0, bracket) : undefined;
|
|
105
|
+
for (let dot = name.lastIndexOf('.'); dot > 0; dot = name.lastIndexOf('.', dot - 1)) {
|
|
106
|
+
if (Object.hasOwn(holders, name.slice(0, dot)))
|
|
107
|
+
return name.slice(0, dot);
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Parallel link groups (§18 Parallelism): link groups run one after another by default, and an internal worker-count
|
|
2
|
+
// setting lets them run in worker threads, each only while the estimated peak heaps of the groups running fit the
|
|
3
|
+
// memory budget. Results merge in group order. Everything that crosses a worker boundary is plain JSON (§19).
|
|
4
|
+
export { defaultMemoryBudget, inProcessRunner, workerRunner, } from "./lib/runner.js";
|
|
5
|
+
export { schedule } from "./lib/schedule.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A worker's setup: the catalog and the budget, each as JSON. */
|
|
2
|
+
export interface Setup {
|
|
3
|
+
catalog: string;
|
|
4
|
+
budget: string;
|
|
5
|
+
}
|
|
6
|
+
/** A job for a worker: one `GroupJob`, as JSON. */
|
|
7
|
+
export interface Request {
|
|
8
|
+
job: string;
|
|
9
|
+
}
|
|
10
|
+
/** A worker's answer: the group's `AnalyzeGroupOutput` as JSON, or the message of the error analysing it threw. */
|
|
11
|
+
export type Reply = {
|
|
12
|
+
output: string;
|
|
13
|
+
} | {
|
|
14
|
+
error: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SerializedCatalog } from '../../../catalog.ts';
|
|
2
|
+
import type { AnalyzeGroupOutput, Budget, Resolver } from '../../analysis/index.ts';
|
|
3
|
+
import { type GroupJob } from '../../analysis/job.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Runs a scan's link groups. `proceed()` is asked before each group starts, in group order: once it says no, no
|
|
6
|
+
* further group runs, and each group not run gives `undefined`. Results come back in group order.
|
|
7
|
+
*/
|
|
8
|
+
export interface GroupRunner {
|
|
9
|
+
run(jobs: readonly GroupJob[], proceed: () => boolean): Promise<(AnalyzeGroupOutput | undefined)[]>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The estimated peak heap per source byte of a link group (§18 Target: at most 400× per source byte, so that a 5 MB
|
|
13
|
+
* group fits in 2 GB).
|
|
14
|
+
*/
|
|
15
|
+
export declare const heapPerSourceByte = 400;
|
|
16
|
+
/**
|
|
17
|
+
* The memory budget a parallel run's estimated peak heaps share by default, in bytes: half of the 8 GB runner the cost
|
|
18
|
+
* target is set on (§18), which leaves room for the main thread's own heap, the source store and the catalog copies.
|
|
19
|
+
*/
|
|
20
|
+
export declare const defaultMemoryBudget: number;
|
|
21
|
+
/** Runs the groups in process, one after another, with `resolver`. */
|
|
22
|
+
export declare function inProcessRunner(resolver: Resolver, catalog: SerializedCatalog, budget: Budget): GroupRunner;
|
|
23
|
+
export interface WorkerRunnerOptions {
|
|
24
|
+
/** How many worker threads may analyze groups at once. */
|
|
25
|
+
workers: number;
|
|
26
|
+
/** The memory, in bytes, the estimated peak heaps of the groups running must fit in. */
|
|
27
|
+
memoryBudget: number;
|
|
28
|
+
catalog: SerializedCatalog;
|
|
29
|
+
budget: Budget;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Runs the groups in up to `workers` worker threads, each with the shipped JS resolver, while their estimated peak heaps
|
|
33
|
+
* fit `memoryBudget`. Each job and each result crosses the boundary as JSON text. Workers start when a run needs them,
|
|
34
|
+
* and stop when it ends.
|
|
35
|
+
*/
|
|
36
|
+
export declare function workerRunner({ workers, memoryBudget, catalog, budget }: WorkerRunnerOptions): GroupRunner;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Group runners (§18 Parallelism): in process, one group after another, or in worker threads under the memory budget.
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
import { analyzeJob, jobBytes } from "../../analysis/job.js";
|
|
4
|
+
import { schedule } from "./schedule.js";
|
|
5
|
+
/**
|
|
6
|
+
* The estimated peak heap per source byte of a link group (§18 Target: at most 400× per source byte, so that a 5 MB
|
|
7
|
+
* group fits in 2 GB).
|
|
8
|
+
*/
|
|
9
|
+
export const heapPerSourceByte = 400;
|
|
10
|
+
/**
|
|
11
|
+
* The memory budget a parallel run's estimated peak heaps share by default, in bytes: half of the 8 GB runner the cost
|
|
12
|
+
* target is set on (§18), which leaves room for the main thread's own heap, the source store and the catalog copies.
|
|
13
|
+
*/
|
|
14
|
+
export const defaultMemoryBudget = 4 * 1024 ** 3;
|
|
15
|
+
/** Runs the groups in process, one after another, with `resolver`. */
|
|
16
|
+
export function inProcessRunner(resolver, catalog, budget) {
|
|
17
|
+
return {
|
|
18
|
+
async run(jobs, proceed) {
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const job of jobs) {
|
|
21
|
+
if (!proceed())
|
|
22
|
+
break;
|
|
23
|
+
results.push(analyzeJob(resolver, job, catalog, budget));
|
|
24
|
+
}
|
|
25
|
+
return jobs.map((_, index) => results[index]);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs the groups in up to `workers` worker threads, each with the shipped JS resolver, while their estimated peak heaps
|
|
31
|
+
* fit `memoryBudget`. Each job and each result crosses the boundary as JSON text. Workers start when a run needs them,
|
|
32
|
+
* and stop when it ends.
|
|
33
|
+
*/
|
|
34
|
+
export function workerRunner({ workers, memoryBudget, catalog, budget }) {
|
|
35
|
+
return {
|
|
36
|
+
async run(jobs, proceed) {
|
|
37
|
+
const setup = { catalog: JSON.stringify(catalog), budget: JSON.stringify(budget) };
|
|
38
|
+
const idle = [];
|
|
39
|
+
const all = [];
|
|
40
|
+
const take = () => {
|
|
41
|
+
const worker = idle.pop();
|
|
42
|
+
if (worker !== undefined)
|
|
43
|
+
return worker;
|
|
44
|
+
const started = new Worker(workerUrl(), { workerData: setup });
|
|
45
|
+
// A worker that exits while idle is never handed another job, which would never be answered.
|
|
46
|
+
started.on('exit', () => {
|
|
47
|
+
const at = idle.indexOf(started);
|
|
48
|
+
if (at !== -1)
|
|
49
|
+
idle.splice(at, 1);
|
|
50
|
+
});
|
|
51
|
+
all.push(started);
|
|
52
|
+
return started;
|
|
53
|
+
};
|
|
54
|
+
const analyze = (index) => new Promise((resolve, reject) => {
|
|
55
|
+
const worker = take();
|
|
56
|
+
const done = () => {
|
|
57
|
+
worker.off('message', message);
|
|
58
|
+
worker.off('error', error);
|
|
59
|
+
worker.off('exit', exit);
|
|
60
|
+
};
|
|
61
|
+
const message = (reply) => {
|
|
62
|
+
done();
|
|
63
|
+
idle.push(worker);
|
|
64
|
+
if ('error' in reply)
|
|
65
|
+
reject(new Error(`link group ${jobs[index].entry.url} failed: ${reply.error}`));
|
|
66
|
+
else
|
|
67
|
+
resolve(JSON.parse(reply.output));
|
|
68
|
+
};
|
|
69
|
+
const error = (cause) => {
|
|
70
|
+
done();
|
|
71
|
+
reject(cause instanceof Error ? cause : new Error(String(cause)));
|
|
72
|
+
};
|
|
73
|
+
const exit = (code) => {
|
|
74
|
+
done();
|
|
75
|
+
reject(new Error(`a link-group worker exited with code ${code}`));
|
|
76
|
+
};
|
|
77
|
+
worker.on('message', message);
|
|
78
|
+
worker.on('error', error);
|
|
79
|
+
worker.on('exit', exit);
|
|
80
|
+
const request = { job: JSON.stringify(jobs[index]) };
|
|
81
|
+
worker.postMessage(request);
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const tasks = jobs.map((job) => ({ heap: heapPerSourceByte * jobBytes(job) }));
|
|
85
|
+
return await schedule(tasks, { workers, memoryBudget }, analyze, proceed);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await Promise.all(all.map((worker) => worker.terminate()));
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** The worker's module: its TypeScript source when this module runs from source, and its build output otherwise. */
|
|
94
|
+
function workerUrl() {
|
|
95
|
+
return new URL(import.meta.url.endsWith('.ts') ? './worker.ts' : './worker.js', import.meta.url);
|
|
96
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** How many groups may run at once, and the memory their estimated peak heaps must fit in, in bytes. */
|
|
2
|
+
export interface ScheduleOptions {
|
|
3
|
+
workers: number;
|
|
4
|
+
memoryBudget: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Runs `run(index)` for each task, in task order, at most `workers` at once. A task starts only once every task before
|
|
8
|
+
* it has started, and only while its estimated peak heap (`heap`) and those of the tasks running fit `memoryBudget`, or
|
|
9
|
+
* when no task is running, so a group whose estimate alone exceeds the budget waits until it can run alone.
|
|
10
|
+
* `proceed()` is asked before each task starts: once it says no, no further task starts, and each task not started
|
|
11
|
+
* gives `undefined`. The results come back in task order, whatever order the tasks finish in, so merging them never
|
|
12
|
+
* depends on completion order. A task that throws rejects the whole schedule once the tasks running have finished.
|
|
13
|
+
*/
|
|
14
|
+
export declare function schedule<T>(tasks: readonly {
|
|
15
|
+
heap: number;
|
|
16
|
+
}[], { workers, memoryBudget }: ScheduleOptions, run: (index: number) => Promise<T>, proceed?: () => boolean): Promise<(T | undefined)[]>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The link-group schedule (§18 Parallelism): which group starts when, and the order results come back in.
|
|
2
|
+
/**
|
|
3
|
+
* Runs `run(index)` for each task, in task order, at most `workers` at once. A task starts only once every task before
|
|
4
|
+
* it has started, and only while its estimated peak heap (`heap`) and those of the tasks running fit `memoryBudget`, or
|
|
5
|
+
* when no task is running, so a group whose estimate alone exceeds the budget waits until it can run alone.
|
|
6
|
+
* `proceed()` is asked before each task starts: once it says no, no further task starts, and each task not started
|
|
7
|
+
* gives `undefined`. The results come back in task order, whatever order the tasks finish in, so merging them never
|
|
8
|
+
* depends on completion order. A task that throws rejects the whole schedule once the tasks running have finished.
|
|
9
|
+
*/
|
|
10
|
+
export function schedule(tasks, { workers, memoryBudget }, run, proceed = () => true) {
|
|
11
|
+
const results = tasks.map(() => undefined);
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let next = 0;
|
|
14
|
+
let running = 0;
|
|
15
|
+
let heap = 0;
|
|
16
|
+
let stopped = false;
|
|
17
|
+
let failure;
|
|
18
|
+
const pump = () => {
|
|
19
|
+
while (!stopped && failure === undefined && next < tasks.length && running < Math.max(1, workers)) {
|
|
20
|
+
const task = tasks[next];
|
|
21
|
+
if (running > 0 && heap + task.heap > memoryBudget)
|
|
22
|
+
break;
|
|
23
|
+
if (!proceed()) {
|
|
24
|
+
stopped = true;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const index = next++;
|
|
28
|
+
running++;
|
|
29
|
+
heap += task.heap;
|
|
30
|
+
run(index).then((result) => {
|
|
31
|
+
results[index] = result;
|
|
32
|
+
finished(task);
|
|
33
|
+
}, (error) => {
|
|
34
|
+
failure ??= { error };
|
|
35
|
+
finished(task);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (running > 0)
|
|
39
|
+
return;
|
|
40
|
+
if (failure !== undefined)
|
|
41
|
+
reject(failure.error);
|
|
42
|
+
else
|
|
43
|
+
resolve(results);
|
|
44
|
+
};
|
|
45
|
+
const finished = (task) => {
|
|
46
|
+
running--;
|
|
47
|
+
heap -= task.heap;
|
|
48
|
+
pump();
|
|
49
|
+
};
|
|
50
|
+
pump();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A link-group worker (§18 Parallelism): analyzes one job at a time with the shipped JS resolver. Jobs and results
|
|
2
|
+
// cross the boundary as JSON text (§19 Serializable seams).
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import { jsResolver } from "../../analysis/index.js";
|
|
5
|
+
import { analyzeJob } from "../../analysis/job.js";
|
|
6
|
+
const setup = workerData;
|
|
7
|
+
const catalog = JSON.parse(setup.catalog);
|
|
8
|
+
const budget = JSON.parse(setup.budget);
|
|
9
|
+
parentPort.on('message', ({ job }) => {
|
|
10
|
+
let reply;
|
|
11
|
+
try {
|
|
12
|
+
reply = { output: JSON.stringify(analyzeJob(jsResolver, JSON.parse(job), catalog, budget)) };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
reply = { error: error instanceof Error ? (error.stack ?? error.message) : String(error) };
|
|
16
|
+
}
|
|
17
|
+
parentPort.postMessage(reply);
|
|
18
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComparedFinding } from '../compare/index.ts';
|
|
2
|
+
import type { Level } from '../completeness/index.ts';
|
|
3
|
+
import type { FailOnState } from '../config/index.ts';
|
|
4
|
+
/** A finding's policy slot (§15): in R1, `{ outcome: 'fail', by }` when it trips the gate, otherwise null. */
|
|
5
|
+
export type PolicySlot = {
|
|
6
|
+
outcome: 'fail';
|
|
7
|
+
by: `fail-on:${FailOnState}`;
|
|
8
|
+
} | null;
|
|
9
|
+
export interface PolicedFinding extends ComparedFinding {
|
|
10
|
+
policy: PolicySlot;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The gate (§15): the states it fails on, whether it ran and passed, and the exit code it gives (§16). The result is
|
|
14
|
+
* the findings' alone; an `incomplete` scan shows in the exit code, 3, and the completeness level.
|
|
15
|
+
*/
|
|
16
|
+
export interface Gate {
|
|
17
|
+
failOn: FailOnState[];
|
|
18
|
+
result: 'pass' | 'fail' | 'not-run';
|
|
19
|
+
exitCode: 0 | 1 | 3;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Applies `--fail-on` (§11 The gate): a finding in a listed state trips the gate, and its policy slot says so. Every
|
|
23
|
+
* other slot is null (not evaluated). The gate fails, with exit code 1, when a finding trips it. With no states listed
|
|
24
|
+
* it does not run.
|
|
25
|
+
*
|
|
26
|
+
* When the gate runs over an `incomplete` scan, the exit code is 3 whether the gate passed or failed (§13, §16).
|
|
27
|
+
* `allowIncomplete` downgrades that to a warning, which the CLI writes, and the exit code is the gate's own.
|
|
28
|
+
*/
|
|
29
|
+
export declare function applyPolicy(findings: readonly ComparedFinding[], options: {
|
|
30
|
+
failOn: readonly FailOnState[];
|
|
31
|
+
level: Level;
|
|
32
|
+
allowIncomplete: boolean;
|
|
33
|
+
}): {
|
|
34
|
+
findings: PolicedFinding[];
|
|
35
|
+
gate: Gate;
|
|
36
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Policy (§19, module 8): decides the policy slot of each finding and the gate result (§15). It knows nothing of
|
|
2
|
+
// rendering.
|
|
3
|
+
/**
|
|
4
|
+
* Applies `--fail-on` (§11 The gate): a finding in a listed state trips the gate, and its policy slot says so. Every
|
|
5
|
+
* other slot is null (not evaluated). The gate fails, with exit code 1, when a finding trips it. With no states listed
|
|
6
|
+
* it does not run.
|
|
7
|
+
*
|
|
8
|
+
* When the gate runs over an `incomplete` scan, the exit code is 3 whether the gate passed or failed (§13, §16).
|
|
9
|
+
* `allowIncomplete` downgrades that to a warning, which the CLI writes, and the exit code is the gate's own.
|
|
10
|
+
*/
|
|
11
|
+
export function applyPolicy(findings, options) {
|
|
12
|
+
const { failOn } = options;
|
|
13
|
+
const policed = findings.map((finding) => {
|
|
14
|
+
const state = failOn.find((listed) => listed === finding.baselineState);
|
|
15
|
+
return { ...finding, policy: state === undefined ? null : { outcome: 'fail', by: `fail-on:${state}` } };
|
|
16
|
+
});
|
|
17
|
+
if (failOn.length === 0)
|
|
18
|
+
return { findings: policed, gate: { failOn: [], result: 'not-run', exitCode: 0 } };
|
|
19
|
+
const failed = policed.some((finding) => finding.policy !== null);
|
|
20
|
+
const exitCode = options.level === 'incomplete' && !options.allowIncomplete ? 3 : failed ? 1 : 0;
|
|
21
|
+
return { findings: policed, gate: { failOn: [...failOn], result: failed ? 'fail' : 'pass', exitCode } };
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Report, ReportFinding } from './index.ts';
|
|
2
|
+
/** The findings a fingerprint or target names in a report. */
|
|
3
|
+
export type Explained = {
|
|
4
|
+
kind: 'found';
|
|
5
|
+
finding: ReportFinding;
|
|
6
|
+
}
|
|
7
|
+
/** A target that names several findings: each is a candidate, in the report's fingerprint order. */
|
|
8
|
+
| {
|
|
9
|
+
kind: 'ambiguous';
|
|
10
|
+
candidates: ReportFinding[];
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'unknown';
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The finding `query` names in `report`. A fingerprint names its finding. Otherwise `query` is a target, written as
|
|
16
|
+
* its target key (`window|Window.fetch|replace`), as realm and name (`window|Window.fetch`) or as its name alone
|
|
17
|
+
* (`Window.fetch`), and it names every finding with that target.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findExplained(report: Report, query: string): Explained;
|
|
20
|
+
/** One line per candidate a target names: its fingerprint, effect and attribution. */
|
|
21
|
+
export declare function renderCandidates(candidates: readonly ReportFinding[]): string;
|
|
22
|
+
/**
|
|
23
|
+
* A finding as `monkeypatchlist explain` renders it: its identity, attribution, baseline state and confidence, then each
|
|
24
|
+
* occurrence with its location, mechanism, attribution evidence, confidence and the `text` of each trace step.
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderExplanation(report: Report, finding: ReportFinding): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// `monkeypatchlist explain` (§14 Where traces appear, §15 Terminal, §16 Commands): one finding from a saved JSON report,
|
|
2
|
+
// rendered with every occurrence and its trace. Nothing is recomputed: every line reads a field the report holds.
|
|
3
|
+
/**
|
|
4
|
+
* The finding `query` names in `report`. A fingerprint names its finding. Otherwise `query` is a target, written as
|
|
5
|
+
* its target key (`window|Window.fetch|replace`), as realm and name (`window|Window.fetch`) or as its name alone
|
|
6
|
+
* (`Window.fetch`), and it names every finding with that target.
|
|
7
|
+
*/
|
|
8
|
+
export function findExplained(report, query) {
|
|
9
|
+
const byFingerprint = report.findings.find((finding) => finding.fingerprint === query);
|
|
10
|
+
if (byFingerprint !== undefined)
|
|
11
|
+
return { kind: 'found', finding: byFingerprint };
|
|
12
|
+
const candidates = report.findings.filter(({ targetKey, target }) => query === targetKey || query === target.name || query === `${target.realm}|${target.name}`);
|
|
13
|
+
if (candidates.length === 0)
|
|
14
|
+
return { kind: 'unknown' };
|
|
15
|
+
if (candidates.length === 1)
|
|
16
|
+
return { kind: 'found', finding: candidates[0] };
|
|
17
|
+
return { kind: 'ambiguous', candidates };
|
|
18
|
+
}
|
|
19
|
+
/** One line per candidate a target names: its fingerprint, effect and attribution. */
|
|
20
|
+
export function renderCandidates(candidates) {
|
|
21
|
+
const width = Math.max(...candidates.map((finding) => finding.fingerprint.length));
|
|
22
|
+
return candidates
|
|
23
|
+
.map((finding) => ` ${finding.fingerprint.padEnd(width)} ${finding.effect} · ${finding.attribution.key}`)
|
|
24
|
+
.map((line) => `${line}\n`)
|
|
25
|
+
.join('');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A finding as `monkeypatchlist explain` renders it: its identity, attribution, baseline state and confidence, then each
|
|
29
|
+
* occurrence with its location, mechanism, attribution evidence, confidence and the `text` of each trace step.
|
|
30
|
+
*/
|
|
31
|
+
export function renderExplanation(report, finding) {
|
|
32
|
+
const { target } = finding;
|
|
33
|
+
const lines = [
|
|
34
|
+
`${target.name} ${finding.effect}`,
|
|
35
|
+
field('Fingerprint', finding.fingerprint),
|
|
36
|
+
field('Target key', finding.targetKey),
|
|
37
|
+
field('Target', [`realm ${target.realm}`, `holder ${target.holder}`, ...(target.shadows === undefined ? [] : [`shadows ${target.shadows}`])].join(' · ')),
|
|
38
|
+
field('Surface', finding.surface),
|
|
39
|
+
// `locks` is true, false or 'unknown' (§8): only a finding known not to lock leaves the line out.
|
|
40
|
+
...(finding.locks === false ? [] : [field('Locks', finding.locks === true ? 'yes' : 'unknown')]),
|
|
41
|
+
field('Attribution', attributionOf(finding)),
|
|
42
|
+
...(finding.baselineState === null
|
|
43
|
+
? []
|
|
44
|
+
: [field('Baseline', `${finding.baselineState}${finding.matchedBy === null ? '' : ` (matched by ${finding.matchedBy})`}`)]),
|
|
45
|
+
...(finding.confidence === null ? [] : [field('Confidence', confidenceOf(finding.confidence))]),
|
|
46
|
+
];
|
|
47
|
+
const count = finding.occurrences.length;
|
|
48
|
+
if (count === 0) {
|
|
49
|
+
lines.push('', 'No occurrences: the scan did not find this baseline entry.');
|
|
50
|
+
}
|
|
51
|
+
finding.occurrences.forEach((occurrence, i) => {
|
|
52
|
+
lines.push('', `Occurrence ${i + 1} of ${count}`, ...occurrenceLines(report, occurrence));
|
|
53
|
+
});
|
|
54
|
+
return `${lines.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
function occurrenceLines(report, occurrence) {
|
|
57
|
+
const { artifact, generated, original, evidence } = occurrence;
|
|
58
|
+
const path = report.artifacts.find((candidate) => candidate.url === artifact.url)?.path ?? null;
|
|
59
|
+
const lines = [
|
|
60
|
+
field('Location', `${path ?? artifact.url}:${generated.line}:${generated.column}`),
|
|
61
|
+
...(path === null ? [] : [field('Artifact', artifact.url)]),
|
|
62
|
+
...(original === null ? [] : [field('Source', `${original.source}:${original.line}:${original.column}`)]),
|
|
63
|
+
field('Mechanism', `${occurrence.mechanism} · realm instance ${occurrence.realmInstance}`),
|
|
64
|
+
];
|
|
65
|
+
const evidenceParts = [
|
|
66
|
+
...(evidence.source === undefined ? [] : [`unmapped source ${evidence.source ?? 'null'}`]),
|
|
67
|
+
...(evidence.rejected === undefined
|
|
68
|
+
? []
|
|
69
|
+
: [`rejected ${evidence.rejected.source}:${evidence.rejected.line}:${evidence.rejected.column}`]),
|
|
70
|
+
...(evidence.slug === undefined ? [] : [`slug ${evidence.slug}`]),
|
|
71
|
+
...(evidence.vendoredBy === undefined ? [] : [`vendored in ${evidence.vendoredBy}`]),
|
|
72
|
+
...(evidence.version === undefined
|
|
73
|
+
? []
|
|
74
|
+
: [`version ${evidence.version.version} (${evidence.version.versionSource})`]),
|
|
75
|
+
];
|
|
76
|
+
if (evidenceParts.length > 0)
|
|
77
|
+
lines.push(field('Evidence', evidenceParts.join(' · ')));
|
|
78
|
+
lines.push(field('Confidence', confidenceOf(occurrence.confidence)), ' Trace');
|
|
79
|
+
const width = Math.max(0, ...occurrence.trace.map((step) => step.kind.length));
|
|
80
|
+
lines.push(...occurrence.trace.map((step) => ` ${step.kind.padEnd(width)} ${step.text}`));
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
83
|
+
function field(label, value) {
|
|
84
|
+
return ` ${label.padEnd(12)} ${value}`;
|
|
85
|
+
}
|
|
86
|
+
function attributionOf({ attribution }) {
|
|
87
|
+
const name = attribution.package === undefined ? [] : [`package ${attribution.package}`];
|
|
88
|
+
const vendored = attribution.vendoredBy === undefined ? [] : [`vendored in ${attribution.vendoredBy}`];
|
|
89
|
+
return [attribution.key, ...name, ...vendored, `provenance ${attribution.provenance}`].join(' · ');
|
|
90
|
+
}
|
|
91
|
+
/** Each confidence dimension's level, with its reasons when it has any (§9). */
|
|
92
|
+
function confidenceOf(confidence) {
|
|
93
|
+
return ['target', 'mutation', 'attribution']
|
|
94
|
+
.map((dimension) => {
|
|
95
|
+
const { level, reasons } = confidence[dimension];
|
|
96
|
+
return `${dimension} ${level}${reasons.length === 0 ? '' : ` (${reasons.join(', ')})`}`;
|
|
97
|
+
})
|
|
98
|
+
.join(' · ');
|
|
99
|
+
}
|