candor-ts 0.34.0 → 0.36.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/AGENTS.md +4 -2
- package/README.md +6 -3
- package/package.json +2 -2
- package/query.mjs +1 -1
- package/scan-core.mjs +374 -17
- package/scan.mjs +2100 -112
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Using candor-ts (instructions for an AI coding agent)
|
|
2
2
|
|
|
3
3
|
You are working in a TypeScript project. **candor-ts** tells you, for every function, which side
|
|
4
|
-
effects it can reach — network,
|
|
4
|
+
effects it can reach — network, calls to LLM/model providers (`Llm`, refines network), filesystem,
|
|
5
|
+
database, subprocess, env, clock, IPC (`Ipc`), logging (`Log`), randomness (`Rand`), clipboard
|
|
6
|
+
(`Clipboard`) — *including effects
|
|
5
7
|
inherited transitively through any chain of calls across files*. Use it instead of tracing call
|
|
6
8
|
chains by hand. The language-agnostic consumption contract is
|
|
7
9
|
[candor-spec/AGENTS.md](https://github.com/tombaldwin/candor-spec/blob/main/AGENTS.md); this file is
|
|
@@ -21,7 +23,7 @@ the TypeScript-specific production + query surface.
|
|
|
21
23
|
> **Already installed? Report the version and ask before upgrading — before you scan.** If this
|
|
22
24
|
> project already has candor (a `.candor/` report directory, or `candor-ts` reachable via `npx`/a
|
|
23
25
|
> global install), do this *first*: run `npx -y candor-ts --version` (offline) and **tell the user
|
|
24
|
-
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.
|
|
26
|
+
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.36)."*
|
|
25
27
|
> On a build too old for the flag, read `candor.version` / `candor.spec` from an existing
|
|
26
28
|
> `.candor/report*.json`, or `npm ls -g candor-ts`.
|
|
27
29
|
>
|
package/README.md
CHANGED
|
@@ -90,7 +90,10 @@ the service reaching `user` and `follows`.
|
|
|
90
90
|
|
|
91
91
|
**The classifier** is curated (the same under-report-and-say-so posture as the other engines): the
|
|
92
92
|
Node builtins (`fs`, `net`/`http`/`tls`, `dns`, `child_process`, `worker_threads`, `node:sqlite`,
|
|
93
|
-
`node:vm`, `process.env`, the clock), the
|
|
93
|
+
`node:vm`, `process.env`, the clock), the **web network globals** (`fetch`, `XMLHttpRequest`,
|
|
94
|
+
`navigator.sendBeacon`, and `WebSocket`/`EventSource` — construction, `send` and `close`, charged
|
|
95
|
+
identically whether they resolve through `lib.dom` or through `@types/node`'s `undici-types`
|
|
96
|
+
re-export), the HTTP/queue/mail tier (axios/got/node-fetch/undici/ws/
|
|
94
97
|
socket.io/nodemailer, gaxios + googleapis-common + google-auth-library, stripe, @sentry/*,
|
|
95
98
|
posthog-node, bull/bullmq), the database drivers (pg/mysql2/mongodb/redis/ioredis/sqlite3/
|
|
96
99
|
better-sqlite3/knex) **and the ORM tier** (TypeORM — with `@Entity("…")` table extraction —
|
|
@@ -198,7 +201,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
|
|
|
198
201
|
| A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
|
|
199
202
|
| Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
|
|
200
203
|
| The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
|
|
201
|
-
| `{ candor: { version, toolchain, spec: "0.
|
|
204
|
+
| `{ candor: { version, toolchain, spec: "0.36" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
|
|
202
205
|
| Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
|
|
203
206
|
| The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
|
|
204
207
|
|
|
@@ -216,7 +219,7 @@ read the Rust source".
|
|
|
216
219
|
|
|
217
220
|
## Status
|
|
218
221
|
|
|
219
|
-
0.30.0, speaking candor-spec 0.
|
|
222
|
+
0.30.0, speaking candor-spec 0.36: the analysis core, the gate (`--policy` / `--gate-json` /
|
|
220
223
|
`.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
|
|
221
224
|
`--include-unknown` dispatch frontier, and ⟨0.24⟩ `gate --report` — the gate applied to an EXISTING
|
|
222
225
|
report, byte-equivalent to `scan --policy`'s verdict), the MCP server, the LSP server, and the watch loop are
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
|
+
"description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.36)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@types/node": "^25.9.2",
|
package/query.mjs
CHANGED
|
@@ -717,7 +717,7 @@ function renderPathHuman(fns, cg, fnQ, eff, hedge = false) {
|
|
|
717
717
|
// package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
|
|
718
718
|
const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
719
719
|
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
|
|
720
|
-
const SPEC_VERSION = "0.
|
|
720
|
+
const SPEC_VERSION = "0.36";
|
|
721
721
|
|
|
722
722
|
// ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
|
|
723
723
|
// One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
|
package/scan-core.mjs
CHANGED
|
@@ -11,12 +11,193 @@ export function isTestPath(p) {
|
|
|
11
11
|
return /(^|\/)(node_modules|__tests__|tests?|spec)(\/|$)/.test(p) || /\.(test|spec)\.[mc]?tsx?$/.test(p);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** R111 — the `Performance` members that READ A CLOCK, as ONE definition read by BOTH tables.
|
|
15
|
+
*
|
|
16
|
+
* WHY THIS IS A SHARED EXPORT AND NOT TWO LITERALS. The same question — "is this `Performance` member
|
|
17
|
+
* effect-bearing?" — is answered in two places, because the same source line resolves to two different
|
|
18
|
+
* declarations depending on `lib`: through lib.dom it lands on `<es-lib>` and is judged by scan.mjs's
|
|
19
|
+
* `parent === "Performance"` arm; through `@types/node` it lands in `perf_hooks.d.ts` and is judged by
|
|
20
|
+
* the κ rule below. R109 and R110 are what those two tables did when each was edited alone: they
|
|
21
|
+
* disagreed in BOTH directions, once each way. This constant does not merge the tables — that is a
|
|
22
|
+
* costed, separate piece of work — but it does remove the ONE degree of freedom that produced both
|
|
23
|
+
* defects, by making the member set a single token that cannot be widened on one side only.
|
|
24
|
+
*
|
|
25
|
+
* MEMBERSHIP IS BY EXECUTED GROUND TRUTH under node 22.12.0, not by name or by intuition:
|
|
26
|
+
* now() two calls in one process return different values → CLOCK
|
|
27
|
+
* mark(name) `performance.mark("m").startTime` is the live elapsed value;
|
|
28
|
+
* two marks 30ms apart differ by 30.05ms → CLOCK
|
|
29
|
+
* measure(name[, start]) `measure("m")` with no end reads NOW (two calls 30ms apart differ) → CLOCK
|
|
30
|
+
* eventLoopUtilization() `.active` advances with wall time between two calls → CLOCK
|
|
31
|
+
*
|
|
32
|
+
* AND THE MEMBERS DELIBERATELY LEFT OUT, each for a measured reason rather than an oversight — the
|
|
33
|
+
* precision half of this rule is the whole job, because User Timing is pervasive in tracing code:
|
|
34
|
+
* timerify(fn) NOT a clock read. MEASURED: it returns a NEW wrapper function (`t !== fn`, and two
|
|
35
|
+
* calls return two different objects); the clock read happens when the WRAPPER runs,
|
|
36
|
+
* observed via a PerformanceObserver `function` entry. The brief for this row asserted
|
|
37
|
+
* timerify "reads the clock"; it does not, and charging it here would be a fabrication
|
|
38
|
+
* on the enclosing function. The hole it leaves is real but is a DIFFERENT class — a
|
|
39
|
+
* factory returning an effectful callable — and is filed as such, not folded in here.
|
|
40
|
+
* toJSON() returns `{nodeTiming, timeOrigin, eventLoopUtilization}`, and MEASURED, its
|
|
41
|
+
* `nodeTiming` is a LIVE REFERENCE (`a.nodeTiming === b.nodeTiming`) — so the clock is
|
|
42
|
+
* read by the later `.duration` property access, not by this call. Same shape as
|
|
43
|
+
* timerify: the call hands you a reader.
|
|
44
|
+
* timeOrigin, nodeTiming, and `PerformanceEntry.startTime`/`.duration`
|
|
45
|
+
* are PROPERTY ACCESSES, and the classify site they would have to be judged at
|
|
46
|
+
* (scan.mjs, `ts.isCallExpression(node) || ts.isNewExpression(node)`) never sees a bare
|
|
47
|
+
* property access. No regex here can reach them; saying so is the point, because
|
|
48
|
+
* "not charged" and "not reachable by this mechanism" are different facts and only the
|
|
49
|
+
* second one tells the next reader where to look. `timeOrigin` is separately
|
|
50
|
+
* interesting — measured CONSTANT within a process, and equal to the wall-clock at
|
|
51
|
+
* process start (`timeOrigin + now()` tracks `Date.now()`) — so it discloses the wall
|
|
52
|
+
* clock without reading it live. Filed, not fixed.
|
|
53
|
+
* clearMarks, clearMeasures, clearResourceTimings, getEntries, getEntriesByName, getEntriesByType,
|
|
54
|
+
* setResourceTimingBufferSize, markResourceTiming, addEventListener, removeEventListener
|
|
55
|
+
* read no clock at the call. `getEntries*` returns entries whose `startTime`s were
|
|
56
|
+
* recorded EARLIER (measured: the returned timestamps do not advance between two calls
|
|
57
|
+
* 30ms apart) — a lookup of past reads, charged at the `mark` that made them.
|
|
58
|
+
*
|
|
59
|
+
* SO THIS IS A VERB LIST, NOT AN INTERFACE KEY, and that is the opposite call from R109's whole-`Storage`
|
|
60
|
+
* predicate for a reason that is about the interface rather than a preference: every `Storage` member
|
|
61
|
+
* touches the store, so a verb list there could only under-report, whereas `Performance` is genuinely
|
|
62
|
+
* mixed and ten of its members demonstrably read nothing. The denylist argument does not apply to a
|
|
63
|
+
* type whose majority surface is inert.
|
|
64
|
+
*
|
|
65
|
+
* ONE MEMBER IS NODE-ONLY AND THAT IS NOT AN ACCIDENT OF SPELLING: lib.dom's `Performance` declares no
|
|
66
|
+
* `eventLoopUtilization` at all, so a lib.dom fixture calling it does not COMPILE (§E3 — checked with
|
|
67
|
+
* `tsc`, not assumed). It is in this one set because the set is keyed on the MEMBER, and the es-lib arm
|
|
68
|
+
* simply never sees that name; an alternative that cannot match is not a fabrication risk, and splitting
|
|
69
|
+
* the set in two to avoid it would reintroduce exactly the drift this constant exists to prevent.
|
|
70
|
+
*/
|
|
71
|
+
export const CLOCK_READING_PERFORMANCE_MEMBERS = /^(now|mark|measure|eventLoopUtilization)$/;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* R114 — the same question asked of `process` and `console`, and the reason both are SHARED constants.
|
|
75
|
+
*
|
|
76
|
+
* R110's own commit wrote the criterion down — *"SPEC §1 defines `Clock` as reading wall-clock or
|
|
77
|
+
* monotonic time; staying in-process is not the question"* — and then applied it only to the module it
|
|
78
|
+
* was about. §9: an audit's boundary must not be drawn around its own trigger. Applied to the rest of
|
|
79
|
+
* the floor, it is violated by `process.uptime`, `os.uptime`, `process.hrtime`/`.bigint` on the MODULE
|
|
80
|
+
* path, and `console.time`/`timeLog`/`timeEnd`.
|
|
81
|
+
*
|
|
82
|
+
* GROUND TRUTH IS EXECUTED, node 22.12.0 — every charge below, measured, never read off a signature:
|
|
83
|
+
*
|
|
84
|
+
* process.uptime() over a 60 ms busy-wait 0.01006425 -> 0.069538625 delta 59.47 ms
|
|
85
|
+
* os.uptime() over a 1200 ms wait 1947079 -> 1947081 delta 2 s (1 s resolution)
|
|
86
|
+
* process.hrtime.bigint() 1947081928072375n -> 1947081932691916n
|
|
87
|
+
* console.time("C"); …80ms…; console.time("D") C: 119.891ms D: 40.088ms
|
|
88
|
+
*
|
|
89
|
+
* That last pair is the one that had to be measured rather than argued: it proves `console.time()` ITSELF
|
|
90
|
+
* takes a timestamp (two timers started 80 ms apart end 80 ms apart), so it is a clock READ and not only
|
|
91
|
+
* console output. SPEC §1's exemption is for console WRITES — it says plain stdout/stderr is not `Log`
|
|
92
|
+
* and not `Fs` — and it says nothing about the monotonic read this API performs to compute a duration.
|
|
93
|
+
* The precedent is R111's `performance.mark()`, charged for recording a timestamp, which is the same
|
|
94
|
+
* operation under another name.
|
|
95
|
+
*
|
|
96
|
+
* TWO PATHS, ONE SET, and for `process` that is not hygiene — they had already DISAGREED. `scan.mjs`'s
|
|
97
|
+
* global arm charged `process.hrtime.bigint()` Clock while this file's floor listed `hrtime` and `bigint`
|
|
98
|
+
* as reviewed-pure, so ONE LINE HAD TWO ANSWERS SELECTED BY SPELLING (measured on 8c7484e, both `lib`
|
|
99
|
+
* shapes):
|
|
100
|
+
*
|
|
101
|
+
* process.hrtime.bigint() -> ["Clock"]
|
|
102
|
+
* import * as p from "node:process"; p.hrtime.bigint() -> absent, SILENT-PURE
|
|
103
|
+
*
|
|
104
|
+
* and the floor's own comment asserted the pure answer was right *"for the reason `Date.now()` is pure in
|
|
105
|
+
* this engine: nothing charges `Clock` for reading a timer"* — false twice over, since `Date.now()`
|
|
106
|
+
* charges Clock (measured) and the global arm already charged `hrtime`. Attack K: a comment explaining
|
|
107
|
+
* why something is safe, written by the change that needed it to be true. The member set now lives here
|
|
108
|
+
* and BOTH readers import it, so the two can no longer be widened separately — the single degree of
|
|
109
|
+
* freedom that produced R109, R110, R111 and this row.
|
|
110
|
+
*
|
|
111
|
+
* `bigint` is in the set because `process.hrtime.bigint()` resolves to a member declared on the `HRTime`
|
|
112
|
+
* interface in `process.d.ts`, so it arrives at κ under its own bare name with module key `process`.
|
|
113
|
+
*
|
|
114
|
+
* THE SWEEP OF WHAT IS DELIBERATELY LEFT PURE, decided on §1's definition and each one EXECUTED, because
|
|
115
|
+
* a charge list without its exclusions is a verb list that will be right today and stale next quarter:
|
|
116
|
+
* os.freemem() 7912144896 -> 7736311808 across a 300 ms wait — it MOVES, and it is not time.
|
|
117
|
+
* os.totalmem() unchanged; host capacity.
|
|
118
|
+
* os.loadavg() unchanged over 200 ms; scheduler load, not a clock.
|
|
119
|
+
* os.cpus()[].times DOES advance — but it is per-core CPU-time ACCOUNTING, not a wall-clock or
|
|
120
|
+
* monotonic read, and `os.cpus()` is reached overwhelmingly for `.length`, so
|
|
121
|
+
* charging it would put Clock on every core-count query.
|
|
122
|
+
* process.cpuUsage() advances; same argument — consumed CPU time is resource accounting.
|
|
123
|
+
* process.resourceUsage() advances; same family.
|
|
124
|
+
* process.memoryUsage() / constrainedMemory() / availableMemory() memory, not time.
|
|
125
|
+
* os.platform/arch/EOL/machine/getPriority, process.pid inert — and these are the PRECISION control:
|
|
126
|
+
* they gain nothing from this change, in both `lib` arms.
|
|
127
|
+
* `os.homedir`/`tmpdir`/`hostname`/`userInfo`/`networkInterfaces` stay `Env` (their own rule, below) and
|
|
128
|
+
* `perf_hooks` stays with CLOCK_READING_PERFORMANCE_MEMBERS: three sets, three questions, no sharing
|
|
129
|
+
* between them, because a shared predicate across unrelated interfaces is a name collision dressed up.
|
|
130
|
+
*/
|
|
131
|
+
export const CLOCK_READING_PROCESS_MEMBERS = /^(uptime|hrtime|bigint)$/;
|
|
132
|
+
export const CLOCK_READING_CONSOLE_MEMBERS = /^(time|timeEnd|timeLog)$/;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* R130 — the WEB NETWORK GLOBALS, and the third resolution split this family has measured (R109
|
|
136
|
+
* `localStorage`, R110 `performance.now`, now `WebSocket`/`EventSource`). Shared constants for the same
|
|
137
|
+
* reason those are: the es-lib arm in scan.mjs and the κ table below BOTH answer for these names, and a
|
|
138
|
+
* single degree of freedom between two tables is what produced all three rows.
|
|
139
|
+
*
|
|
140
|
+
* THE SPLIT. `@types/node` does not declare `WebSocket`/`EventSource` itself — `web-globals/fetch.d.ts`
|
|
141
|
+
* re-exports them from the `undici-types` PACKAGE, so the resolved declaration is not under `@types/node`
|
|
142
|
+
* and the ⟨0.32⟩ node-core floor (`declIsNodeTypes`) is never consulted. κ had no `undici-types` rule, so
|
|
143
|
+
* the call landed on pure. Resolve the SAME LINE with `"DOM"` in `lib` and the declaration lands in
|
|
144
|
+
* `typescript/lib/lib.dom.d.ts`, where the es-lib arm has charged `Net` since ⟨0.29⟩. MEASURED on
|
|
145
|
+
* PUBLISHED candor-ts 0.34.0, one variable (`lib: ["ES2022"]` vs `lib: ["ES2022","DOM"]`, `types:
|
|
146
|
+
* ["node"]` in both — both fixtures `tsc`-clean):
|
|
147
|
+
*
|
|
148
|
+
* export function connect(u: string) { return new WebSocket(u); }
|
|
149
|
+
* lib.dom -> inferred ["Net"], incomplete ["Net"] deny Net -> exit 1
|
|
150
|
+
* @types/node -> inferred [], invisible ["undici-types"] ALL FIVE policy forms -> exit 0
|
|
151
|
+
*
|
|
152
|
+
* and identically for `new EventSource(u)` and for a fixture whose URL is the literal
|
|
153
|
+
* `"wss://evil.example.com/x"` (lib.dom captures the host; the node arm reports nothing to capture).
|
|
154
|
+
*
|
|
155
|
+
* `NODE_CORE_REVIEWED` says of the net cluster *"`WebSocket` is deliberately absent — it CONNECTS"*. That
|
|
156
|
+
* sentence was true of the list it sits in and false of the engine: the name never reached that list.
|
|
157
|
+
*
|
|
158
|
+
* GROUND TRUTH IS EXECUTED, node 22.12.0, against a real `http.createServer` listener on 127.0.0.1 that
|
|
159
|
+
* counts what arrives — not read off a signature:
|
|
160
|
+
*
|
|
161
|
+
* new WebSocket(`ws://127.0.0.1:${port}/socket`) -> the server's `upgrade` handler fires once
|
|
162
|
+
* ws.send("SECRET-EXFIL") -> one client frame, unmasked payload
|
|
163
|
+
* EXACTLY "SECRET-EXFIL"
|
|
164
|
+
* new EventSource(`http://127.0.0.1:${port}/sse`) -> one HTTP request to /sse
|
|
165
|
+
* (node 22.12 needs --experimental-eventsource;
|
|
166
|
+
* @types/node declares it unconditionally, so
|
|
167
|
+
* source using it type-checks either way)
|
|
168
|
+
*
|
|
169
|
+
* TWO SETS, TWO QUESTIONS. The CTOR set is what OPENS the connection — it is host-ESTABLISHING, so a
|
|
170
|
+
* runtime URL there must mark the surface `incomplete` (scan.mjs `netEstablishing`). The WIRE set is what
|
|
171
|
+
* moves bytes on a connection someone else opened; its argument 0 is the PAYLOAD, never an endpoint, so it
|
|
172
|
+
* must NOT be read for a host literal (see NET_USE_VERBS in scan.mjs — `ws.send("api.example.com")`
|
|
173
|
+
* publishing `hosts:["api.example.com"]` is the same fabrication ⟨0.29⟩ fixed for dgram `send`).
|
|
174
|
+
*
|
|
175
|
+
* `send` IS THE ONE THAT WAS SILENT IN BOTH ARMS, which is the R111 shape — agreement between the two
|
|
176
|
+
* paths is the weakest signal here, not the strongest. `export function exfil(w: WebSocket, s: string) {
|
|
177
|
+
* w.send(s); }` was ABSENT from `functions` under lib.dom (a positive purity claim, no `invisible`, no
|
|
178
|
+
* `Unknown`) and `inferred: []` under `@types/node`, on published 0.34.0, while the es-lib arm three
|
|
179
|
+
* lines away already charges `XMLHttpRequest.send` for the identical operation.
|
|
180
|
+
*
|
|
181
|
+
* FAILURE DIRECTION, stated before the code: these are ADDITIVE name sets. Too NARROW and a connecting
|
|
182
|
+
* global stays pure — the cardinal direction; too WIDE and an inert construction over-charges `Net`,
|
|
183
|
+
* which is visible and complainable. The sets are deliberately the names lib.dom and `undici-types` BOTH
|
|
184
|
+
* declare, so the two arms converge rather than one arm being copied. `close` is in the wire set because
|
|
185
|
+
* it writes a close frame to the peer and because the node:net rule already charges `socket.end()` for
|
|
186
|
+
* the same teardown; `addEventListener`/`onmessage` are NOT — they register a local handler.
|
|
187
|
+
* `WebSocketStream`/`WebSocketError` exist in `undici-types` but are NOT re-exported as globals by
|
|
188
|
+
* `web-globals/fetch.d.ts` (checked, not assumed), so they are reachable only by importing `undici-types`
|
|
189
|
+
* directly, which no runtime package does.
|
|
190
|
+
*/
|
|
191
|
+
export const CONNECTING_WEB_CTORS = /^(WebSocket|EventSource)$/;
|
|
192
|
+
export const WEB_WIRE_MEMBERS = /^(send|close)$/;
|
|
193
|
+
|
|
14
194
|
// ---- κ — the curated classifier (CLASSIFIER §2: the dispatch/execution boundary, not builders) ----
|
|
15
195
|
// Node builtins + a curated npm tier (the same under-report-and-say-so posture as the crate table:
|
|
16
196
|
// an unlisted package contributes nothing — never a guess).
|
|
17
|
-
// One rules TABLE, two readers: kappa() classifies a
|
|
18
|
-
//
|
|
19
|
-
//
|
|
197
|
+
// One rules TABLE, two readers, and ⟨R137⟩ they now ask it the SAME question: kappa() classifies a
|
|
198
|
+
// call; kappaKnows() answers "did a rule COVER this call" for the coverage ledger — same module, same
|
|
199
|
+
// member, same regexes. It used to answer the weaker "is this package named anywhere in the table",
|
|
200
|
+
// and that reading disagreed with κ in the silent direction (see kappaKnows below).
|
|
20
201
|
// [module-name regex, member regex (null = any member), effect]
|
|
21
202
|
// The member token a rule matches against is the resolved declaration's name, EXCEPT a constructor
|
|
22
203
|
// call (`new X()`), whose synthesized token is "new" (its decl `name` is empty — see CLASSIFY). This
|
|
@@ -112,6 +293,20 @@ export const KAPPA_RULES = [
|
|
|
112
293
|
// claim an effect this engine cannot defend against the other three.
|
|
113
294
|
[/^(node:)?process$/, /^(loadEnvFile|writeReport)$/, "Fs"],
|
|
114
295
|
[/^(node:)?process$/, /^(getuid|geteuid|getgid|getegid|getgroups)$/, "Env"],
|
|
296
|
+
// R114 — the monotonic clocks that the floor below reviewed pure BY NAME. `uptime` and `hrtime`/
|
|
297
|
+
// `bigint` are REMOVED from that floor's member list in the same change, rather than left in a list
|
|
298
|
+
// titled "reviewed effect-free" while κ contradicts it one screen up. Set + ground truth at
|
|
299
|
+
// CLOCK_READING_PROCESS_MEMBERS; `scan.mjs`'s global-`process` arm reads the SAME object.
|
|
300
|
+
[/^(node:)?process$/, CLOCK_READING_PROCESS_MEMBERS, "Clock"],
|
|
301
|
+
// R114 — `console.time`/`timeLog`/`timeEnd` take and read a monotonic timestamp (EXECUTED: two timers
|
|
302
|
+
// started 80 ms apart report 119.891ms and 40.088ms). SPEC §1 exempts console WRITES from `Log`/`Fs`;
|
|
303
|
+
// it does not make the clock read behind them invisible. The es-lib arm in scan.mjs reads this same
|
|
304
|
+
// object for the lib.dom `Console` interface.
|
|
305
|
+
[/^(node:)?console$/, CLOCK_READING_CONSOLE_MEMBERS, "Clock"],
|
|
306
|
+
// R114 — `os.uptime()` is seconds since boot: monotonic, and it advanced 2 s over a 1.2 s wait when
|
|
307
|
+
// executed. Its own member rule rather than a shared set, because `os` shares no member NAME with
|
|
308
|
+
// `process` here and pretending otherwise would be the name collision the constants' comment warns of.
|
|
309
|
+
[/^(node:)?os$/, /^uptime$/, "Clock"],
|
|
115
310
|
// node:module — the compile cache is a real on-disk cache directory.
|
|
116
311
|
[/^(node:)?module$/, /^(enableCompileCache|flushCompileCache|getCompileCacheDir)$/, "Fs"],
|
|
117
312
|
// node:util — `debuglog(section)`/`debug(section)` READ $NODE_DEBUG to decide whether the returned
|
|
@@ -119,7 +314,83 @@ export const KAPPA_RULES = [
|
|
|
119
314
|
[/^(node:)?util$/, /^(debuglog|debug)$/, "Env"],
|
|
120
315
|
// The Web Crypto RNG reached through @types/node's own global typings rather than through lib.dom
|
|
121
316
|
// (the es-lib arm at the classify site covers the `Crypto` interface; this covers the node spelling).
|
|
317
|
+
//
|
|
318
|
+
// ON @types/node 25.9.2 THIS RULE DOES NOT FIRE FOR THE MEMBERS IT NAMES, and that is measured, not
|
|
319
|
+
// suspected: `web-globals/crypto.d.ts` declares only `var crypto` and the `Crypto`/`CryptoKey`/
|
|
320
|
+
// `SubtleCrypto` type aliases — every callable member is declared in `crypto.d.ts` (the `webcrypto`
|
|
321
|
+
// namespace), so `declModule` on the resolved `getRandomValues`/`randomUUID` declaration returns
|
|
322
|
+
// `crypto`, and the `(node:)?crypto` → Rand rule below is what actually charges them. Verified by
|
|
323
|
+
// printing the resolved declaration file under `lib: ["ES2022"], types: ["node"]`: both land in
|
|
324
|
+
// `@types/node/crypto.d.ts`. The rule is KEPT — a member DefinitelyTyped moves back into the
|
|
325
|
+
// web-globals file must not become silently pure on the day it moves — but its comment used to claim
|
|
326
|
+
// it "covers the node spelling", which is a guarantee, and a guarantee is what stops a property being
|
|
327
|
+
// measured (attack K). It is a belt over braces, not the load-bearing rule; the `crypto` rule below is.
|
|
122
328
|
[/^web-globals\/crypto$/, /^(getRandomValues|randomUUID|generateKey)/, "Rand"],
|
|
329
|
+
// R110 — `performance.now()` is a monotonic CLOCK READ, and it was silent under `@types/node`.
|
|
330
|
+
// R111 — and so are `mark()`, `measure()` and `eventLoopUtilization()`, silently pure in BOTH arms.
|
|
331
|
+
//
|
|
332
|
+
// THE MIRROR OF R109, POINTING THE OTHER WAY. Resolved through lib.dom, `performance.now()` charges
|
|
333
|
+
// `Clock` at the es-lib arm in scan.mjs (`parent === "Performance" && name === "now"`). Resolved
|
|
334
|
+
// through `@types/node` it charged NOTHING: the declaration lands in `perf_hooks.d.ts`, whose
|
|
335
|
+
// NODE_CORE_REVIEWED entry marks the module reviewed-pure with the justification "(in-process
|
|
336
|
+
// timing)" — which conflates "does not leave the process" with "reads no clock". SPEC §1 defines
|
|
337
|
+
// `Clock` as *reading wall-clock or monotonic time*; staying in-process is not the question.
|
|
338
|
+
//
|
|
339
|
+
// Together with R109 the pair establishes that the two resolution paths disagree in BOTH directions,
|
|
340
|
+
// so neither is the trusted one. The direction chosen here is not "copy lib.dom": `now()` was
|
|
341
|
+
// ground-truthed by EXECUTION under node 22.12.0 — two calls in one process return different values,
|
|
342
|
+
// and `timeOrigin + now()` tracks `Date.now()` to within a few ms.
|
|
343
|
+
//
|
|
344
|
+
// THE MODULE KEY IS `perf_hooks`, NOT `web-globals/performance`, and that distinction is the whole
|
|
345
|
+
// reason this needed measuring rather than mirroring the crypto line above. `web-globals/
|
|
346
|
+
// performance.d.ts` declares `var performance` and nothing callable — its type is
|
|
347
|
+
// `perf_hooks.Performance`, so `declModule` on the resolved `now` declaration returns `perf_hooks`.
|
|
348
|
+
// A rule keyed on `web-globals/performance` would never fire. Printed the resolved declaration file
|
|
349
|
+
// for both arms before writing this: lib.dom → `typescript/lib/lib.dom.d.ts`, @types/node →
|
|
350
|
+
// `@types/node/perf_hooks.d.ts`.
|
|
351
|
+
//
|
|
352
|
+
// AND THE lib.dom ARM DOES MOVE, ON ONE SPELLING — said here rather than claimed untouched.
|
|
353
|
+
// `import { performance } from "node:perf_hooks"` (and the bare `"perf_hooks"` specifier) resolves
|
|
354
|
+
// its `now` into `perf_hooks.d.ts` NO MATTER WHICH `lib` is configured, so that spelling was silently
|
|
355
|
+
// pure under BOTH configs before this rule and is charged under both after it. That is a THIRD
|
|
356
|
+
// under-report, not part of the divergence — the two arms AGREED on it, and agreed wrongly, which is
|
|
357
|
+
// why engine agreement is the weakest signal this project has. Measured across nine spellings, both
|
|
358
|
+
// arms, pre and post: the GLOBAL `performance.now()` spelling under lib.dom is byte-identical before
|
|
359
|
+
// and after, so the convergence control holds in the direction it exists for — lib.dom loses nothing,
|
|
360
|
+
// it only gains where it was wrong too. `worker_threads.performance` is NOT a spelling: @types/node
|
|
361
|
+
// 25.9.2 exports no `performance` from that module and the fixture does not compile (§E3 applied
|
|
362
|
+
// before the claim was written, not after — it was in an earlier draft of this comment).
|
|
363
|
+
//
|
|
364
|
+
// A VERB LIST, NOT AN INTERFACE KEY, and this is the opposite call from R109's whole-`Storage`
|
|
365
|
+
// predicate. Every member of `Storage` touches the persistent store, so a verb list there could only
|
|
366
|
+
// under-report. `Performance` is genuinely MIXED: ten of its members — `clearMarks`, `clearMeasures`,
|
|
367
|
+
// `clearResourceTimings`, `getEntries`, `getEntriesByName`, `getEntriesByType`, `markResourceTiming`,
|
|
368
|
+
// `setResourceTimingBufferSize`, `addEventListener`, `removeEventListener` — read no clock at all,
|
|
369
|
+
// and charging them `Clock` would fabricate.
|
|
370
|
+
//
|
|
371
|
+
// R111 WIDENED THIS FROM A LIST OF ONE TO A LIST OF FOUR, and the member set now lives in
|
|
372
|
+
// `CLOCK_READING_PERFORMANCE_MEMBERS` at the top of this file, which the lib.dom arm in scan.mjs reads
|
|
373
|
+
// TOO. R110's original text said widening "would re-open the divergence in the opposite direction,
|
|
374
|
+
// because the lib.dom arm charges `now` and nothing else" — that was true of a widening applied to one
|
|
375
|
+
// table, and it is exactly why this widening was applied to the shared constant instead. The two arms
|
|
376
|
+
// are measured to agree on all four members, both directions, in this commit's own controls.
|
|
377
|
+
//
|
|
378
|
+
// `now` is declared exactly ONCE in all of @types/node (`perf_hooks.d.ts:83`, on `interface
|
|
379
|
+
// Performance`), and the module key is `perf_hooks` rather than a name, so this member regex cannot
|
|
380
|
+
// land on an unrelated `now`/`mark`/`measure` in some other module — the κ table asks the MODULE first.
|
|
381
|
+
[/^(node:)?perf_hooks$/, CLOCK_READING_PERFORMANCE_MEMBERS, "Clock"],
|
|
382
|
+
// R130 — `undici-types`, the package `@types/node` re-exports the web network globals FROM. Not
|
|
383
|
+
// `@types/undici` and not `undici`, so neither `declModule`'s `@types/X -> X` remap nor the `undici`
|
|
384
|
+
// rule below reaches it, and the ⟨0.32⟩ node-core floor does not either (its predicate is the FILE,
|
|
385
|
+
// and this file is not under `@types/node`). Two MEMBER-precise rules rather than a whole-module one:
|
|
386
|
+
// `undici-types` also declares `Headers`, `FormData`, `Request` and `Response`, whose construction and
|
|
387
|
+
// accessors are inert, and whose call sites are ordinary furniture in any project using global `fetch`
|
|
388
|
+
// — a whole-module Net here would fabricate on `new Headers()` and `res.json()`. Measured which
|
|
389
|
+
// declarations those spellings resolve to before choosing (all of them land in `undici-types/fetch.d.ts`).
|
|
390
|
+
// The CTOR rule matches because scan.mjs synthesizes the CLASS NAME as κ's member token for a
|
|
391
|
+
// connecting constructor — the `CONNECTING_CTORS` mechanism `new http.ClientRequest()` already uses.
|
|
392
|
+
[/^undici-types$/, CONNECTING_WEB_CTORS, "Net"],
|
|
393
|
+
[/^undici-types$/, WEB_WIRE_MEMBERS, "Net"],
|
|
123
394
|
// the curated npm tier
|
|
124
395
|
[/^(axios|got|node-fetch|undici|ws|socket\.io(-client)?|nodemailer)$/, null, "Net"],
|
|
125
396
|
// gaxios is the axios-like HTTP client under googleapis (request/get/post/put/patch/delete/head do
|
|
@@ -238,16 +509,32 @@ export const KAPPA_RULES = [
|
|
|
238
509
|
[/^(typeorm|@nestjs\/typeorm)$/,
|
|
239
510
|
/^(find|save|remove|softRemove|recover|insert|update|upsert|delete|restore|count|exist|sum|average|minimum|maximum|query|clear|increment|decrement|getMany|getOne|getOneOrFail|getRawMany|getRawOne|getCount|getExists|execute|stream|transaction|initialize|connect|synchronize|runMigrations|undoLastMigration|dropDatabase)/,
|
|
240
511
|
"Db"],
|
|
512
|
+
// ⟨R137, the CLASSIFIER half⟩ THE THREE ORMS BESIDE typeorm WERE MISSING ITS LIFECYCLE VERBS — §F1.3,
|
|
513
|
+
// separate implementations of one question that drifted. The typeorm rule above deliberately carries
|
|
514
|
+
// `initialize|connect|synchronize|runMigrations|undoLastMigration|dropDatabase|clear|stream` "the
|
|
515
|
+
// DataSource lifecycle that performs connection/DDL I/O", after `buildPostgresDataSource` read PURE on
|
|
516
|
+
// a real app. Its three siblings list only the QUERY surface, so the equivalent calls resolved to
|
|
517
|
+
// nothing — and because `kappaKnows` was package-granular they were not even ledgered (that is the
|
|
518
|
+
// other half of R137). Every verb added below is the named analogue of one typeorm already has:
|
|
519
|
+
// connect/authenticate <- connect · disconnect/close <- (the pairing verb) · sync* <- synchronize
|
|
520
|
+
// drop* <- dropDatabase · truncate <- clear · watch <- stream
|
|
521
|
+
// GROUND TRUTH IS EXECUTED for the one this was found on, node 22.12.0, real `mongoose@8` from npm
|
|
522
|
+
// against a `net.createServer` on 127.0.0.1 counting arrivals:
|
|
523
|
+
// await mongoose.connect("mongodb://127.0.0.1:<port>/gt") -> 1 TCP connection, 299 bytes sent
|
|
524
|
+
// The other five are ANALOGY to typeorm's own list, not execution, and are labelled as such rather
|
|
525
|
+
// than shared a sentence with the measured one. ADDITIVE and module-gated, so the direction this
|
|
526
|
+
// fails in is over-charge on a same-named member of one of these four ORMs; forgetting a verb
|
|
527
|
+
// under-fixes and cannot under-report.
|
|
241
528
|
[/^(@prisma\/client|\.prisma|\.prisma\/client)$/,
|
|
242
|
-
/^(\$?(queryRaw|executeRaw|transaction)|find(Many|Unique|First)|create|createMany|update|updateMany|upsert|delete|deleteMany|aggregate|count|groupBy)/,
|
|
529
|
+
/^(\$?(queryRaw|executeRaw|transaction|connect|disconnect)|find(Many|Unique|First)|create|createMany|update|updateMany|upsert|delete|deleteMany|aggregate|count|groupBy)/,
|
|
243
530
|
"Db"],
|
|
244
531
|
[/^mongoose$/,
|
|
245
|
-
/^(find|save|create|insertMany|updateOne|updateMany|replaceOne|deleteOne|deleteMany|aggregate|countDocuments|estimatedDocumentCount|distinct|exec|bulkWrite)/,
|
|
532
|
+
/^(find|save|create|insertMany|updateOne|updateMany|replaceOne|deleteOne|deleteMany|aggregate|countDocuments|estimatedDocumentCount|distinct|exec|bulkWrite|connect|disconnect|close|drop|sync|watch)/,
|
|
246
533
|
"Db"],
|
|
247
534
|
// Sequelize is EXECUTE-ON-CALL: `Model.findAll()/create()/update()/destroy()` issue the query and
|
|
248
535
|
// return a promise — so its verbs are the I/O boundary.
|
|
249
536
|
[/^sequelize$/,
|
|
250
|
-
/^(find|create|update|destroy|upsert|count|max|min|sum|increment|decrement|reload|save|query|transaction)/,
|
|
537
|
+
/^(find|create|update|destroy|upsert|count|max|min|sum|increment|decrement|reload|save|query|transaction|authenticate|sync|close|drop|truncate)/,
|
|
251
538
|
"Db"],
|
|
252
539
|
// Drizzle is a BUILDER: `db.select().from().where()` / `db.insert().values()` / `db.update().set()` /
|
|
253
540
|
// `db.delete().where()` issue NOTHING until a terminal `.execute()`/await/`.all()`/`.get()`/`.run()` (or
|
|
@@ -353,10 +640,23 @@ export const NODE_CORE_REVIEWED = [
|
|
|
353
640
|
// ── wholly reviewed, effect-free at the call boundary ──
|
|
354
641
|
// Deterministic in-process computation and string manipulation: assert (throws), async_hooks (in-process
|
|
355
642
|
// hook registration), buffer, constants, diagnostics_channel (in-process pub/sub), domain, events, path,
|
|
356
|
-
// perf_hooks
|
|
643
|
+
// perf_hooks, punycode, querystring, string_decoder, url, util/types, zlib.
|
|
644
|
+
// `perf_hooks` USED TO SAY "(in-process timing)" HERE, and that justification was wrong on its own
|
|
645
|
+
// terms (R110): SPEC §1's `Clock` is *reading wall-clock or monotonic time*, and whether the read
|
|
646
|
+
// leaves the process is not the question — the `timers` note three lines down gets this exactly right
|
|
647
|
+
// for the neighbouring module and this one did not. `performance.now()` resolves its declaration into
|
|
648
|
+
// this file, so the module-wide entry made it silently pure under any tsconfig naming `lib` without
|
|
649
|
+
// `DOM`, while the SAME line resolved through lib.dom charged `Clock`. κ now takes `now` above, before
|
|
650
|
+
// this floor is consulted; the REST of perf_hooks (the observer/histogram/entry-list surface) stays
|
|
651
|
+
// reviewed-pure here, which is the precision half of that fix and is pinned by its own controls.
|
|
357
652
|
// Console/TTY I/O — `console`, `readline`, `tty`: §1 has no Console effect and this engine already
|
|
358
653
|
// suppresses the fabricated `Net` on `process.stdout.write` for the same reason; classifying them here
|
|
359
654
|
// would contradict that decision one door along.
|
|
655
|
+
// …AND THAT ARGUMENT IS ABOUT THE WRITE, WHICH IS NOT ALL `console` DOES (R114). `console.time`,
|
|
656
|
+
// `timeLog` and `timeEnd` take and read a monotonic timestamp to compute the duration they print —
|
|
657
|
+
// EXECUTED, two timers started 80 ms apart end 80 ms apart — so κ takes those three above, before this
|
|
658
|
+
// floor is consulted, and the REST of `console` stays reviewed-pure here. Same shape as `perf_hooks`
|
|
659
|
+
// one line up: a module whose justification was true of the surface its author had in mind.
|
|
360
660
|
// `stream` and its submodules: transport plumbing. A stream's effect belongs to the concrete source or
|
|
361
661
|
// sink, and is charged where THAT was constructed (`fs.createReadStream` → Fs) — charging `.pipe()` too
|
|
362
662
|
// would double-count the same open.
|
|
@@ -374,15 +674,34 @@ export const NODE_CORE_REVIEWED = [
|
|
|
374
674
|
// (Request/Response/Headers/FormData) is inert construction. It is listed here so the floor does not
|
|
375
675
|
// stack a second, reasonless `Unknown` on top of a call this engine already answers precisely.
|
|
376
676
|
// `web-globals/storage` is DELIBERATELY ABSENT: Node's `localStorage`/`sessionStorage` persist to a
|
|
377
|
-
// file, so it is not inert and has not been modelled — it fails closed.
|
|
677
|
+
// file, so it is not inert and has not been modelled — it fails closed HERE.
|
|
678
|
+
//
|
|
679
|
+
// AND "IT FAILS CLOSED" WAS A FALSE GUARANTEE FOR AS LONG AS IT STOOD (R109). This table is consulted
|
|
680
|
+
// only when the declaration came from `@types/node`. The SAME source line resolved through lib.dom —
|
|
681
|
+
// which is `lib: [...,"DOM"]` and also the DEFAULT `lib` for any ES target, i.e. the common case —
|
|
682
|
+
// never reached this floor at all: it landed on scan.mjs's conventionally-pure `<es-lib>` arm and read
|
|
683
|
+
// SILENT-PURE, so `deny Unknown` exited 0 over `localStorage.setItem("token", secret)`. One line, two
|
|
684
|
+
// answers, selected by tsconfig. The lib.dom half now has its own `parent === "Storage"` branch at the
|
|
685
|
+
// es-lib arm and converges on this answer; the guarantee has TWO halves and only one of them lives in
|
|
686
|
+
// this file. Same split as the `crypto` note below, which is where the fix shape came from.
|
|
687
|
+
//
|
|
688
|
+
// A FULL SWEEP of the twin set, because an audit scoped to its own trigger misses the next instance:
|
|
689
|
+
// asking `nodeCoreUnreviewed(m, <unenumerated member>)` for every file in `@types/node/web-globals`
|
|
690
|
+
// returns FAILS-CLOSED for `storage` and reviewed-pure for the other fifteen. `storage` was the only
|
|
691
|
+
// member of this class, and it is closed.
|
|
378
692
|
[/^web-globals\/(abortcontroller|blob|console|crypto|domexception|encoding|events|fetch|importmeta|messaging|navigator|performance|streams|timers|url)$/, null],
|
|
379
693
|
// ── mixed modules: the reviewed-pure half, member by member ──
|
|
380
694
|
// node:crypto is deterministic computation plus entropy; κ above takes the entropy (`random*`,
|
|
381
695
|
// `generateKey*`, `generatePrime*`). Written as a denylist of ONE: `setEngine` loads a shared OpenSSL
|
|
382
696
|
// engine library into the process, which is a native-code load, not a hash.
|
|
383
697
|
[/^(node:)?crypto$/, /^(?!setEngine$)/],
|
|
384
|
-
// node:os — the host-identity reads are Env above
|
|
385
|
-
// (platform/arch/cpus/freemem/
|
|
698
|
+
// node:os — the host-identity reads are Env above, `uptime` is Clock above (R114), and the rest is
|
|
699
|
+
// inert introspection (platform/arch/cpus/freemem/totalmem/loadavg/EOL/…). This line USED TO NAME
|
|
700
|
+
// `uptime` in that list, which is how a monotonic clock stayed pure: a module-wide `null` entry is a
|
|
701
|
+
// positive purity claim over every member κ does not take, and its prose is the only place the claim
|
|
702
|
+
// is legible. `freemem`/`loadavg`/`cpus().times` all MOVE between calls and are still left here on
|
|
703
|
+
// purpose — moving is not the criterion, reading a wall-clock or monotonic TIME is (SPEC §1). Each was
|
|
704
|
+
// executed; the measurements are at CLOCK_READING_PROCESS_MEMBERS.
|
|
386
705
|
[/^(node:)?os$/, null],
|
|
387
706
|
// node:util — everything but `debuglog`/`debug`, which κ takes as Env above.
|
|
388
707
|
[/^(node:)?util$/, null],
|
|
@@ -444,11 +763,18 @@ export const NODE_CORE_REVIEWED = [
|
|
|
444
763
|
// rather than left to fail closed, because the alternative is an `Unknown` on a call that returns a
|
|
445
764
|
// number.
|
|
446
765
|
[/^(node:)?process$/,
|
|
447
|
-
// `
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
// and
|
|
451
|
-
|
|
766
|
+
// R114 REMOVED `uptime`, `hrtime` and `bigint` FROM THIS LINE. They were here under a comment claiming
|
|
767
|
+
// `bigint` was "pure for the reason `Date.now()` is pure in this engine: nothing charges `Clock` for
|
|
768
|
+
// reading a timer" — an assertion that was false in both halves. `Date.now()` charges Clock (measured),
|
|
769
|
+
// and `scan.mjs`'s global-`process` arm was ALREADY charging `hrtime` Clock, so this list was the
|
|
770
|
+
// dissenting half of a two-answer disagreement rather than a review. They are κ's now
|
|
771
|
+
// (CLOCK_READING_PROCESS_MEMBERS), which is consulted BEFORE this floor — so leaving them here would
|
|
772
|
+
// have been inert, and that is exactly why they had to go: an inert name in a list titled "reviewed
|
|
773
|
+
// effect-free" is how the next reader learns the wrong fact.
|
|
774
|
+
// `cpuUsage`, `resourceUsage`, `memoryUsage`, `constrainedMemory` and `availableMemory` STAY, measured
|
|
775
|
+
// and deliberately: they advance between calls, and advancing is not the criterion — §1's `Clock` is a
|
|
776
|
+
// wall-clock or monotonic TIME read, and consumed-CPU and heap bytes are resource accounting.
|
|
777
|
+
/^(|new|version|versions|arch|platform|release|config|features|moduleLoadList|getActiveResourcesInfo|cpuUsage|resourceUsage|memoryUsage|constrainedMemory|availableMemory|exit|exitCode|abort|finalization|allowedNodeEnvironmentFlags|assert|emitWarning|nextTick|sourceMapsEnabled|setSourceMapsEnabled|getBuiltinModule|hasUncaughtExceptionCaptureCallback|setUncaughtExceptionCaptureCallback|cwd|env|argv|argv0|execArgv|execPath|pid|ppid|title|debugPort|stdout|stdin|stderr|openStdin|ref|unref|getReport|report|throwDeprecation|traceDeprecation|noDeprecation)$/],
|
|
452
778
|
// The EventEmitter surface, wherever a core module RE-DECLARES it for typed events instead of
|
|
453
779
|
// inheriting it. `process.on("exit", …)` resolves to an overload declared in `process.d.ts`, not to
|
|
454
780
|
// `events.d.ts`, so the module-wide `events` entry above never saw it and an inert listener
|
|
@@ -489,8 +815,39 @@ export const KAPPA_PURE = new Set([
|
|
|
489
815
|
"class-validator", "class-transformer", "reflect-metadata",
|
|
490
816
|
"rxjs", "zod", "lodash", "ramda", "date-fns",
|
|
491
817
|
]);
|
|
492
|
-
|
|
493
|
-
|
|
818
|
+
/**
|
|
819
|
+
* ⟨R137⟩ Did κ COVER THIS CALL? — the κ-coverage ledger's gate, asked of the same (module, member)
|
|
820
|
+
* pair `kappa()` itself was asked, against the same table and the same regexes.
|
|
821
|
+
*
|
|
822
|
+
* IT USED TO ASK A PACKAGE-GRANULAR QUESTION — `KAPPA_RULES.some(([mre]) => mre.test(moduleName))`,
|
|
823
|
+
* the member regex never consulted — which is a SECOND, weaker reading of the one table, and it
|
|
824
|
+
* disagreed with κ in the silent direction: the moment any rule named a package, every OTHER member
|
|
825
|
+
* of that package stopped being disclosed. R130 added two MEMBER-PRECISE `undici-types` rules and
|
|
826
|
+
* that flipped `undici-types` from uncovered to covered wholesale — 5 rows moved from
|
|
827
|
+
* `invisible:["undici-types"]` to entirely absent (a positive purity claim, SPEC §2 rule 3), 11 lost
|
|
828
|
+
* their `invisible`, and the report's whole `coverage:{uncovered:[…]}` block disappeared with its
|
|
829
|
+
* stderr advisory.
|
|
830
|
+
*
|
|
831
|
+
* AND THE CLASS IS NOT `undici-types`. Every member-precise npm rule has the same shape, and the
|
|
832
|
+
* trigger was the mildest instance of it. `mongoose`'s rule lists the QUERY verbs and not `connect`,
|
|
833
|
+
* so `mongoose.connect(uri)` — EXECUTED against a 127.0.0.1 listener: 1 TCP connection, 299 bytes on
|
|
834
|
+
* the wire — left the report with no Db, no Unknown, no `invisible` and no coverage entry, under a
|
|
835
|
+
* stderr line reading "nothing hidden — every effect sits where its name says it should".
|
|
836
|
+
*
|
|
837
|
+
* FAILURE DIRECTION: too WIDE ⇒ over-disclosure. A partly-classified package now contributes
|
|
838
|
+
* `invisible:[pkg]` and a `coverage.uncovered` entry for the calls κ did not answer. `invisible`
|
|
839
|
+
* carries no effect and no `Unknown`, so it cannot flip a `deny <Effect>` gate and cannot silence
|
|
840
|
+
* anything — the direction this cannot fail in is silence, which is the side to be wrong on.
|
|
841
|
+
*
|
|
842
|
+
* `KAPPA_PURE` is unchanged and is still the whole-package ratification outlet: a package whose
|
|
843
|
+
* REMAINING surface has been reviewed effect-free belongs there, so this needed no second table. A
|
|
844
|
+
* WHOLE-MODULE rule (`null` member regex) still covers every member, because its author classified
|
|
845
|
+
* the module's whole call surface — that is what the null means.
|
|
846
|
+
*/
|
|
847
|
+
export function kappaKnows(moduleName, member) {
|
|
848
|
+
if (KAPPA_PURE.has(moduleName)) return true;
|
|
849
|
+
const m = member ?? "";
|
|
850
|
+
return KAPPA_RULES.some(([mre, vre]) => mre.test(moduleName) && (!vre || vre.test(m)));
|
|
494
851
|
}
|
|
495
852
|
|
|
496
853
|
// Refine the Exec cliff (spec §4 ⟨0.5⟩): the effects a literal, statically-known subprocess head
|