candor-ts 0.13.0 → 0.14.1

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.1",
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,9 +1091,57 @@ 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
+ }
1117
+ // The synthesized static-initializer unit for a `class C { static { … } }` block (spec §2 unitKind
1118
+ // "initializer"). A static block runs at class-DEFINITION time, not instance construction — but its
1119
+ // body's effects otherwise walked up in `enclosing` to the ClassDeclaration, which maps to the
1120
+ // `C.constructor` unit, so a static-init effect was MISLABELED as the instance ctor (and carried no
1121
+ // unitKind). Mint it as its own unit, lazily, mirroring `moduleUnit`. (An anonymous class expression's
1122
+ // static block keys under `<anonymous>`; there is at most one static-init unit per class name.)
1123
+ function staticBlockUnit(node) {
1124
+ const cls = node.parent;
1125
+ const sf = node.getSourceFile();
1126
+ const mod = moduleOf(sf);
1127
+ const cname = (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && cls.name ? cls.name.text : "<anonymous>";
1128
+ const qual = `${mod}.${cname}.<static-init>`;
1129
+ let rec = fns.get(qual);
1130
+ if (!rec) {
1131
+ rec = { local: "<static-init>", direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
1132
+ cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), why: new Set(),
1133
+ entry: false, unitKind: "initializer",
1134
+ loc: `${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}:1` };
1135
+ fns.set(qual, rec);
1136
+ }
1137
+ return qual;
1138
+ }
1094
1139
  // nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
1095
1140
  function enclosing(node) {
1096
1141
  for (let p = node; p; p = p.parent) {
1142
+ // A `static { … }` block is its own initializer unit (class-definition time), NOT the instance ctor
1143
+ // the ClassDeclaration maps to — intercept before the nodeName lookup would fold it into .constructor.
1144
+ if (ts.isClassStaticBlockDeclaration(p)) return staticBlockUnit(p);
1097
1145
  // A call/effect lexically inside a DECORATOR (`@factory(arg)`) runs at class-DEFINITION time, NOT in
1098
1146
  // the decorated declaration's body. The parent chain of a decorator's expression is
1099
1147
  // CallExpression → Decorator → MethodDeclaration/ClassDeclaration/Parameter, so `enclosing` otherwise
@@ -1104,6 +1152,9 @@ function enclosing(node) {
1104
1152
  if (ts.isDecorator(p)) return null;
1105
1153
  const n = nodeName.get(p);
1106
1154
  if (n) return n;
1155
+ // Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
1156
+ // file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
1157
+ if (ts.isSourceFile(p)) return moduleUnit(p);
1107
1158
  }
1108
1159
  return null;
1109
1160
  }
@@ -2236,7 +2287,10 @@ for (const [name, rec] of fns) {
2236
2287
  // them are NOT in `inferred`, so it is a LOWER BOUND when this is non-empty. Omitted when none.
2237
2288
  if (rec.blind.size) entry.invisible = [...rec.blind].sort();
2238
2289
  if (rec.entry) entry.entryPoint = true;
2239
- if (rec.isCjsExport) entry.unitKind = "export"; // spec 0.5 draft, informative — per-unit, not by name
2290
+ // unitKind (spec §2, informative — per-unit, not by name): the synthesized `<module>` initializer
2291
+ // carries its own kind (set at mint), a CJS export is tagged "export".
2292
+ if (rec.unitKind) entry.unitKind = rec.unitKind;
2293
+ else if (rec.isCjsExport) entry.unitKind = "export"; // spec 0.5 draft, informative — per-unit, not by name
2240
2294
  functions.push(entry);
2241
2295
  }
2242
2296
  // `package` names what this report COVERS — a consumer chaining it registers coverage even when