candor-ts 0.28.1 → 0.28.2
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/contract.mjs +10 -1
- package/package.json +2 -2
- package/scan.mjs +27 -9
package/contract.mjs
CHANGED
|
@@ -41,5 +41,14 @@ export function printAgents() {
|
|
|
41
41
|
try { off += fs.writeSync(1, buf, off, buf.length - off); } // a short write is legal
|
|
42
42
|
catch (e) { if (e.code !== "EAGAIN") throw e; Atomics.wait(idle, 0, 0, 1); }
|
|
43
43
|
}
|
|
44
|
-
} catch (e) {
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e.code !== "EPIPE") throw e;
|
|
46
|
+
// SWALLOWED, BUT NOT SILENT. Exiting non-zero would make `--agents | head` a failure, which it is
|
|
47
|
+
// not. But a truncated contract with exit 0 and an empty stderr is verbatim the defect this function
|
|
48
|
+
// was rewritten to remove ("an agent piping candor-ts --agents into its context silently read a
|
|
49
|
+
// third of its own instructions") — so the reader-left case has to be STATED. stderr may be closed
|
|
50
|
+
// too; that write is best-effort by construction.
|
|
51
|
+
try { fs.writeSync(2, `candor-ts: --agents output was cut short at ${off} of ${buf.length} bytes `
|
|
52
|
+
+ `— the reader closed the pipe. This contract is INCOMPLETE.\n`); } catch { /* nothing left to tell */ }
|
|
53
|
+
}
|
|
45
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"lint": "eslint *.mjs",
|
|
23
|
-
"test": "npm run lint && node --test test-unit.mjs && node test.mjs && node test-mcp.mjs && node test-lsp.mjs && node test-watch.mjs && npm run test:probe && npm run test:fuzz",
|
|
23
|
+
"test": "npm run lint && node --test test-unit.mjs && node test.mjs --parallel && node test-mcp.mjs && node test-lsp.mjs && node test-watch.mjs && npm run test:probe && npm run test:fuzz",
|
|
24
24
|
"test:unit": "node --test test-unit.mjs",
|
|
25
25
|
"test:probe": "node fabrication_probe.mjs",
|
|
26
26
|
"test:fuzz": "node fuzz.mjs",
|
package/scan.mjs
CHANGED
|
@@ -5447,17 +5447,30 @@ function resolveDepEntryKey(pkg, subpath) {
|
|
|
5447
5447
|
// `Mid.hook` — no body — and charged `Base.hook` Unknown even though `Impl.hook` is the sole concrete
|
|
5448
5448
|
// implementation and fully analyzed. The two-level fixture could not see it, which is also why the
|
|
5449
5449
|
// measured corpus deltas characterise one-level hierarchies only.
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5450
|
+
// DIRECT overrides only — the transitive climb was REVERTED, and the reason is worth keeping. Climbing
|
|
5451
|
+
// removed the base member's Unknown for `Base → Mid (re-declares abstract) → Impl`, but the dispatch-site
|
|
5452
|
+
// fan-out (see `classOverrides` at the call site) is still ONE level, so nothing then carried Impl's
|
|
5453
|
+
// effect caller-ward: the caller vanished from the report entirely, which under SPEC §2 rule 3 is a
|
|
5454
|
+
// positive purity claim. `deny Unknown` AND `deny Fs` both passed over a real Fs. The Unknown was
|
|
5455
|
+
// COMPENSATING for the non-transitive fan-out, and the climb deleted the compensation without supplying
|
|
5456
|
+
// the thing it compensated for. Over-charging that shape is the survivable direction; under-reporting it
|
|
5457
|
+
// is not. The real fix is to build `classOverrides` transitively once, as `classDescendants` already is —
|
|
5458
|
+
// filed, not attempted here under a shipped-regression clock.
|
|
5459
|
+
const answeredByLocalBody = (node) => (classOverrides.get(node) ?? []).some((om) => !!om.body);
|
|
5455
5460
|
// The IMPLEMENTATION of the overload set this body-less signature belongs to, if we analyzed it.
|
|
5456
5461
|
// Keyed on the SYMBOL, not the unit name: every overload shares one symbol at any scope, where the
|
|
5457
5462
|
// qual does not (a function-scoped unit carries a `#line:col` suffix to keep sibling scopes apart).
|
|
5463
|
+
// `!!d.body` ALONE IS NOT "is an implementation" — a ts.ModuleDeclaration has a `.body` too (its
|
|
5464
|
+
// ModuleBlock), so `declare function f(); declare namespace f {}` — the UMD/ambient shape of jQuery,
|
|
5465
|
+
// lodash, moment, chalk — matched the namespace as f's implementation, dropped the Unknown, and
|
|
5466
|
+
// reported the caller PURE. That is the axios cardinal sin reopened BY the pass that closed it, and it
|
|
5467
|
+
// shipped in 0.28.1. The kind guard is the fix: only a declaration form that can carry a FUNCTION body
|
|
5468
|
+
// can be an overload implementation.
|
|
5469
|
+
const canCarryABody = (d) => ts.isFunctionDeclaration(d) || ts.isMethodDeclaration(d)
|
|
5470
|
+
|| ts.isConstructorDeclaration(d) || ts.isGetAccessorDeclaration(d) || ts.isSetAccessorDeclaration(d);
|
|
5458
5471
|
const overloadImplOf = (node) => {
|
|
5459
5472
|
const sym = node.name ? checker.getSymbolAtLocation(node.name) : undefined;
|
|
5460
|
-
return (sym?.declarations ?? []).find((d) => d !== node && !!d.body);
|
|
5473
|
+
return (sym?.declarations ?? []).find((d) => d !== node && canCarryABody(d) && !!d.body);
|
|
5461
5474
|
};
|
|
5462
5475
|
for (const [qual, meta] of bodylessDecls) {
|
|
5463
5476
|
const rec = fns.get(qual);
|
|
@@ -5469,10 +5482,15 @@ for (const [qual, meta] of bodylessDecls) {
|
|
|
5469
5482
|
// and the caller reached an empty unit and read PURE — a silent under-report that predates this pass
|
|
5470
5483
|
// (`outer` calling a nested overloaded `inner` that writes a file was pure before any of this). Edge
|
|
5471
5484
|
// the signature to the implementation and the existing fixpoint carries the real effect through.
|
|
5485
|
+
// The `continue` must depend on an EDGE BEING FORMED, not merely on finding a declaration. `nodeName`
|
|
5486
|
+
// is minted only for units in `projectFiles`, while the symbol's declarations span the whole program —
|
|
5487
|
+
// a `/// <reference>` pull-in, a file outside the tsconfig `include`, a `declare module` augmentation.
|
|
5488
|
+
// Skipping on a target we could not resolve drops the charge AND forms no edge: the disclosure pass
|
|
5489
|
+
// failing OPEN. Fall through to the Unknown instead, which is what "we could not follow this" means.
|
|
5472
5490
|
const implDecl = overloadImplOf(meta.node);
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
if (target
|
|
5491
|
+
const target = implDecl ? nodeName.get(implDecl) : undefined;
|
|
5492
|
+
if (target) {
|
|
5493
|
+
if (target !== qual) rec.edges.add(target);
|
|
5476
5494
|
continue;
|
|
5477
5495
|
}
|
|
5478
5496
|
rec.direct.add("Unknown");
|