candor-ts 0.7.6 → 0.7.7

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.7.6",
3
+ "version": "0.7.7",
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/policy.mjs CHANGED
@@ -56,10 +56,12 @@ export function parsePolicy(text) {
56
56
  return { deny, allow, forbid };
57
57
  }
58
58
 
59
- /** §6.2 scope match: by NAME SEGMENT over ".", last segment a prefix. */
59
+ /** §6.2 scope match: by NAME SEGMENT, last segment a prefix.
60
+ * Segments split on BOTH "." and "::" — Rust/Java qualify with "::" while TS uses ".", and a shared
61
+ * policy must match across engines (a `Foo::bar` scope authored against Rust was inert in TS before). */
60
62
  export function scopeMatches(name, scope) {
61
- const segs = name.split(".");
62
- const parts = scope.split(".");
63
+ const segs = name.split(/[.:]+/).filter(Boolean);
64
+ const parts = scope.split(/[.:]+/).filter(Boolean);
63
65
  if (parts.length === 0 || parts.length > segs.length) return false;
64
66
  const last = parts[parts.length - 1], init = parts.slice(0, -1);
65
67
  outer: for (let i = 0; i + parts.length <= segs.length; i++) {
package/query.mjs CHANGED
@@ -95,7 +95,15 @@ switch (cmd) {
95
95
  printAgents(); // shared with scan.mjs — one implementation, can't diverge within an install
96
96
  break;
97
97
  case "parsepolicy": {
98
- emit(parsePolicy(fs.readFileSync(args[0], "utf8")));
98
+ // An unreadable/missing file is a clean exit-2 error, not an uncaught readFileSync stack trace.
99
+ let text;
100
+ try {
101
+ text = fs.readFileSync(args[0], "utf8");
102
+ } catch {
103
+ console.error(`candor: policy ${args[0] ?? "(no file given)"} could not be read`);
104
+ process.exit(2);
105
+ }
106
+ emit(parsePolicy(text));
99
107
  break;
100
108
  }
101
109
  case "show": {
@@ -234,8 +242,18 @@ switch (cmd) {
234
242
  for (const c of rev.get(n) ?? []) if (!affected.has(c)) { affected.add(c); queue.push(c); }
235
243
  }
236
244
  const violations = [];
237
- if (maybePolicy && maybePolicy !== "0" && maybePolicy !== "1" && fs.existsSync(maybePolicy)) {
238
- const pol = parsePolicy(fs.readFileSync(maybePolicy, "utf8"));
245
+ // A present policy arg (anything but the 0/1 verbosity sentinels) MUST exist and be readable —
246
+ // a typo'd path must be LOUD, not silently "no policy → ok:true, exit 0" (mirrors scan's --policy,
247
+ // which exits 2 on an unreadable file: a gate that can't read its policy can't certify anything).
248
+ if (maybePolicy && maybePolicy !== "0" && maybePolicy !== "1") {
249
+ let text;
250
+ try {
251
+ text = fs.readFileSync(maybePolicy, "utf8");
252
+ } catch {
253
+ console.error(`candor: policy ${maybePolicy} could not be read; whatif NOT evaluated against it`);
254
+ process.exit(2);
255
+ }
256
+ const pol = parsePolicy(text);
239
257
  for (const r of pol.deny) {
240
258
  if (r.effects.length && !r.effects.includes(eff)) continue; // pure ([]) forbids ANY effect
241
259
  for (const fn of affected)
package/scan.mjs CHANGED
@@ -87,7 +87,11 @@ for (let i = 0; i < argv.length; i++) {
87
87
  if (a === "--out") outPrefix = v; else policyPath = v;
88
88
  i++;
89
89
  }
90
- else if (a.startsWith("--")) { console.error(`candor-ts: unknown flag ${a} (${usage})`); process.exit(2); }
90
+ // Any leading-dash token that isn't a recognized flag is an unknown flag — NOT a positional target
91
+ // (SPEC §6.2/§7). `-h`/`-V`/`--help`/`--version` are print-and-exit modes consumed above, so by here
92
+ // a single-dash token (`-x`, the typo `-policy`) can only be a mistake; treating it as the scan
93
+ // target would silently scan the wrong thing.
94
+ else if (a.startsWith("-")) { console.error(`candor-ts: unknown flag ${a} (${usage})`); process.exit(2); }
91
95
  else if (target === null) target = a;
92
96
  else if (outPrefix === null) outPrefix = a; // legacy positional prefix
93
97
  else { console.error(`candor-ts: unexpected extra argument ${a} (${usage})`); process.exit(2); }
@@ -193,8 +197,8 @@ if (!wantJson) fs.mkdirSync(path.dirname(path.resolve(outPrefix)), { recursive:
193
197
  const deps = JSON.parse(fs.readFileSync(pkg, "utf8")).dependencies ?? {};
194
198
  if (Object.keys(deps).length > 0)
195
199
  console.error("candor-ts: WARNING — the target declares dependencies but has no node_modules; " +
196
- "imports will not resolve and most functions will read Unknown. " +
197
- "Run `npm install` in the target first.");
200
+ "imports won't resolve, so calls into those packages read pure/invisible (not Unknown) " +
201
+ "and effects through them are silently dropped. Run `npm install` in the target first.");
198
202
  } catch {}
199
203
  }
200
204
  // Prisma's client types are GENERATED — a project with the prisma dependency but no generated
@@ -2084,7 +2088,10 @@ if (policyPath) {
2084
2088
  const incompleteMap = new Map();
2085
2089
  for (const [name, rec] of fns) if (rec.incomplete.size) incompleteMap.set(name, rec.incomplete);
2086
2090
  const v = evaluatePolicy(parsePolicy(text), functions, cg, incompleteMap);
2087
- for (const line of v) console.log(line);
2091
+ // In --json mode stdout is the §2 envelope and must stay pure JSON — route the gate's
2092
+ // [AS-EFF-…] violation lines to stderr so a `candor-ts --json --policy … | jq` pipe never breaks.
2093
+ const emitViolation = wantJson ? (l) => console.error(l) : (l) => console.log(l);
2094
+ for (const line of v) emitViolation(line);
2088
2095
  if (v.length) {
2089
2096
  console.error(`candor-ts: ${v.length} policy violation(s)`);
2090
2097
  process.exit(1);