candor-ts 0.7.0 → 0.7.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.
- package/AGENTS.md +1 -0
- package/package.json +1 -1
- package/query-core.mjs +68 -0
- package/query.mjs +18 -0
package/AGENTS.md
CHANGED
|
@@ -75,6 +75,7 @@ Q impact $P <fn-query> # THE BLAST RADIUS: {fn, affectedCount, affe
|
|
|
75
75
|
Q callers $P <fn-query> 1 # the lower-level form: {of, direct, transitive} — works for pure fns
|
|
76
76
|
Q path $P <fn> <Effect> # how a fn reaches an effect: the chain to the nearest source
|
|
77
77
|
Q map $P 1 # {module: {effects, functions}}
|
|
78
|
+
Q containment $P [baseline-prefix] # §6.1 boundary-effect dispersion; with a baseline = AS-EFF-010 ratchet (exit 1 on a leak)
|
|
78
79
|
Q whatif $P <fn> <Effect> [policy] # pre-edit gate verdict (exit 1 if it would violate)
|
|
79
80
|
Q diff $P <baseline-prefix> 1 # per-function effect delta (exit 1 on a gained effect)
|
|
80
81
|
Q gains $P <baseline-prefix> # supply-chain alarm: {gained, byFunction} — effects a surface grew
|
package/package.json
CHANGED
package/query-core.mjs
CHANGED
|
@@ -229,6 +229,74 @@ export function map(fns) {
|
|
|
229
229
|
.map(([k, v]) => [k, { effects: [...v.effects].sort(), functions: v.functions }]));
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// containment (SPEC §6.1) — how well each BOUNDARY effect stays in one layer (dispersion, NOT a count),
|
|
233
|
+
// with the AS-EFF-010 ratchet when a baseline is given. Mirrors candor-java Query.containment and
|
|
234
|
+
// candor-query cmd_containment: boundary effects are scored, ambient ones reported-not-scored; a layer is
|
|
235
|
+
// the segment AFTER the common dotted prefix ("(root)" when no package layer follows). Uses DIRECT effects.
|
|
236
|
+
export const CONTAINED = ["Db", "Net", "Exec", "Fs", "Ipc", "Clipboard"];
|
|
237
|
+
export const AMBIENT = ["Log", "Clock", "Rand", "Env"];
|
|
238
|
+
function commonPrefixLen(fns) {
|
|
239
|
+
let best = null;
|
|
240
|
+
for (const e of fns) {
|
|
241
|
+
const segs = e.fn.split(".");
|
|
242
|
+
if (best === null) { best = segs; continue; }
|
|
243
|
+
let i = 0; const n = Math.min(best.length, segs.length);
|
|
244
|
+
while (i < n && best[i] === segs[i]) i++;
|
|
245
|
+
best = best.slice(0, i);
|
|
246
|
+
}
|
|
247
|
+
return (best ?? []).length;
|
|
248
|
+
}
|
|
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.
|
|
255
|
+
const segs = fn.split(".");
|
|
256
|
+
return prefixLen + 1 < segs.length ? segs[prefixLen] : "(root)";
|
|
257
|
+
}
|
|
258
|
+
export function containment(fns, baseFns) {
|
|
259
|
+
const pl = commonPrefixLen(fns);
|
|
260
|
+
const known = new Set([...CONTAINED, ...AMBIENT]);
|
|
261
|
+
const byEff = {}; // effect -> { layer -> count }, over DIRECT effects
|
|
262
|
+
for (const e of fns) for (const eff of (e.direct ?? [])) {
|
|
263
|
+
if (!known.has(eff)) continue;
|
|
264
|
+
const layer = layerOf(e.fn, pl);
|
|
265
|
+
(byEff[eff] ??= {})[layer] = (byEff[eff][layer] ?? 0) + 1;
|
|
266
|
+
}
|
|
267
|
+
// RATCHET: a baseline was given — flag any contained effect now in a layer it wasn't in (a leak), note removals.
|
|
268
|
+
if (baseFns) {
|
|
269
|
+
const bpl = commonPrefixLen(baseFns);
|
|
270
|
+
const baseLayers = {};
|
|
271
|
+
for (const e of baseFns) for (const eff of (e.direct ?? [])) {
|
|
272
|
+
if (!CONTAINED.includes(eff)) continue;
|
|
273
|
+
(baseLayers[eff] ??= new Set()).add(layerOf(e.fn, bpl));
|
|
274
|
+
}
|
|
275
|
+
const leaks = [], cleanups = [];
|
|
276
|
+
for (const eff of CONTAINED) {
|
|
277
|
+
const now = new Set(Object.keys(byEff[eff] ?? {}));
|
|
278
|
+
const was = baseLayers[eff] ?? new Set();
|
|
279
|
+
for (const l of now) if (!was.has(l)) leaks.push(`${eff} → ${l}`);
|
|
280
|
+
for (const l of was) if (!now.has(l)) cleanups.push(`${eff} ⊘ ${l}`);
|
|
281
|
+
}
|
|
282
|
+
return { leaks: leaks.sort(), cleanups: cleanups.sort() };
|
|
283
|
+
}
|
|
284
|
+
// REPORT: the containment diagnostic.
|
|
285
|
+
const contained = [];
|
|
286
|
+
for (const eff of CONTAINED) {
|
|
287
|
+
const layers = byEff[eff]; if (!layers) continue;
|
|
288
|
+
const entries = Object.entries(layers);
|
|
289
|
+
const tot = entries.reduce((a, [, n]) => a + n, 0);
|
|
290
|
+
const owner = entries.slice().sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
|
|
291
|
+
const placement = Object.fromEntries(entries.slice().sort((a, b) => a[0].localeCompare(b[0])));
|
|
292
|
+
contained.push({ effect: eff, containmentPct: Math.floor((100 * owner[1]) / tot),
|
|
293
|
+
layers: entries.length, owner: owner[0], placement });
|
|
294
|
+
}
|
|
295
|
+
const ambient = {};
|
|
296
|
+
for (const eff of AMBIENT) if (byEff[eff]) ambient[eff] = Object.keys(byEff[eff]).length;
|
|
297
|
+
return { contained, ambient };
|
|
298
|
+
}
|
|
299
|
+
|
|
232
300
|
export function reachable(fns) {
|
|
233
301
|
const roots = fns.filter((e) => e.entryPoint);
|
|
234
302
|
const byEff = {};
|
package/query.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { printAgents } from "./contract.mjs";
|
|
|
27
27
|
import { impact as coreImpact, path as corePath, gains as coreGains,
|
|
28
28
|
show as coreShow, blindspots as coreBlindspots,
|
|
29
29
|
callers as coreCallers, callersFrontier, loadHierarchy,
|
|
30
|
+
containment as coreContainment,
|
|
30
31
|
loadReport, loadCallgraph, matches } from "./query-core.mjs";
|
|
31
32
|
const emit = (v) => console.log(JSON.stringify(v, null, 1));
|
|
32
33
|
|
|
@@ -83,6 +84,23 @@ switch (cmd) {
|
|
|
83
84
|
.map(([k, v]) => [k, { effects: [...v.effects].sort(), functions: v.functions }])));
|
|
84
85
|
break;
|
|
85
86
|
}
|
|
87
|
+
case "containment": {
|
|
88
|
+
// SPEC §6.1 boundary-effect dispersion; with a baseline prefix it's the AS-EFF-010 ratchet (exit 1 on a
|
|
89
|
+
// new leak), matching candor-java / candor-query. JSON-only, like every other candor-ts query command.
|
|
90
|
+
const [prefix, basePrefix] = args;
|
|
91
|
+
if (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);
|
|
98
|
+
emit(r);
|
|
99
|
+
process.exit(r.leaks.length ? 1 : 0);
|
|
100
|
+
}
|
|
101
|
+
emit(coreContainment(loadReport(prefix)));
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
86
104
|
case "diff": {
|
|
87
105
|
// per-function effect delta vs a baseline: {changes: [{fn, gained, lost}]} — the envelope shape
|
|
88
106
|
// the conformance suite pins (diff-vs-self must be {changes: []}).
|