candor-ts 0.4.3 → 0.4.5
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 +13 -0
- package/package.json +2 -1
- package/query.mjs +3 -9
- package/scan.mjs +36 -34
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.
|
|
3
|
+
"version": "0.4.5",
|
|
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
|
-
//
|
|
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.mjs — one 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,41 +25,35 @@ 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
|
|
|
31
32
|
// ---- args ----------------------------------------------------------------------------------------
|
|
33
|
+
// ONE pass: the first non-flag is the target; value-taking flags consume the next arg and FAIL on a
|
|
34
|
+
// missing/flag-shaped value; an unknown flag fails; flags may precede the target. `--agents` is a
|
|
35
|
+
// flag (a print-and-exit MODE) — it must NOT fire when it is the VALUE of --out/--policy, which the
|
|
36
|
+
// value-consuming skip handles, nor produce a "lying unknown flag" error for a real flag given first.
|
|
37
|
+
const usage = "usage: candor-ts <dir | file.ts | tsconfig.json> [--out <prefix>] [--policy <file>] [--allow-js] [--agents]";
|
|
32
38
|
const argv = process.argv.slice(2);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
process.exit(2);
|
|
44
|
-
}
|
|
45
|
-
const target = argv[0];
|
|
46
|
-
let outPrefix = null, policyPath = process.env.CANDOR_POLICY ?? null, allowJs = false;
|
|
47
|
-
for (let i = 1; i < argv.length; i++) {
|
|
48
|
-
if (argv[i] === "--out") outPrefix = argv[++i];
|
|
49
|
-
else if (argv[i] === "--policy") policyPath = argv[++i];
|
|
50
|
-
else if (argv[i] === "--allow-js") allowJs = true;
|
|
51
|
-
else if (argv[i].startsWith("--")) {
|
|
52
|
-
// An unknown flag must FAIL, not be silently ignored: a typo'd --policy drops the gate; an
|
|
53
|
-
// agent following a newer doc against an older binary deserves a loud "upgrade me" signal.
|
|
54
|
-
console.error(`candor-ts: unknown flag ${argv[i]} (usage: candor-ts <dir> [--out <prefix>] [--policy <file>] [--allow-js] [--agents])`);
|
|
55
|
-
process.exit(2);
|
|
39
|
+
let target = null, outPrefix = null, policyPath = process.env.CANDOR_POLICY ?? null, allowJs = false, wantAgents = false;
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const a = argv[i];
|
|
42
|
+
if (a === "--agents") wantAgents = true;
|
|
43
|
+
else if (a === "--allow-js") allowJs = true;
|
|
44
|
+
else if (a === "--out" || a === "--policy") {
|
|
45
|
+
const v = argv[i + 1];
|
|
46
|
+
if (v === undefined || v.startsWith("--")) { console.error(`candor-ts: ${a} requires a value (${usage})`); process.exit(2); }
|
|
47
|
+
if (a === "--out") outPrefix = v; else policyPath = v;
|
|
48
|
+
i++;
|
|
56
49
|
}
|
|
57
|
-
else if (
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
console.error(`candor-ts:
|
|
61
|
-
process.exit(2);
|
|
50
|
+
else if (a.startsWith("--")) { console.error(`candor-ts: unknown flag ${a} (${usage})`); process.exit(2); }
|
|
51
|
+
else if (target === null) target = a;
|
|
52
|
+
else if (outPrefix === null) outPrefix = a; // legacy positional prefix
|
|
53
|
+
else { console.error(`candor-ts: unexpected extra argument ${a} (${usage})`); process.exit(2); }
|
|
62
54
|
}
|
|
55
|
+
if (wantAgents) { printAgents(); process.exit(0); }
|
|
56
|
+
if (target === null) { console.error(usage); process.exit(2); }
|
|
63
57
|
|
|
64
58
|
// ---- project discovery (a dir, a single file, or a tsconfig) --------------------------------------
|
|
65
59
|
function isTestPath(p) {
|
|
@@ -180,7 +174,10 @@ fs.mkdirSync(path.dirname(path.resolve(outPrefix)), { recursive: true });
|
|
|
180
174
|
// scan and a .d.ts resolution). Version-aware trust (§2.1): a report from a DIFFERENT engine
|
|
181
175
|
// version is downgraded to Unknown rather than silently trusted. Duplicate hashes (two same-named
|
|
182
176
|
// exports in one package) UNION — a sound over-approximation, documented.
|
|
183
|
-
|
|
177
|
+
// ONE version source: package.json. A second hardcoded literal (the envelope's, the --agents
|
|
178
|
+
// banner's) that drifted from this would make the engine distrust its OWN reports at the §2.1
|
|
179
|
+
// staleness check (`d.candor?.version !== ENGINE_VERSION`), silently downgrading every chained dep.
|
|
180
|
+
const ENGINE_VERSION = `candor-ts-${JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version}`;
|
|
184
181
|
const crossDeps = new Map(); // hash -> {inferred:Set, hosts:[], cmds:[], paths:[], tables:[]}
|
|
185
182
|
// Packages a loaded sibling report COVERS — exempt from the κ ledger even when a call joins no
|
|
186
183
|
// entry (reports omit pure functions: the silence is the purity claim, SPEC §2 rule 3 — the
|
|
@@ -377,9 +374,13 @@ function moduleOf(sf) {
|
|
|
377
374
|
const rel = path.relative(rootDir, path.resolve(sf.fileName)).replace(/\.[mc]?[tj]sx?$/, "");
|
|
378
375
|
return rel.split(path.sep).join(".");
|
|
379
376
|
}
|
|
380
|
-
|
|
381
|
-
|
|
377
|
+
// `_lastCjs` is set by markCjs when localName() returns a CJS export-surface name, read right after
|
|
378
|
+
// the call to tag THAT unit (spec 0.5 draft unitKind: "export"). Keyed to the unit, not a project-
|
|
379
|
+
// wide name set — a same-named ordinary TS function in another file must NOT be mislabeled.
|
|
380
|
+
let _lastCjs = false;
|
|
381
|
+
const markCjs = (v) => { if (v) _lastCjs = true; return v; };
|
|
382
382
|
function localName(node) {
|
|
383
|
+
_lastCjs = false;
|
|
383
384
|
if (ts.isFunctionDeclaration(node) && node.name) return node.name.text;
|
|
384
385
|
if (ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) && node.parent.name)
|
|
385
386
|
return `${node.parent.name.text}.${node.name.getText()}`;
|
|
@@ -477,11 +478,12 @@ for (const sf of sources) {
|
|
|
477
478
|
nodeName.set(node, ctorQual);
|
|
478
479
|
}
|
|
479
480
|
const n = localName(node);
|
|
481
|
+
const isCjsExport = _lastCjs; // captured immediately: localName set it for THIS node only
|
|
480
482
|
if (n) {
|
|
481
483
|
const qual = `${mod}.${n}`;
|
|
482
484
|
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
483
485
|
fns.set(qual, { local: n, direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
484
|
-
cmds: new Set(), paths: new Set(), why: new Set(), entry: false,
|
|
486
|
+
cmds: new Set(), paths: new Set(), why: new Set(), entry: false, isCjsExport,
|
|
485
487
|
loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}` });
|
|
486
488
|
nodeName.set(node, qual);
|
|
487
489
|
if ((ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node)) && node.initializer)
|
|
@@ -822,12 +824,12 @@ for (const [name, rec] of fns) {
|
|
|
822
824
|
if (inf.includes("Fs") && rec.paths.size) entry.paths = [...rec.paths].sort();
|
|
823
825
|
if (rec.direct.has("Unknown") && rec.why.size) entry.unknownWhy = [...rec.why].sort();
|
|
824
826
|
if (rec.entry) entry.entryPoint = true;
|
|
825
|
-
if (
|
|
827
|
+
if (rec.isCjsExport) entry.unitKind = "export"; // spec 0.5 draft, informative — per-unit, not by name
|
|
826
828
|
functions.push(entry);
|
|
827
829
|
}
|
|
828
830
|
// `package` names what this report COVERS — a consumer chaining it registers coverage even when
|
|
829
831
|
// `functions` is empty (an all-pure package's report is its purity claim, SPEC §2 rule 3).
|
|
830
|
-
const envelope = { candor: { version:
|
|
832
|
+
const envelope = { candor: { version: ENGINE_VERSION, toolchain: `node-${process.versions.node}`, spec: "0.4" },
|
|
831
833
|
package: pkgName, functions };
|
|
832
834
|
fs.writeFileSync(`${outPrefix}.json`, JSON.stringify(envelope, null, 1));
|
|
833
835
|
const cg = {};
|