candor-ts 0.39.3 → 0.40.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 CHANGED
@@ -23,7 +23,7 @@ the TypeScript-specific production + query surface.
23
23
  > **Already installed? Report the version and ask before upgrading — before you scan.** If this
24
24
  > project already has candor (a `.candor/` report directory, or `candor-ts` reachable via `npx`/a
25
25
  > global install), do this *first*: run `npx -y candor-ts --version` (offline) and **tell the user
26
- > plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.39)."*
26
+ > plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.40)."*
27
27
  > On a build too old for the flag, read `candor.version` / `candor.spec` from an existing
28
28
  > `.candor/report*.json`, or `npm ls -g candor-ts`.
29
29
  >
@@ -62,9 +62,12 @@ set the exit code. A checked-in `.candor/config` (spec §3.4; `policy <file>` /
62
62
  anchored to the config's repo) is the no-env-wiring floor; flag → env → config → default.
63
63
 
64
64
  **The AS-EFF-005 baseline guard** (spec §7): set `CANDOR_BASELINE=<saved report.json>` (or the
65
- config `baseline` key) and the scan compares per function — an EXISTING function that gained an
66
- effect versus the baseline fails the run (exit 1, `[AS-EFF-005]` lines, records join `--gate-json`);
67
- new functions are exempt. Fail-closed: an unparseable baseline, or one from a different engine
65
+ config `baseline` key) and the scan compares per function — a function that gained an effect versus
66
+ the baseline fails the run (exit 1, `[AS-EFF-005]` lines, records join `--gate-json` with `origin`);
67
+ ⟨0.40⟩ a function ABSENT from the baseline is compared against nothing (a new effectful function fails,
68
+ `origin:"new"`; a new pure one passes; a new `Unknown`-only one is named in a note) — keys are
69
+ `Module.fn`, so a renamed file reads as absent: run `candor diff <this run's report> <baseline>` before
70
+ re-recording. Fail-closed: an unparseable baseline, or one from a different engine
68
71
  build, is invalid gate input — exit 2 WITHOUT evaluating (never a silent skip); an absent file is a
69
72
  note and the guard is inactive. `query diff` is the read-only twin: it DISCLOSES a build mismatch
70
73
  (⚠, exit 0) instead of failing — use the scan-time guard, not `diff`, as the CI gate. Semantics
package/README.md CHANGED
@@ -50,9 +50,12 @@ scan target; relative values resolve against the config's repo, so CI is "point
50
50
  configured-but-unusable config/policy/baseline fails loud (exit 2), never silently gateless.
51
51
 
52
52
  The scan-time **baseline guard** (AS-EFF-005, spec §7) makes effect *regressions* un-shippable:
