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 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)."*
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.15" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
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.15.x, speaking candor-spec 0.15: the analysis core, the gate (`--policy` / `--gate-json` /
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.15.0",
4
- "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.15)",
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.15";
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 build and spec version (offline)`);
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). Banner + USAGE + the full described subcommand list + the github footer.
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
- console.log(`candor-ts-query ${PKG_VERSION} — read-only queries over a candor report (candor-spec ${SPEC_VERSION})
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
- ${usage()}
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.15";
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 ${PKG_VERSION} — TypeScript/JavaScript effect scanner (candor-spec ${SPEC_VERSION})
58
+ console.log(`candor-ts — the TypeScript/JavaScript effect analyzer.
59
59
 
60
- USAGE: candor-ts <dir | file.ts | tsconfig.json> [--out <prefix>] [--json] [--policy <file>] [--gate-json <file>] [--allow-js] [--agents] [--version]
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
- <target> a dir, a .ts file, or a tsconfig.json to scan
63
- --out <prefix> write the report to <prefix>.json + <prefix>.callgraph.json
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
- CANDOR_BASELINE=<report.json> (or a .candor/config \`baseline\` key) runs the AS-EFF-005 regression
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
- if (prior === undefined) continue; // new function — not a regression
2653
- const gained = [...inferred.get(name)].filter((x) => !prior.has(x)).sort();
2654
- if (gained.length) {
2655
- gateViolations.push({ rule: "AS-EFF-005", fn: name, effects: gained,
2656
- detail: `\`${name}\` gained effect { ${gained.join(", ")} } not present in the baseline` });
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 ✓");