candor-ts 0.28.0 → 0.28.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/README.md CHANGED
@@ -210,7 +210,7 @@ read the Rust source".
210
210
 
211
211
  ## Status
212
212
 
213
- 0.19.x, speaking candor-spec 0.28: the analysis core, the gate (`--policy` / `--gate-json` /
213
+ 0.28.0, speaking candor-spec 0.28: 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/contract.mjs CHANGED
@@ -21,7 +21,25 @@ export function printAgents() {
21
21
  // and drains on the way out, scan.mjs exits. THE SIBLING ROUTE AGAIN — sharing the PRINTER does not
22
22
  // share the EXIT, and the divergence lived in the caller the shared function was meant to protect.
23
23
  // Fixing it here rather than in scan.mjs is deliberate: the next caller inherits the fix.
24
+ //
25
+ // Two failure modes the first version of this did not handle, both specific to a PIPE, which is the
26
+ // case it exists for:
27
+ // EPIPE — the reader stopped early (`| head -n5`, `| grep -m1`, a consumer that closed). The old
28
+ // console.log path exited 0 silently; a bare writeSync raises, and a print-and-exit mode
29
+ // answering a contract request with a Node stack trace and exit 1 is a worse regression
30
+ // than the truncation this replaced. Swallowed — the reader left, that is not our error.
31
+ // EAGAIN — once stdout has been initialised, libuv puts the pipe in non-blocking mode and writeSync
32
+ // THROWS rather than short-writing as soon as the payload exceeds the 64 KiB pipe buffer.
33
+ // The contract is 24 KiB today, so this is latent, not live — and it would come back as
34
+ // exactly the truncation-plus-noise this function was written to remove. Retry with a
35
+ // small backoff (Atomics.wait is the only synchronous sleep available here).
24
36
  let off = 0;
25
37
  const buf = Buffer.from(out, "utf8");
26
- while (off < buf.length) off += fs.writeSync(1, buf, off, buf.length - off); // a short write is legal
38
+ const idle = new Int32Array(new SharedArrayBuffer(4));
39
+ try {
40
+ while (off < buf.length) {
41
+ try { off += fs.writeSync(1, buf, off, buf.length - off); } // a short write is legal
42
+ catch (e) { if (e.code !== "EAGAIN") throw e; Atomics.wait(idle, 0, 0, 1); }
43
+ }
44
+ } catch (e) { if (e.code !== "EPIPE") throw e; }
27
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.28)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/scan.mjs CHANGED
@@ -2829,11 +2829,13 @@ for (const sf of sources) {
2829
2829
  endLine: sf.getLineAndCharacterOfPosition(node.getEnd()).line + 1 });
2830
2830
  nodeName.set(node, qual);
2831
2831
  // Record a unit minted for a declaration with NO BODY, for the §3 disclosure pass below (see it
2832
- // for the argument). MIRROR `fns.set`'s last-write-wins EXACTLY — `.delete` on the bodied write is
2833
- // load-bearing, not tidiness: an overload set is N body-less signatures followed by the
2834
- // implementation under the SAME qual, so a plain `.add` would mark every real overloaded function
2835
- // in the project unanalysable and charge its callers Unknown. That is the over-charge half of the
2836
- // fix, and it is pinned by a fixture (`over`/`callsOver`, still `Fs` after this pass).
2832
+ // for the argument). The `.delete` mirrors `fns.set`'s last-write-wins for the MODULE-LEVEL
2833
+ // overload set (N body-less signatures then the implementation, all under one qual) — but it is
2834
+ // NOT sufficient on its own, and assuming it was is how this fix shipped an over-charge: a
2835
+ // FUNCTION-SCOPED unit gets `#line:col` appended to its qual, so each signature holds a distinct
2836
+ // key, the implementation's delete never reaches them, and every signature was charged
2837
+ // `Unknown[native:…]` over code that is fully visible. The disclosure pass below therefore asks
2838
+ // the SYMBOL — which every overload in a set shares, at any scope — and that is the real guard.
2837
2839
  {
2838
2840
  const hasBodySlot = ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)
2839
2841
  || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node)
@@ -5440,10 +5442,39 @@ function resolveDepEntryKey(pkg, subpath) {
5440
5442
  // zod's `ZodType._parse` one — excluding them takes zod's delta to +0 and hono's from +18 to +9, and
5441
5443
  // every one of the nine that remain is a true positive (`Deno.mkdir`/`writeFile` are Fs, `Deno.
5442
5444
  // upgradeWebSocket` and `FetcherLike.fetch` are Net, all declared in local `.d.ts` shims with no body).
5443
- const answeredByLocalBody = (node) => (classOverrides.get(node) ?? []).some((om) => !!om.body);
5445
+ // Does a LOCAL body answer this declaration? Climbs `classOverrides` TRANSITIVELY: that index records
5446
+ // DIRECT-subclass overrides only, so `Base (abstract) → Mid (abstract) → Impl (bodied)` resolved to
5447
+ // `Mid.hook` — no body — and charged `Base.hook` Unknown even though `Impl.hook` is the sole concrete
5448
+ // implementation and fully analyzed. The two-level fixture could not see it, which is also why the
5449
+ // measured corpus deltas characterise one-level hierarchies only.
5450
+ const answeredByLocalBody = (node, seen = new Set()) => {
5451
+ if (seen.has(node)) return false; // cycle guard: heritage clauses can be circular in bad input
5452
+ seen.add(node);
5453
+ return (classOverrides.get(node) ?? []).some((om) => !!om.body || answeredByLocalBody(om, seen));
5454
+ };
5455
+ // The IMPLEMENTATION of the overload set this body-less signature belongs to, if we analyzed it.
5456
+ // Keyed on the SYMBOL, not the unit name: every overload shares one symbol at any scope, where the
5457
+ // qual does not (a function-scoped unit carries a `#line:col` suffix to keep sibling scopes apart).
5458
+ const overloadImplOf = (node) => {
5459
+ const sym = node.name ? checker.getSymbolAtLocation(node.name) : undefined;
5460
+ return (sym?.declarations ?? []).find((d) => d !== node && !!d.body);
5461
+ };
5444
5462
  for (const [qual, meta] of bodylessDecls) {
5445
5463
  const rec = fns.get(qual);
5446
5464
  if (!rec || answeredByLocalBody(meta.node)) continue;
5465
+ // An overload SIGNATURE with a local implementation is not unanswerable — but neither is it empty,
5466
+ // and skipping it silently was this fix's first mistake in the other direction. At MODULE level every
5467
+ // overload shares one qual, so a call resolving to the signature lands on the implementation's unit
5468
+ // by construction. FUNCTION-SCOPED, the quals differ, the checker resolves the call to the SIGNATURE,
5469
+ // and the caller reached an empty unit and read PURE — a silent under-report that predates this pass
5470
+ // (`outer` calling a nested overloaded `inner` that writes a file was pure before any of this). Edge
5471
+ // the signature to the implementation and the existing fixpoint carries the real effect through.
5472
+ const implDecl = overloadImplOf(meta.node);
5473
+ if (implDecl) {
5474
+ const target = nodeName.get(implDecl);
5475
+ if (target && target !== qual) rec.edges.add(target);
5476
+ continue;
5477
+ }
5447
5478
  rec.direct.add("Unknown");
5448
5479
  // §4's dividing line, same as everywhere else in this file. An `abstract` member IS an unresolved
5449
5480
  // DISPATCH — owner type and member are both nameable, which is what `dispatch:` reserves itself for,