@vantageos/mcp-doctor 0.7.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -3,6 +3,138 @@
3
3
  All notable changes to `@vantageos/mcp-doctor` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 0.8.0 — 2026-10-11
7
+
8
+ Measured against the same standard pin as 0.7.0, `b714129a110237c5521d5269ee61ab919408a17f` (91
9
+ rules). 0.7.0 covered 64 of them; 0.8.0 adds a detector for each of the 27 left, so the registry
10
+ covers all of them:
11
+
12
+ ```
13
+ npx tsx scripts/standard-coverage.ts b714129a110237c5521d5269ee61ab919408a17f ../elpi-corp-std
14
+ -> 91/91 rules covered by a registered detector (pin b714129a110237c5521d5269ee61ab919408a17f)
15
+ ```
16
+
17
+ The 27 are derived from that script's `UNCOVERED` rows at 0.7.0, not listed from memory: **C-23 C-24
18
+ C-27 C-28 · S-8 S-9 S-10 S-11 S-13 S-14 S-15 S-16 S-17 S-18 S-19 S-20 S-21 S-22 S-23 S-24 S-25 S-26
19
+ S-27 · E-5 · A-3 A-4 A-5**. Every one has a RED test committed before its implementation and a bite
20
+ mutation on foreign material, one per form of the guarded thing, with its legitimate pole
21
+ (`scripts/v080-foreign-bite-probe.sh`, `npm run test:v080-foreign-bite`).
22
+
23
+ ### Added — one detector per uncovered rule
24
+ - **Trunk gates, static half read, gate half named as owed:** **C23** judgement-sites, **C24**
25
+ acceptance-at-dispatch, **C27** subpackage-types-in-isolation, **C28** deploy-failure-signal.
26
+ - **Server:** **S8** token-passthrough, **S9** confused-deputy-proxy, **S10** ssrf-guard, **S11**
27
+ result-contract, **S13** progress-cancel, **S14** version-negotiation, **S15** completion-security,
28
+ **S16** local-footprint, **S17** forward-auth, **S18** session-integrity, **S19** auth-url-open,
29
+ **S20** scope-minimization, **S21** stdio-proxy-sandbox, **S22** origin-validation, **S23**
30
+ protocol-version-header, **S24** tool-rate-limit, **S25** resource-discipline, **S26**
31
+ list-changed-parity, **S27** elicitation-floor.
32
+ - **Extension:** **E5** extension-isolation. **App:** **A3** sandbox-respected, **A4** wcag-axe-gate,
33
+ **A5** performance-budgets.
34
+ - A dynamique or gate row keeps the 0.7.0 semantics: red where the tree contradicts the rule, `absent`
35
+ only when the subject is provably absent (each matcher carries a positive control and a failed
36
+ control throws), otherwise `could-not-judge` naming the half that is owed. Nothing that has an unread
37
+ half returns `present`.
38
+ - Per-detector reading, limits and verdicts: README, "Detectors added in 0.8.0".
39
+
40
+ ### Changed — Argus rulings on 0.7.0 (PR #55)
41
+ 1. **An owed human verdict is a structured field, not a state and not a prefix.** There is no fifth
42
+ state. A `could-not-judge` result may carry `owedVerdict: { kind: "human", rule: "C-23", sites:
43
+ [{ anchor, element?, what }] }`; the runner refuses it on any other state or with zero sites. S-5
44
+ (i)/(j) use it in place of the `VERDICT DUE (C-23)` prefix (removed from the prose), and so do C-24,
45
+ C-28 and S-20. The human report prints an "owed human verdicts (N sites)" section; the new `--json`
46
+ flag prints `{ surface, exitCode, owedHumanVerdicts, results, skipped }` with the field in each
47
+ result.
48
+ 2. **S-5 (g) and (l) match a quoted key.** `"client_id_metadata_document_supported": true` (and the
49
+ single-quoted spelling, and the same for `authorization_response_iss_parameter_supported`) was a
50
+ false red; each spelling x each key x `true` / `false` is now a test.
51
+ 3. **A-11 recognises every literal form of the sandbox domain.** Any quoted `*.claudemcpcontent.com`
52
+ with a label in front of the suffix is a typed literal: any label (hex or not, any case, hyphens,
53
+ several labels), any quoting (double, single, a template with no interpolation), a scheme prefix,
54
+ and a literal joined to the suffix (`"vp" + ".claudemcpcontent.com"`). The bare suffix constant and
55
+ an interpolated label are not literals. One test per form.
56
+
57
+ ### Fixed — found by running the new detectors on foreign material
58
+ Each was a false positive or a false green found on `vantage-mcp-boilerplate` or `vantage-peers`,
59
+ turned into a RED test before the fix:
60
+ - **C-28**: a deploy named in a YAML comment is not a deploy step.
61
+ - **C-27**: a workflow is read for the commands its `run:` keys run (inline and block), not for every
62
+ line holding the word; an `echo`, a job name or a comment is not a type check or an install.
63
+ - **S-8, S-24**: a marker word inside a string (an error message, a description) is prose, not an
64
+ audience check or a rate guard; a quoted property key still counts.
65
+ - **S-10**: a blocklist written as CIDR pairs (`["10.0.0.0", 8]`, what `node:net` BlockList takes) states
66
+ the union; the two metadata hostnames are satisfied by name resolution; a host classification
67
+ (`startsWith("127.")` choosing http or https) is not address validation outside a file that reaches
68
+ out.
69
+ - **S-13**: a TypeScript method signature named `fetch(request: Request, ...)` is not an outbound call.
70
+ - **S-17**: `iss` is validated in the file that redeems the code, not anywhere in the tree.
71
+ - **S-22**: a quoted `"0.0.0.0"` in a blocklist constant is not a bind; CORS `origin: "*"` is not an
72
+ Origin validation.
73
+ - **S-25**: an interpolated `${proto}://${host}` is a computed URL, not a custom scheme.
74
+ - **A-3, A-4**: a tree that serves `ui://` resources and holds no UI code is `could-not-judge`, not a
75
+ verdict on code that is not there.
76
+
77
+ ### Revised after review (PR #56, Argus REVISE at `b60df486`)
78
+ **The exit-0 pole is reachable.** `computeExitCode` returned 2 on any `could-not-judge`, and six new rules
79
+ had no way to say `present`, so no server that registered a tool could be approved. Each rule now declares
80
+ its choice, in its detector header and here:
81
+ - **S-11, S-13, S-16, A-4, A-5, A-3 — choice (b), a static positive path on positive evidence.** `present`
82
+ names the half it read (`static half: ...`) and the half it did NOT read (`NOT read by this run: ...`);
83
+ nothing is claimed about the unread half.
84
+ - S-11: every registered tool declares an `outputSchema` and no error code contradicts; a tool with none
85
+ whose window shows no result shape is `could-not-judge`, named.
86
+ - S-13: every outbound call carries a timeout (or none is emitted), no stale id, no answering cancel handler.
87
+ - S-16: no read outside the object, and a transport posture is identified (stdio, HTTP with a token check, a
88
+ Unix socket); tools with no identifiable transport are `could-not-judge`.
89
+ - A-4: an axe dependency and something that runs it; the CI result stays the gate's.
90
+ - A-5: the measured gzipped size is under 250 KB; with no built bundle in the tree it is `could-not-judge`.
91
+ - A-3: UI code, HTML shells and HTML held in a string literal (`ui://` served from `text: \`<html>\``) are
92
+ read; `ui://` served with no UI code anywhere stays `could-not-judge` (the code is elsewhere).
93
+ - S-14 (not in the review's list, same defect, found by the green-pole test): an SDK-first server whose
94
+ tree shows the official SDK (import or dependency) is `present` on its static half; a server class with
95
+ no SDK evidence stays `could-not-judge`.
96
+ - **S-20 — choice (c), an owed human verdict.** A result whose only open question is the human verdict on the
97
+ exposed sites declares `owedVerdict.mechanicalHalfComplete: true`. **Exit-code treatment, defined and
98
+ tested (`tests/v080-exit-code-owed.test.ts`):** such a result is OWED, not unreadable. It does not raise
99
+ exit 2 (exit 0 when nothing else does) and its sites stay printed and counted in `owedHumanVerdicts`. A
100
+ `could-not-judge` with no such declaration (S-5, whose probes are also owed) exits 2 as before, and a
101
+ violation still exits 1. Not defined: S-23 and S-10 on an SDK HTTP server still end in `could-not-judge`
102
+ (the SDK version that validates the header is not readable from the tree) — they hold exit 2 for such a tree.
103
+ - `reconcile()` (the `--mode=both` divergence) now carries the owed sites of either side.
104
+ - `fixtures/oracle-green-conforming` is repaired to conform to the 0.8.0 rules it failed: a real token-bucket
105
+ rate guard on the tool path (S-24), a stdio transport, a bridge-only UI shell, an axe gate, a built bundle.
106
+ A test asserts it exits 0 statically with S-11/13/16/24 and A-3/4/5 `present`, and exits 1 once the guard
107
+ is removed.
108
+
109
+ **Two security detectors no longer fail open.**
110
+ - **S-8** read the incoming Authorization header only through `.get("authorization")` or a quoted bracket, so
111
+ `authorization: c.req.raw.headers.authorization` forwarded to `fetch` read `present`. The census of reads
112
+ (a call, a bracket, DOT access with or without optional chaining, a destructuring renamed or not, any case)
113
+ is one test per form.
114
+ - **S-22** accepted a bare `allowedOrigins:` as protection. It is now read by VALUE: a wildcard (bare, in a
115
+ list, as a host pattern) and an empty list are red; a list of concrete origins is `present`; a variable is
116
+ resolved in the tree (wildcard / empty / concrete) or reported `could-not-judge` naming it; the protection
117
+ flag with no list is red. One test per shape.
118
+
119
+ **Sweep.**
120
+ - Source the scan does not read (`.js .mjs .cjs .jsx .mts .cts`; declarations, tests, config and
121
+ `_generated/` excluded): the code-reading rules (C-23, S-8..S-27, E-5, A-3) say `could-not-judge` naming the
122
+ files instead of `absent`. `src/bin/**` is product source (only a top-level `bin/` is tooling), and tools
123
+ registered by tooling are not the server's.
124
+ - `present` on a word: S-9 (needs a consent lookup, an equality test on `redirect_uri`, a random state), S-20
125
+ (a bare challenge declares no scope), S-24 (a rate-limit identifier must be CALLED), S-17 (the issuer is a
126
+ word of the store key, not the letters of `permissionId`), S-16 (a `footprint` KEY with content, not the
127
+ word), E-5 (zero files read is `could-not-judge`).
128
+ - Comment text as evidence: C-23 (a genre is declared by a code literal or a comment opening with `genre:`),
129
+ A-4 (workflow and test-file comments are not a run).
130
+ - Silent skips: S-15 (case-clause handlers are read; unreadable sites are named), A-5 (oversize, dangling or
131
+ unreadable UI paths are `could-not-judge`, named), C-27 (an unreadable deployable directory is named).
132
+
133
+ ### Process
134
+ - `tests/__oracle__/oracle-complete-server.static.json` is regenerated: 27 rows added, no existing row
135
+ changed.
136
+ - Version `0.8.0` is strictly above the live `latest` (`0.7.0`), read from the registry.
137
+
6
138
  ## 0.7.0 — 2026-10-10
7
139
 
8
140
  Measured against the ElPi-Corp standard pinned at `b714129a110237c5521d5269ee61ab919408a17f`
package/README.md CHANGED
@@ -46,7 +46,7 @@ The path is the root of the MCP server sources (the folder holding `server.ts` /
46
46
  The binary's own `--help` is the authority; this is what it prints:
47
47
 
48
48
  ```
49
- usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>]
49
+ usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>] [--json]
50
50
 
51
51
  --mode=static read the tree only (default)
52
52
  --mode=dynamic launch the server and judge what it serves
@@ -56,6 +56,7 @@ usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<co
56
56
  fails (default 0)
57
57
  --backend=DIR the backend source (a convex/ dir) whose door list S-28 reads;
58
58
  default: the nearest convex/ above the tree
59
+ --json print the report as JSON (results, owedHumanVerdicts, exitCode)
59
60
  --version print the version and exit
60
61
  --help print this and exit
61
62
 
@@ -77,6 +78,11 @@ The exit code keeps three outcomes distinct, which is what makes the tool safe i
77
78
  - **1** — at least one violation.
78
79
  - **2** — no violation, but at least one detector abstained. **An abstention is never a pass**: it
79
80
  means part of the tree was not judged, and the row names which part and why.
81
+ **Exception, defined in 0.8.0:** a `could-not-judge` that carries an `owedVerdict` declaring
82
+ `mechanicalHalfComplete: true` is an OWED human verdict, not an abstention: the detector ran its whole
83
+ mechanical half and exposed the sites a person must decide (C-23: "green is: the sites are exposed"). It does
84
+ not raise exit 2; the sites stay printed and counted (`owedHumanVerdicts`). Any other `could-not-judge`, with
85
+ or without an `owedVerdict`, still exits 2.
80
86
 
81
87
  **One known limit in 0.3.2, stated rather than hidden:**
82
88
 
@@ -140,7 +146,7 @@ where the tree contradicts the rule, and otherwise `could-not-judge` naming the
140
146
  | **C32** host-key-additive | C-32 | `_meta` objects (followed through spreads and consts): `openai/outputTemplate` needs `ui.resourceUri`, `openai/visibility` needs `ui.visibility` | absent / non-conforming / could-not-judge (served `tools/list` owed) |
141
147
  | **C33** identity-tool | C-33 | on an authenticated server, the identity tool: no argument, `readOnlyHint: true`, an `outputSchema` that declares `id` (inline or a same-tree const) | absent / non-conforming / could-not-judge |
142
148
  | **S29**..**S34** | S-29..S-34 | the `events` capability and the three `events/*` handler literals; descriptors (`name`, bilingual description, non-empty `delivery`, `inputSchema`, `payloadSchema`); the `-32012`/`-32602`/`whsec_`, `-32015`/`verification`, five-header/`standardwebhooks`/262144-byte/410-413 evidence; `events.accessRecheckSeconds > 0` in the server manifest | absent (no events surface) / non-conforming / could-not-judge (dynamic half named) |
143
- | **S5** authz-elements (f)-(l) | S-5 (f)..(l) | PRM `resource` and its path-suffixed route, CIMD + `"none"`, `iss` flag, `/authorize` handler (auto-approval, organisation from a query parameter), jwt-bearer, OIDC discovery | absent / non-conforming / could-not-judge (`VERDICT DUE (C-23)` sites exposed; probes owed) |
149
+ | **S5** authz-elements (f)-(l) | S-5 (f)..(l) | PRM `resource` and its path-suffixed route, CIMD + `"none"`, `iss` flag, `/authorize` handler (auto-approval, organisation from a query parameter), jwt-bearer, OIDC discovery | absent / non-conforming / could-not-judge (sites exposed in `owedVerdict`; probes owed) |
144
150
  | **E7** middleware-events | E-7 | writes to `delivery`/`arguments`/`id`/`refreshBefore`/`cursor` and replacement of `ctx.output` inside an `events/*` branch of a hook | absent / present / non-conforming |
145
151
  | **E8** official-ext-declared | E-8 | `capabilities.extensions` as an object, each official key's MUST methods registered | absent / present / non-conforming / could-not-judge (unknown official extension) |
146
152
  | **E9** host-invoked-tools | E-9 | `openai/settings.readTool`, `openai/mentions.searchTool`: registered, `readOnlyHint: true`, `outputSchema`, no required input, `"app"` visibility, ResourceLink | absent / present / non-conforming |
@@ -153,6 +159,62 @@ where the tree contradicts the rule, and otherwise `could-not-judge` naming the
153
159
  | **A11** sandbox-domain-derived | A-11 | a `ui://` resource served over HTTP and a SHA-256 + `.claudemcpcontent.com` derivation | absent / non-conforming / could-not-judge |
154
160
  | **A12** file-write | A-12 | `openai/resources/write`: `ifMatch`, `conflict` + `too-large` handled, received URI only | absent / present / non-conforming |
155
161
 
162
+ ### Detectors added in 0.8.0 (standard `b714129a`) — the 27 rules 0.7.0 left uncovered
163
+
164
+ One detector per remaining rule, so `scripts/standard-coverage.ts` derives **91/91**. Each keeps the
165
+ semantics of a dynamique row: red where the tree contradicts the rule, `absent` only when the subject
166
+ is provably absent (a positive control proves the matcher can see it), otherwise `could-not-judge`
167
+ naming the half that is owed. Comments never count; where a marker word could be prose (a description,
168
+ an error message) it counts only as code or as a quoted property key.
169
+
170
+ | ID | Rule | The static run reads | Verdicts |
171
+ |----|------|----------------------|----------|
172
+ | **C23** judgement-sites | C-23 | every control that declares a judgement genre (the declared column, split on `+`, is the authority): what it returns, whether the due sites travel in `owedVerdict`, a positive control beside any zero | absent / present (sites exposed) / non-conforming |
173
+ | **C24** acceptance-at-dispatch | C-24 | dispatch documents (`tasks/`, `dispatch/`, `briefs/`, issue templates) that build, extend or run a detector / conformance instrument, and the acceptance-property block in them (EN or FR) | absent / non-conforming / could-not-judge (the verdict-side equality is the owed gate; `owedVerdict`) |
174
+ | **C27** subpackage-types-in-isolation | C-27 | each deployable sub-package with a `tsconfig` (C-26's own derivation): a type check that runs after a FROZEN install, read from the Dockerfile `RUN` lines, the railway `buildCommand` and the CI `run:` commands | absent / non-conforming (none / before any install / after a non-frozen install) / could-not-judge (the run result is the owed gate) |
175
+ | **C28** deploy-failure-signal | C-28 | CI deploy steps (railway, vercel, fly, wrangler, netlify, serverless, gcloud run, kubectl, helm); an `if: failure()` step that notifies, or a served-identity-equals-commit check | absent / non-conforming / could-not-judge (a platform-managed deploy, or a signal present; that a person received it is the owed human verdict, `owedVerdict`) |
176
+ | **S8** token-passthrough | S-8 | token verifiers (audience in the same file for `jwtVerify` / `jwt.verify`, anywhere for a library verifier) and any outbound `Authorization` built from the caller's own header, `authInfo.token`, or a variable assigned from them | absent / present / non-conforming / could-not-judge |
177
+ | **S9** confused-deputy-proxy | S-9 | a proxy file (`/authorize` route, `redirect(`, an upstream): consent registry, `redirect_uri` compared exactly (not by `startsWith` / `includes` / `endsWith` / `match` / a regular expression), `__Host-` cookies, `state` after the consent check | absent / present / non-conforming |
178
+ | **S10** ssrf-guard | S-10 | outbound calls whose URL is a caller member (`args.url`, `delivery.url` ...), directly, aliased or destructured; hand-written address tests; `redirect: "follow"`; a blocklist that states at least three members of the union but not all (CIDR pairs count; the metadata hostnames are satisfied by name resolution) | absent / non-conforming / could-not-judge (the loopback-callback and 302 probes are owed) |
179
+ | **S11** result-contract | S-11 | tools returning `structuredContent` without an `outputSchema`; error codes in the reserved range -32020..-32099; a string `code` in a JSON-RPC error | absent / non-conforming / could-not-judge (served conformance owed) |
180
+ | **S13** progress-cancel | S-13 | outbound calls without a `signal` / `timeout`; a literal `requestId` / `progressToken` in an emitted notification; a cancellation handler that answers | absent / non-conforming / could-not-judge (progress monotony, late cancel, `initialize` owed) |
181
+ | **S14** version-negotiation | S-14 | a hand-rolled `initialize` answering a fixed `protocolVersion` | absent / non-conforming / could-not-judge (the negotiation probe is owed) |
182
+ | **S15** completion-security | S-15 | the `completions` capability, `completion/complete`, `completable(`: input validation and a rate limit in the handler | absent / non-conforming / could-not-judge (no-disclosure owed) |
183
+ | **S16** local-footprint | S-16 | reads of `~/.ssh`, cloud credentials, shell history; home-directory reads with no `footprint` in the manifest; HTTP bound to loopback with no token and no socket | absent / non-conforming / could-not-judge (the runtime file trace is owed) |
184
+ | **S17** forward-auth | S-17 | an authorization code redeemed without `iss` handled in the redeeming file; credentials keyed without the issuer; dynamic registration with no CIMD | absent / present / non-conforming |
185
+ | **S18** session-integrity | S-18 | `sessionIdGenerator`: predictable, not a CSPRNG, not bound `<user>:<id>`; a session lookup used as authentication | absent / present / non-conforming |
186
+ | **S19** auth-url-open | S-19 | URL opening through a shell, scheme validation by blocklist, no http(s) allowlist | absent / present / non-conforming |
187
+ | **S20** scope-minimization | S-20 | omnibus scopes, the whole catalogue in `scopes_supported`; the sites that check a CLAIMED scope | absent / present / non-conforming / could-not-judge (`owedVerdict`: does authorization rest on the claim alone) |
188
+ | **S21** stdio-proxy-sandbox | S-21 | child-process launches: a sandbox marker, a log within eight lines, an authorization word for a dangerous command | absent / present / non-conforming |
189
+ | **S22** origin-validation | S-22 | a Streamable HTTP transport: the SDK's DNS rebinding protection, or an `Origin` read followed by a 403; `0.0.0.0` named | absent / present / non-conforming |
190
+ | **S23** protocol-version-header | S-23 | the `MCP-Protocol-Version` read (any case) followed by a 400; a hand-rolled route that never reads it | absent / present / non-conforming / could-not-judge (the SDK owns the check; a probe is owed) |
191
+ | **S24** tool-rate-limit | S-24 | each registered tool's window for a rate-limit word as code (a quoted key counts), or a central `CallToolRequestSchema` handler that limits; each unguarded tool is named | absent / present / non-conforming |
192
+ | **S25** resource-discipline | S-25 | unvalidated dereference of a caller-derived value in a resource handler, a custom scheme that is not RFC 3986, `blob:` that is not base64, binary content without `mimeType` | absent / present / non-conforming |
193
+ | **S26** list-changed-parity | S-26 | `resources` / `prompts` declaring `listChanged: true` against the matching `list_changed` emission, per kind | absent / present / non-conforming |
194
+ | **S27** elicitation-floor | S-27 | sensitive property keys of a `requestedSchema`; MRTR `input_required` with no elicitation fallback and no floor declaration (`midCallInputAtFloor: false`) | absent / non-conforming / could-not-judge (the floor probe is owed) |
195
+ | **E5** extension-isolation | E-5 | global writes (`globalThis`, `process.env`, built-in prototypes), writes to an `identifier`, writes into the shared `extensions` map under a key that is not the extension's own | absent / present / non-conforming |
196
+ | **A3** sandbox-respected | A-3 | `window.parent`, `window.top`, `localStorage`, `sessionStorage`, `document.cookie` in UI code and in the `<script>` blocks of HTML shells | absent / present / non-conforming / could-not-judge (ui:// served, no UI code in the tree) |
197
+ | **A4** wcag-axe-gate | A-4 | an axe dependency and a script, workflow or test that runs it | absent / non-conforming / could-not-judge (0 blocking violations is the owed gate) |
198
+ | **A5** performance-budgets | A-5 | gzipped size of the UI bundle files (`dist/` and `build/` read for size only): one file or one directory over 250 KB | absent / non-conforming / could-not-judge (render, roundtrip, memory probes owed) |
199
+
200
+ **What `present` means for a rule whose other half is dynamic (0.8.0).** S-11, S-13, S-14, S-16, A-3, A-4
201
+ and A-5 return `present` on a static positive path: the verdict says `static half: ...` and names the half it
202
+ did NOT read (`NOT read by this run: ...`) — the served output conforms, progress is monotone, the negotiation
203
+ probe, the runtime file trace, the CI result of the axe run, render/roundtrip/memory. `--mode=dynamic` does not
204
+ implement those reads. Read `present` on these rows as "the static half holds", never as "the rule is held".
205
+
206
+ **An owed human verdict is a field, not a state.** There is no fifth state. A `could-not-judge` whose
207
+ owed half is judgement carries `owedVerdict: { kind: "human", rule: "C-23", sites: [{ anchor, element,
208
+ what }] }` (S-5 (i)/(j), C-24, C-28, S-20). The human report lists the sites in an "owed human verdicts"
209
+ section, counted; `--json` prints `{ surface, exitCode, owedHumanVerdicts, results, skipped }` and the
210
+ field is in each result. The `VERDICT DUE (C-23)` prose prefix is gone: a consumer counts owed
211
+ verdicts from the field, never from prose.
212
+
213
+ **S-5 (g) and (l)** match their key bare, double-quoted or single-quoted. **A-11** calls a typed
214
+ literal any quoted `*.claudemcpcontent.com` (any label, any case, any quoting, a scheme, several
215
+ labels, or a literal joined to the suffix); the bare suffix constant and an interpolated label are not
216
+ literals.
217
+
156
218
  Amended rules, adapted in place: **C-2** (a tool name is at most 64 characters), **C-15** (a `title`,
157
219
  and no enumerated parameter spanning a read and a write verb), **C-16** (event payloads), **S-3**
158
220
  (`skills/list` and `skills/get`), **S-4** (the closed trigger: manifest `minProtocolVersion >=
@@ -166,7 +228,7 @@ destructured binding or an object property.
166
228
 
167
229
  The coverage grid (`analysis/mcp-doctor-coverage-grid.md`) is derived by
168
230
  `npx tsx scripts/standard-coverage.ts <pin> <ElPi-Corp checkout>`; the foreign-material bite proof is
169
- `npm run test:v070-foreign-bite` (see `scripts/v070-foreign-bite-probe.sh`).
231
+ `npm run test:v070-foreign-bite` and `npm run test:v080-foreign-bite` (see `scripts/v070-foreign-bite-probe.sh` and `scripts/v080-foreign-bite-probe.sh`).
170
232
 
171
233
  **DLocalIdentity exemptions.** Two, both reported as `EXEMPT` rather than hidden: the
172
234
  `@vantageos/cloud-identity` package itself (read from the target's `package.json` name), and a site
package/dist/doctor.d.ts CHANGED
@@ -50,9 +50,47 @@ interface Tree {
50
50
  * so when it finds none. Unset means "not supplied", never "no backend".
51
51
  */
52
52
  backendRoot?: string;
53
+ /**
54
+ * Product-looking source the scan did NOT read (`.js` `.mjs` `.cjs` `.jsx` `.mts` `.cts`, relative paths;
55
+ * declarations, tests and config files excluded). A rule that says `absent` on the strength of the files
56
+ * it read must not say it while this list is non-empty: it says could-not-judge and names them.
57
+ */
58
+ unscannedSource?: string[];
53
59
  }
54
60
 
55
61
  type State = "present" | "absent" | "non-conforming" | "could-not-judge";
62
+ /**
63
+ * One site on which a human verdict is due (C-23): the mechanical half of the rule ran in full and
64
+ * the remaining question is judgement, which an instrument may not answer.
65
+ */
66
+ interface OwedVerdictSite {
67
+ /** file:line the verdict is about. */
68
+ anchor: string;
69
+ /** What the human must decide at that site, in one sentence. */
70
+ what: string;
71
+ /** The rule element the site belongs to, when the rule has lettered elements (e.g. "S-5 (i)"). */
72
+ element?: string;
73
+ }
74
+ /**
75
+ * "A human verdict is owed", as a STRUCTURED field of a could-not-judge result. There is NO fifth
76
+ * state: C-23's "sites exposed, verdict due" is a could-not-judge whose owed half is judgement, and
77
+ * a consumer counts owed verdicts by reading this field, never by scanning prose (derive-never-type).
78
+ * Only a could-not-judge may carry it, and never with zero sites (a zero is an event, not a rest).
79
+ */
80
+ interface OwedVerdict {
81
+ kind: "human";
82
+ /** The rule that makes the verdict owed: C-23 (a judgement control exposes its sites). */
83
+ rule: string;
84
+ /**
85
+ * True when the detector ran the whole mechanical half of its rule and the ONLY thing left is the
86
+ * human verdict on `sites` (C-23: "green is: the sites are exposed"). The exit code then treats the
87
+ * result as OWED, not unreadable: it does not raise exit 2, and the sites stay printed and counted
88
+ * (`owedHumanVerdicts`). Absent or false: something besides the human verdict is unread (a probe, a
89
+ * dynamic half), and the result exits 2 like any could-not-judge.
90
+ */
91
+ mechanicalHalfComplete?: boolean;
92
+ sites: OwedVerdictSite[];
93
+ }
56
94
  interface DetectorResult {
57
95
  id: string;
58
96
  /** display anchor(s) file:line — the site the verdict is about. */
@@ -65,6 +103,8 @@ interface DetectorResult {
65
103
  * and the runner refuses it. Ignored for the three decisive states.
66
104
  */
67
105
  reason?: string;
106
+ /** Present only on a could-not-judge whose owed half is a human verdict. See `OwedVerdict`. */
107
+ owedVerdict?: OwedVerdict;
68
108
  }
69
109
  /** A single tool as reported by the RUNNING server over the MCP `tools/list` call. */
70
110
  interface RuntimeTool {
@@ -285,6 +325,20 @@ declare function runDoctorAsync(target: string, opts?: {
285
325
  */
286
326
  declare function renderSkipped(skipped: SkippedDir[]): string;
287
327
  declare function renderTable(surface: string, results: DetectorResult[], skipped?: SkippedDir[]): string;
328
+ /** Total owed human-verdict sites across the results — counted from the structured field. */
329
+ declare function countOwedHumanVerdicts(results: DetectorResult[]): number;
330
+ /**
331
+ * The owed-human-verdicts section: every site a human must decide, counted, read from the
332
+ * structured `owedVerdict` field. Empty string (section omitted) when none are owed — a count of
333
+ * zero is printed by the JSON output, which always carries it.
334
+ */
335
+ declare function renderOwedVerdicts(results: DetectorResult[]): string;
336
+ /**
337
+ * The machine-readable report (`--json`). The same results the table prints, plus the exit code and
338
+ * the owed-human-verdict count, so a consumer reads "a verdict is owed" from a field and never from
339
+ * prose.
340
+ */
341
+ declare function renderJson(surface: string, results: DetectorResult[], skipped: SkippedDir[]): string;
288
342
  /**
289
343
  * The process exit code a run's results carry, three-way with precedence:
290
344
  * 1 — a VIOLATION: at least one detector is `non-conforming` (something IS
@@ -297,4 +351,4 @@ declare function renderTable(surface: string, results: DetectorResult[], skipped
297
351
  */
298
352
  declare function computeExitCode(results: DetectorResult[]): number;
299
353
 
300
- export { type LiveDetector, type RunDoctorOptions, type RunMode, type Session, computeExitCode, isLiveDetector, renderSkipped, renderTable, runDoctor, runDoctorAsync, runDoctorReport };
354
+ export { type LiveDetector, type RunDoctorOptions, type RunMode, type Session, computeExitCode, countOwedHumanVerdicts, isLiveDetector, renderJson, renderOwedVerdicts, renderSkipped, renderTable, runDoctor, runDoctorAsync, runDoctorReport };