candor-ts 0.8.4 → 0.8.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/mcp.mjs CHANGED
@@ -162,12 +162,14 @@ const TOOLS = {
162
162
  candor_diff: {
163
163
  description: "The per-function effect delta versus a baseline report: gained (introduced vs inherited) and lost effects. 'What did this change do to the effect surface?'.",
164
164
  schema: { type: "object", properties: { baseline: { type: "string", description: "the baseline report prefix" }, ...reportArg }, required: ["baseline"] },
165
- run: (a, p) => Q.diff(Q.loadReport(p), Q.loadReport(a.baseline)),
165
+ run: (a, p) => ({ baseline_version: Q.reportVersion(a.baseline) ?? "", engine_version: Q.reportVersion(p) ?? "",
166
+ ...Q.diff(Q.loadReport(p), Q.loadReport(a.baseline)) }),
166
167
  },
167
168
  candor_gains: {
168
169
  description: "The supply-chain alarm: effects the surface GAINED versus a baseline (package-level + per-function) — 'did this dependency bump add Net/Exec somewhere?'.",
169
170
  schema: { type: "object", properties: { baseline: { type: "string", description: "the baseline report prefix" }, ...reportArg }, required: ["baseline"] },
170
- run: (a, p) => Q.gains(Q.loadReport(p), Q.loadReport(a.baseline)),
171
+ run: (a, p) => ({ baseline_version: Q.reportVersion(a.baseline) ?? "", engine_version: Q.reportVersion(p) ?? "",
172
+ ...Q.gains(Q.loadReport(p), Q.loadReport(a.baseline)) }),
171
173
  },
172
174
  };
173
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.8)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/query-core.mjs CHANGED
@@ -55,6 +55,21 @@ function normFns(parsed, source) {
55
55
  return out;
56
56
  }
57
57
 
58
+ /** The producing engine build of the report(s) at a prefix (the §2.1 envelope `candor.version`) — null
59
+ * when unreadable/absent (a legacy bare array has no provenance). Baselines are comparable only to
60
+ * their own producing version (§2.1): diff/gains consumers disclose a mismatch (an engine swap makes
61
+ * "gained" effects ambiguous — unmasking vs regression — the baseline-invalidation rule, AGENTS §2a). */
62
+ export function reportVersion(prefix) {
63
+ const files = fs.existsSync(`${prefix}.json`) ? [`${prefix}.json`] : siblings(prefix, isReport);
64
+ for (const f of files) {
65
+ try {
66
+ const v = JSON.parse(fs.readFileSync(f, "utf8"))?.candor?.version;
67
+ if (v) return String(v);
68
+ } catch { /* unreadable sibling — keep looking */ }
69
+ }
70
+ return null;
71
+ }
72
+
58
73
  export function loadReport(prefix) {
59
74
  if (fs.existsSync(`${prefix}.json`)) {
60
75
  // The PRIMARY report parse must DISCLOSE-and-tolerate like the sibling path — a bare JSON.parse here
package/query.mjs CHANGED
@@ -30,7 +30,7 @@ import { impact as coreImpact, path as corePath, gains as coreGains,
30
30
  show as coreShow, blindspots as coreBlindspots,
31
31
  callers as coreCallers, callersFrontier, loadHierarchy,
32
32
  containment as coreContainment,
33
- loadReport, loadCallgraph, matches } from "./query-core.mjs";
33
+ loadReport, loadCallgraph, matches , reportVersion } from "./query-core.mjs";
34
34
  const emit = (v) => console.log(JSON.stringify(v, null, 1));
35
35
 
36
36
  // ONE version + spec source, the SAME way scan.mjs reads them: PKG_VERSION is the bare semver from
@@ -180,8 +180,19 @@ switch (cmd) {
180
180
  if (gained.length || lost.length) changes.push({ fn, gained, lost });
181
181
  }
182
182
  changes.sort((a, b) => a.fn.localeCompare(b.fn));
183
- emit({ changes });
184
- process.exit(changes.some((c) => c.gained.length) ? 1 : 0);
183
+ // §2.1: a baseline is comparable only to its own producing build — disclose a mismatch (the gains
184
+ // may be the engine reclassifying after a coverage batch, not the code changing). Same note + JSON
185
+ // provenance fields as the Rust candor-query (cross-engine parity, item 10).
186
+ const engineV = reportVersion(curPrefix), baseV = reportVersion(basePrefix);
187
+ const versionMismatch = engineV && baseV && engineV !== baseV;
188
+ if (versionMismatch)
189
+ console.error(`candor-ts: ⚠ baseline @${baseV} ≠ engine @${engineV} — some changes may be the engine reclassifying, not your code. Treat an engine swap as baseline-invalidating: review, then regenerate the baseline.`);
190
+ emit({ baseline_version: baseV ?? "", engine_version: engineV ?? "", changes });
191
+ // diff DISCLOSES (the posture) — it is not a gate. Its gained-effect exit 1 is a convenience for
192
+ // same-build ratchet use; under a version mismatch that signal is BOGUS (unmasking, not regression),
193
+ // so exit 0 and let the ⚠ inform — never deliver the wave as a CI failure (review §2.1: guards fail
194
+ // closed, queries disclose).
195
+ process.exit(!versionMismatch && changes.some((c) => c.gained.length) ? 1 : 0);
185
196
  break; // unreachable (process.exit), but eslint can't prove it — defends against fallthrough
186
197
  }
187
198
  case "reachable": {
@@ -215,7 +226,10 @@ switch (cmd) {
215
226
  // the supply-chain alarm (SPEC §5.1): {gained:[Effect], byFunction:[{fn,effect}]} — what the
216
227
  // surface gained between two reports (base → cur), the cross-engine machine-readable form.
217
228
  const [curPrefix, basePrefix] = args;
218
- emit(coreGains(loadReport(curPrefix), loadReport(basePrefix)));
229
+ const gv = reportVersion(curPrefix), gbv = reportVersion(basePrefix);
230
+ if (gv && gbv && gv !== gbv)
231
+ console.error(`candor-ts: ⚠ baseline @${gbv} ≠ engine @${gv} — a "gained capability" may be the engine reclassifying, not the dependency changing. Regenerate both reports with one build to compare releases.`);
232
+ emit({ baseline_version: gbv ?? "", engine_version: gv ?? "", ...coreGains(loadReport(curPrefix), loadReport(basePrefix)) });
219
233
  break;
220
234
  }
221
235
  case "path": {