candor-ts 0.21.0 → 0.23.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
@@ -12,7 +12,7 @@ chains by hand.
12
12
  > **Already installed? Report the version and ask before upgrading — before you scan.** If this
13
13
  > project already has candor (a `.candor/` report directory, or `candor-ts` reachable via `npx`/a
14
14
  > global install), do this *first*: run `npx -y candor-ts --version` (offline) and **tell the user
15
- > plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.21)."*
15
+ > plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.23)."*
16
16
  > On a build too old for the flag, read `candor.version` / `candor.spec` from an existing
17
17
  > `.candor/report*.json`, or `npm ls -g candor-ts`.
18
18
  >
package/Cases.ts CHANGED
@@ -32,6 +32,10 @@ class Holder { cb: () => void = () => {}; }
32
32
  const h = new Holder();
33
33
  export function unknown_dyn(): void { const cb = h.cb; cb(); }
34
34
 
35
+ // An OPAQUE callback handed to a SYNCHRONOUS invoker (forEach) is an unresolvable call -> Unknown,
36
+ // never silently pure (four-way sync-callback-invoker rung).
37
+ export function sync_callback_opaque(xs: number[], cb: (x: number) => void): void { xs.forEach(cb); }
38
+
35
39
  // --- multi-effect union in one body ---
36
40
  export function combined(): void { try { fsm.readFileSync("/tmp/x"); netm.connect(1, "h"); } catch {} }
37
41
 
package/README.md CHANGED
@@ -184,7 +184,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
184
184
  | A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
185
185
  | Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
186
186
  | The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
