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 +7 -4
- package/README.md +7 -4
- package/package.json +2 -2
- package/query.mjs +1 -1
- package/scan-core.mjs +52 -0
- package/scan.mjs +2037 -391
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.
|
|
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 —
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|