candor-ts 0.13.0 → 0.14.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 +1 -1
- package/README.md +2 -2
- package/package.json +2 -2
- package/query.mjs +1 -1
- package/scan.mjs +56 -2
package/AGENTS.md
CHANGED
|
@@ -12,7 +12,7 @@ chains by hand.
|
|
|
12
12
|
> **Already installed? Report the version and ask before upgrading — before you scan.** If this
|
|
13
13
|
> project already has candor (a `.candor/` report directory, or `candor-ts` reachable via `npx`/a
|
|
14
14
|
> global install), do this *first*: run `npx -y candor-ts --version` (offline) and **tell the user
|
|
15
|
-
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.
|
|
15
|
+
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.14)."*
|
|
16
16
|
> On a build too old for the flag, read `candor.version` / `candor.spec` from an existing
|
|
17
17
|
> `.candor/report*.json`, or `npm ls -g candor-ts`.
|
|
18
18
|
>
|
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
|
|
|
184
184
|
| A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
|
|
185
185
|
| Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
|
|
186
186
|
| The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
|
|
187
|
-
| `{ candor: { version, toolchain, spec: "0.
|
|
187
|
+
| `{ candor: { version, toolchain, spec: "0.14" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
|
|
188
188
|
| Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
|
|
189
189
|
| The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
|
|
190
190
|
|
|
@@ -202,7 +202,7 @@ read the Rust source".
|
|
|
202
202
|
|
|
203
203
|
## Status
|
|
204
204
|
|
|
205
|
-
0.
|
|
205
|
+
0.14.x, speaking candor-spec 0.14: the analysis core, the gate (`--policy` / `--gate-json` /
|
|
206
206
|
`.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
|
|
207
207
|
`--include-unknown` dispatch frontier), the MCP server, the LSP server, and the watch loop are
|
|
208
208
|
real, behaviorally tested (`npm test` — the behavioral suite across six harnesses), **soundness-fuzzed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
|
+
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.14)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@types/node": "^25.9.2",
|
package/query.mjs
CHANGED
|
@@ -94,7 +94,7 @@ function renderPathHuman(fns, cg, fnQ, eff) {
|
|
|
94
94
|
// package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
|
|
95
95
|
const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
96
96
|
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
|
|
97
|
-
const SPEC_VERSION = "0.
|
|
97
|
+
const SPEC_VERSION = "0.14";
|
|
98
98
|
|
|
99
99
|
// ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
|
|
100
100
|
// One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
|
package/scan.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
41
41
|
// literal stamped into the envelope's `spec` field, so the doc lines and the report can never drift.
|
|
42
42
|
// Reused, never re-littered.
|
|
43
43
|
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
|
|
44
|
-
const SPEC_VERSION = "0.
|
|
44
|
+
const SPEC_VERSION = "0.14";
|
|
45
45
|
|
|
46
46
|
// --version: a print-and-exit MODE, handled before the main arg walk so it never depends on a target.
|
|
47
47
|
// Fully OFFLINE — candor never phones home. Staying current is the AGENT's job: read the installed
|
|
@@ -1091,9 +1091,57 @@ function enumerateGetters(owner, type) {
|
|
|
1091
1091
|
}
|
|
1092
1092
|
}
|
|
1093
1093
|
|
|
1094
|
+
// The synthesized `<module>` unit for a source file's TOP-LEVEL executable statements (spec §2
|
|
1095
|
+
// unitKind "initializer" — java's `<clinit>` twin). Top-level `await fetch(…)`, a bare
|
|
1096
|
+
// `readFileSync(…)`, an IIFE, `export const r = await fetch(…)` execute at MODULE-LOAD time and
|
|
1097
|
+
// belong to nothing named — without this unit their effects reached the SourceFile in `enclosing`,
|
|
1098
|
+
// resolved to `null`, and were DROPPED → a false "pure" verdict (the cardinal sin: ESM top-level
|
|
1099
|
+
// await / serverless handler files / side-effecting config modules scanned as functions: []). This
|
|
1100
|
+
// is the field-initializer `Class.constructor` synthesis (~scan.mjs:774) one level up: the module
|
|
1101
|
+
// body is the file's own initializer. Minted LAZILY — only when a top-level statement actually
|
|
1102
|
+
// attributes an effect/edge here — so a pure top-level never gains a unit (pure units are omitted).
|
|
1103
|
+
// The qual mirrors sibling top-level units (`moduleOf(sf).<module>`); the bare local is `<module>`.
|
|
1104
|
+
function moduleUnit(sf) {
|
|
1105
|
+
const mod = moduleOf(sf);
|
|
1106
|
+
const qual = `${mod}.<module>`;
|
|
1107
|
+
let rec = fns.get(qual);
|
|
1108
|
+
if (!rec) {
|
|
1109
|
+
rec = { local: "<module>", direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
1110
|
+
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), why: new Set(),
|
|
1111
|
+
entry: false, unitKind: "initializer",
|
|
1112
|
+
loc: `${path.relative(rootDir, sf.fileName)}:1:1` };
|
|
1113
|
+
fns.set(qual, rec);
|
|
1114
|
+
}
|
|
1115
|
+
return qual;
|
|
1116
|
+
}
|
|
1117
|
+
// The synthesized static-initializer unit for a `class C { static { … } }` block (spec §2 unitKind
|
|
1118
|
+
// "initializer"). A static block runs at class-DEFINITION time, not instance construction — but its
|
|
1119
|
+
// body's effects otherwise walked up in `enclosing` to the ClassDeclaration, which maps to the
|
|
1120
|
+
// `C.constructor` unit, so a static-init effect was MISLABELED as the instance ctor (and carried no
|
|
1121
|
+
// unitKind). Mint it as its own unit, lazily, mirroring `moduleUnit`. (An anonymous class expression's
|
|
1122
|
+
// static block keys under `<anonymous>`; there is at most one static-init unit per class name.)
|
|
1123
|
+
function staticBlockUnit(node) {
|
|
1124
|
+
const cls = node.parent;
|
|
1125
|
+
const sf = node.getSourceFile();
|
|
1126
|
+
const mod = moduleOf(sf);
|
|
1127
|
+
const cname = (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && cls.name ? cls.name.text : "<anonymous>";
|
|
1128
|
+
const qual = `${mod}.${cname}.<static-init>`;
|
|
1129
|
+
let rec = fns.get(qual);
|
|
1130
|
+
if (!rec) {
|
|
1131
|
+
rec = { local: "<static-init>", direct: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
|
|
1132
|
+
cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), why: new Set(),
|
|
1133
|
+
entry: false, unitKind: "initializer",
|
|
1134
|
+
loc: `${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}:1` };
|
|
1135
|
+
fns.set(qual, rec);
|
|
1136
|
+
}
|
|
1137
|
+
return qual;
|
|
1138
|
+
}
|
|
1094
1139
|
// nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
|
|
1095
1140
|
function enclosing(node) {
|
|
1096
1141
|
for (let p = node; p; p = p.parent) {
|
|
1142
|
+
// A `static { … }` block is its own initializer unit (class-definition time), NOT the instance ctor
|
|
1143
|
+
// the ClassDeclaration maps to — intercept before the nodeName lookup would fold it into .constructor.
|
|
1144
|
+
if (ts.isClassStaticBlockDeclaration(p)) return staticBlockUnit(p);
|
|
1097
1145
|
// A call/effect lexically inside a DECORATOR (`@factory(arg)`) runs at class-DEFINITION time, NOT in
|
|
1098
1146
|
// the decorated declaration's body. The parent chain of a decorator's expression is
|
|
1099
1147
|
// CallExpression → Decorator → MethodDeclaration/ClassDeclaration/Parameter, so `enclosing` otherwise
|
|
@@ -1104,6 +1152,9 @@ function enclosing(node) {
|
|
|
1104
1152
|
if (ts.isDecorator(p)) return null;
|
|
1105
1153
|
const n = nodeName.get(p);
|
|
1106
1154
|
if (n) return n;
|
|
1155
|
+
// Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
|
|
1156
|
+
// file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
|
|
1157
|
+
if (ts.isSourceFile(p)) return moduleUnit(p);
|
|
1107
1158
|
}
|
|
1108
1159
|
return null;
|
|
1109
1160
|
}
|
|
@@ -2236,7 +2287,10 @@ for (const [name, rec] of fns) {
|
|
|
2236
2287
|
// them are NOT in `inferred`, so it is a LOWER BOUND when this is non-empty. Omitted when none.
|
|
2237
2288
|
if (rec.blind.size) entry.invisible = [...rec.blind].sort();
|
|
2238
2289
|
if (rec.entry) entry.entryPoint = true;
|
|
2239
|
-
|
|
2290
|
+
// unitKind (spec §2, informative — per-unit, not by name): the synthesized `<module>` initializer
|
|
2291
|
+
// carries its own kind (set at mint), a CJS export is tagged "export".
|
|
2292
|
+
if (rec.unitKind) entry.unitKind = rec.unitKind;
|
|
2293
|
+
else if (rec.isCjsExport) entry.unitKind = "export"; // spec 0.5 draft, informative — per-unit, not by name
|
|
2240
2294
|
functions.push(entry);
|
|
2241
2295
|
}
|
|
2242
2296
|
// `package` names what this report COVERS — a consumer chaining it registers coverage even when
|