candor-ts 0.31.0 → 0.32.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/AGENTS.md +15 -3
- package/README.md +9 -3
- package/lsp.mjs +111 -4
- package/mcp.mjs +105 -35
- package/package.json +2 -2
- package/policy.mjs +304 -41
- package/query-core.mjs +236 -44
- package/query.mjs +265 -46
- package/scan-core.mjs +206 -1
- package/scan.mjs +286 -23
package/AGENTS.md
CHANGED
|
@@ -21,7 +21,7 @@ the TypeScript-specific production + query surface.
|
|
|
21
21
|
> **Already installed? Report the version and ask before upgrading — before you scan.** If this
|
|
22
22
|
> project already has candor (a `.candor/` report directory, or `candor-ts` reachable via `npx`/a
|
|
23
23
|
> global install), do this *first*: run `npx -y candor-ts --version` (offline) and **tell the user
|
|
24
|
-
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.
|
|
24
|
+
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.32)."*
|
|
25
25
|
> On a build too old for the flag, read `candor.version` / `candor.spec` from an existing
|
|
26
26
|
> `.candor/report*.json`, or `npm ls -g candor-ts`.
|
|
27
27
|
>
|
|
@@ -210,7 +210,13 @@ refactor, as a showMessage and a transient diagnostic, cleared on the file's nex
|
|
|
210
210
|
CAVEAT — the MCP/LSP gate verdicts are computed FROM THE REPORT: the engine's own `--policy` /
|
|
211
211
|
`--gate-json` run additionally fails an allow rule whose literal surface is incomplete (a masked
|
|
212
212
|
endpoint — internal state, not a report field), so treat a report-side green as advisory and the
|
|
213
|
-
scan-time gate as the CI truth.
|
|
213
|
+
scan-time gate as the CI truth. ⟨0.21⟩/⟨0.30⟩/⟨0.32⟩ — where the REPORT cannot support a green verdict
|
|
214
|
+
at all (a declared `unanalyzed`, an out-of-scope denied effect the producer's peek named, or a class the
|
|
215
|
+
producing scan never OPENED), every route says so: `gate --report` exits 2, `candor_gate` and
|
|
216
|
+
`candor_unverified` withhold `ok` and name the cause, and the LSP — which has neither an exit code nor a
|
|
217
|
+
verdict document — discloses on `window/logMessage` + `window/showMessage`. **An empty editor is not an
|
|
218
|
+
all-clear**: code the scan never read has no line to squiggle, so its absence from the diagnostics IS
|
|
219
|
+
the incompleteness. Re-scan those sources WITH the policy.
|
|
214
220
|
|
|
215
221
|
Name queries resolve exact > segment-suffix (`db.save` matches `src.db.save`, never
|
|
216
222
|
`src.db.save_all`) > substring — the same ladder as the other engines. The trailing `1` is the
|
|
@@ -232,10 +238,16 @@ want-JSON flag.
|
|
|
232
238
|
- **The classifier is curated** — the node builtins plus a growing npm tier; the README's
|
|
233
239
|
"classifier" paragraph is the ONE current list (this file deliberately doesn't duplicate it — a
|
|
234
240
|
vendored copy here drifted a full generation once).
|
|
235
|
-
An unlisted package contributes nothing — an effect through it is invisible, not `Unknown`. The
|
|
241
|
+
An unlisted npm package contributes nothing — an effect through it is invisible, not `Unknown`. The
|
|
236
242
|
scanner **names these per scan**: the receipt's coverage-ledger line (marker: `classifier doesn't
|
|
237
243
|
cover`) lists every npm package the code demonstrably calls that candor's classifier neither
|
|
238
244
|
classifies nor has reviewed-pure — read it before concluding "no effect" through anything it names.
|
|
245
|
+
⟨0.32⟩ **NODE CORE is the exception, and it fails closed.** Node's builtins are a finite set, every
|
|
246
|
+
module's surface has been reviewed, and a core member that is neither classified nor reviewed
|
|
247
|
+
effect-free reads `Unknown[native:<module>.<member>]` rather than pure. `repl.start()`,
|
|
248
|
+
`process.dlopen()`, `process.chdir()`, `v8.setFlagsFromString()` and `new worker_threads.Worker(file)`
|
|
249
|
+
are holes you can see and `deny Unknown[native]` can select — and an API node adds next year is a hole
|
|
250
|
+
too, until someone reviews it.
|
|
239
251
|
- **`process.env.X` reads are `Env`** (a property read, not a call); `Date.now()` is `Clock`.
|
|
240
252
|
- **DI-style code reads `Unknown` a lot, by design**: a function-typed parameter or field being
|
|
241
253
|
called is genuinely indeterminate (rimraf's injected-fs style yields many `Unknown`s — that's the
|
package/README.md
CHANGED
|
@@ -95,12 +95,18 @@ socket.io/nodemailer, gaxios + googleapis-common + google-auth-library, stripe,
|
|
|
95
95
|
posthog-node, bull/bullmq), the database drivers (pg/mysql2/mongodb/redis/ioredis/sqlite3/
|
|
96
96
|
better-sqlite3/knex) **and the ORM tier** (TypeORM — with `@Entity("…")` table extraction —
|
|
97
97
|
Prisma, Mongoose, Sequelize, drizzle-orm), plus execa/cross-spawn/shelljs/open, fs-extra/
|
|
98
|
-
graceful-fs/rimraf/glob/chokidar, dotenv, winston/pino/bunyan. An unlisted package contributes
|
|
98
|
+
graceful-fs/rimraf/glob/chokidar, dotenv, winston/pino/bunyan. An unlisted npm package contributes
|
|
99
99
|
nothing — candor never guesses an effect — but the scan **names it**: the receipt's coverage-ledger
|
|
100
100
|
line (marker: `classifier doesn't cover`) lists every package the code demonstrably calls that
|
|
101
101
|
candor's classifier neither classifies nor has reviewed-pure, and each function carries the
|
|
102
102
|
`invisible` list it (transitively) reaches.
|
|
103
103
|
|
|
104
|
+
⟨0.32⟩ **Node core is the one part of the classifier that is a denylist, not a curated list.** The
|
|
105
|
+
builtins are a finite set, so every module's surface is reviewed and a core member that is neither
|
|
106
|
+
classified nor reviewed effect-free reads `Unknown[native:<module>.<member>]` — never pure. That is
|
|
107
|
+
why `v8.writeHeapSnapshot()` is `Fs`, `inspector.open()` is `Net`, and `repl.start()`,
|
|
108
|
+
`process.dlopen()` and `new worker_threads.Worker(file)` are visible holes rather than silence.
|
|
109
|
+
|
|
104
110
|
## MCP server — candor as agent ground truth
|
|
105
111
|
|
|
106
112
|
`candor-ts-mcp` exposes the read-only queries as an [MCP](https://modelcontextprotocol.io) server, so
|
|
@@ -192,7 +198,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
|
|
|
192
198
|
| A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
|
|
193
199
|
| Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
|
|
194
200
|
| The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
|
|
195
|
-
| `{ candor: { version, toolchain, spec: "0.
|
|
201
|
+
| `{ candor: { version, toolchain, spec: "0.32" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
|
|
196
202
|
| Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
|
|
197
203
|
| The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
|
|
198
204
|
|
|
@@ -210,7 +216,7 @@ read the Rust source".
|
|
|
210
216
|
|
|
211
217
|
## Status
|
|
212
218
|
|
|
213
|
-
0.30.0, speaking candor-spec 0.
|
|
219
|
+
0.30.0, speaking candor-spec 0.32: the analysis core, the gate (`--policy` / `--gate-json` /
|
|
214
220
|
`.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
|
|
215
221
|
`--include-unknown` dispatch frontier, and ⟨0.24⟩ `gate --report` — the gate applied to an EXISTING
|
|
216
222
|
report, byte-equivalent to `scan --policy`'s verdict), the MCP server, the LSP server, and the watch loop are
|
package/lsp.mjs
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* gate also fails an allow rule whose literal surface is INCOMPLETE (a masked/invisible endpoint,
|
|
12
12
|
* kept internal per the java/rust engines — not a report field), so no-squiggle here can still be
|
|
13
13
|
* red in CI. The engine's --gate-json is the authoritative form (same caveat as MCP candor_gate).
|
|
14
|
+
* ⟨0.21⟩/⟨0.30⟩/⟨0.32⟩ — and where the report itself cannot support a green verdict (a declared
|
|
15
|
+
* `unanalyzed`, the producer's peek naming an out-of-scope denied effect, or a class the scan never
|
|
16
|
+
* OPENED), the squiggles are not the whole verdict and the gap is DISCLOSED on window/logMessage +
|
|
17
|
+
* window/showMessage. No diagnostic: code the scan never read has no line here to sit on. See
|
|
18
|
+
* `discloseIncompleteness`.
|
|
14
19
|
* • Hover: effect PROVENANCE — for each inherited effect, the `path` hop chain to the function that
|
|
15
20
|
* performs it directly ("Net via mid → leaf (source)"), plus unknownWhy when the fn discloses opacity.
|
|
16
21
|
* • CodeAction (pre-edit whatif): inside a function the report knows, one action per BOUNDARY effect
|
|
@@ -45,7 +50,7 @@ import { createRequire } from "node:module";
|
|
|
45
50
|
import nodePath from "node:path";
|
|
46
51
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
47
52
|
import * as Q from "./query-core.mjs";
|
|
48
|
-
import { discoverConfigPolicy, evaluatePolicy, parsePolicy, scopeMatches, reportNetClasses, parseUnknownAliases, discoverConfigText, policyVocabularyAnchor, policyErrorText, unanswerableScoped, wholePolicyUnanswerable, resolveReasonClasses, fatalPolicyErrors, policyZeroRules } from "./policy.mjs";
|
|
53
|
+
import { discoverConfigPolicy, evaluatePolicy, parsePolicy, scopeMatches, reportNetClasses, parseUnknownAliases, discoverConfigText, policyVocabularyAnchor, policyErrorText, unanswerableScoped, wholePolicyUnanswerable, resolveReasonClasses, fatalPolicyErrors, policyZeroRules, reportUnits } from "./policy.mjs";
|
|
49
54
|
|
|
50
55
|
// Version: from the sibling package.json when running inside the npm package; a single-file BUNDLE of
|
|
51
56
|
// this server (the IDE-plugin embedding) has no sibling package.json — fall back rather than crash.
|
|
@@ -280,6 +285,26 @@ function hoverAt(docPath, line) {
|
|
|
280
285
|
// ---- Diagnostics (the live gate) ---------------------------------------------------------------------
|
|
281
286
|
const warned = new Set();
|
|
282
287
|
function warnOnce(message) { if (!warned.has(message)) { warned.add(message); logMessage(message); } }
|
|
288
|
+
/**
|
|
289
|
+
* ⟨0.32⟩ A LOUDER `warnOnce`, for the causes that are EXIT-BEARING on every other route.
|
|
290
|
+
*
|
|
291
|
+
* Everything `warnOnce` carries today is a disclosure the CLI makes AT EXIT 0 — ⟨0.24⟩ ruled count-0 "a
|
|
292
|
+
* disclosure, not an exit code", the unevaluated-rule rows ride alongside a verdict, and `ignored` does
|
|
293
|
+
* not move `ok`. The incompleteness causes below are the other kind: `gate --report` EXITS 2 over them,
|
|
294
|
+
* `candor_gate` withholds `ok`, and `fix-gate`/`unverified --strict` exit 2. This surface has no exit
|
|
295
|
+
* code and no verdict document, so the whole rung collapses onto messages — and putting the LOUDER cause
|
|
296
|
+
* on the QUIETER channel (an output pane the developer has to go and open) would invert an ordering the
|
|
297
|
+
* other four routes agree on. So: the log line carries the detail and the repair, and a `window/
|
|
298
|
+
* showMessage` says that CI is red over these bytes. Both are `warnOnce`-deduped on the LOG text, which
|
|
299
|
+
* is a function of the policy and the report rather than of the edit, so a save loop cannot turn either
|
|
300
|
+
* into the per-keystroke noise that gets an advisory switched off.
|
|
301
|
+
*/
|
|
302
|
+
function warnLoudOnce(message, brief) {
|
|
303
|
+
if (warned.has(message)) return;
|
|
304
|
+
warned.add(message);
|
|
305
|
+
logMessage(message);
|
|
306
|
+
showMessage(2, brief); // Warning — the same severity the activity push uses for a red gate
|
|
307
|
+
}
|
|
283
308
|
// ⟨0.24⟩ the file `activePolicy()` last read, so the vocabulary anchor can be the POLICY FILE's dir.
|
|
284
309
|
let activePolicyPath = null;
|
|
285
310
|
function activePolicy() {
|
|
@@ -337,6 +362,77 @@ const zeroRulePolicyWarn = (what) =>
|
|
|
337
362
|
+ `here is NOT an all-clear: \`gate\` REFUSES over this policy outright (exit 2, SPEC §6.2). If you did `
|
|
338
363
|
+ `not mean to gate, remove the policy configuration rather than pointing it at a file with no rules.`);
|
|
339
364
|
|
|
365
|
+
/**
|
|
366
|
+
* ⟨0.21⟩/⟨0.30⟩/⟨0.32⟩ THE INCOMPLETENESS CAUSES, ON THE ROUTE THAT CARRIED NONE OF THEM.
|
|
367
|
+
*
|
|
368
|
+
* MEASURED at `9f22581` — the commit titled "four routes, one rule", where there were FIVE. Over a
|
|
369
|
+
* report whose `excluded` records a `dist/shipped.js` running `curl … | sh` that the scan never opened,
|
|
370
|
+
* under a discovered `deny Exec`: `gate --report` exits 2 naming the class, the MCP `candor_gate` tool
|
|
371
|
+
* withholds `ok`, and this server published `[]` — no diagnostic, no log line, no message. `lsp.mjs` had
|
|
372
|
+
* zero occurrences of `excluded`, `peeked` or `unread`, and it is silent on the ⟨0.21⟩ and ⟨0.30⟩ causes
|
|
373
|
+
* for the same reason: `diagnosticsFor` read NO completeness key at all. This is the SHIPPED EDITOR
|
|
374
|
+
* EXPERIENCE — `integrations/vscode` and the JetBrains plugin both bundle this exact server — so the
|
|
375
|
+
* cardinal sin arrived in the place with the fewest ways to notice it: an editor showing a clean file
|
|
376
|
+
* over code nothing read. The argument was already in this file four lines below, about a different
|
|
377
|
+
* cause: "an empty editor reads as 'the gate is green'".
|
|
378
|
+
*
|
|
379
|
+
* ONE RULE, NOT A SIXTH SPELLING OF IT. The condition is the CLI gate's, read off the same
|
|
380
|
+
* `reportCompleteness` value the CLI and MCP routes read, applied ONCE:
|
|
381
|
+
*
|
|
382
|
+
* · `unread` (⟨0.32⟩) is gated on `pol.deny.length` — only a `deny`/`pure` rule's answer depends on
|
|
383
|
+
* code outside the scan's scope, so refusing an `allow`/`forbid`/`only` policy for want of a peek
|
|
384
|
+
* would be an over-charge. `pure` RIDES the deny vector (parsePolicy pushes it there), so the test
|
|
385
|
+
* is `deny.length` and not a search for the token, and flattening that away would delete the rule
|
|
386
|
+
* for every `pure`-only policy.
|
|
387
|
+
* · `unanalyzed` (⟨0.21⟩) and `outOfScope` (⟨0.30⟩) are UNCONDITIONAL, exactly as they are on the CLI
|
|
388
|
+
* gate: a unit candor could not read is unread whatever the policy asks, and `outOfScope` is by
|
|
389
|
+
* construction the peek's findings UNDER the producer's own policy.
|
|
390
|
+
*
|
|
391
|
+
* ALL THREE, not just the one this rung is named for. They are one `gincomplete` disjunction on the CLI
|
|
392
|
+
* and one `incomplete` on the MCP tool; carrying one of three into this route would have left the same
|
|
393
|
+
* defect, in the same function, for the next audit to find — which is the habit this whole rung exists
|
|
394
|
+
* to break. Each cause is NAMED ONLY WHEN IT FIRED, the MCP tool's rule: a sentence about unanalyzed
|
|
395
|
+
* code beside an incompleteness that is really an unopened `dist/` sends the reader to the wrong repair.
|
|
396
|
+
*
|
|
397
|
+
* NO DIAGNOSTIC IS DRAWN, and that is the answer to "a squiggle on what". There is no source range for a
|
|
398
|
+
* file the scan never opened: the code is absent from `functions` precisely because nothing looked, so
|
|
399
|
+
* the only line available is line 0 of whatever document happens to be open — which would attribute the
|
|
400
|
+
* hole to an innocent file, in every language the client maps, on every open. That is the fabrication
|
|
401
|
+
* mirror of the silence being fixed. The ⟨0.24⟩ judged-nothing hedge reached the same conclusion for the
|
|
402
|
+
* same reason ("there is no line to pin it to"); the activity overlay's line-0 diagnostic is not a
|
|
403
|
+
* counter-example, because its record NAMES the edited file and this one names no file in the workspace.
|
|
404
|
+
*/
|
|
405
|
+
function discloseIncompleteness(unanalyzed, outOfScope, unread) {
|
|
406
|
+
const causes = [];
|
|
407
|
+
// The CLI's order (`unanalyzed` → `outOfScope` → `unread`), so a report tripping two of them reads the
|
|
408
|
+
// same way here as it does in CI. The repairs genuinely differ — a parse to fix, a selector that
|
|
409
|
+
// REACHES the code, a scan that was ASKED the question — so each cause carries its own.
|
|
410
|
+
if (unanalyzed.length)
|
|
411
|
+
causes.push(`the report DECLARES ${unanalyzed.length} unit(s) candor could not analyze, and a gate `
|
|
412
|
+
+ `cannot be green over unanalyzed code:\n`
|
|
413
|
+
+ unanalyzed.map((u) => ` ${u.path || "(unnamed unit)"}${u.reason ? ` (${u.reason})` : ""}`).join("\n"));
|
|
414
|
+
if (outOfScope.length)
|
|
415
|
+
causes.push(`the report names ${outOfScope.length} function(s) OUTSIDE the scan's scope performing `
|
|
416
|
+
+ `an effect this policy denies (the producer's peek found them). The gate did not judge them, so the verdict is `
|
|
417
|
+
+ `INCOMPLETE rather than a pass — re-scan with a selector that REACHES that code:\n`
|
|
418
|
+
+ outOfScope.map((o) => ` ${o.fn} performs ${(o.effects ?? []).join(", ")}`).join("\n"));
|
|
419
|
+
if (unread.length)
|
|
420
|
+
causes.push(`the scan did not READ ${unread.join(", ")}. Those files' effects are absent from the `
|
|
421
|
+
+ `report because nothing looked, not because there are none, so nothing in them can be squiggled `
|
|
422
|
+
+ `here — re-scan the sources WITH this policy (candor-ts <dir> --policy <file>); a scan that was `
|
|
423
|
+
+ `never asked cannot certify what it never opened.`);
|
|
424
|
+
if (!causes.length) return;
|
|
425
|
+
warnLoudOnce(
|
|
426
|
+
`candor-lsp: this report cannot support a GREEN gate — \`gate --report\` over the same bytes exits 2 `
|
|
427
|
+
+ `(INCOMPLETE), so the squiggles in this editor are NOT the whole verdict:\n`
|
|
428
|
+
+ causes.map((c) => ` · ${c}`).join("\n")
|
|
429
|
+
+ `\n NO diagnostic can be drawn for any of the above: the code it names is not in the report, so it `
|
|
430
|
+
+ `has no line in this editor to sit on. Its ABSENCE from the squiggles is the incompleteness itself, `
|
|
431
|
+
+ `not an all-clear.`,
|
|
432
|
+
`candor gate: INCOMPLETE — this report cannot support a green verdict (CI exits 2 over these bytes). `
|
|
433
|
+
+ `See the candor log for what went unjudged and how to fix it.`);
|
|
434
|
+
}
|
|
435
|
+
|
|
340
436
|
function diagnosticsFor(docPath) {
|
|
341
437
|
const text = activePolicy();
|
|
342
438
|
if (text === null || !hasReport(reportPrefix)) return [];
|
|
@@ -374,6 +470,15 @@ function diagnosticsFor(docPath) {
|
|
|
374
470
|
warnOnce(`candor-lsp: ${dpol.ignored.length} line(s) of the configured policy were DROPPED by the `
|
|
375
471
|
+ `parse, so the gate you are seeing is SMALLER than the gate that was written (SPEC §6.2 ⟨0.28⟩):\n`
|
|
376
472
|
+ dpol.ignored.map((g) => ` line ${g.line}: ${g.text}`).join("\n"));
|
|
473
|
+
// ⟨0.21⟩/⟨0.30⟩/⟨0.32⟩ …AND THE CAUSES THAT MAKE THE GATE ITSELF INCOMPLETE — see
|
|
474
|
+
// `discloseIncompleteness` for the mechanism and the channel argument. Read through the SAME
|
|
475
|
+
// `reportCompleteness` the CLI gate, `fix-gate`, `unverified` and the MCP tools read, so the editor
|
|
476
|
+
// cannot come to a different view of one report from the CI job that judges the same commit. The
|
|
477
|
+
// `unread` condition is applied HERE, to the value, for the reason the CLI states at its own call
|
|
478
|
+
// site: one list, one condition, so two consumers of it cannot disagree about a run.
|
|
479
|
+
const dcomp = Q.reportCompleteness(reportPrefix);
|
|
480
|
+
discloseIncompleteness(dcomp.unanalyzed ?? [], dcomp.outOfScope ?? [],
|
|
481
|
+
dpol.deny.length ? (dcomp.unread ?? []) : []);
|
|
377
482
|
// ⟨0.24⟩ THE ANSWERABILITY WITHHOLD, which this surface ran WITHOUT — `evaluatePolicy` was called with no
|
|
378
483
|
// `withhold` predicate and the DEFAULT netClass mode, so both directions of the §3.1 harm were live in the
|
|
379
484
|
// editor. Measured against the CLI on one report and one policy: `deny Unknown[reflect]` drew NO squiggle
|
|
@@ -382,9 +487,11 @@ function diagnosticsFor(docPath) {
|
|
|
382
487
|
// never carried. `authoritative: true` for the same reason the MCP gate takes it: an entry without
|
|
383
488
|
// `netClass` gets the EMPTY set rather than a derivation from `hosts`, so the report is the only source of
|
|
384
489
|
// the class and nothing is re-classified with this machine's evidence about the producer's project.
|
|
385
|
-
|
|
490
|
+
// ⟨0.32⟩ the same unit identity the CLI gate uses over these bytes (SPEC §2.2).
|
|
491
|
+
const dunits = reportUnits(fns);
|
|
492
|
+
const dnet = reportNetClasses(fns, { authoritative: true, units: dunits });
|
|
386
493
|
const { unevaluated: dunevaluated, withhold: dwithhold } =
|
|
387
|
-
unanswerableScoped(dpol, fns, resolveReasonClasses(fns, Q.loadCallgraph(reportPrefix)), dnet);
|
|
494
|
+
unanswerableScoped(dpol, fns, resolveReasonClasses(fns, Q.loadCallgraph(reportPrefix), dunits), dnet, dunits);
|
|
388
495
|
// ⟨0.29⟩ `forbid`/`allow` are STRIPPED and DISCLOSED here too. This path handed the whole policy to
|
|
389
496
|
// `evaluatePolicy`, so an editor drew AS-EFF-009 squiggles from a report — evidence SPEC §3.1 rules
|
|
390
497
|
// cannot support them — or, with no sidecar, drew nothing and said nothing, which reads as "no layering
|
|
@@ -403,7 +510,7 @@ function diagnosticsFor(docPath) {
|
|
|
403
510
|
for (const u of dunevaluated)
|
|
404
511
|
warnOnce(`candor-lsp: ${u.why}\n NO diagnostics are drawn for that rule — their ABSENCE here is the refusal, not an all-clear.`);
|
|
405
512
|
const violations = evaluatePolicy(dwp.answerable, fns, Q.loadCallgraph(reportPrefix),
|
|
406
|
-
new Map(), new Set(), dnet, dwithhold);
|
|
513
|
+
new Map(), new Set(), dnet, dwithhold, dunits);
|
|
407
514
|
const locByFn = new Map(fns.filter((e) => e.loc).map((e) => [e.fn, locParts(e.loc)]));
|
|
408
515
|
const out = [];
|
|
409
516
|
for (const v of violations) {
|
package/mcp.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import nodePath from "node:path";
|
|
|
21
21
|
import * as Q from "./query-core.mjs";
|
|
22
22
|
import { discoverConfigPolicy, evaluatePolicy, parsePolicy, scopeMatches, reportNetClasses,
|
|
23
23
|
parseUnknownAliases, discoverConfigText, policyVocabularyAnchor, policyErrorText,
|
|
24
|
-
unanswerableScoped, wholePolicyUnanswerable, resolveReasonClasses, fatalPolicyErrors,
|
|
24
|
+
unanswerableScoped, wholePolicyUnanswerable, resolveReasonClasses, fatalPolicyErrors, reportUnits,
|
|
25
25
|
policyZeroRules } from "./policy.mjs";
|
|
26
26
|
|
|
27
27
|
const VERSION = createRequire(import.meta.url)("./package.json").version; // single-sourced, like scan.mjs
|
|
@@ -216,20 +216,28 @@ function capCallers(r) {
|
|
|
216
216
|
// `{"entryPoints":0,"effects":{}}`, `candor_containment` → `{"contained":[],"ambient":{}}` — five flat
|
|
217
217
|
// all-clears with no hedge. `candor_show`/`candor_impact` already fail closed on the fn-existence guard.
|
|
218
218
|
//
|
|
219
|
+
// ⟨0.32⟩ **AND THAT LAST SENTENCE WAS TRUE OF ONE CAUSE AND HID THREE TOOLS.** The fn-existence guard
|
|
220
|
+
// fires when the report contains no such NAME; it says nothing about a report that contains the name and
|
|
221
|
+
// declares part of the tree unread. MEASURED over this server's own stdio transport on a report whose
|
|
222
|
+
// `excluded` names one class with `peeked: false`: `candor_impact`, `candor_callers` and `candor_path` all
|
|
223
|
+
// answered FLAT — the SILENT half of the class `candor_show`/`candor_map` had the loud half of. All three
|
|
224
|
+
// now take this wrapper; all three have fixed key sets, so nothing is nested and nothing is displaced.
|
|
225
|
+
//
|
|
219
226
|
// THE AGENT IS THE CONSUMER THAT CANNOT ASK A FOLLOW-UP QUESTION. A human running the CLI at least sees an
|
|
220
227
|
// oddly bare answer; a loop reading `blindspots.sources.length === 0` records "no blind spots" and moves on.
|
|
221
228
|
// Additive and a no-op on a complete report (`completenessFields` returns `{}`), so every pinned tool shape
|
|
222
229
|
// is unchanged on an ordinary one.
|
|
223
230
|
// The collision loop this used to carry is gone with ⟨0.28⟩ Rung A: `candor_map` was its only possible
|
|
224
|
-
// trigger and now takes `
|
|
231
|
+
// trigger and now takes `nestWithCaveat`, where its namespace sits one level down and nothing is
|
|
232
|
+
// displaced. Every remaining caller has a fixed
|
|
225
233
|
// key set, so the condition is not constructible — and a guard whose condition cannot arise reads as
|
|
226
234
|
// coverage.
|
|
227
235
|
const withCompleteness = (p, doc) => ({ ...doc, ...Q.completenessFields(Q.reportCompleteness(p)) });
|
|
228
|
-
// ⟨0.28⟩ RUNG A, ON THE AGENT CHANNEL — SPEC §2: a verb whose pinned shape cannot carry the caveat
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
236
|
+
// ⟨0.28⟩ RUNG A, ON THE AGENT CHANNEL — SPEC §2: a verb whose pinned shape cannot carry the caveat
|
|
237
|
+
// changes SHAPE over a hedging report. Two tools qualify and both are handled here, because THE MCP HALF
|
|
238
|
+
// HAS BEEN THE MISSED ROUTE TWICE IN THIS REPO — and it is the worse one: an agent reading
|
|
239
|
+
// `Object.keys(map).length === 0` records "this codebase performs no effects" and moves on, with no
|
|
240
|
+
// follow-up question available to it.
|
|
233
241
|
//
|
|
234
242
|
// candor_show the CLI's `show` is pinned to an ARRAY; this tool returns `Q.show(...)`, the same array,
|
|
235
243
|
// and had no completeness reader at all.
|
|
@@ -239,30 +247,45 @@ const withCompleteness = (p, doc) => ({ ...doc, ...Q.completenessFields(Q.report
|
|
|
239
247
|
//
|
|
240
248
|
// A no-op on a complete report, so both pinned tool shapes are unchanged on an ordinary one.
|
|
241
249
|
// ⟨0.28⟩ THE DOC ARRIVES AS A THUNK, and that is the whole fix rather than a style preference.
|
|
242
|
-
// This took `doc` by value, so JavaScript evaluated `Q.show(loadReportLoud(p), a.fn)` BEFORE
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// the caveat document), so this was the MCP half of a rung the CLI had shipped — the third time today
|
|
248
|
-
// that half was the one nobody checked.
|
|
250
|
+
// This took `doc` by value, so JavaScript evaluated `Q.show(loadReportLoud(p), a.fn)` BEFORE the helper
|
|
251
|
+
// was ever called — and `Q.show`'s fn-existence guard throws. Over a judged-nothing report
|
|
252
|
+
// `candor_show <anything>` therefore answered "no function matching …" to an AGENT: a determined
|
|
253
|
+
// negative about the code, produced by a report that examined none of it, on the surface where a wrong
|
|
254
|
+
// answer is acted on rather than read.
|
|
249
255
|
//
|
|
250
|
-
// Hedging is now decided BEFORE the answer is computed, so no guard inside the verb can pre-empt it.
|
|
251
256
|
// AND CORRUPTION IS NOT A HEDGE — the deferral made that distinction load-bearing where it had been
|
|
252
257
|
// free. `mustHedge` is true on the `unreadable` arm too, so simply deferring turned `candor_map` over a
|
|
253
258
|
// CORRUPT report from a loud tool error into `{"incomplete": true}`: a disclosure where the contract
|
|
254
259
|
// says refuse (§2 ⟨0.24⟩ — a signature key that cannot be read impeaches the document, it does not
|
|
255
|
-
// qualify it).
|
|
256
|
-
//
|
|
260
|
+
// qualify it). So the loud causes fall THROUGH to the thunk, whose loader throws; corruption anywhere in
|
|
261
|
+
// the set wins over a hedge elsewhere in it.
|
|
262
|
+
//
|
|
263
|
+
// ⟨0.32⟩ **AND THE HEDGING BRANCH NOW RETURNS THE RESULT *AND* THE WARNING — RULED 2026-08-25.** It used
|
|
264
|
+
// to return `Q.completenessFields(comp)` alone, which was tolerable while the only triggers were a scan
|
|
265
|
+
// that had FAILED (there was no answer to withhold). ⟨0.32⟩'s unread-class cause armed the same
|
|
266
|
+
// substitution on nearly every no-policy report, so MEASURED over this server's own stdio transport, on a
|
|
267
|
+
// report whose `excluded` names one unpeeked class: `candor_show` and `candor_map` both handed the agent
|
|
268
|
+
// `{"incomplete":true}` and NOTHING ELSE. These two tools CERTIFY NOTHING — no `ok`, no verdict — so
|
|
269
|
+
// there is no claim for a pessimism rule to protect, and an agent that asked what the code does was
|
|
270
|
+
// answered with a caveat about a question it did not ask. `candor_gate` and the `--strict` advisory
|
|
271
|
+
// tools are on the other side of that boundary and are untouched: they answer `ok` and must keep
|
|
272
|
+
// refusing (⟨0.24⟩; conformance PARTs 62 and 67).
|
|
273
|
+
//
|
|
274
|
+
// The result NESTS under the caller's key so `map`'s module namespace cannot collide with the caveat
|
|
275
|
+
// vocabulary, matching the CLI's `putNestedWithCaveat` byte for byte.
|
|
257
276
|
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
277
|
+
// THE THUNK IS STILL CALLED LAST AND ITS THROW IS STILL HONOURED. `Q.show`'s fn-existence guard is a
|
|
278
|
+
// REFUSAL, not a result, and over a hedging report that refusal is not a fact about the code — so the
|
|
279
|
+
// caveat stands alone there, exactly as this surface answered before. That is the one case where the
|
|
280
|
+
// warning is all there is, and it is because there is no data, not because the data was withheld.
|
|
281
|
+
const nestWithCaveat = (p, key, doc) => {
|
|
261
282
|
const comp = Q.reportCompleteness(p);
|
|
262
283
|
const call = () => (typeof doc === "function" ? doc() : doc);
|
|
263
284
|
if (comp?.unreadable?.length) return call(); // refuse loudly, via the loader
|
|
264
|
-
if (Q.mustHedge(comp)) return
|
|
265
|
-
|
|
285
|
+
if (!Q.mustHedge(comp)) return call();
|
|
286
|
+
let result;
|
|
287
|
+
try { result = call(); } catch { return Q.completenessFields(comp); }
|
|
288
|
+
return { [key]: result, ...Q.completenessFields(comp) };
|
|
266
289
|
};
|
|
267
290
|
// ⟨0.28⟩ The graph the three graph verbs answer over: the §2.2 sidecar when there is one, else the
|
|
268
291
|
// report's own embedded `calls` edges (Q.reportCallsGraph) — the same fallback the CLI and rust/java
|
|
@@ -279,7 +302,14 @@ const TOOLS = {
|
|
|
279
302
|
candor_impact: {
|
|
280
303
|
description: "Backward blast radius: every effectful function that transitively calls `fn`, and which runtime entry points are downstream. Answers 'if I change this, what surfaces at runtime?' — the cheapest possible alternative to tracing callers by hand.",
|
|
281
304
|
schema: { type: "object", properties: { fn: { type: "string", description: "the function/unit to assess" }, ...reportArg }, required: ["fn"] },
|
|
282
|
-
|
|
305
|
+
// ⟨0.32⟩ `withCompleteness` — this tool had NO completeness reader, and neither did `candor_path` or
|
|
306
|
+
// `candor_callers`. MEASURED over this server's own stdio transport on a report whose `excluded`
|
|
307
|
+
// names one class with `peeked: false`: all three answered FLAT, and `affectedCount: 0` on the AGENT
|
|
308
|
+
// channel is *nothing calls this, safe to change* — read by a consumer with no follow-up question
|
|
309
|
+
// available to it. Fixed key sets, so the caveat spreads at the root and every pinned tool shape is
|
|
310
|
+
// unchanged on a complete report (`completenessFields` → `{}`). These three certify nothing, so they
|
|
311
|
+
// are on the descriptive side of the ⟨0.32⟩ boundary stated on `nestWithCaveat` below.
|
|
312
|
+
run: (a, p) => { const fns = loadReportLoud(p); return withCompleteness(p, capImpact(Q.impact(fns, graphOrReportEdges(p, fns), a.fn))); },
|
|
283
313
|
},
|
|
284
314
|
candor_where: {
|
|
285
315
|
description: "Which functions perform a given effect (e.g. Net, Db, Exec, Fs) — `directly` vs `inherited` via a callee. The effect-surface map.",
|
|
@@ -294,22 +324,25 @@ const TOOLS = {
|
|
|
294
324
|
candor_path: {
|
|
295
325
|
description: "Forward provenance: the shortest call chain from `fn` to the nearest function that performs `effect` DIRECTLY — 'this reaches Net through WHAT?'.",
|
|
296
326
|
schema: { type: "object", properties: { fn: { type: "string" }, effect: { type: "string" }, ...reportArg }, required: ["fn", "effect"] },
|
|
297
|
-
|
|
327
|
+
// ⟨0.32⟩ see `candor_impact` above: `path: []` to an agent is *this function does not reach that
|
|
328
|
+
// effect*, and a hop through an unread unit breaks the chain.
|
|
329
|
+
run: (a, p) => { const fns = loadReportLoud(p); return withCompleteness(p, Q.path(fns, graphOrReportEdges(p, fns), a.fn, a.effect)); },
|
|
298
330
|
},
|
|
299
331
|
candor_callers: {
|
|
300
332
|
description: "Who calls `fn` — direct (one hop) and transitive callers over the effect-relevant call graph.",
|
|
301
333
|
schema: { type: "object", properties: { fn: { type: "string" }, ...reportArg }, required: ["fn"] },
|
|
302
|
-
|
|
334
|
+
// ⟨0.32⟩ see `candor_impact` above: an empty `direct` to an agent is *nobody calls this*.
|
|
335
|
+
run: (a, p) => withCompleteness(p, capCallers(Q.callers(graphOrReportEdges(p, loadReportLoud(p)), a.fn))),
|
|
303
336
|
},
|
|
304
337
|
candor_show: {
|
|
305
338
|
description: "A function's effects (inferred = transitive, direct = own body) plus its literal surfaces (hosts/cmds/paths/tables) when present.",
|
|
306
339
|
schema: { type: "object", properties: { fn: { type: "string" }, ...reportArg }, required: ["fn"] },
|
|
307
|
-
run: (a, p) =>
|
|
340
|
+
run: (a, p) => nestWithCaveat(p, "functions", () => Q.show(loadReportLoud(p), a.fn)),
|
|
308
341
|
},
|
|
309
342
|
candor_map: {
|
|
310
343
|
description: "Per-module effect overview: each module's union of effects and function count. The architecture-at-a-glance.",
|
|
311
344
|
schema: { type: "object", properties: { ...reportArg } },
|
|
312
|
-
run: (_a, p) =>
|
|
345
|
+
run: (_a, p) => nestWithCaveat(p, "modules", () => Q.map(loadReportLoud(p))),
|
|
313
346
|
},
|
|
314
347
|
candor_whatif: {
|
|
315
348
|
description: "Hypothetically add `effect` to `fn` and report the blast radius; with `policy`, also the deny-rule violations it would cause. Pre-edit gate check.",
|
|
@@ -358,7 +391,7 @@ const TOOLS = {
|
|
|
358
391
|
},
|
|
359
392
|
},
|
|
360
393
|
candor_gate: {
|
|
361
|
-
description: "The policy verdict over this report: { ok, violations:[{rule, fn, effects, detail}] } — 'would this repo pass its architecture gate?'. Uses `policy` if given, else the repo's checked-in .candor/config policy (spec §3.4). ALWAYS CHECK `ok`, never the length of `violations`: a rule whose narrowing evidence the report does not carry is NOT EVALUATED, and then the result is `{ ok:false, refused:true, reason, unevaluated:[{rule, why}] }` WITH NO `violations` KEY — an absent key, not an empty list, because the gate is making no claim there. `unevaluated` also rides a firing verdict (a certain violation dominates a refusal). `
|
|
394
|
+
description: "The policy verdict over this report: { ok, violations:[{rule, fn, effects, detail}] } — 'would this repo pass its architecture gate?'. Uses `policy` if given, else the repo's checked-in .candor/config policy (spec §3.4). ALWAYS CHECK `ok`, never the length of `violations`: a rule whose narrowing evidence the report does not carry is NOT EVALUATED, and then the result is `{ ok:false, refused:true, reason, unevaluated:[{rule, why}] }` WITH NO `violations` KEY — an absent key, not an empty list, because the gate is making no claim there. `unevaluated` also rides a firing verdict (a certain violation dominates a refusal). `incomplete:true` means the gate CANNOT be green, and the key beside it says which of the three causes fired: `unanalyzed` (the report declares code candor could not analyze), `outOfScope` (the producer's peek NAMED a function outside the scan's scope performing an effect this policy denies), or `unread` (a class the producing scan never OPENED — its effects are absent because nothing looked, not because there are none; re-scan those sources WITH this policy). Computed from the report — the engine's own --gate-json run is the authoritative CI form: it additionally fails an allow rule whose literal surface is INCOMPLETE (a masked/invisible endpoint), which is not a report field, so a green here can still be red in CI.",
|
|
362
395
|
schema: { type: "object", properties: { policy: { type: "string", description: "path to a §6.2 policy file (optional; defaults to the repo's .candor/config `policy`)" }, ...reportArg }, required: [] },
|
|
363
396
|
run: (a, p) => {
|
|
364
397
|
let text, polPath = a.policy ?? null;
|
|
@@ -405,8 +438,11 @@ const TOOLS = {
|
|
|
405
438
|
: `a report found at prefix \`${clip(p)}\` failed to load — refusing to gate over a report that did not load cleanly; a partial signature makes a green verdict meaningless (the effects of the report that did not load are exactly the ones a violation would come from). Re-run the scan`));
|
|
406
439
|
const gfns = g.functions;
|
|
407
440
|
const cg = Q.loadCallgraph(p);
|
|
408
|
-
|
|
409
|
-
|
|
441
|
+
// ⟨0.32⟩ the same unit identity the CLI gate uses over these bytes (SPEC §2.2) — one object, so this
|
|
442
|
+
// surface cannot join two members by name while the CLI keeps them apart.
|
|
443
|
+
const gunits = reportUnits(gfns);
|
|
444
|
+
const gnet = reportNetClasses(gfns, { authoritative: true, units: gunits });
|
|
445
|
+
const { unevaluated, withhold } = unanswerableScoped(pol, gfns, resolveReasonClasses(gfns, cg, gunits), gnet, gunits);
|
|
410
446
|
// ⟨0.29⟩ …AND THE TWO WHOLE-POLICY UNANSWERABLE KINDS. This tool passed the WHOLE policy to
|
|
411
447
|
// `evaluatePolicy`, so `forbid` was answered from a report — MEASURED: with no callgraph sidecar,
|
|
412
448
|
// `violations: 0` and nothing disclosed (a silent green over a rule that was never enforced); with a
|
|
@@ -421,12 +457,24 @@ const TOOLS = {
|
|
|
421
457
|
// to suppress a certain violation).
|
|
422
458
|
if (wp.onlyUnanswerable)
|
|
423
459
|
throw new Error(`this policy asks only questions a report cannot answer — ${wp.unevaluated[0].why}`);
|
|
424
|
-
const v = evaluatePolicy(wp.answerable, gfns, cg, new Map(), new Set(), gnet, withhold);
|
|
460
|
+
const v = evaluatePolicy(wp.answerable, gfns, cg, new Map(), new Set(), gnet, withhold, gunits);
|
|
425
461
|
// ⟨0.21⟩ COMPLETENESS MANIFEST — this tool implemented no incompleteness rule at all: it answered
|
|
426
462
|
// `{ok:true, violations:[]}` over a report DECLARING `unanalyzed`, where the CLI exits 2. A gate
|
|
427
463
|
// cannot be green over code candor never analyzed, and the manifest travels ON the report, so the
|
|
428
464
|
// same verdict follows from it here. Additive to the pinned `{ok, violations}` shape.
|
|
429
|
-
|
|
465
|
+
// ⟨0.30⟩/⟨0.32⟩ …AND THE OTHER TWO CAUSES, which this route carried NEITHER of — the sibling-route
|
|
466
|
+
// habit again, on the channel whose consumer cannot ask a follow-up question. Both ride the REPORT,
|
|
467
|
+
// exactly as they do for `gate --report`, so this tool re-reads the producer's record rather than
|
|
468
|
+
// deriving anything of its own:
|
|
469
|
+
// · `outOfScope` (⟨0.30⟩) — the producer's peek NAMED a function outside the scan's scope
|
|
470
|
+
// performing an effect this policy denies. The CLI exits 2; this tool answered `ok: true`.
|
|
471
|
+
// · `unread` (⟨0.32⟩) — a class the producer never OPENED (`excluded[].peeked: false`), so its
|
|
472
|
+
// effects are absent from `functions` because nothing looked. Decided by the policy applied NOW
|
|
473
|
+
// (only a `deny`/`pure` rule's answer depends on code outside the scan's scope), which is the
|
|
474
|
+
// same condition both CLI routes apply, from the same value, once.
|
|
475
|
+
const gscope = g.outOfScope ?? [];
|
|
476
|
+
const gunread = pol.deny.length ? g.unread : [];
|
|
477
|
+
const incomplete = g.unanalyzed.length > 0 || gscope.length > 0 || gunread.length > 0;
|
|
430
478
|
// ⟨0.24⟩ …and a report that JUDGED NOTHING is not an all-clear (SPEC §2's three-row table, bound to
|
|
431
479
|
// every report-reading route by §3.1: "the obligation is on the reading, not on the route by which
|
|
432
480
|
// the report arrived"). This tool is exactly such a route — it gates whatever `report` points at,
|
|
@@ -476,7 +524,15 @@ const TOOLS = {
|
|
|
476
524
|
+ "claim about any unit they contain. The named report(s) are the gap; the verdict above covers "
|
|
477
525
|
+ "only the siblings that DID judge. Re-scan those sources, or point `report` at the package that "
|
|
478
526
|
+ "has them." } : {};
|
|
479
|
-
|
|
527
|
+
// EACH CAUSE NAMED ONLY WHEN IT FIRED. `unanalyzed: []` beside `incomplete: true` would be a
|
|
528
|
+
// positive claim that candor read every file — the safe-LOOKING value, and the one an agent would
|
|
529
|
+
// act on — for an incompleteness with a different cause and a different repair.
|
|
530
|
+
const inc = incomplete
|
|
531
|
+
? { incomplete: true,
|
|
532
|
+
...(g.unanalyzed.length ? { unanalyzed: g.unanalyzed } : {}),
|
|
533
|
+
...(gscope.length ? { outOfScope: gscope } : {}),
|
|
534
|
+
...(gunread.length ? { unread: gunread } : {}) }
|
|
535
|
+
: {};
|
|
480
536
|
// ⟨0.24⟩ PRECEDENCE (SPEC §3.1 `7271c69`/`4c79958`): violation (1) > refusal (2) > incomplete (2), and
|
|
481
537
|
// the REFUSAL SHAPE is the one the CLI writes — `ok:false`, `refused:true`, and NO `violations` KEY AT
|
|
482
538
|
// ALL, because the gate is making no claim about violations and `[]` is precisely the claim it cannot
|
|
@@ -504,7 +560,10 @@ const TOOLS = {
|
|
|
504
560
|
+ "over a report declaring code candor could NOT analyze, `ok` IS ABSENT and `{incomplete:true, "
|
|
505
561
|
+ "unanalyzed}` takes its place — a function in an unanalyzed file is missing from the report "
|
|
506
562
|
+ "entirely, so it cannot be enumerated as an unverified pass, and an empty array there is not "
|
|
507
|
-
+ "an all-clear (spec §3.2). `
|
|
563
|
+
+ "an all-clear (spec §3.2). `incomplete:true` also rides the two SCOPE causes, on the same "
|
|
564
|
+
+ "terms as candor_gate: `outOfScope` (the producer's peek named a function outside the scan's "
|
|
565
|
+
+ "scope performing a denied effect) and `unread` (a class the producing scan never OPENED — "
|
|
566
|
+
+ "re-scan those sources WITH this policy). `ok` is ABSENT for a further reason too, and the entries that come "
|
|
508
567
|
+ "with it are the sharpest ones: where the policy narrows on evidence this report does not carry, "
|
|
509
568
|
+ "candor_gate REFUSES — and this verb then NAMES each function the gate could not judge, as "
|
|
510
569
|
+ "`{fn, rule, why}` where `why` is THE MISSING EVIDENCE and never a derived class, plus the gate's "
|
|
@@ -537,8 +596,19 @@ const TOOLS = {
|
|
|
537
596
|
if (policyAskedNothing(upol)) return zeroRuleCaveat(polPath, p);
|
|
538
597
|
// ⟨0.28⟩ `noManifest` (SPEC §2 row 3) rides here too — a report with no `analyzed` key declares
|
|
539
598
|
// nothing, and listing it under `judgedNothing` would be the false disclosure the rung split out.
|
|
599
|
+
// ⟨0.30⟩/⟨0.32⟩ …AND THE TWO SCOPE CAUSES, which this call was the last route in the package to
|
|
600
|
+
// read. MEASURED: over a report whose `excluded` names a class the producing scan never opened,
|
|
601
|
+
// under `deny Exec`, `candor_gate` in THIS SAME PROCESS answered `{ok:false, incomplete:true,
|
|
602
|
+
// unread:[…]}` while this tool answered `{ok:true, unverified:[]}` — and the CLI sibling
|
|
603
|
+
// `unverified --strict` over the identical bytes omitted `ok`, named the class and exited 2.
|
|
604
|
+
// ⟨0.24⟩'s MUST binds every channel a verb answers on: an advisory verb may be LESS certain than
|
|
605
|
+
// the gate, NEVER MORE. The `unread` condition is the gate's, off the same value, applied once —
|
|
606
|
+
// only a `deny`/`pure` rule's answer depends on code outside the scan's scope, and `pure` rides
|
|
607
|
+
// the `deny` vector, so this is `deny.length` rather than a search for the token.
|
|
608
|
+
const uUnread = upol.deny.length ? (ucomp.unread ?? []) : [];
|
|
540
609
|
return Q.advisoryAnswer(Q.unverified(loadReportLoud(p), upol, scopeMatches),
|
|
541
|
-
ucomp.unanalyzed, ucomp.judgedNothing, ucomp.unreadable, ucomp.noManifest
|
|
610
|
+
ucomp.unanalyzed, ucomp.judgedNothing, ucomp.unreadable, ucomp.noManifest,
|
|
611
|
+
ucomp.outOfScope ?? [], uUnread);
|
|
542
612
|
},
|
|
543
613
|
},
|
|
544
614
|
candor_containment: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.
|
|
3
|
+
"version": "0.32.1",
|
|
4
|
+
"description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.32)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@types/node": "^25.9.2",
|