53
- point `CANDOR_BASELINE` (or the config's `baseline` key) at a saved report, and any existing
53
+ point `CANDOR_BASELINE` (or the config's `baseline` key) at a saved report, and any
54
54
  function that **gained** an effect fails the scan — exit 1, the records join the `--gate-json`
55
- verdict. New functions are exempt (reviewed as new code, not a regression). The guard is
55
+ verdict, each carrying `origin` (`existing` / `new` / `unknown`). ⟨0.40⟩ A function ABSENT from the
56
+ baseline is compared against nothing, so a new effectful function fails too; a new pure one passes,
57
+ and a new `Unknown`-only one is named in a note. `Module.fn` keys mean a renamed file reads as absent:
58
+ review with `candor diff <this run's report> <baseline>` before re-recording. The guard is
56
59
  fail-closed like the policy gate: a present-but-unparseable baseline, or one produced by a
57
60
  different engine build (§2.1 — an engine upgrade is baseline-invalidating), exits 2 **without
58
61
  evaluating**; only a genuinely absent file is a one-line note (guard not active). Keep the two
@@ -202,7 +205,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
202
205
  | A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
203
206
  | Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
204
207
  | The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
205
- | `{ candor: { version, toolchain, spec: "0.39" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
208
+ | `{ candor: { version, toolchain, spec: "0.40" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
206
209
  | Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
207
210
  | The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
208
211
 
@@ -220,7 +223,7 @@ read the Rust source".
220
223
 
221
224
  ## Status
222
225
 
223
- Speaking candor-spec 0.39 (the package version is in `package.json`): the analysis core, the gate (`--policy` / `--gate-json` /
226
+ Speaking candor-spec 0.40 (the package version is in `package.json`): the analysis core, the gate (`--policy` / `--gate-json` /
224
227
  `.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
225
228
  `--include-unknown` dispatch frontier, and ⟨0.24⟩ `gate --report` — the gate applied to an EXISTING
226
229
  report, byte-equivalent to `scan --policy`'s verdict), the MCP server, the LSP server, and the watch loop are
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.39.3",
3
+ "version": "0.40.0",
4
4
  "mcpName": "io.github.tombaldwin/candor",
5
- "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.39)",
5
+ "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.40)",
6
6
  "type": "module",
7
7
  "dependencies": {
8
8
  "@types/node": "^25.9.2",
package/query.mjs CHANGED
@@ -730,7 +730,7 @@ function renderPathHuman(fns, cg, fnQ, eff, hedge = false) {
730
730
  // package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
731
731
  const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
732
732
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
733
- const SPEC_VERSION = "0.39";
733
+ const SPEC_VERSION = "0.40";
734
734
 
735
735
  // ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
736
736
  // One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
package/scan-core.mjs CHANGED
@@ -259,12 +259,64 @@ export const RESERVED_SIDECAR_SEGMENTS =
259
259
  // into a use-verb denylist beside the other two; that is a wider change with its own
260
260
  // over-charge bill to price, so it is filed rather than smuggled in here.
261
261
 
262
+ //
263
+ // SOUNDNESS R781 (the destination half) — THE URL-FIRST CLIENT VERBS OF THE WHOLE-MODULE Net PACKAGES WERE
264
+ // MISSING. κ charges every member of `undici`/`got`/`axios`/… `Net`, so `undici.stream(u, …)`,
265
+ // `undici.pipeline(u, …)`, `undici.upgrade(u)` carried the effect while contributing no host, and beside
266
+ // `undici.request("https://ok.example/a")` the gate `allow Net in <fn> ok.example` exited 0 — EXECUTED,
267
+ // a local server logged each request to the caller's URL. `stream` and `pipeline` were ALREADY named in
268
+ // `NET_REQUEST_NAMED` below as undici's URL-taking callables: two tables answering one question, and only
269
+ // one was consulted for masking. That set is now held to be a SUBSET of this one (test.mjs), so the next
270
+ // request callable added there cannot be missing here. `axios`'s `postForm`/`putForm`/`patchForm` take the
271
+ // URL first exactly like `post`/`put`/`patch` and are written in with them (R346: the family, not the
272
+ // spelling in hand). FAILURE DIRECTION of these additions: a whole-module Net package whose `stream` takes
273
+ // no destination is now marked incomplete — an over-charge, fail-closed. The pinned ts roster has ZERO
274
+ // unmasked Net calls under any of these names (MASKPROBE over all 28 entries), so the price there is 0.
275
+ //
276
+ // NOT HERE, DELIBERATELY: `listen`/`bind`. This set answers "does this call name the DESTINATION it reaches?",
277
+ // and a listen/bind address names the process's OWN address (SPEC §2 ⟨0.40⟩ "A BIND OR LISTEN ADDRESS IS
278
+ // WHERE THE PROCESS LISTENS"). Putting `listen` here would be wrong in the silent direction: a call in this
279
+ // set marks `incomplete` only when NO host was captured, so `server.listen("10.0.0.5")` (a pipe path that
280
+ // `hostLiteral` reads as a host) would publish the local address AND certify it. The accept side is its own
281
+ // question with its own set, NET_ACCEPTING below.
262
282
  export const NET_ESTABLISHING = new Set(["request", "get", "post", "put", "patch", "delete", "head",
263
283
  "options", "connect", "createConnection", "fetch",
284
+ "stream", "pipeline", "upgrade", "postForm", "putForm", "patchForm",
264
285
  "lookup", "lookupService", "reverse", "resolve", "resolve4", "resolve6", "resolveAny",
265
286
  "resolveCname", "resolveCaa", "resolveMx", "resolveNaptr", "resolveNs", "resolvePtr",
266
287
  "resolveSoa", "resolveSrv", "resolveTlsa", "resolveTxt"]);
267
288
 
289
+ // SOUNDNESS R817 / SPEC §2 ⟨0.40⟩ — THE CALLS THAT ACCEPT. "A UNIT THAT ACCEPTS A CONNECTION TALKS TO PEERS NO
290
+ // LITERAL CAN NAME, SO ITS `Net` SURFACE IS INCOMPLETE": node's server `listen` is the call that hands every
291
+ // arriving connection to the server's handler, so it fixes the peer exactly as `connect` does for a client —
292
+ // except that whoever connects chooses it. Read by scan.mjs's `netAccepting`, which marks `incomplete`
293
+ // UNCONDITIONALLY (no literal makes an accept determined) and suppresses any capture from the call's
294
+ // arguments (its address is local — the R809 fabrication). MEASURED before the fix (SOUNDNESS R781's listen
295
+ // half, PART 96 c_accept): `netm.connect(80, "ok.example")` beside `netm.createServer(h).listen(8080)` read
296
+ // `hosts: ["ok.example"]`, complete, and `allow Net ok.example` exited 0 over a server writing to anyone.
297
+ //
298
+ // MEMBER-KEYED, MODULE-FREE ON PURPOSE, and the module is the derived half. `listen` is read off the RESOLVED
299
+ // declaration, so every server type node ships reaches it through `net.Server.listen` — `http`/`https`/
300
+ // `http2`(`createSecureServer`)/`tls` servers, `new net.Server(h)`, and a project `class S extends
301
+ // net.Server` — without being enumerated; and κ already decides WHICH modules' `listen` is a network call (the
302
+ // net cluster; `socket.io`'s whole-module rule). A `listen` κ does not classify `Net` never reaches this set,
303
+ // so it cannot over-charge a JSON-RPC `connection.listen()`. Two module-specific accepts sit in scan.mjs beside
304
+ // the reader, because their member name is too common to key on alone: `inspector.open` (starts the debugger's
305
+ // WebSocket server) and `ws`'s `WebSocketServer` construction.
306
+ //
307
+ // NOT AN ACCEPT: `bind`. A datagram socket's `bind` receives; its replies go out through `send`, which carries
308
+ // its own locator (SPEC: "a datagram receive is not an accept"), and "A bind marks nothing". `createServer` is
309
+ // not one either: it constructs a server and registers a handler, and nothing arrives until a `listen` — which
310
+ // is marked in whichever unit calls it, and `incomplete` reaches that unit's callers over the edge like any
311
+ // other. FAILURE DIRECTION: forgetting an accept spelling here UNDER-reports (an allowlist, R781's warning);
312
+ // the spellings this does not see are listed at the reader.
313
+ export const NET_ACCEPTING = new Set(["listen"]);
314
+
315
+ // The NAMED imports `scan.mjs`'s `importedFromNetPkg` treats as a package's REQUEST CALLABLE (`import {
316
+ // fetch, request, stream, pipeline } from "undici"`). Every one takes its URL first, so every one must also
317
+ // be host-ESTABLISHING above — R781's two-tables shape; test.mjs asserts the subset relation.
318
+ export const NET_REQUEST_NAMED = new Set(["fetch", "request", "stream", "pipeline"]);
319
+
268
320
  // Fs/Exec USE-verbs whose LOCATOR was fixed earlier, not an arg of THIS call — so a missing literal
269
321
  // here is the legitimate split-construct/use shape, never the masking signal (the establishing-
270
322
  // allowlist discipline, generalized from Net to all 4 effects; sweep [11]). Fs: the fd/FileHandle