candor-ts 0.40.0 → 0.40.2

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.
Files changed (4) hide show
  1. package/README.md +14 -0
  2. package/package.json +6 -3
  3. package/policy.mjs +8 -0
  4. package/scan.mjs +1356 -108
package/README.md CHANGED
@@ -185,6 +185,20 @@ functions analyzed): its DI-style fs injection means many functions read `Unknow
185
185
  that's the contract working, not noise. The report says "can reach", never "does"; an absent
186
186
  literal is never a claim of absence.
187
187
 
188
+ **Framework dependency injection (spec §2's bounded-CHA clause).** rimraf's injection is a function-valued
189
+ parameter, which reads `Unknown`. A DI *container* is measured separately, on NestJS 10 with an executed fixture:
190
+
191
+ - a provider entry `{ provide: Store, useClass | useValue | useFactory | useExisting: … }` whose token is a project
192
+ CLASS is read as a conversion: the substitute joins `Store`'s candidates, so `this.store.save()` is charged the
193
+ union of `Store.save` and the substitute's `save` (SOUNDNESS R1061 — silent before, all three forms executed);
194
+ - a parameter typed by an INTERFACE (`@Inject("SINK") sink: Sink`) dispatches over the project's visible
195
+ implementors of `Sink`, so a `useClass` that `implements Sink` is charged; a `useValue` object literal that
196
+ implements nothing reads `Unknown` (`dispatch:`).
197
+
198
+ **Named miss:** a provider list the scan cannot see (built at runtime, or imported from an unscanned package), a
199
+ token that is a variable rather than the class itself, and a substitute supplied by a dependency are not
200
+ substitutions to this engine. Gate a DI-heavy service with `deny <E> Unknown` as well as `deny <E>`.
201
+
188
202
  ## Cross-engine consistency — machine-checked
189
203
 
190
204
  candor-ts is one of the **four code engines** (with the reference engine candor-java, the Rust
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.40.0",
3
+ "version": "0.40.2",
4
4
  "mcpName": "io.github.tombaldwin/candor",
5
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
- "@types/node": "^25.9.2",
9
- "typescript": "^6.0.3"
8
+ "@types/node": "25.9.2",
9
+ "typescript": "6.0.3"
10
10
  },
11
11
  "bin": {
12
12
  "candor-ts": "./scan.mjs",
@@ -21,6 +21,8 @@
21
21
  },
22
22
  "scripts": {
23
23
  "lint": "eslint *.mjs",
24
+ "prepack": "node -e \"require('fs').copyFileSync('package-lock.json','npm-shrinkwrap.json')\"",
25
+ "postpack": "node -e \"require('fs').rmSync('npm-shrinkwrap.json',{force:true})\"",
24
26
  "test": "npm run lint && node --test test-unit.mjs && node test.mjs --parallel && node test-mcp.mjs && node test-lsp.mjs && node test-watch.mjs && npm run test:probe && npm run test:fuzz",
25
27
  "test:unit": "node --test test-unit.mjs",
26
28
  "test:probe": "node fabrication_probe.mjs",
@@ -45,6 +47,7 @@
45
47
  "node": ">=20"
46
48
  },
47
49
  "files": [
50
+ "npm-shrinkwrap.json",
48
51
  "Cases.ts",
49
52
  "scan.mjs",
50
53
  "query.mjs",
package/policy.mjs CHANGED
@@ -1296,11 +1296,19 @@ export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map()
1296
1296
  // a rule whose destinations all resolve while its `from` names nothing has bound nothing, and that is
1297
1297
  // exactly the typo that leaves an operator believing a leaf is protected.
1298
1298
  for (const r of pol.only ?? []) zeroCount.set(r.raw, 0);
1299
+ // ⟨SOUNDNESS R952⟩ …and a SCOPED `allow`. It was never enrolled, so `allow Net in nosuch.fn ok.example` — a
1300
+ // typo'd scope — bound nothing, said nothing, and exited 0: a gate that cannot fail, exactly the `deny` typo
1301
+ // this pass exists for, one rule kind over. A scoped allow makes a promise about the functions its scope names;
1302
+ // naming none is the same unanswerable rule. A SCOPELESS allow applies to every function and stays exempt, as a
1303
+ // scopeless `deny` does. Disclosure only: the line and `zeroMatch` entry `deny` already gets, exit unchanged
1304
+ // (candor-rust's half, same shape).
1305
+ for (const r of pol.allow ?? []) if (r.scope) zeroCount.set(r.raw, 0);
1299
1306
  if (zeroCount.size) {
1300
1307
  const names = new Set(functions.map((f) => f.fn));
1301
1308
  for (const k of Object.keys(callgraph ?? {})) names.add(k);
1302
1309
  for (const n of names) {
1303
1310
  for (const r of pol.deny) if (r.scope && scopeMatches(n, r.scope)) zeroCount.set(r.raw, zeroCount.get(r.raw) + 1);
1311
+ for (const r of pol.allow ?? []) if (r.scope && scopeMatches(n, r.scope)) zeroCount.set(r.raw, zeroCount.get(r.raw) + 1);
1304
1312
  for (const r of pol.forbid) {
1305
1313
  if (scopeMatches(n, r.from) || scopeMatches(n, r.to)) zeroCount.set(r.raw, zeroCount.get(r.raw) + 1);
1306
1314
  }