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 +1 -1
- package/query-core.mjs +26 -0
- package/query.mjs +8 -1
- package/scan.mjs +6 -2
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
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();
|