candor-ts 0.38.3 → 0.39.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 +1 -1
- package/README.md +2 -2
- package/mcp.mjs +12 -0
- package/package.json +2 -2
- package/policy.mjs +47 -0
- package/query-core.mjs +34 -2
- package/query.mjs +53 -3
- package/scan.mjs +577 -51
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@ the TypeScript-specific production + query surface.
|
|
|
23
23
|
> **Already installed? Report the version and ask before upgrading — before you scan.** If this
|
|
24
24
|
> project already has candor (a `.candor/` report directory, or `candor-ts` reachable via `npx`/a
|
|
25
25
|
> global install), do this *first*: run `npx -y candor-ts --version` (offline) and **tell the user
|
|
26
|
-
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.
|
|
26
|
+
> plainly which version they're on** — e.g. *"This project is on candor-ts `<version>` (spec 0.39)."*
|
|
27
27
|
> On a build too old for the flag, read `candor.version` / `candor.spec` from an existing
|
|
28
28
|
> `.candor/report*.json`, or `npm ls -g candor-ts`.
|
|
29
29
|
>
|
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on eve
|
|
|
201
201
|
| A call resolving to a *type* (function-typed field/param) → `Unknown`, never silent-pure | SPEC §4 |
|
|
202
202
|
| Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
|
|
203
203
|
| The literal surfaces `hosts`/`cmds`/`paths`/`tables`, literal-read only | SPEC §2 |
|
|
204
|
-
| `{ candor: { version, toolchain, spec: "0.
|
|
204
|
+
| `{ candor: { version, toolchain, spec: "0.39" }, functions }` envelope; pure fns omitted | SPEC §2/§2.1 |
|
|
205
205
|
| Call-graph sidecar with **every** analyzed function a key | SPEC §2.2 |
|
|
206
206
|
| The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
|
|
207
207
|
|
|
@@ -219,7 +219,7 @@ read the Rust source".
|
|
|
219
219
|
|
|
220
220
|
## Status
|
|
221
221
|
|
|
222
|
-
0.30.0, speaking candor-spec 0.
|
|
222
|
+
0.30.0, speaking candor-spec 0.39: the analysis core, the gate (`--policy` / `--gate-json` /
|
|
223
223
|
`.candor/config`), the full §3.1 query surface (including `containment`, `blindspots`, the
|
|
224
224
|
`--include-unknown` dispatch frontier, and ⟨0.24⟩ `gate --report` — the gate applied to an EXISTING
|
|
225
225
|
report, byte-equivalent to `scan --policy`'s verdict), the MCP server, the LSP server, and the watch loop are
|
package/mcp.mjs
CHANGED
|
@@ -344,6 +344,9 @@ const TOOLS = {
|
|
|
344
344
|
// available to it. Fixed key sets, so the caveat spreads at the root and every pinned tool shape is
|
|
345
345
|
// unchanged on a complete report (`completenessFields` → `{}`). These three certify nothing, so they
|
|
346
346
|
// are on the descriptive side of the ⟨0.32⟩ boundary stated on `nestWithCaveat` below.
|
|
347
|
+
// SOUNDNESS R507 — see the dispatcher's `oneSubject` guard. `impact` answers about ONE function, so
|
|
348
|
+
// a selector naming several is REFUSED rather than silently resolved to an arbitrary one.
|
|
349
|
+
oneSubject: true,
|
|
347
350
|
run: (a, p) => { const fns = loadReportLoud(p); return withCompleteness(p, capImpact(Q.impact(fns, graphOrReportEdges(p, fns), a.fn))); },
|
|
348
351
|
},
|
|
349
352
|
candor_where: {
|
|
@@ -361,6 +364,7 @@ const TOOLS = {
|
|
|
361
364
|
schema: { type: "object", properties: { fn: { type: "string" }, effect: { type: "string" }, ...reportArg }, required: ["fn", "effect"] },
|
|
362
365
|
// ⟨0.32⟩ see `candor_impact` above: `path: []` to an agent is *this function does not reach that
|
|
363
366
|
// effect*, and a hop through an unread unit breaks the chain.
|
|
367
|
+
oneSubject: true, // SOUNDNESS R507 — see `candor_impact` and the dispatcher's guard
|
|
364
368
|
run: (a, p) => { const fns = loadReportLoud(p); return withCompleteness(p, Q.path(fns, graphOrReportEdges(p, fns), a.fn, a.effect)); },
|
|
365
369
|
},
|
|
366
370
|
candor_callers: {
|
|
@@ -914,6 +918,14 @@ function handle(msg) {
|
|
|
914
918
|
const names = [...new Set([...Object.keys(Q.loadCallgraph(prefix)), ...loadReportLoud(prefix).map((e) => e.fn)])];
|
|
915
919
|
if (Q.matches(names, args.fn).length === 0)
|
|
916
920
|
return result(id, { content: [{ type: "text", text: `candor: no function matching \`${clip(args.fn)}\` in this report` }], isError: true });
|
|
921
|
+
// SOUNDNESS R507 — the OTHER half of the same resolution, and scoped to the tools that answer
|
|
922
|
+
// about ONE subject. `candor_show`/`candor_callers`/`candor_whatif` answer over the WHOLE
|
|
923
|
+
// best-tier set, so several matches WIDEN their answer rather than substituting a subject;
|
|
924
|
+
// `candor_path`/`candor_impact` take one and print a confident verdict about it. Refusing here
|
|
925
|
+
// rather than inside the verbs keeps the decision beside the zero-match guard it is the twin of.
|
|
926
|
+
const amb = t.oneSubject ? Q.ambiguousSelector(names, args.fn) : null;
|
|
927
|
+
if (amb)
|
|
928
|
+
return result(id, { content: [{ type: "text", text: `candor: \`${clip(args.fn)}\` names ${amb.length} functions in this report — refusing to answer about one of them. Re-run with the full name: ${amb.map((n) => clip(n)).join(", ")}` }], isError: true });
|
|
917
929
|
}
|
|
918
930
|
const out = t.run(args, prefix);
|
|
919
931
|
// Minified, not pretty-printed: the consumer is an AGENT (it parses the JSON), so the indentation
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "candor-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"mcpName": "io.github.tombaldwin/candor",
|
|
5
|
-
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.
|
|
5
|
+
"description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.39)",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@types/node": "^25.9.2",
|
package/policy.mjs
CHANGED
|
@@ -670,6 +670,20 @@ export function refusalVerdict(spec, reason, unevaluated = null) {
|
|
|
670
670
|
* ⟨0.24⟩ §3.3.1 requires every ordering to be locale-INDEPENDENT. `Array.prototype.sort` has been
|
|
671
671
|
* stable since ES2019, so rows with an equal key keep their arrival order.
|
|
672
672
|
*/
|
|
673
|
+
// ⟨0.23⟩/⟨0.39⟩ Say out loud, WITHOUT moving the verdict, which synthetic `interfaceUnion` entries a
|
|
674
|
+
// rule matched. One printer, called from the two VERDICT routes (`scan --policy` and `gate --report`),
|
|
675
|
+
// so §3.1's byte-equality between them covers the note as well as the rows.
|
|
676
|
+
export function noteSyntheticHits(violations, err = console.error) {
|
|
677
|
+
const hits = violations?.syntheticHits ?? [];
|
|
678
|
+
if (!hits.length) return;
|
|
679
|
+
err(`candor-ts: note — ${hits.length} ⟨0.23⟩ interfaceUnion entr`
|
|
680
|
+
+ (hits.length === 1 ? "y matches a policy rule and is NOT gated as a function"
|
|
681
|
+
: "ies match a policy rule and are NOT gated as functions")
|
|
682
|
+
+ ": the key names a BODILESS declaration, and the effects under it are the CHA union over "
|
|
683
|
+
+ "implementors, each of which IS gated under its own entry.");
|
|
684
|
+
for (const h of hits) err(` \`${h.fn}\` { ${h.effects.join(", ")} } would have matched \`${h.rule}\``);
|
|
685
|
+
}
|
|
686
|
+
|
|
673
687
|
export function sortViolations(violations) {
|
|
674
688
|
const c = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
|
|
675
689
|
return violations.sort((a, b) =>
|
|
@@ -1066,6 +1080,11 @@ export function classFilterExcludes(r, entry, eff, reasonAcc, netClassOf, units
|
|
|
1066
1080
|
|
|
1067
1081
|
export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map(), partners = new Set(), netClasses = null, withhold = null, units = null, hashByName = null) {
|
|
1068
1082
|
const out = [];
|
|
1083
|
+
// ⟨0.39⟩ the synthetic `interfaceUnion` entries a rule MATCHED — carried on the returned array (a
|
|
1084
|
+
// non-index property, so `length`, iteration and `JSON.stringify` are all untouched) rather than
|
|
1085
|
+
// printed from here, because this function is also called for sub-evaluations that must stay silent.
|
|
1086
|
+
const syntheticHits = [];
|
|
1087
|
+
out.syntheticHits = syntheticHits;
|
|
1069
1088
|
// `Llm` ⟨0.13⟩ reaches the SAME hosts surface as Net (an Llm host WAS captured as a Net host literal).
|
|
1070
1089
|
const surfaces = { Net: "hosts", Llm: "hosts", Exec: "cmds", Fs: "paths", Db: "tables" };
|
|
1071
1090
|
// §6.2 ⟨0.19⟩: `reasonClass` (all classes on the fn) rides an AS-EFF-006 Unknown violation; ⟨0.20⟩ `netClass`
|
|
@@ -1130,6 +1149,34 @@ export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map()
|
|
|
1130
1149
|
// The gate's TEST and the class list it REPORTS both read it, so the two can't disagree about one function.
|
|
1131
1150
|
const netClassOf = netClassResolver(incomplete, partners, netClasses, units);
|
|
1132
1151
|
for (const f of functions) {
|
|
1152
|
+
// ⟨0.23⟩/⟨0.39⟩ A SYNTHETIC `interfaceUnion` ENTRY IS NOT A UNIT, so it cannot PERFORM anything and
|
|
1153
|
+
// must not become a violation ROW. Its `fn` names a BODILESS declaration (`Store.save` on an
|
|
1154
|
+
// interface); the effects under it are the CHA union over implementors, published under that hash so
|
|
1155
|
+
// a CHAINED CONSUMER's dispatch resolves across the scan boundary. Every effect it carries is
|
|
1156
|
+
// already on an implementor's OWN entry in the same report — which IS a unit, and IS gated one loop
|
|
1157
|
+
// iteration away.
|
|
1158
|
+
//
|
|
1159
|
+
// MEASURED HERE, not inherited from the reference engine's argument: over `interface Entry { state()
|
|
1160
|
+
// }` with a single STRUCTURAL implementor, `deny Unknown` went exit 0 -> 1 the moment ⟨0.39⟩ un-gated
|
|
1161
|
+
// the union — while the package's own `stringify` resolved the dispatch PRECISELY (a real `calls`
|
|
1162
|
+
// edge to the structural member) and carried no Unknown at all. The union's Unknown is an artifact of
|
|
1163
|
+
// what the WIRE can name (a structural implementor has no class name to key on), never a fact about
|
|
1164
|
+
// a body this scan read, so gating on it is a fabricated violation in the producer's own verdict.
|
|
1165
|
+
// candor-java measured the identical flip (`Policy.java:778`) and takes the identical course.
|
|
1166
|
+
//
|
|
1167
|
+
// DISCLOSED, NOT DROPPED: the hits are recorded on the returned array and printed by the verdict
|
|
1168
|
+
// routes, because the one thing a reader of a chained dependency's report wants to know here is that
|
|
1169
|
+
// the dependency publishes a dispatch surface reaching a denied effect.
|
|
1170
|
+
if (f?.interfaceUnion === true) {
|
|
1171
|
+
for (const r of pol.deny) {
|
|
1172
|
+
if (r.scope && !scopeMatches(f.fn, r.scope)) continue;
|
|
1173
|
+
const inf = Array.isArray(f.inferred) ? f.inferred : [];
|
|
1174
|
+
const hits = r.effects.length === 0 ? inf.filter((e) => e !== "Unknown")
|
|
1175
|
+
: inf.filter((e) => r.effects.includes(e));
|
|
1176
|
+
if (hits.length) syntheticHits.push({ fn: f.fn, effects: hits.slice().sort(), rule: r.raw });
|
|
1177
|
+
}
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1133
1180
|
// ⟨0.32⟩ the KEY identifies the unit; `f.fn` is the NAME, and the name is what a policy SCOPE matches
|
|
1134
1181
|
// and what the verdict row prints (§3.3.1 byte-equality with `scan --policy` rests on it).
|
|
1135
1182
|
const uk = unitKey(units, f);
|
package/query-core.mjs
CHANGED
|
@@ -196,7 +196,7 @@ function normFn(e) {
|
|
|
196
196
|
// non-string in `unknownWhy` reaches `reasonClass()` and one in `hosts`/`netClass` reaches the ⟨0.20⟩
|
|
197
197
|
// destination-class matcher.
|
|
198
198
|
for (const k of ["unknownWhy", "netClass", "hosts", "cmds", "paths", "tables",
|
|
199
|
-
"declared", "undeclared", "overdeclared"]) if (k in e) o[k] = arr(e[k]);
|
|
199
|
+
"declared", "undeclared", "overdeclared", "dispatchesOn"]) if (k in e) o[k] = arr(e[k]);
|
|
200
200
|
return o;
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -215,8 +215,12 @@ function normFn(e) {
|
|
|
215
215
|
// on, the ⟨0.19⟩/⟨0.20⟩ class fields it scopes with, and the `calls` edges the reason-class fixpoint runs
|
|
216
216
|
// over. `loc`/`hash`/`unitKind`/`invisible`/`unresolved` are deliberately NOT here: no verdict reads them,
|
|
217
217
|
// so refusing on them would be a spurious refusal on a report whose gate-relevant content is intact.
|
|
218
|
+
// ⟨0.39⟩ `dispatchesOn` is a VERDICT key, not a diagnostic one: a chained consumer unions the effects
|
|
219
|
+
// of every implementor published under each key it names, so a present-but-unparseable one coerced to
|
|
220
|
+
// `[]` silently drops that union — an effect the consumer really reaches, gone, with no hedge. Exactly
|
|
221
|
+
// the fail-OPEN direction this list exists to refuse.
|
|
218
222
|
const VERDICT_STR_ARRAY_KEYS = ["inferred", "direct", "calls", "unknownWhy", "netClass", "hosts",
|
|
219
|
-
"declared", "undeclared", "overdeclared"];
|
|
223
|
+
"declared", "undeclared", "overdeclared", "dispatchesOn"];
|
|
220
224
|
const isStrArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
221
225
|
export function entryCorruptKeys(e) {
|
|
222
226
|
if (!e || typeof e !== "object" || Array.isArray(e)) return ["<entry is not an object>"];
|
|
@@ -1382,6 +1386,29 @@ export function matches(names, q) {
|
|
|
1382
1386
|
return best === 0 ? [] : names.filter((n) => matchTier(n, q) >= best);
|
|
1383
1387
|
}
|
|
1384
1388
|
|
|
1389
|
+
/** SOUNDNESS R507/R497 — THE SELECTOR THAT NAMES SEVERAL FUNCTIONS, for the verbs that answer about ONE.
|
|
1390
|
+
*
|
|
1391
|
+
* Returns the distinct best-tier candidates when MORE THAN ONE survives, else null. The asymmetry it
|
|
1392
|
+
* closes is the whole defect: `path` and `impact` ALREADY refuse at exit 2 when ZERO functions match —
|
|
1393
|
+
* only MANY was answered silently, by taking `targets[0]` and printing a confident verdict about a
|
|
1394
|
+
* function the caller did not ask about. Measured in candor-java on `auth-2.25.60`: `path
|
|
1395
|
+
* resolveCredentials Exec` had FOURTEEN dot-anchored candidates and answered a confident negative about
|
|
1396
|
+
* `AnonymousCredentialsProvider` while three of the fourteen perform `Exec`. A negative is a claim in
|
|
1397
|
+
* this family; a negative about a substituted subject is a fabricated one.
|
|
1398
|
+
*
|
|
1399
|
+
* Anchoring is NOT this function's job and is already done: `matchTier` requires a `[.$#]` boundary
|
|
1400
|
+
* before a suffix match (tier 2), so `ProfileCredentialsProvider` never matches inside
|
|
1401
|
+
* `InstanceProfileCredentialsProvider` — that half of R497 is what candor-ts already had, and it is why
|
|
1402
|
+
* an EXACT match (tier 3) still resolves alone and is never counted as ambiguity.
|
|
1403
|
+
*
|
|
1404
|
+
* DISTINCT names, because the verbs resolve over the union of the callgraph's keys and the report's
|
|
1405
|
+
* `fn`s and one function is routinely in both — a duplicate is one subject, not two.
|
|
1406
|
+
*/
|
|
1407
|
+
export function ambiguousSelector(names, q) {
|
|
1408
|
+
const m = [...new Set(matches(names, q))].sort(byCodePoint);
|
|
1409
|
+
return m.length > 1 ? m : null;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1385
1412
|
// Exported for consumers that answer MANY caller-count questions over one loaded graph (the LSP
|
|
1386
1413
|
// codeLens): building the inversion once per request instead of once per `callers()` call.
|
|
1387
1414
|
export function reverseGraph(cg) {
|
|
@@ -1599,6 +1626,11 @@ export function callersFrontier(cg, fns, hierarchy, q) {
|
|
|
1599
1626
|
const possible = [];
|
|
1600
1627
|
for (const f of fns) {
|
|
1601
1628
|
if (confirmed.has(f.fn)) continue;
|
|
1629
|
+
// ⟨0.39⟩: a synthetic `interfaceUnion` entry is the union over an abstraction member's implementors,
|
|
1630
|
+
// not a function with a body — it cannot CALL anything, so it is not a possible caller. Un-gating
|
|
1631
|
+
// ⟨0.23⟩ made these default rather than opt-in, and this arm then named the bodiless DECLARATION
|
|
1632
|
+
// beside the dispatcher. Found by the four-way frontier differential on (producer=java, consumer=ts).
|
|
1633
|
+
if (f.interfaceUnion) continue;
|
|
1602
1634
|
const hits = new Set();
|
|
1603
1635
|
for (const w of f.unknownWhy ?? []) {
|
|
1604
1636
|
if (!w.startsWith("dispatch:")) continue;
|
package/query.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { parsePolicy, scopeMatches, discoverConfigPolicy, parseUnknownAliases, d
|
|
|
27
27
|
evaluatePolicy, reportNetClasses, resolveReasonClasses, discoverConfigPath,
|
|
28
28
|
policyVocabularyAnchor, policyErrorText, policyRefusalUnevaluated, policyUnreadable, policyZeroRules,
|
|
29
29
|
fatalPolicyErrors, refusalVerdict, sortViolations,
|
|
30
|
-
unanswerableScoped, wholePolicyUnanswerable, reportUnits } from "./policy.mjs";
|
|
30
|
+
unanswerableScoped, wholePolicyUnanswerable, reportUnits, noteSyntheticHits } from "./policy.mjs";
|
|
31
31
|
import { hasReport, refusalMarkerFor, refusalSentence } from "./query-core.mjs";
|
|
32
32
|
import { printAgents, writeStdoutSync, writeSinkAtomic, isCandorConfigSink } from "./contract.mjs";
|
|
33
33
|
import { bestFinds } from "./surface.mjs";
|
|
@@ -42,7 +42,7 @@ import { impact as coreImpact, path as corePath, gains as coreGains,
|
|
|
42
42
|
containment as coreContainment, diff as coreDiff,
|
|
43
43
|
where as coreWhere, map as coreMap, whatif as coreWhatif,
|
|
44
44
|
fix as coreFix, fixGate as coreFixGate, unverified as coreUnverified,
|
|
45
|
-
matches as coreMatches, gainsCoverage, gainsCompletenessFields, parseClassFilter, ClassFilterError,
|
|
45
|
+
matches as coreMatches, ambiguousSelector, gainsCoverage, gainsCompletenessFields, parseClassFilter, ClassFilterError,
|
|
46
46
|
loadReport, loadCallgraph, reportCallsGraph, loadGateReport, gateReportInputFiles,
|
|
47
47
|
reportVersion, reportPackage,
|
|
48
48
|
advisoryAnswer,
|
|
@@ -679,6 +679,19 @@ function renderPathHuman(fns, cg, fnQ, eff, hedge = false) {
|
|
|
679
679
|
console.error(`candor-query path: no function matching '${fnQ}'`);
|
|
680
680
|
process.exit(2);
|
|
681
681
|
}
|
|
682
|
+
// SOUNDNESS R507 — the human renderer's OWN resolution, refused on the same condition. The gate in the
|
|
683
|
+
// `path` case above resolves over the UNION of the callgraph keys and the report's `fn`s; this one
|
|
684
|
+
// resolves over the report alone, and the two name sets can put their best tier in different places
|
|
685
|
+
// (an exact callgraph key makes the union unambiguous while the report-only set still holds two
|
|
686
|
+
// suffix matches). Same helper, so the two cannot answer the question differently.
|
|
687
|
+
{
|
|
688
|
+
const ambH = ambiguousSelector(fns.map((e) => e.fn), fnQ);
|
|
689
|
+
if (ambH) {
|
|
690
|
+
console.error(`candor-ts-query path: '${fnQ}' names ${ambH.length} functions — refusing to answer `
|
|
691
|
+
+ `about one of them. Re-run with the full name:\n ${ambH.join("\n ")}`);
|
|
692
|
+
process.exit(2);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
682
695
|
const startEntry = fns.find((e) => e.fn === start);
|
|
683
696
|
const inferred = startEntry?.inferred ?? [];
|
|
684
697
|
if (!inferred.includes(eff)) {
|
|
@@ -717,7 +730,7 @@ function renderPathHuman(fns, cg, fnQ, eff, hedge = false) {
|
|
|
717
730
|
// package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
|
|
718
731
|
const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
719
732
|
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
|
|
720
|
-
const SPEC_VERSION = "0.
|
|
733
|
+
const SPEC_VERSION = "0.39";
|
|
721
734
|
|
|
722
735
|
// ---- the §3.3.1 canonical query grammar (⟨0.10⟩, additive over 0.9) --------------------------------
|
|
723
736
|
// One shape for every verb: `<verb> <verb-args…> [--report <locator>] [--policy <file>] [--json]
|
|
@@ -1725,6 +1738,20 @@ switch (cmd) {
|
|
|
1725
1738
|
if (coreMatches(knownFnNames(impCg, impFns), q).length === 0) {
|
|
1726
1739
|
console.error(`candor-ts-query impact: no function matching '${q}'`); process.exit(2);
|
|
1727
1740
|
}
|
|
1741
|
+
// SOUNDNESS R507, the same refusal on the verb that answers the OTHER direction — and here the
|
|
1742
|
+
// substitution was HALF-hidden, which is worse than `path`'s. `coreImpact` walks the reverse graph
|
|
1743
|
+
// from EVERY target (so `affectedCount` is a union over all of them) and then prints
|
|
1744
|
+
// `fn: targets[0]`: a blast radius computed for several functions, attributed to one. A reader
|
|
1745
|
+
// checking whether `save` is safe to change gets a count that may belong to a different `save`.
|
|
1746
|
+
// See the `path` gate above for the asymmetry and for why one resolution decides both outcomes.
|
|
1747
|
+
{
|
|
1748
|
+
const amb = ambiguousSelector(knownFnNames(impCg, impFns), q);
|
|
1749
|
+
if (amb) {
|
|
1750
|
+
console.error(`candor-ts-query impact: '${q}' names ${amb.length} functions — refusing to answer `
|
|
1751
|
+
+ `about one of them. Re-run with the full name:\n ${amb.join("\n ")}`);
|
|
1752
|
+
process.exit(2);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1728
1755
|
// ⟨0.32⟩ `putAnswer`, not `put` — this verb had NO completeness reader at all (see `CALLERS_SOWHAT`
|
|
1729
1756
|
// above for the measurement and the boundary). `affectedCount: 0` is the strongest claim in this
|
|
1730
1757
|
// verb's vocabulary, and over a report whose own `excluded` names a class nothing opened it rests on
|
|
@@ -1995,6 +2022,27 @@ switch (cmd) {
|
|
|
1995
2022
|
if (coreMatches(knownFnNames(cg, fns), fn).length === 0) {
|
|
1996
2023
|
console.error(`candor-ts-query path: no function matching '${fn}'`); process.exit(2);
|
|
1997
2024
|
}
|
|
2025
|
+
// SOUNDNESS R507 — …AND THE OTHER HALF OF THE SAME RESOLUTION: MANY. Note the asymmetry that IS the
|
|
2026
|
+
// defect: this verb has refused at exit 2 for ZERO matches for a long time (the gate directly above),
|
|
2027
|
+
// while SEVERAL matches were answered silently — `targets[0]`, an arbitrary one, printed as a
|
|
2028
|
+
// confident verdict about a function the caller never named. Measured in candor-java on
|
|
2029
|
+
// `auth-2.25.60`: `path resolveCredentials Exec` had FOURTEEN anchored candidates and returned a
|
|
2030
|
+
// confident negative about `AnonymousCredentialsProvider` while three of the fourteen perform `Exec`.
|
|
2031
|
+
// A negative is a claim in this family, so a negative about a substituted subject is a fabricated one
|
|
2032
|
+
// — strictly worse than unhelpful.
|
|
2033
|
+
//
|
|
2034
|
+
// ONE RESOLUTION, TWO OUTCOMES: the same `knownFnNames` set and the same `matches` ladder decide
|
|
2035
|
+
// zero and many, so the two cannot drift apart the way a second, parallel check would. Anchoring is
|
|
2036
|
+
// already in `matchTier` (this engine has always had R497's first half), so an EXACT match resolves
|
|
2037
|
+
// alone and is never refused.
|
|
2038
|
+
{
|
|
2039
|
+
const amb = ambiguousSelector(knownFnNames(cg, fns), fn);
|
|
2040
|
+
if (amb) {
|
|
2041
|
+
console.error(`candor-ts-query path: '${fn}' names ${amb.length} functions — refusing to answer `
|
|
2042
|
+
+ `about one of them. Re-run with the full name:\n ${amb.join("\n ")}`);
|
|
2043
|
+
process.exit(2);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
1998
2046
|
// ⟨0.32⟩ THE COMPLETENESS READER THIS VERB DID NOT HAVE (see `PATH_SOWHAT` above). `path: []` is the
|
|
1999
2047
|
// determined negative here — *this function does not reach that effect* — and a hop through an unread
|
|
2000
2048
|
// unit BREAKS the chain, so a hedging report can produce that answer for a function that really does
|
|
@@ -2500,6 +2548,8 @@ switch (cmd) {
|
|
|
2500
2548
|
// fire AS-EFF-008 "no visible literal" on every report entry whose surface the wire does not carry.
|
|
2501
2549
|
const gviol = evaluatePolicy(gwp.answerable,
|
|
2502
2550
|
g.functions, {}, new Map(), new Set(), gnet, gwithhold, gunits);
|
|
2551
|
+
// ⟨0.39⟩ see scan.mjs's twin — one printer, both verdict routes.
|
|
2552
|
+
noteSyntheticHits(gviol);
|
|
2503
2553
|
// Route the human output exactly as a scan does: to stderr whenever stdout carries the verdict
|
|
2504
2554
|
// document, so `candor-ts-query gate … --json | jq` sees pure JSON.
|
|
2505
2555
|
const gsay = (json || gateJsonPath === "-") ? (l) => console.error(l) : (l) => console.log(l);
|