candor-ts 0.4.2 → 0.4.3
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 +10 -1
- package/package.json +1 -1
- package/query.mjs +11 -0
- package/scan.mjs +54 -10
package/AGENTS.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
You are working in a TypeScript project. **candor-ts** tells you, for every function, which side
|
|
4
4
|
effects it can reach — network, filesystem, database, subprocess, env, clock — *including effects
|
|
5
5
|
inherited transitively through any chain of calls across files*. Use it instead of tracing call
|
|
6
|
-
chains by hand.
|
|
6
|
+
chains by hand.
|
|
7
|
+
|
|
8
|
+
> **This document ships inside the package.** `npx -y candor-ts --agents` prints the contract for
|
|
9
|
+
> the *installed* version — always prefer that over a vendored or fetched copy, which can describe
|
|
10
|
+
> a different candor-ts than the one you are running.
|
|
11
|
+
|
|
12
|
+
The language-agnostic consumption contract is
|
|
7
13
|
[candor-spec/AGENTS.md](https://github.com/tombaldwin/candor-spec/blob/main/AGENTS.md); this file is
|
|
8
14
|
the TypeScript-specific production + query surface.
|
|
9
15
|
|
|
@@ -32,6 +38,9 @@ pure functions are omitted** — a function present in the callgraph sidecar but
|
|
|
32
38
|
`.functions[]` is pure (as far as the engine resolved). In *neither* file = never analyzed
|
|
33
39
|
(a test file? an unexported arrow inside an object literal?) — conclude nothing.
|
|
34
40
|
|
|
41
|
+
A dist-CJS export unit (a `module.exports` surface scanned with `--allow-js`) carries
|
|
42
|
+
`unitKind: "export"` (spec 0.5 draft, informative); ordinary functions omit the field.
|
|
43
|
+
|
|
35
44
|
**Multi-package (monorepos / private deps):** point `CANDOR_DEPS` at the dependencies' reports
|
|
36
45
|
(a path list, or a directory of `*.json`); an unclassified call into a package with a loaded
|
|
37
46
|
report inherits that function's recorded transitive effects and literal surfaces, joined by the
|
package/package.json
CHANGED
package/query.mjs
CHANGED
|
@@ -43,6 +43,17 @@ const emit = (v) => console.log(JSON.stringify(v, null, 1));
|
|
|
43
43
|
|
|
44
44
|
const [, , cmd, ...args] = process.argv;
|
|
45
45
|
switch (cmd) {
|
|
46
|
+
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"));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
46
57
|
case "parsepolicy": {
|
|
47
58
|
emit(parsePolicy(fs.readFileSync(args[0], "utf8")));
|
|
48
59
|
break;
|
package/scan.mjs
CHANGED
|
@@ -30,8 +30,16 @@ const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
30
30
|
|
|
31
31
|
// ---- args ----------------------------------------------------------------------------------------
|
|
32
32
|
const argv = process.argv.slice(2);
|
|
33
|
+
if (argv.includes("--agents")) {
|
|
34
|
+
// The agent contract for THE INSTALLED VERSION — AGENTS.md ships in the npm tarball, so doc
|
|
35
|
+
// and engine cannot drift (the spec §2.1 version-trust rule applied to documentation).
|
|
36
|
+
const semver = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
|
|
37
|
+
console.log(`<!-- candor-ts ${semver} · the agent contract for this installed version -->`);
|
|
38
|
+
process.stdout.write(fs.readFileSync(path.join(ENGINE_DIR, "AGENTS.md"), "utf8"));
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
33
41
|
if (argv.length === 0) {
|
|
34
|
-
console.error("usage: node scan.mjs <dir | file.ts | tsconfig.json> [--out <prefix>] [--policy <file>]");
|
|
42
|
+
console.error("usage: node scan.mjs <dir | file.ts | tsconfig.json> [--out <prefix>] [--policy <file>] [--agents]");
|
|
35
43
|
process.exit(2);
|
|
36
44
|
}
|
|
37
45
|
const target = argv[0];
|
|
@@ -40,7 +48,17 @@ for (let i = 1; i < argv.length; i++) {
|
|
|
40
48
|
if (argv[i] === "--out") outPrefix = argv[++i];
|
|
41
49
|
else if (argv[i] === "--policy") policyPath = argv[++i];
|
|
42
50
|
else if (argv[i] === "--allow-js") allowJs = true;
|
|
43
|
-
else if (
|
|
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);
|
|
56
|
+
}
|
|
57
|
+
else if (!outPrefix) outPrefix = argv[i]; // legacy positional prefix
|
|
58
|
+
}
|
|
59
|
+
if (target.startsWith("--")) {
|
|
60
|
+
console.error(`candor-ts: unknown flag ${target} (see usage)`);
|
|
61
|
+
process.exit(2);
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
// ---- project discovery (a dir, a single file, or a tsconfig) --------------------------------------
|
|
@@ -54,11 +72,34 @@ let rootDir, fileNames, compilerOptions = {
|
|
|
54
72
|
types: ["node"],
|
|
55
73
|
strict: true,
|
|
56
74
|
};
|
|
75
|
+
// The scanner CLASSIFIES through the builtin typings, so `node` always rides in `types` — a
|
|
76
|
+
// project's `types: []` (legitimate for its own build) would blind the effect analysis itself.
|
|
77
|
+
function withNodeTypes(options) {
|
|
78
|
+
const t = options.types && options.types.length ? options.types : [];
|
|
79
|
+
return { ...options, types: [...new Set([...t, "node"])] };
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
function fromTsconfig(cfgPath, baseDir) {
|
|
58
83
|
const cfg = ts.readConfigFile(cfgPath, ts.sys.readFile);
|
|
59
84
|
const parsed = ts.parseJsonConfigFileContent(cfg.config ?? {}, ts.sys, baseDir);
|
|
60
|
-
compilerOptions =
|
|
61
|
-
|
|
85
|
+
compilerOptions = withNodeTypes(parsed.options);
|
|
86
|
+
let names = parsed.fileNames;
|
|
87
|
+
// SOLUTION-STYLE configs (`files: [], references: [...]` — hono, most monorepo roots) list no
|
|
88
|
+
// sources themselves; follow the references one level and union their file lists (skipping
|
|
89
|
+
// test/bench configs by the same path rule). Found by the published-package probe: hono read
|
|
90
|
+
// "no TypeScript sources".
|
|
91
|
+
if (names.length === 0 && (parsed.projectReferences ?? []).length > 0) {
|
|
92
|
+
for (const ref of parsed.projectReferences) {
|
|
93
|
+
const refPath = ts.resolveProjectReferencePath(ref);
|
|
94
|
+
if (!fs.existsSync(refPath) || isTestPath(path.relative(baseDir, refPath))) continue;
|
|
95
|
+
const sub = ts.readConfigFile(refPath, ts.sys.readFile);
|
|
96
|
+
const subParsed = ts.parseJsonConfigFileContent(sub.config ?? {}, ts.sys, path.dirname(refPath));
|
|
97
|
+
if (names.length === 0) compilerOptions = withNodeTypes(subParsed.options);
|
|
98
|
+
names = names.concat(subParsed.fileNames);
|
|
99
|
+
}
|
|
100
|
+
names = [...new Set(names)];
|
|
101
|
+
}
|
|
102
|
+
return names.filter((f) => !isTestPath(path.relative(baseDir, f)));
|
|
62
103
|
}
|
|
63
104
|
const stat = fs.existsSync(target) ? fs.statSync(target) : null;
|
|
64
105
|
if (!stat) { console.error(`candor-ts: no such path: ${target}`); process.exit(2); }
|
|
@@ -139,7 +180,7 @@ fs.mkdirSync(path.dirname(path.resolve(outPrefix)), { recursive: true });
|
|
|
139
180
|
// scan and a .d.ts resolution). Version-aware trust (§2.1): a report from a DIFFERENT engine
|
|
140
181
|
// version is downgraded to Unknown rather than silently trusted. Duplicate hashes (two same-named
|
|
141
182
|
// exports in one package) UNION — a sound over-approximation, documented.
|
|
142
|
-
const ENGINE_VERSION = "candor-ts-0.4.
|
|
183
|
+
const ENGINE_VERSION = "candor-ts-0.4.3";
|
|
143
184
|
const crossDeps = new Map(); // hash -> {inferred:Set, hosts:[], cmds:[], paths:[], tables:[]}
|
|
144
185
|
// Packages a loaded sibling report COVERS — exempt from the κ ledger even when a call joins no
|
|
145
186
|
// entry (reports omit pure functions: the silence is the purity claim, SPEC §2 rule 3 — the
|
|
@@ -336,6 +377,8 @@ function moduleOf(sf) {
|
|
|
336
377
|
const rel = path.relative(rootDir, path.resolve(sf.fileName)).replace(/\.[mc]?[tj]sx?$/, "");
|
|
337
378
|
return rel.split(path.sep).join(".");
|
|
338
379
|
}
|
|
380
|
+
const cjsLocal = new Set(); // CJS export-surface units (spec 0.5 draft unitKind: "export")
|
|
381
|
+
const markCjs = (v) => { if (v) cjsLocal.add(v); return v; };
|
|
339
382
|
function localName(node) {
|
|
340
383
|
if (ts.isFunctionDeclaration(node) && node.name) return node.name.text;
|
|
341
384
|
if (ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) && node.parent.name)
|
|
@@ -371,10 +414,10 @@ function localName(node) {
|
|
|
371
414
|
if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.EqualsToken && p.right === node) {
|
|
372
415
|
const lhs = p.left.getText().replace(/\s+/g, "");
|
|
373
416
|
if (lhs === "module.exports")
|
|
374
|
-
return (ts.isFunctionExpression(node) && node.name?.text)
|
|
375
|
-
|| path.basename(node.getSourceFile().fileName).replace(/\.[mc]?jsx?$/, "");
|
|
417
|
+
return markCjs((ts.isFunctionExpression(node) && node.name?.text)
|
|
418
|
+
|| path.basename(node.getSourceFile().fileName).replace(/\.[mc]?jsx?$/, ""));
|
|
376
419
|
const m = lhs.match(/^(?:module\.)?exports\.([A-Za-z_$][\w$]*)$/);
|
|
377
|
-
if (m) return m[1];
|
|
420
|
+
if (m) return markCjs(m[1]);
|
|
378
421
|
}
|
|
379
422
|
if (ts.isPropertyAssignment(p) && p.initializer === node && ts.isObjectLiteralExpression(p.parent)) {
|
|
380
423
|
const g = p.parent.parent;
|
|
@@ -382,7 +425,7 @@ function localName(node) {
|
|
|
382
425
|
&& g.right === p.parent && g.left.getText().replace(/\s+/g, "") === "module.exports")
|
|
383
426
|
// .text, not getText(): a string-literal key keeps its quotes under getText, minting a
|
|
384
427
|
// hash like pkg#"sign" the consumer's pkg#sign join can never hit (/code-review).
|
|
385
|
-
return p.name.text ?? p.name.getText();
|
|
428
|
+
return markCjs(p.name.text ?? p.name.getText());
|
|
386
429
|
}
|
|
387
430
|
}
|
|
388
431
|
return null;
|
|
@@ -779,11 +822,12 @@ for (const [name, rec] of fns) {
|
|
|
779
822
|
if (inf.includes("Fs") && rec.paths.size) entry.paths = [...rec.paths].sort();
|
|
780
823
|
if (rec.direct.has("Unknown") && rec.why.size) entry.unknownWhy = [...rec.why].sort();
|
|
781
824
|
if (rec.entry) entry.entryPoint = true;
|
|
825
|
+
if (cjsLocal.has(rec.local)) entry.unitKind = "export"; // spec 0.5 draft, informative
|
|
782
826
|
functions.push(entry);
|
|
783
827
|
}
|
|
784
828
|
// `package` names what this report COVERS — a consumer chaining it registers coverage even when
|
|
785
829
|
// `functions` is empty (an all-pure package's report is its purity claim, SPEC §2 rule 3).
|
|
786
|
-
const envelope = { candor: { version: "candor-ts-0.4.
|
|
830
|
+
const envelope = { candor: { version: "candor-ts-0.4.3", toolchain: `node-${process.versions.node}`, spec: "0.4" },
|
|
787
831
|
package: pkgName, functions };
|
|
788
832
|
fs.writeFileSync(`${outPrefix}.json`, JSON.stringify(envelope, null, 1));
|
|
789
833
|
const cg = {};
|