candor-ts 0.15.0 → 0.16.0
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 +63 -5
- package/scan.mjs +112 -22
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.16)."*
|
|
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.16" }, 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.16.x, speaking candor-spec 0.16: 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.16.0",
|
|
4
|
+
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.16)",
|
|
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.16";
|
|
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]
|
|
@@ -307,7 +307,7 @@ const SUBCOMMANDS = [
|
|
|
307
307
|
const usage = () => {
|
|
308
308
|
const w = Math.max(...SUBCOMMANDS.map(([n, a]) => `${n} ${a}`.trimEnd().length));
|
|
309
309
|
const lines = SUBCOMMANDS.map(([n, a, d]) => ` ${`${n} ${a}`.trimEnd().padEnd(w)} ${d}`);
|
|
310
|
-
lines.push(` ${"-V, --version".padEnd(w)} print the
|
|
310
|
+
lines.push(` ${"-V, --version".padEnd(w)} print the installed version + upgrade line (offline)`);
|
|
311
311
|
lines.push(` ${"-h, --help".padEnd(w)} show this help`);
|
|
312
312
|
return `USAGE: candor-ts-query <command> [args]\n\n${lines.join("\n")}`;
|
|
313
313
|
};
|
|
@@ -321,12 +321,54 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
// -h / --help: a print-and-exit MODE, handled before the switch (so `-h`'s single dash is never
|
|
324
|
-
// mistaken for a command).
|
|
324
|
+
// mistaken for a command). House-style page: identity + model paragraph + COMMON/ALL ACTIONS
|
|
325
|
+
// (the action names derived from SUBCOMMANDS, so the list can never go stale) + OPTIONS + footer.
|
|
326
|
+
// The exit-2 error path keeps the denser fully-described usage() above.
|
|
325
327
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
326
|
-
|
|
328
|
+
const names = SUBCOMMANDS.map(([n]) => n);
|
|
329
|
+
const allActions = [names.slice(0, 9), names.slice(9)].map((row) => ` ${row.join(" ")}`).join("\n");
|
|
330
|
+
console.log(`candor-ts-query — read-only queries over a candor report.
|
|
327
331
|
|
|
328
|
-
|
|
332
|
+
Answers come from the report candor-ts wrote — discovered by walking up from the
|
|
333
|
+
cwd to a .candor/ dir (CANDOR_REPORT overrides; --report pins a locator). No
|
|
334
|
+
re-scan, no network. Every engine speaks the same grammar, so these actions and
|
|
335
|
+
flags match the rest of the family.
|
|
329
336
|
|
|
337
|
+
USAGE
|
|
338
|
+
candor-ts-query <action> [args] [options]
|
|
339
|
+
|
|
340
|
+
COMMON ACTIONS
|
|
341
|
+
where <Effect> the functions that perform an effect
|
|
342
|
+
path <fn> <Effect> the call path by which a function reaches an effect
|
|
343
|
+
callers <fn> who calls a function, direct and transitive
|
|
344
|
+
tour [N] the N most surprising transitive reaches (default 10)
|
|
345
|
+
blindspots the Unknown sources worth resolving, ranked by reach
|
|
346
|
+
gains <current> <base> what a new version newly reaches (the supply-chain diff)
|
|
347
|
+
fix <fn> <Effect> the boundary hoist that would clear a violation
|
|
348
|
+
|
|
349
|
+
ALL ACTIONS
|
|
350
|
+
${allActions}
|
|
351
|
+
|
|
352
|
+
OPTIONS (uniform across every engine)
|
|
353
|
+
--report <locator> use this report instead of discovering .candor/
|
|
354
|
+
--policy <file> evaluate a policy — exit 1 on a violation (whatif, fix, fix-gate,
|
|
355
|
+
unverified; CANDOR_POLICY / a .candor/config \`policy\` key when absent)
|
|
356
|
+
--json machine-readable output
|
|
357
|
+
--include-unknown callers: also list the unresolved-dispatch frontier
|
|
358
|
+
--strict unverified: exit 1 on an unverified hole (advisory otherwise)
|
|
359
|
+
-V, --version print the installed version + upgrade line (offline)
|
|
360
|
+
-h, --help show this help
|
|
361
|
+
|
|
362
|
+
diff and gains take two positional report locators: <current> <baseline>. Run
|
|
363
|
+
candor-ts-query with no action for the full per-action argument list.
|
|
364
|
+
|
|
365
|
+
EXAMPLES
|
|
366
|
+
candor-ts-query where Db
|
|
367
|
+
candor-ts-query path app.orders.render Net
|
|
368
|
+
candor-ts-query gains new/.candor/report.json old/.candor/report.json
|
|
369
|
+
candor-ts-query fix-gate --policy candor.policy
|
|
370
|
+
|
|
371
|
+
Docs: candor.poly.io · Verify an install: candor doctor
|
|
330
372
|
See https://github.com/tombaldwin/candor`);
|
|
331
373
|
process.exit(0);
|
|
332
374
|
}
|
|
@@ -354,6 +396,9 @@ switch (cmd) {
|
|
|
354
396
|
// written; the paths silently vanished) and dropped Exec `cmds` entirely. Call the shared show so
|
|
355
397
|
// the CLI and the MCP `candor_show` are one implementation that cannot diverge again.
|
|
356
398
|
const { prefix, args: [q] } = resolveReportVerb(args, 1);
|
|
399
|
+
// A missing/empty <query> is a LOUD usage error (exit 2, like candor-java) — never a silently-empty
|
|
400
|
+
// `[]` at exit 0, which reads as an authoritative "no such function" over a question never asked.
|
|
401
|
+
if (!q) { console.error("usage: candor-ts-query show <query> [--report <locator>] [--json]"); process.exit(2); }
|
|
357
402
|
emit(coreShow(loadReportOrDie(prefix), q));
|
|
358
403
|
break;
|
|
359
404
|
}
|
|
@@ -362,6 +407,9 @@ switch (cmd) {
|
|
|
362
407
|
// Hand-copies of core functions in this file have drifted three times (show, callers, diff); the
|
|
363
408
|
// fix each time was the same: delegate, keep query.mjs as arg-parsing + emit + exit codes only.
|
|
364
409
|
const { prefix, args: [eff] } = resolveReportVerb(args, 1);
|
|
410
|
+
// A missing/empty <Effect> is a LOUD usage error (exit 2, like candor-java's missing-arg path) —
|
|
411
|
+
// never an authoritative-empty {directly:[],inherited:[]} at exit 0 (a false all-clear shape).
|
|
412
|
+
if (!eff) { console.error("usage: candor-ts-query where <Effect> [--report <locator>] [--json]"); process.exit(2); }
|
|
365
413
|
emit(coreWhere(loadReportOrDie(prefix), eff));
|
|
366
414
|
break;
|
|
367
415
|
}
|
|
@@ -370,6 +418,9 @@ switch (cmd) {
|
|
|
370
418
|
// it, the byte-for-byte {of,direct,transitive} shape is unchanged (cross-engine parity). Call the
|
|
371
419
|
// shared query-core so the CLI and MCP compute one truth (the prior inline copy had drifted before).
|
|
372
420
|
const { prefix, args: [q], includeUnknown } = resolveReportVerb(args, 1, { includeUnknown: true });
|
|
421
|
+
// A missing/empty <query> is a LOUD usage error (exit 2, like candor-java) — never an empty
|
|
422
|
+
// {of:[],direct:[],transitive:[]} at exit 0 (reads as "nothing reaches it" for a fn never named).
|
|
423
|
+
if (!q) { console.error("usage: candor-ts-query callers <query> [--include-unknown] [--report <locator>] [--json]"); process.exit(2); }
|
|
373
424
|
const cg = loadCallgraph(prefix);
|
|
374
425
|
if (includeUnknown) emit(callersFrontier(cg, loadReportOrDie(prefix), loadHierarchy(prefix), q));
|
|
375
426
|
else emit(coreCallers(cg, q));
|
|
@@ -465,6 +516,9 @@ switch (cmd) {
|
|
|
465
516
|
// blast radius (backward dual of reachable) — reuses the shared query-core, the same logic the
|
|
466
517
|
// MCP server serves. SPEC §3.1: {fn, affectedCount, affected, entryPoints:[{fn,inferred}]}.
|
|
467
518
|
const { prefix, args: [q] } = resolveReportVerb(args, 1);
|
|
519
|
+
// A missing/empty <query> is a LOUD usage error (exit 2, like candor-java) — never an
|
|
520
|
+
// affectedCount:0 blast radius at exit 0 for a function that was never named.
|
|
521
|
+
if (!q) { console.error("usage: candor-ts-query impact <query> [--report <locator>] [--json]"); process.exit(2); }
|
|
468
522
|
emit(coreImpact(loadReportOrDie(prefix), loadCallgraph(prefix), q));
|
|
469
523
|
break;
|
|
470
524
|
}
|
|
@@ -587,6 +641,10 @@ switch (cmd) {
|
|
|
587
641
|
// pinned JSON shape. parseCanonical otherwise swallows --json, so detect it explicitly (as `tour` does).
|
|
588
642
|
const wantJson = args.includes("--json");
|
|
589
643
|
const { prefix, args: [fn, eff] } = resolveReportVerb(args, 2);
|
|
644
|
+
// BOTH positionals are required (`path <fn> <Effect>`) — a missing/empty one is a LOUD usage error
|
|
645
|
+
// (exit 2, like candor-java). Before this gate, one arg slid through as `<fn> undefined` and printed
|
|
646
|
+
// "does not perform undefined" at exit 0 — a false all-clear over a question that was never posed.
|
|
647
|
+
if (!fn || !eff) { console.error("usage: candor-ts-query path <fn> <Effect> [--report <locator>] [--json]"); process.exit(2); }
|
|
590
648
|
const fns = loadReportOrDie(prefix);
|
|
591
649
|
const cg = loadCallgraph(prefix);
|
|
592
650
|
if (wantJson) emit(corePath(fns, cg, fn, eff)); // conformance PART 5 shape — UNCHANGED
|
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.16";
|
|
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
|
|
@@ -55,25 +55,45 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
|
55
55
|
// -h / --help: a print-and-exit MODE (like --version), handled before the arg walk so `-h` (a single
|
|
56
56
|
// dash) is never mistaken for the scan target by the positional fallthrough below.
|
|
57
57
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
58
|
-
console.log(`candor-ts
|
|
58
|
+
console.log(`candor-ts — the TypeScript/JavaScript effect analyzer.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Reads TS/JS source through the TypeScript compiler API — no build needed. Calls
|
|
61
|
+
are resolved through the checker; a call that cannot be resolved reads Unknown,
|
|
62
|
+
never silently pure. The report lands in .candor/, where candor-ts-query and the
|
|
63
|
+
umbrella \`candor\` CLI discover it.
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
--json print the report as JSON to stdout (instead of writing files)
|
|
65
|
-
--policy <file> enforce a policy file (deny/pure/allow/forbid, candor-spec §6.2) — exit 1 on a
|
|
66
|
-
violation, 2 if unreadable; honours $CANDOR_POLICY when the flag is absent
|
|
67
|
-
--gate-json <f> write the structured gate verdict { spec, ok, violations } as JSON (candor-spec §3.3)
|
|
68
|
-
--allow-js also scan plain JS/Node (.js/.mjs/.cjs), not just TypeScript
|
|
69
|
-
--agents print the agent contract for this build (AGENTS.md)
|
|
70
|
-
-V, --version print the build and spec version (offline)
|
|
71
|
-
-h, --help show this help
|
|
65
|
+
USAGE
|
|
66
|
+
candor-ts <dir | file.ts | tsconfig.json> [flags]
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
guard against a saved same-build report: exit 1 when an existing function gained an effect, exit 2
|
|
75
|
-
on an unparseable or different-build baseline (never evaluated), a stderr note when absent.
|
|
68
|
+
The target is a project directory, a single .ts file, or a tsconfig.json.
|
|
76
69
|
|
|
70
|
+
OPTIONS
|
|
71
|
+
--out <prefix> write the report to <prefix>.json + <prefix>.callgraph.json
|
|
72
|
+
--json print the report as JSON to stdout (instead of writing files)
|
|
73
|
+
--policy <file> enforce a policy file (deny/pure/allow/forbid) — exit 1 on a
|
|
74
|
+
violation, 2 if unreadable
|
|
75
|
+
--gate-json <file> write the structured gate verdict { spec, ok, violations } as JSON
|
|
76
|
+
--allow-js also scan plain JS/Node (.js/.mjs/.cjs), not just TypeScript
|
|
77
|
+
--agents print the agent contract for this build (AGENTS.md)
|
|
78
|
+
-V, --version print the installed version + upgrade line (offline)
|
|
79
|
+
-h, --help show this help
|
|
80
|
+
|
|
81
|
+
ENVIRONMENT / CONFIG
|
|
82
|
+
CANDOR_POLICY=<file> the policy when --policy is absent (a .candor/config
|
|
83
|
+
\`policy\` key works too)
|
|
84
|
+
CANDOR_BASELINE=<report.json> (or a .candor/config \`baseline\` key) runs the AS-EFF-005
|
|
85
|
+
regression guard against a saved same-build report: exit 1
|
|
86
|
+
when an existing function gained an effect, exit 2 on an
|
|
87
|
+
unparseable or different-build baseline (never evaluated),
|
|
88
|
+
a stderr note when absent
|
|
89
|
+
|
|
90
|
+
EXAMPLES
|
|
91
|
+
candor-ts .
|
|
92
|
+
candor-ts src --allow-js
|
|
93
|
+
candor-ts . --policy candor.policy --gate-json gate.json
|
|
94
|
+
candor-ts-query where Db query the report this scan wrote
|
|
95
|
+
|
|
96
|
+
Docs: candor.poly.io · Verify an install: candor doctor
|
|
77
97
|
See https://github.com/tombaldwin/candor`);
|
|
78
98
|
process.exit(0);
|
|
79
99
|
}
|
|
@@ -2616,6 +2636,20 @@ let gateViolations = [];
|
|
|
2616
2636
|
// · Valid + same build → per-fn compare: an EXISTING fn gaining an effect is an [AS-EFF-005]
|
|
2617
2637
|
// violation (exit 1, joins --gate-json); a fn absent from the baseline is NEW code, reviewed as
|
|
2618
2638
|
// such, not a regression. Baselines omit pure fns (spec §2), so absent-prior means no prior claim.
|
|
2639
|
+
//
|
|
2640
|
+
// ⟨0.16⟩ Callgraph-aware existence (SPEC §7 item 5). Reports OMIT pure functions, so a fn that
|
|
2641
|
+
// shipped PURE and now performs an effect is absent from the baseline report and reads as exempt "new
|
|
2642
|
+
// code" — the sharpest supply-chain shape escaping the guard. Fix: key existence on the baseline
|
|
2643
|
+
// CALLGRAPH sidecar (<baseline>.callgraph.json, §2.2 — it lists every project fn INCLUDING pure
|
|
2644
|
+
// leaves), exactly as `gains`'s `origin` existence test does (query-core.mjs `gains`: a fn is
|
|
2645
|
+
// "existing" if it is a baseline-callgraph node — a caller key or a callee):
|
|
2646
|
+
// · sidecar PRESENT + loaded → a fn that is a baseline-callgraph node has baseline effect set ∅
|
|
2647
|
+
// (pure → omitted from the report) and any effect now is a GAIN violation. pure→effectful is caught.
|
|
2648
|
+
// A fn in NEITHER report nor callgraph genuinely did not exist → stays exempt "new".
|
|
2649
|
+
// · sidecar ABSENT → degrade to report-only existence (pre-⟨0.16⟩: a formerly-pure fn reads as new;
|
|
2650
|
+
// still catches an already-effectful fn WIDENING). One stderr note that the guard is weaker.
|
|
2651
|
+
// · sidecar PRESENT-but-CORRUPT → fail closed (exit 2), like a corrupt baseline: a broken sidecar
|
|
2652
|
+
// must not silently NARROW the guard back to report-only.
|
|
2619
2653
|
if (baselinePath !== null) {
|
|
2620
2654
|
const shownB = baselinePath === "" ? "(configured empty)" : baselinePath;
|
|
2621
2655
|
if (baselinePath !== "" && !fs.existsSync(baselinePath)) {
|
|
@@ -2647,14 +2681,66 @@ if (baselinePath !== null) {
|
|
|
2647
2681
|
for (const e of arr) {
|
|
2648
2682
|
if (e && typeof e.fn === "string" && e.fn) base.set(e.fn, new Set(Array.isArray(e.inferred) ? e.inferred : []));
|
|
2649
2683
|
}
|
|
2684
|
+
// ⟨0.16⟩ Load the baseline callgraph sidecar next to the baseline report. The sidecar for a
|
|
2685
|
+
// report at <stem>.json is <stem>.callgraph.json (scan.mjs writes exactly this pair). Three states:
|
|
2686
|
+
// loaded — a parsed object → its node set (every key + every callee) keys existence, mirroring
|
|
2687
|
+
// the `gains` origin test (query-core.mjs). A baseline-callgraph node whose baseline
|
|
2688
|
+
// effects are ∅ (pure → omitted from the report) that now performs an effect is a GAIN.
|
|
2689
|
+
// absent — no sidecar file → degrade to report-only existence + one stderr note (guard weaker).
|
|
2690
|
+
// corrupt — file present but not parseable / not a plain object → fail closed (exit 2). A broken
|
|
2691
|
+
// sidecar must not silently narrow the guard (SPEC §7 item 5).
|
|
2692
|
+
// shownB may be "(configured empty)"; the real path is baselinePath here (non-null, exists).
|
|
2693
|
+
const sidecarPath = baselinePath.replace(/\.json$/i, "") + ".callgraph.json";
|
|
2694
|
+
let cgNodes = null; // null = sidecar absent (report-only degrade)
|
|
2695
|
+
if (fs.existsSync(sidecarPath)) {
|
|
2696
|
+
let baseCg = null;
|
|
2697
|
+
try { baseCg = JSON.parse(fs.readFileSync(sidecarPath, "utf8")); } catch { baseCg = undefined; }
|
|
2698
|
+
// A non-object parse (null / array / number) is a corrupt sidecar: it cannot list nodes, and
|
|
2699
|
+
// treating it as "absent" would silently narrow the guard — fail closed like a corrupt baseline.
|
|
2700
|
+
if (baseCg === undefined || baseCg === null || typeof baseCg !== "object" || Array.isArray(baseCg)) {
|
|
2701
|
+
console.error(`candor-ts: the baseline callgraph ${sidecarPath} is present but could not be parsed `
|
|
2702
|
+
+ `(corrupt/truncated?) — failing (exit 2); a broken sidecar must not silently narrow the guard to `
|
|
2703
|
+
+ `report-only. Regenerate the baseline with this build.`);
|
|
2704
|
+
process.exit(2);
|
|
2705
|
+
}
|
|
2706
|
+
// The node set = every caller key + every callee (a pure leaf appears only as a callee), exactly
|
|
2707
|
+
// as `gains` computes cgNodes. Non-array edge values are tolerated (skipped), matching loadCallgraph.
|
|
2708
|
+
cgNodes = new Set(Object.entries(baseCg).flatMap(([k, vs]) => [k, ...(Array.isArray(vs) ? vs : [])]));
|
|
2709
|
+
} else {
|
|
2710
|
+
console.error(`candor-ts: no baseline callgraph sidecar at ${sidecarPath} — the AS-EFF-005 guard is `
|
|
2711
|
+
+ `WEAKER: existence falls back to the report, which omits pure functions, so a formerly-PURE fn `
|
|
2712
|
+
+ `turning effectful reads as new code and is NOT caught (only an already-effectful fn widening is). `
|
|
2713
|
+
+ `Regenerate the baseline with --out so the .callgraph.json is written alongside it.`);
|
|
2714
|
+
}
|
|
2715
|
+
const unknownOnly = []; // ⟨0.16⟩ advisory: fns that gained ONLY Unknown vs the baseline
|
|
2650
2716
|
for (const name of [...inferred.keys()].sort()) {
|
|
2651
2717
|
const prior = base.get(name);
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2718
|
+
// ⟨0.16⟩ Existence ladder: in the baseline REPORT → its recorded inferred set is the prior;
|
|
2719
|
+
// else a baseline-callgraph NODE (sidecar present) → it existed and was pure, so prior = ∅ (any
|
|
2720
|
+
// effect now is a gain); else genuinely absent → new code, exempt. Without the sidecar (cgNodes
|
|
2721
|
+
// null) only the report path decides, the pre-⟨0.16⟩ semantics.
|
|
2722
|
+
const priorSet = prior !== undefined ? prior
|
|
2723
|
+
: (cgNodes !== null && cgNodes.has(name)) ? new Set() // baseline-pure node → ∅ prior
|
|
2724
|
+
: null; // new function — not a regression
|
|
2725
|
+
if (priorSet === null) continue;
|
|
2726
|
+
const gained = [...inferred.get(name)].filter((x) => !priorSet.has(x)).sort();
|
|
2727
|
+
if (!gained.length) continue;
|
|
2728
|
+
// ⟨0.16⟩ the ratchet fires only on gaining a REAL boundary effect. An Unknown-ONLY gain is
|
|
2729
|
+
// the §4 trust marker, not an effect (`pure` policies exclude it), and on version bumps it is
|
|
2730
|
+
// dominated by resolution noise — DISCLOSE it (advisory), never fail the gate on it. Mirrors the
|
|
2731
|
+
// reference engine (candor-scan gate.rs check_baseline).
|
|
2732
|
+
const real = gained.filter((x) => x !== "Unknown");
|
|
2733
|
+
if (!real.length) { unknownOnly.push(name); continue; }
|
|
2734
|
+
gateViolations.push({ rule: "AS-EFF-005", fn: name, effects: real,
|
|
2735
|
+
detail: `\`${name}\` gained effect { ${real.join(", ")} } not present in the baseline` });
|
|
2736
|
+
}
|
|
2737
|
+
if (unknownOnly.length) {
|
|
2738
|
+
unknownOnly.sort();
|
|
2739
|
+
const shown = unknownOnly.slice(0, 3).join(", ");
|
|
2740
|
+
const more = unknownOnly.length > 3 ? ` (+${unknownOnly.length - 3} more)` : "";
|
|
2741
|
+
console.error(`candor-ts: note — ${unknownOnly.length} function(s) gained an unresolved call `
|
|
2742
|
+
+ `(Unknown) vs the baseline but no real effect — advisory, NOT a regression (Unknown is the §4 `
|
|
2743
|
+
+ `trust marker, dominated by resolution noise on version bumps): ${shown}${more}`);
|
|
2658
2744
|
}
|
|
2659
2745
|
}
|
|
2660
2746
|
}
|
|
@@ -2721,6 +2807,10 @@ if (gateJsonPath) {
|
|
|
2721
2807
|
// gateViolations is non-empty only when a gate surface (policy / baseline) was active and fired.
|
|
2722
2808
|
if (gateViolations.length) {
|
|
2723
2809
|
console.error(`candor-ts: ${gateViolations.length} policy violation(s)`);
|
|
2810
|
+
// FAILURE-only pointer at the engine's own remedy verb (append-only, same stream as the summary; a
|
|
2811
|
+
// zero-violation run is byte-identical — the exit code, violation lines and summary text are pinned
|
|
2812
|
+
// by the conformance suite and stay untouched).
|
|
2813
|
+
console.error("→ candor-ts-query fix-gate names the remedy for each");
|
|
2724
2814
|
process.exit(1);
|
|
2725
2815
|
}
|
|
2726
2816
|
if (policyPath !== null) console.error("candor-ts: policy ✓");
|