candor-ts 0.5.7 → 0.5.9

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 (3) hide show
  1. package/package.json +1 -1
  2. package/scan-core.mjs +10 -2
  3. package/scan.mjs +20 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
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/scan-core.mjs CHANGED
@@ -79,9 +79,17 @@ export const KAPPA_RULES = [
79
79
  [/^mongoose$/,
80
80
  /^(find|save|create|insertMany|updateOne|updateMany|replaceOne|deleteOne|deleteMany|aggregate|countDocuments|estimatedDocumentCount|distinct|exec|bulkWrite)/,
81
81
  "Db"],
82
- [/^(sequelize|drizzle-orm)$/,
83
- /^(find|create|update|destroy|upsert|count|max|min|sum|query|select|insert|delete|execute|transaction)/,
82
+ // Sequelize is EXECUTE-ON-CALL: `Model.findAll()/create()/update()/destroy()` issue the query and
83
+ // return a promise — so its verbs are the I/O boundary.
84
+ [/^sequelize$/,
85
+ /^(find|create|update|destroy|upsert|count|max|min|sum|increment|decrement|reload|save|query|transaction)/,
84
86
  "Db"],
87
+ // Drizzle is a BUILDER: `db.select().from().where()` / `db.insert().values()` / `db.update().set()` /
88
+ // `db.delete().where()` issue NOTHING until a terminal `.execute()`/await/`.all()`/`.get()`/`.run()` (or
89
+ // the relational `db.query.x.findMany/findFirst`). Listing select/insert/update/delete as Db fabricated
90
+ // the effect onto a pure builder chain (the typeorm rule's `createQueryBuilder` discipline, violated).
91
+ // VERB-PRECISE: only the terminal execution verbs; the builder heads under-report (sound) until executed.
92
+ [/^drizzle-orm$/, /^(execute|transaction|findMany|findFirst|all|get|run)$/, "Db"],
85
93
  // Nest's HttpService wraps axios — the request verbs are Net.
86
94
  [/^@nestjs\/axios$/, /^(get|post|put|patch|delete|head|request)$/, "Net"],
87
95
  ];
package/scan.mjs CHANGED
@@ -724,6 +724,17 @@ function edgeToTargets(rec, decls) {
724
724
  for (const d of decls) { const t = nodeName.get(d); if (t) rec.edges.add(t); }
725
725
  }
726
726
 
727
+ // Callee names that INVOKE a function/method argument (so a fn-reference passed to one is reachable
728
+ // through it). Array/iterable HOFs, the timer/microtask schedulers, and Promise continuations. A
729
+ // STORE/compare/log sink (`set`/`push`/`add`/`includes`/`indexOf`/`concat`/`log`/`stringify`/…) is
730
+ // deliberately ABSENT — edging there would fabricate the fn's effects on a pure path (the cardinal sin).
731
+ const HOF_INVOKERS = new Set([
732
+ "map", "forEach", "filter", "reduce", "reduceRight", "find", "findIndex", "findLast", "findLastIndex",
733
+ "some", "every", "flatMap", "sort", "group", "groupBy", "partition", "mapValues", "flatMapDeep",
734
+ "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "requestAnimationFrame", "requestIdleCallback",
735
+ "then", "catch", "finally", "nextTick",
736
+ ]);
737
+
727
738
  // ---- pass 2: per call site, the (CLASSIFY)/(EDGE)/(UNKNOWN) resolution of SEMANTICS §4 ------------
728
739
  function visitCalls(node) {
729
740
  if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
@@ -761,7 +772,15 @@ function visitCalls(node) {
761
772
  // the Rust engine's fn-as-value edge. An inline closure is already charged lexically; a non-fn
762
773
  // argument resolves to no minted unit (`nodeName` miss) and adds nothing — no fabrication. Gated
763
774
  // on a non-local callee so a local callee that merely STORES (never invokes) keeps its precision.
764
- if (mod !== "<local>") {
775
+ // ONLY a callee that actually INVOKES its fn argument makes the reference reachable here. The
776
+ // earlier version edged for ANY non-local callee — fabricating the fn's effects onto a pure path
777
+ // (the cardinal sin) for STORE/compare/log sinks that never call it (`map.set(k, fn)`,
778
+ // `arr.push(fn)`, `arr.includes(fn)`, `console.log(fn)`, `[fn]`). Gate on a known INVOKING HOF by
779
+ // callee name; a custom non-local HOF that invokes its arg is an honest under-report (sound),
780
+ // never a fabrication. (A LOCAL callee keeps its precise callback-flow below.)
781
+ const calleeName = ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text
782
+ : ts.isIdentifier(node.expression) ? node.expression.text : null;
783
+ if (mod !== "<local>" && calleeName && HOF_INVOKERS.has(calleeName)) {
765
784
  for (const a of node.arguments ?? []) {
766
785
  if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) continue;
767
786
  const d2 = realDecl(checker.getSymbolAtLocation(a));