candor-ts 0.4.4 → 0.4.6

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/contract.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // The agent contract for THE INSTALLED VERSION — AGENTS.md ships in the npm tarball, so the doc and
6
+ // engine cannot drift (the spec §2.1 version-trust rule applied to documentation). ONE implementation
7
+ // used by both scan.mjs and query.mjs, so `--agents` output can never diverge within an install.
8
+ export function printAgents() {
9
+ const dir = path.dirname(fileURLToPath(import.meta.url)); // the package root (where AGENTS.md ships)
10
+ const semver = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).version;
11
+ console.log(`<!-- candor-ts ${semver} · the agent contract for this installed version -->`);
12
+ process.stdout.write(fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8"));
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.4)",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -35,6 +35,7 @@
35
35
  "scan.mjs",
36
36
  "query.mjs",
37
37
  "policy.mjs",
38
+ "contract.mjs",
38
39
  "README.md",
39
40
  "AGENTS.md",
40
41
  "PROVE-IT.md",
package/query.mjs CHANGED
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
 
21
21
  import { parsePolicy, scopeMatches } from "./policy.mjs";
22
+ import { printAgents } from "./contract.mjs";
22
23
 
23
24
  // ---- the §3.1 match ladder: exact > segment-suffix > substring ------------------------------------
24
25
  function matchTier(name, q) {
@@ -44,16 +45,9 @@ const emit = (v) => console.log(JSON.stringify(v, null, 1));
44
45
  const [, , cmd, ...args] = process.argv;
45
46
  switch (cmd) {
46
47
  case "--agents":
47
- case "agents": {
48
- // The agent contract for THE INSTALLED VERSION ships in the npm tarball, cannot drift.
49
- const { dirname: qDirname, join: qJoin } = await import("node:path");
50
- const { fileURLToPath: qFile } = await import("node:url");
51
- const dir = qDirname(qFile(import.meta.url));
52
- const semver = JSON.parse(fs.readFileSync(qJoin(dir, "package.json"), "utf8")).version;
53
- console.log(`<!-- candor-ts ${semver} · the agent contract for this installed version -->`);
54
- process.stdout.write(fs.readFileSync(qJoin(dir, "AGENTS.md"), "utf8"));
48
+ case "agents":
49
+ printAgents(); // shared with scan.mjsone implementation, can't diverge within an install
55
50
  break;
56
- }
57
51
  case "parsepolicy": {
58
52
  emit(parsePolicy(fs.readFileSync(args[0], "utf8")));
59
53
  break;
package/scan.mjs CHANGED
@@ -25,6 +25,7 @@ import path from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { createRequire } from "node:module";
27
27
  import { parsePolicy, evaluatePolicy } from "./policy.mjs";
28
+ import { printAgents } from "./contract.mjs";
28
29
 
29
30
  const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
30
31
 
@@ -51,14 +52,7 @@ for (let i = 0; i < argv.length; i++) {
51
52
  else if (outPrefix === null) outPrefix = a; // legacy positional prefix
52
53
  else { console.error(`candor-ts: unexpected extra argument ${a} (${usage})`); process.exit(2); }
53
54
  }
54
- if (wantAgents) {
55
- // The agent contract for THE INSTALLED VERSION — AGENTS.md ships in the npm tarball, so doc and
56
- // engine cannot drift (the spec §2.1 version-trust rule applied to documentation).
57
- const semver = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
58
- console.log(`<!-- candor-ts ${semver} · the agent contract for this installed version -->`);
59
- process.stdout.write(fs.readFileSync(path.join(ENGINE_DIR, "AGENTS.md"), "utf8"));
60
- process.exit(0);
61
- }
55
+ if (wantAgents) { printAgents(); process.exit(0); }
62
56
  if (target === null) { console.error(usage); process.exit(2); }
63
57
 
64
58
  // ---- project discovery (a dir, a single file, or a tsconfig) --------------------------------------
@@ -311,6 +305,21 @@ function firstStringLiteral(node) {
311
305
  }
312
306
  return null;
313
307
  }
308
+ // Refine the Exec cliff (spec §4 ⟨0.5⟩): the effects a literal, statically-known subprocess head
309
+ // implies, matched by basename. ADDED to a caller that already carries Exec (a subprocess is still
310
+ // spawned — Exec is never dropped); an unrecognised head returns [] and keeps the bare cliff (never
311
+ // guess). A candor engine reads Fs/Env only — spec §7 item 12 (the analyzer self-boundary) guarantees
312
+ // it, so that case is spec-supplied. Only UNAMBIGUOUS single-effect tools belong here: a multi-modal
313
+ // head (git status local vs git push Net; rsync local vs remote; make/npm run project code) would
314
+ // fabricate the effect for its common case. The reference engines share this table verbatim.
315
+ function commandHeadEffects(cmd) {
316
+ const base = cmd.trim().split(/\s+/)[0].split(/[/\\]/).pop();
317
+ if (["curl", "wget", "http", "ssh", "scp"].includes(base)) return ["Net"];
318
+ if (["psql", "mysql", "sqlite3", "mongosh", "redis-cli"].includes(base)) return ["Db"];
319
+ if (["candor", "candor-run.sh", "candor-scan", "candor-query", "candor-java",
320
+ "candor-classify", "candor-report", "cargo-candor"].includes(base)) return ["Env", "Fs"];
321
+ return [];
322
+ }
314
323
  // host[:port] from an address/URL literal; non-address strings yield nothing (never fabricate).
315
324
  function hostLiteral(s) {
316
325
  const m = s.match(/^[a-z][a-z0-9+.-]*:\/\/([^/]+)/i); // scheme://host[:port]/…
@@ -689,7 +698,11 @@ function visitCalls(node) {
689
698
  }
690
699
  if (eff === "Exec") {
691
700
  const lit = firstStringLiteral(node);
692
- if (lit) rec.cmds.add(lit.trim().split(/\s+/)[0]); // the program of a command line
701
+ if (lit) {
702
+ rec.cmds.add(lit.trim().split(/\s+/)[0]); // the program of a command line
703
+ // a known literal head refines the cliff (curl→Net, candor→Fs/Env); Exec stays
704
+ for (const e of commandHeadEffects(lit)) rec.direct.add(e);
705
+ }
693
706
  }
694
707
  if (eff === "Fs") {
695
708
  const lit = firstStringLiteral(node);