candor-ts 0.7.1 → 0.7.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.7.1",
3
+ "version": "0.7.5",
4
4
  "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.5)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/query-core.mjs CHANGED
@@ -247,8 +247,13 @@ function commonPrefixLen(fns) {
247
247
  return (best ?? []).length;
248
248
  }
249
249
  function layerOf(fn, prefixLen) {
250
+ // The layer = the first segment after the common prefix, the leaf excluded. candor-ts names functions
251
+ // with a FILE.fn (free fns) or FILE.Class.method tail — a SHALLOW 1-segment-minimum tail — so the rule is
252
+ // `prefixLen + 1 < length` (matching candor-rust's layer_of). candor-java uses `+2` because its names carry
253
+ // an extra Package.Class.method segment; copying that here collapsed every 2-segment free function to
254
+ // "(root)", killing the dispersion signal on real TS reports.
250
255
  const segs = fn.split(".");
251
- return prefixLen + 2 < segs.length ? segs[prefixLen] : "(root)";
256
+ return prefixLen + 1 < segs.length ? segs[prefixLen] : "(root)";
252
257
  }
253
258
  export function containment(fns, baseFns) {
254
259
  const pl = commonPrefixLen(fns);
package/query.mjs CHANGED
@@ -89,7 +89,12 @@ switch (cmd) {
89
89
  // new leak), matching candor-java / candor-query. JSON-only, like every other candor-ts query command.
90
90
  const [prefix, basePrefix] = args;
91
91
  if (basePrefix) {
92
- const r = coreContainment(loadReport(prefix), loadReport(basePrefix));
92
+ const baseFns = loadReport(basePrefix);
93
+ if (baseFns.length === 0) { // fail CLOSED (exit 2), not a wall of bogus "everything leaked" (exit 1)
94
+ console.error(`candor-ts: no report at baseline prefix '${basePrefix}' — check the path`);
95
+ process.exit(2);
96
+ }
97
+ const r = coreContainment(loadReport(prefix), baseFns);
93
98
  emit(r);
94
99
  process.exit(r.leaks.length ? 1 : 0);
95
100
  }
package/scan-core.mjs CHANGED
@@ -63,6 +63,13 @@ export const KAPPA_RULES = [
63
63
  [/^(node:)?worker_threads$/, /^(postMessage|receiveMessageOnPort)$/, "Ipc"],
64
64
  // node:cluster — `fork()` spawns a worker PROCESS and wires its IPC channel.
65
65
  [/^(node:)?cluster$/, /^fork$/, "Ipc"],
66
+ // node:vm executes a runtime-supplied code STRING in-process — `runInThisContext`/`runInContext`/
67
+ // `runInNewContext`/`compileFunction`, and the same verbs on a `new vm.Script(code)`. Like `eval`,
68
+ // the effects are whatever the code does (opaque) → genuinely Unknown (NOT Exec: no subprocess).
69
+ // Was unmodeled inside the κ-covered @types/node, so `vm.runInThisContext(code)` read SILENT-PURE
70
+ // (a code-execution sink reported pure — found by real-world corpus testing). The why is attached at
71
+ // the classify site (the only κ rule that resolves to the Unknown trust-marker, SPEC §4).
72
+ [/^(node:)?vm$/, /^(runInThisContext|runInContext|runInNewContext|compileFunction)$/, "Unknown"],
66
73
  [/^(node:)?sqlite$/, null, "Db"],
67
74
  // the curated npm tier
68
75
  [/^(axios|got|node-fetch|undici|ws|socket\.io(-client)?|nodemailer)$/, null, "Net"],
package/scan.mjs CHANGED
@@ -251,7 +251,17 @@ function declModule(decl) {
251
251
  if (m) return m[1];
252
252
  if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
253
253
  m = f.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
254
- if (m) return m[1];
254
+ if (m) {
255
+ // `@types/X` (DefinitelyTyped) provides types for the RUNTIME package X — map it to X so the curated κ
256
+ // tier (keyed by the runtime name: pg/ws/…) fires. Without this a package typed via @types resolved to
257
+ // "@types/pg", the `pg`→Db rule never matched, and the resolved-but-unmodeled external decl read
258
+ // SILENT-PURE — `pool.query()` in a real TS Postgres app (which MUST have @types/pg installed to use
259
+ // pg) reported pure (found by a node_modules corpus run). Scoped runtime pkgs use the `__` convention:
260
+ // `@types/babel__core` → `@babel/core`.
261
+ const tm = m[1].match(/^@types\/(.+)$/);
262
+ if (tm) return tm[1].includes("__") ? "@" + tm[1].replace("__", "/") : tm[1];
263
+ return m[1];
264
+ }
255
265
  return f;
256
266
  }
257
267
 
@@ -1489,7 +1499,13 @@ function visitCalls(node) {
1489
1499
  if (eff && (ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
1490
1500
  && rootsAtStdStream(node.expression.expression))
1491
1501
  eff = null;
1492
- if (eff) rec.direct.add(eff);
1502
+ if (eff) {
1503
+ rec.direct.add(eff);
1504
+ // a κ rule that resolves to the Unknown trust-marker (node:vm code execution) is a direct
1505
+ // Unknown SOURCE — SPEC §4 requires a why on it, like eval's `reflect:eval`. (The rest of
1506
+ // the κ table is concrete effects, which carry no why.)
1507
+ if (eff === "Unknown") rec.why.add(`reflect:${mod.replace(/^node:/, "")}.${member}`);
1508
+ }
1493
1509
  // the literal surfaces, read only at a CLASSIFIED call (SPEC §2)
1494
1510
  if (eff === "Net") {
1495
1511
  const lit = firstStringLiteral(node);
@@ -1658,6 +1674,19 @@ function visitCalls(node) {
1658
1674
  const owner = enclosing(node);
1659
1675
  if (owner) fns.get(owner).direct.add(geff);
1660
1676
  }
1677
+ // dynamic `require(<non-literal>)` — the CJS twin of `import(m)` (which already discloses Unknown):
1678
+ // it loads an arbitrary module and runs its top-level code, so the effects are opaque → Unknown. A
1679
+ // LITERAL `require('fs')` is a static, resolvable load (pure until a member call), so ONLY a
1680
+ // non-literal arg is the escape. Gated like `fetch`: a bare `require` whose symbol is NOT a project
1681
+ // declaration (a project's own `function require()` shadow never fabricates). Under-disclose Unknown,
1682
+ // never a concrete effect. (Found by real-world corpus testing; sibling of the node:vm fix.)
1683
+ if (ts.isIdentifier(callee) && callee.text === "require"
1684
+ && node.arguments?.length === 1 && !ts.isStringLiteralLike(node.arguments[0])
1685
+ && !(checker.getSymbolAtLocation(callee)?.declarations ?? [])
1686
+ .some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
1687
+ const owner = enclosing(node);
1688
+ if (owner) { fns.get(owner).direct.add("Unknown"); fns.get(owner).why.add("reflect:require"); }
1689
+ }
1661
1690
  // Object.assign(target, ...sources) copies each SOURCE's own enumerable props → invokes their
1662
1691
  // getters (the object-spread twin). Enumerate the sources' local getters.
1663
1692
  if (callee.getText().replace(/\s+/g, "") === "Object.assign") {