candor-ts 0.5.23 → 0.5.25

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.5.23",
3
+ "version": "0.5.25",
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
@@ -205,6 +205,32 @@ export function impact(fns, cg, q) {
205
205
  return { fn: tgt ?? q, affectedCount: affected.length, affected, entryPoints };
206
206
  }
207
207
 
208
+ // blindspots (SPEC §3.1 ⟨0.6⟩): the Unknown SOURCES — fns whose OWN body has an unresolvable call (so
209
+ // they carry `unknownWhy`), each ranked by its Unknown blast radius (the transitive callers that inherit
210
+ // Unknown through it). The actionable inverse of a widely-propagated Unknown: a report can read mostly
211
+ // Unknown from a handful of root causes — this names them, ranked, to declare/resolve/accept. Matches
212
+ // candor-java/candor-query: { sources:[{fn,why,reaches,affected}], totalUnknown }.
213
+ export function blindspots(fns, cg) {
214
+ const rev = reverseGraph(cg);
215
+ const totalUnknown = fns.filter((e) => (e.inferred ?? []).includes("Unknown")).length;
216
+ const sources = [];
217
+ for (const e of fns) {
218
+ const why = e.unknownWhy ?? [];
219
+ if (why.length === 0) continue; // a SOURCE carries its own unknownWhy; a purely-transitive Unknown does not
220
+ const reached = new Set();
221
+ const queue = [e.fn];
222
+ const seen = new Set([e.fn]);
223
+ while (queue.length) {
224
+ const n = queue.pop();
225
+ for (const c of rev.get(n) ?? []) if (!seen.has(c)) { seen.add(c); reached.add(c); queue.push(c); }
226
+ }
227
+ const affected = [...reached].sort();
228
+ sources.push({ fn: e.fn, why, reaches: affected.length, affected });
229
+ }
230
+ sources.sort((a, b) => b.reaches - a.reaches || a.fn.localeCompare(b.fn)); // most-smearing first, stable
231
+ return { sources, totalUnknown };
232
+ }
233
+
208
234
  // path: the FORWARD provenance — a shortest BFS over the calls graph from `fn` to the nearest unit
209
235
  // that performs `eff` DIRECTLY (the source). Matches candor-query's {effect, fn, path:[{fn,loc,source}]}.
210
236
  export function path(fns, cg, fnQ, eff) {
package/query.mjs CHANGED
@@ -25,7 +25,7 @@ import { printAgents } from "./contract.mjs";
25
25
  // a `matchTier` missing `#` (so the SAME query resolved differently between `impact` and `callers` on a
26
26
  // JVM `Type#method` report). Importing the shared functions removes all three divergences (review find).
27
27
  import { impact as coreImpact, path as corePath, gains as coreGains,
28
- show as coreShow,
28
+ show as coreShow, blindspots as coreBlindspots,
29
29
  loadReport, loadCallgraph, matches } from "./query-core.mjs";
30
30
  const emit = (v) => console.log(JSON.stringify(v, null, 1));
31
31
 
@@ -127,6 +127,13 @@ switch (cmd) {
127
127
  emit(coreImpact(loadReport(prefix), loadCallgraph(prefix), q));
128
128
  break;
129
129
  }
130
+ case "blindspots": {
131
+ // the Unknown SOURCES, ranked by blast radius — the actionable inverse of a widely-propagated
132
+ // Unknown (SPEC §3.1 ⟨0.6⟩): { sources:[{fn,why,reaches,affected}], totalUnknown }.
133
+ const [prefix] = args;
134
+ emit(coreBlindspots(loadReport(prefix), loadCallgraph(prefix)));
135
+ break;
136
+ }
130
137
  case "gains": {
131
138
  // the supply-chain alarm (SPEC §5.1): {gained:[Effect], byFunction:[{fn,effect}]} — what the
132
139
  // surface gained between two reports (base → cur), the cross-engine machine-readable form.
package/scan.mjs CHANGED
@@ -36,7 +36,7 @@ const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
36
36
  // literal stamped into the envelope's `spec` field, so the doc lines and the report can never drift.
37
37
  // Reused, never re-littered.
38
38
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
39
- const SPEC_VERSION = "0.5";
39
+ const SPEC_VERSION = "0.6";
40
40
 
41
41
  // --version: a print-and-exit MODE, handled before the main arg walk so it never depends on a target.
42
42
  // Fully OFFLINE — candor never phones home. Staying current is the AGENT's job: read the installed
@@ -1935,7 +1935,11 @@ for (const [name, rec] of fns) {
1935
1935
  if (inf.includes("Db") && rec.tables.size) entry.tables = [...rec.tables].sort();
1936
1936
  if (inf.includes("Exec") && rec.cmds.size) entry.cmds = [...rec.cmds].sort();
1937
1937
  if (inf.includes("Fs") && rec.paths.size) entry.paths = [...rec.paths].sort();
1938
- if (rec.direct.has("Unknown") && rec.why.size) entry.unknownWhy = [...rec.why].sort();
1938
+ // ⟨0.6⟩ unknownWhy — REQUIRED on a DIRECT Unknown SOURCE (this fn's own body has the unresolvable call,
1939
+ // so `rec.direct` carries Unknown), absent on a purely-transitive Unknown. The rich per-site reasons
1940
+ // (rec.why: callback:/dispatch:/dynamic-key:) when recorded, else a generic fallback so a source is
1941
+ // never left un-tagged — the source/transitive split the `blindspots` query needs (SPEC §3.1/§4).
1942
+ if (rec.direct.has("Unknown")) entry.unknownWhy = rec.why.size ? [...rec.why].sort() : ["unresolved"];
1939
1943
  // HONESTY: the npm packages this fn transitively reaches that κ couldn't see through — effects through
1940
1944
  // them are NOT in `inferred`, so it is a LOWER BOUND when this is non-empty. Omitted when none.
1941
1945
  if (rec.blind.size) entry.invisible = [...rec.blind].sort();