187
- | `{ candor: { version, toolchain, spec: "0.21" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
187
+ | `{ candor: { version, toolchain, spec: "0.23" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
188
188
  | Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
189
189
  | The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
190
190
 
@@ -202,7 +202,7 @@ read the Rust source".
202
202
 
203
203
  ## Status
204
204
 
205
- 0.19.x, speaking candor-spec 0.21: the analysis core, the gate (`--policy` / `--gate-json` /
205
+ 0.19.x, speaking candor-spec 0.23: the analysis core, the gate (`--policy` / `--gate-json` /
206
206
  `.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
207
207
  `--include-unknown` dispatch frontier), the MCP server, the LSP server, and the watch loop are
208
208
  real, behaviorally tested (`npm test` — the behavioral suite across six harnesses), **soundness-fuzzed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.21.0",
4
- "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.21)",
3
+ "version": "0.23.0",
4
+ "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.23)",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@types/node": "^25.9.2",
@@ -13,7 +13,9 @@
13
13
  "candor-ts-mcp": "./mcp.mjs",
14
14
  "candor-ts-watch": "./watch.mjs",
15
15
  "candor-mcp": "./mcp.mjs",
16
- "candor-lsp": "./lsp.mjs"
16
+ "candor-lsp": "./lsp.mjs",
17
+ "candor-ts-verify": "./verify.mjs",
18
+ "candor-ts-sensitivity": "./sensitivity.mjs"
17
19
  },
18
20
  "scripts": {
19
21
  "lint": "eslint *.mjs",
@@ -55,7 +57,14 @@
55
57
  "surface.mjs",
56
58
  "mcp.mjs",
57
59
  "watch.mjs",
58
- "lsp.mjs"
60
+ "lsp.mjs",
61
+ "verify.mjs",
62
+ "verify-core.mjs",
63
+ "verify-preload.mjs",
64
+ "verify-emit.mjs",
65
+ "verify-loader.mjs",
66
+ "verify-syscall.mjs",
67
+ "sensitivity.mjs"
59
68
  ],
60
69
  "devDependencies": {
61
70
  "@eslint/js": "^9.39.4",
package/query-core.mjs CHANGED
@@ -29,7 +29,7 @@ function siblings(prefix, predicate) {
29
29
  // sibling is `.encountered-*`/`.calibrated.json` passes the existence check but loads ZERO functions →
30
30
  // an authoritative-empty result (silent under-report; review find). `.gate.json` has no functions array,
31
31
  // so merging it "disclosed a malformed report" on every query over the recommended CI layout — noisy, excluded.
32
- export const isReport = (f) => !f.endsWith(".callgraph.json") && !f.endsWith(".hierarchy.json") && !f.includes(".encountered-") && !f.endsWith(".calibrated.json") && !f.endsWith(".gate.json");
32
+ export const isReport = (f) => !f.endsWith(".callgraph.json") && !f.endsWith(".hierarchy.json") && !f.endsWith(".locs.json") && !f.includes(".encountered-") && !f.endsWith(".calibrated.json") && !f.endsWith(".gate.json");
33
33
 
34
34
  // A report exists at the prefix if there's an exact `<prefix>.json` (candor-ts) OR a sibling
35
35
  // `<prefix>.<crate>.scan.json` (the candor-scan/Rust multi-report form) — the loaders read both, so a
package/query.mjs CHANGED
@@ -202,7 +202,7 @@ function renderPathHuman(fns, cg, fnQ, eff) {
202
202
  // package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
203
203
  const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
204
204
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
205
- const SPEC_VERSION = "0.21";
205
+ const SPEC_VERSION = "0.23";
206
206
 
207
207
  // ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
208
208
  // One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
package/scan-core.mjs CHANGED
@@ -109,6 +109,11 @@ export const KAPPA_RULES = [
109
109
  /^(capture|captureImmediate|identify|identifyImmediate|alias|groupIdentify|flush|shutdown|isFeatureEnabled|getFeatureFlag|getFeatureFlagPayload|getAllFlags|getAllFlagsAndPayloads|getRemoteConfigPayload|reloadFeatureFlags)$/,
110
110
  "Net"],
111
111
  [/^(pg|mysql2?|mongodb|ioredis|redis|sqlite3|better-sqlite3|knex)$/, null, "Db"],
112
+ // Template-literal SQL clients that EXECUTE the query through a `sql`…`` tag: postgres.js (porsager),
113
+ // @vercel/postgres, slonik. Whole-module Db (the client factory + the tag both reach the connection, like
114
+ // pg's `new Pool()`). NOT `sql-template-tag` — that only BUILDS a query object (executed via pg), so it is
115
+ // a pure builder and stays uncurated. The tag call is classified through the TaggedTemplateExpression arm.
116
+ [/^(postgres|@vercel\/postgres|slonik)$/, null, "Db"],
112
117
  // bull/bullmq are Redis-backed job queues — the queue/worker/job ops issue Redis commands (Db). Their
113
118
  // surface is almost entirely I/O, but be VERB-precise (the I/O ops) so inert event-wiring
114
119
  // (`queue.on(...)`) and `new Queue()`/`new Worker()` construction (which only opens a lazy connection)
@@ -163,8 +168,14 @@ export const KAPPA_RULES = [
163
168
  // builders; `createQueryBuilder` is pure, its `getMany`/`execute` is the I/O). Found on the
164
169
  // first framework-APP scan: a TypeORM/Nest application — Db-heavy by construction — read zero
165
170
  // Db because the ORM resolved into an unlisted package (the JVM's Spring-Data lesson, replayed).
171
+ // The verb set is the QUERY surface PLUS the DataSource lifecycle that performs connection/DDL I/O:
172
+ // `initialize`/`connect` OPEN the pool (a real round-trip to the server), and
173
+ // `synchronize`/`runMigrations`/`undoLastMigration`/`dropDatabase` execute DDL/migration SQL — all Db.
174
+ // (Found dogfooding ukri-tfs: `buildPostgresDataSource` did `new DataSource(o).initialize()` and read
175
+ // PURE — a false all-clear on a fn that opens a database connection.) Still module-gated to typeorm, so
176
+ // these generic-looking verbs only fire on a typeorm-typed receiver; the pure builder heads stay pure.
166
177
  [/^(typeorm|@nestjs\/typeorm)$/,
167
- /^(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)/,
178
+ /^(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)/,
168
179
  "Db"],
169
180
  [/^(@prisma\/client|\.prisma|\.prisma\/client)$/,
170
181
  /^(\$?(queryRaw|executeRaw|transaction)|find(Many|Unique|First)|create|createMany|update|updateMany|upsert|delete|deleteMany|aggregate|count|groupBy)/,