candor-ts 0.5.28 → 0.7.1

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.5.28",
3
+ "version": "0.7.1",
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
@@ -229,6 +229,69 @@ 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
+ const segs = fn.split(".");
251
+ return prefixLen + 2 < segs.length ? segs[prefixLen] : "(root)";
252
+ }
253
+ export function containment(fns, baseFns) {
254
+ const pl = commonPrefixLen(fns);
255
+ const known = new Set([...CONTAINED, ...AMBIENT]);
256
+ const byEff = {}; // effect -> { layer -> count }, over DIRECT effects
257
+ for (const e of fns) for (const eff of (e.direct ?? [])) {
258
+ if (!known.has(eff)) continue;
259
+ const layer = layerOf(e.fn, pl);
260
+ (byEff[eff] ??= {})[layer] = (byEff[eff][layer] ?? 0) + 1;
261
+ }
262
+ // RATCHET: a baseline was given — flag any contained effect now in a layer it wasn't in (a leak), note removals.
263
+ if (baseFns) {
264
+ const bpl = commonPrefixLen(baseFns);
265
+ const baseLayers = {};
266
+ for (const e of baseFns) for (const eff of (e.direct ?? [])) {
267
+ if (!CONTAINED.includes(eff)) continue;
268
+ (baseLayers[eff] ??= new Set()).add(layerOf(e.fn, bpl));
269
+ }
270
+ const leaks = [], cleanups = [];
271
+ for (const eff of CONTAINED) {
272
+ const now = new Set(Object.keys(byEff[eff] ?? {}));
273
+ const was = baseLayers[eff] ?? new Set();
274
+ for (const l of now) if (!was.has(l)) leaks.push(`${eff} → ${l}`);
275
+ for (const l of was) if (!now.has(l)) cleanups.push(`${eff} ⊘ ${l}`);
276
+ }
277
+ return { leaks: leaks.sort(), cleanups: cleanups.sort() };
278
+ }
279
+ // REPORT: the containment diagnostic.
280
+ const contained = [];
281
+ for (const eff of CONTAINED) {
282
+ const layers = byEff[eff]; if (!layers) continue;
283
+ const entries = Object.entries(layers);
284
+ const tot = entries.reduce((a, [, n]) => a + n, 0);
285
+ const owner = entries.slice().sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
286
+ const placement = Object.fromEntries(entries.slice().sort((a, b) => a[0].localeCompare(b[0])));
287
+ contained.push({ effect: eff, containmentPct: Math.floor((100 * owner[1]) / tot),
288
+ layers: entries.length, owner: owner[0], placement });
289
+ }
290
+ const ambient = {};
291
+ for (const eff of AMBIENT) if (byEff[eff]) ambient[eff] = Object.keys(byEff[eff]).length;
292
+ return { contained, ambient };
293
+ }
294
+
232
295
  export function reachable(fns) {
233
296
  const roots = fns.filter((e) => e.entryPoint);
234
297
  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,18 @@ 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 r = coreContainment(loadReport(prefix), loadReport(basePrefix));
93
+ emit(r);
94
+ process.exit(r.leaks.length ? 1 : 0);
95
+ }
96
+ emit(coreContainment(loadReport(prefix)));
97
+ break;
98
+ }
86
99
  case "diff": {
87
100
  // per-function effect delta vs a baseline: {changes: [{fn, gained, lost}]} — the envelope shape
88
101
  // the conformance suite pins (diff-vs-self must be {changes: []}).