candor-ts 0.13.0 → 0.14.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.13)."*
15
+ > plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.14)."*
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/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.13" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
187
+ | `{ candor: { version, toolchain, spec: "0.14" }, 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.13.x, speaking candor-spec 0.13: the analysis core, the gate (`--policy` / `--gate-json` /
205
+ 0.14.x, speaking candor-spec 0.14: 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.13.0",
4
- "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.13)",
3
+ "version": "0.14.0",
4
+ "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.14)",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@types/node": "^25.9.2",
package/query.mjs CHANGED
@@ -94,7 +94,7 @@ function renderPathHuman(fns, cg, fnQ, eff) {
94
94
  // package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
95
95
  const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
96
96
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
97
- const SPEC_VERSION = "0.13";
97
+ const SPEC_VERSION = "0.14";
98
98
 
99
99
  // ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
100
100
  // One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
package/scan.mjs CHANGED
@@ -41,7 +41,7 @@ const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
41
41
  // literal stamped into the envelope's `spec` field, so the doc lines and the report can never drift.
42
42
  // Reused, never re-littered.
43
43
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
44
- const SPEC_VERSION = "0.13";
44
+ const SPEC_VERSION = "0.14";
45
45
 
46
46
  // --version: a print-and-exit MODE, handled before the main arg walk so it never depends on a target.
47
47
  // Fully OFFLINE — candor never phones home. Staying current is the AGENT's job: read the installed
@@ -1091,6 +1091,29 @@ function enumerateGetters(owner, type) {
1091
1091
  }
1092
1092
  }
1093
1093
 
1094
+ // The synthesized `<module>` unit for a source file's TOP-LEVEL executable statements (spec §2
1095
+ // unitKind "initializer" — java's `<clinit>` twin). Top-level `await fetch(…)`, a bare
1096
+ // `readFileSync(…)`, an IIFE, `export const r = await fetch(…)` execute at MODULE-LOAD time and
1097
+ // belong to nothing named — without this unit their effects reached the SourceFile in `enclosing`,
1098
+ // resolved to `null`, and were DROPPED → a false "pure" verdict (the cardinal sin: ESM top-level
1099
+ // await / serverless handler files / side-effecting config modules scanned as functions: []). This
1100
+ // is the field-initializer `Class.constructor` synthesis (~scan.mjs:774) one level up: the module
1101
+ // body is the file's own initializer. Minted LAZILY — only when a top-level statement actually
1102
+ // attributes an effect/edge here — so a pure top-level never gains a unit (pure units are omitted).
1103
+ // The qual mirrors sibling top-level units (`moduleOf(sf).<module>`); the bare local is `<module>`.
1104
+ function moduleUnit(sf) {
1105
+ const mod = moduleOf(sf);
1106
+ const qual = `${mod}.<module>`;
1107
+ let rec = fns.get(qual);
1108
+ if (!rec) {
1109
+ rec = { local: "<module>", direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
1110
+ cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), why: new Set(),
1111
+ entry: false, unitKind: "initializer",
1112
+ loc: `${path.relative(rootDir, sf.fileName)}:1:1` };
1113
+ fns.set(qual, rec);
1114
+ }
1115
+ return qual;
1116
+ }
1094
1117
  // nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
1095
1118
  function enclosing(node) {
1096
1119
  for (let p = node; p; p = p.parent) {
@@ -1104,6 +1127,9 @@ function enclosing(node) {
1104
1127
  if (ts.isDecorator(p)) return null;
1105
1128
  const n = nodeName.get(p);
1106
1129
  if (n) return n;
1130
+ // Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
1131
+ // file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
1132
+ if (ts.isSourceFile(p)) return moduleUnit(p);
1107
1133
  }
1108
1134
  return null;
1109
1135
  }
@@ -2236,7 +2262,10 @@ for (const [name, rec] of fns) {
2236
2262
  // them are NOT in `inferred`, so it is a LOWER BOUND when this is non-empty. Omitted when none.
2237
2263
  if (rec.blind.size) entry.invisible = [...rec.blind].sort();
2238
2264
  if (rec.entry) entry.entryPoint = true;
2239
- if (rec.isCjsExport) entry.unitKind = "export"; // spec 0.5 draft, informative — per-unit, not by name
2265
+ // unitKind (spec §2, informative — per-unit, not by name): the synthesized `<module>` initializer
2266
+ // carries its own kind (set at mint), a CJS export is tagged "export".
2267
+ if (rec.unitKind) entry.unitKind = rec.unitKind;
2268
+ else if (rec.isCjsExport) entry.unitKind = "export"; // spec 0.5 draft, informative — per-unit, not by name
2240
2269
  functions.push(entry);
2241
2270
  }
2242
2271
  // `package` names what this report COVERS — a consumer chaining it registers coverage even when