candor-ts 0.25.0 → 0.26.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.25)."*
15
+ > plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.26)."*
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
@@ -192,7 +192,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
192
192
  | A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
193
193
  | Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
194
194
  | The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
195
- | `{ candor: { version, toolchain, spec: "0.25" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
195
+ | `{ candor: { version, toolchain, spec: "0.26" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
196
196
  | Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
197
197
  | The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
198
198
 
@@ -210,7 +210,7 @@ read the Rust source".
210
210
 
211
211
  ## Status
212
212
 
213
- 0.19.x, speaking candor-spec 0.25: the analysis core, the gate (`--policy` / `--gate-json` /
213
+ 0.19.x, speaking candor-spec 0.26: the analysis core, the gate (`--policy` / `--gate-json` /
214
214
  `.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
215
215
  `--include-unknown` dispatch frontier, and ⟨0.24⟩ `gate --report` — the gate applied to an EXISTING
216
216
  report, byte-equivalent to `scan --policy`'s verdict), the MCP server, the LSP server, and the watch loop are
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.25.0",
4
- "description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.25)",
3
+ "version": "0.26.0",
4
+ "description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.26)",
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "@types/node": "^25.9.2",
package/query-core.mjs CHANGED
@@ -696,13 +696,37 @@ export const byCodePoint = (a, b) => {
696
696
  };
697
697
 
698
698
  // Reflexive+transitive subtype test over the hierarchy sidecar.
699
- function isSubtypeOf(type, owner, hierarchy) {
700
- if (type === owner) return true;
699
+ //
700
+ // ⟨0.26⟩ THREE-VALUED, because the format now distinguishes what it could not before. SPEC §2.2 makes the
701
+ // KEY SET the manifest: a producer emits a key for every type it indexed, `[]` included, so a type with NO
702
+ // key is one the pass never looked at. `hierarchy[t] ?? []` read those two cases alike and answered
703
+ // `false` — a positive claim about a type nobody analysed.
704
+ //
705
+ // MEASURED before the rung, doctoring only the sidecar of a real scan: removing the REACHING implementor's
706
+ // entry silently dropped the dispatcher from `possibleViaUnknownDispatch` (`[]` where the control gives
707
+ // `[Dispatcher.run]`), while removing the sidecar ENTIRELY left it correct — the ⟨0.24⟩ per-file rule
708
+ // over-lists. LESS information was SAFER than partial information. candor-java behaved identically, which
709
+ // is what said the defect was the FORMAT rather than either consumer.
710
+ //
711
+ // A POSITIVE DOMINATES: if a known path reaches `owner` the answer is YES even when another branch ran
712
+ // into an unindexed type — the relation is established and an unknown branch cannot un-establish it. NO is
713
+ // reserved for a walk that stayed entirely inside types the sidecar answers for.
714
+ function subtypeOf(type, owner, hierarchy) {
715
+ if (type === owner) return "YES";
716
+ let sawUnindexed = false;
701
717
  const seen = new Set(), stack = [type];
702
718
  while (stack.length) {
703
- for (const s of hierarchy[stack.pop()] ?? []) { if (s === owner) return true; if (!seen.has(s)) { seen.add(s); stack.push(s); } }
719
+ const cur = stack.pop();
720
+ if (!Object.prototype.hasOwnProperty.call(hierarchy, cur)) { sawUnindexed = true; continue; }
721
+ for (const s of hierarchy[cur]) { if (s === owner) return "YES"; if (!seen.has(s)) { seen.add(s); stack.push(s); } }
704
722
  }
705
- return false;
723
+ return sawUnindexed ? "UNANSWERABLE" : "NO";
724
+ }
725
+
726
+ // The two-valued form. UNANSWERABLE collapses to TRUE — disclose, never drop — which is the direction
727
+ // §2.2 ⟨0.26⟩ requires and the opposite of what absence used to do.
728
+ function isSubtypeOf(type, owner, hierarchy) {
729
+ return subtypeOf(type, owner, hierarchy) !== "NO";
706
730
  }
707
731
 
708
732
  // callers + the unresolved-dispatch frontier (--include-unknown, SPEC §3.1/§4 0.7): the CONFIRMED set,
@@ -1236,7 +1260,7 @@ export function narrowingContext(fns, cg = {}, policyParsed = null) {
1236
1260
  const u = byRaw.get(r.raw);
1237
1261
  if (!u) continue; // unreachable: every held triple has a group
1238
1262
  const cur = heldByFn.get(f.fn) ?? new Map();
1239
- cur.set(`${r.raw}${eff}`, { fn: f.fn, rule: r.raw, effect: eff, why: u.why });
1263
+ cur.set(`${r.raw}\0${eff}`, { fn: f.fn, rule: r.raw, effect: eff, why: u.why });
1240
1264
  heldByFn.set(f.fn, cur);
1241
1265
  }
1242
1266
  return {
package/query.mjs CHANGED
@@ -232,7 +232,7 @@ function renderPathHuman(fns, cg, fnQ, eff) {
232
232
  // package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
233
233
  const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
234
234
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
235
- const SPEC_VERSION = "0.25";
235
+ const SPEC_VERSION = "0.26";
236
236
 
237
237
  // ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
238
238
  // One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
package/scan.mjs CHANGED
@@ -44,7 +44,7 @@ const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
44
44
  // literal stamped into the envelope's `spec` field, so the doc lines and the report can never drift.
45
45
  // Reused, never re-littered.
46
46
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
47
- const SPEC_VERSION = "0.25";
47
+ const SPEC_VERSION = "0.26";
48
48
 
49
49
  // --version: a print-and-exit MODE, handled before the main arg walk so it never depends on a target.
50
50
  // Fully OFFLINE — candor never phones home. Staying current is the AGENT's job: read the installed
@@ -4371,9 +4371,13 @@ for (const [name, rec] of fns) {
4371
4371
  hash: `${pkgName}#${rec.local}`, // SPEC §2: the cross-package join key (package + local tail)
4372
4372
  inferred: inf,
4373
4373
  direct: [...rec.direct].sort(),
4374
- declared: [],
4375
- undeclared: [],
4376
- overdeclared: [],
4374
+ // ⟨0.26⟩ `declared`/`undeclared`/`overdeclared` are DELIBERATELY ABSENT. They are the §5
4375
+ // capability-reconciliation outputs and this engine runs no such pass, so emitting `[]` would be a
4376
+ // positive claim — `undeclared: []` reads as "this function performs no undeclared effect", an
4377
+ // AS-EFF-001 all-clear from a check that never ran. SPEC §2 ⟨0.26⟩: present means the pass ran,
4378
+ // absent means it did not, and `[]` from an engine that computed nothing is forbidden.
4379
+ // They were emitted as hardcoded constants "for cross-engine schema parity" — a schema-parity check
4380
+ // is exactly what made that look conforming, which is why the rule now forbids requiring them.
4377
4381
  unresolved: inf.includes("Unknown"),
4378
4382
  };
4379
4383
  // Inline call edges (§2 `calls`) — the SAME edges the callgraph sidecar carries, embedded per entry so a
@@ -4909,7 +4913,13 @@ for (const sf of sources) {
4909
4913
  supers.push(d && d.name ? `${moduleOf(d.getSourceFile())}.${namespacePrefixOf(d)}${d.name.getText()}` : t.expression.getText());
4910
4914
  }
4911
4915
  }
4912
- if (supers.length) hierarchy[`${mod}.${namespacePrefixOf(node)}${node.name.getText()}`] = supers;
4916
+ // ⟨0.26⟩ A KEY FOR EVERY TYPE INDEXED, `[]` INCLUDED — the key set IS the manifest (SPEC §2.2).
4917
+ // This was `if (supers.length)`, so a type with no supertypes was OMITTED and absence meant BOTH
4918
+ // "no supertypes" and "never indexed". Measured: removing one entry from a real sidecar silently
4919
+ // dropped a `callers --include-unknown` frontier row, while removing the sidecar ENTIRELY left it
4920
+ // correct — so LESS information was SAFER than partial information. §2.2's own example has always
4921
+ // shown `"app.Base": []`; this producer contradicted it.
4922
+ hierarchy[`${mod}.${namespacePrefixOf(node)}${node.name.getText()}`] = supers;
4913
4923
  }
4914
4924
  ts.forEachChild(node, walk);
4915
4925
  })(sf);