@vantageos/backend-doctor 0.1.4 → 0.1.6

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.
@@ -139,6 +139,74 @@ const CONTRACT_MARKER = /isolation-contract:|client-safe-empty|@client-must-not-
139
139
  // A TYPED EMPTY / UNAUTHORIZED RETURN in a wrapper's customCtx (or a handler): the
140
140
  // no-org path returns a shape the subscribing client can render instead of throwing.
141
141
  const TYPED_EMPTY_RETURN = /return\s+\{[^}]*\b(?:unauthorized|forbidden|noOrg|no_org|denied|empty)\b|return\s+\{[^}]*\b(?:orgSlug|orgId)\s*:\s*null\b|return\s+\{\s*items\s*:\s*\[\s*\]/i;
142
+ // A NO-ORG GUARD condition — an `if (!org…)` / `if (org === null)` shape that owns
143
+ // the no-org branch of a handler. Used only by the domination heuristic below to
144
+ // establish that a typed-empty return which precedes an org-throw plausibly owns the
145
+ // no-org path (rather than sitting behind some unrelated condition).
146
+ const NO_ORG_GUARD = /if\s*\(\s*!\s*(?:scope\.|ctx\.[A-Za-z0-9_.]*)?(?:org|orgId|orgSlug|workspaceId|workspace|identity)\b|if\s*\([^)]*\b(?:org|orgId|orgSlug|workspaceId)\s*(?:===|==)\s*null/i;
147
+ // DOMINATION heuristic (Eta false-positive — a DOMINATED no-org throw). A public
148
+ // query whose handler RETURNS a typed empty/unauthorized shape on the no-org branch
149
+ // BEFORE it ever reaches the org-throw does NOT fire that throw into the subscription
150
+ // on the no-org path — the throw guards an org-PRESENT path (a CRM `dashboard.ts`
151
+ // shape: `if (!org) return { items: [], unauthorized: true }` at the top, then a
152
+ // later `requireWorkspaceId(...)` / `throw` that only runs once an org IS present, so
153
+ // the subscribing client never receives an uncaught Server Error). Judged TEXTUALLY
154
+ // and CONSERVATIVELY — we never silently flip a real throw to green:
155
+ // "dominated" a typed-empty return lexically precedes the first org-throw AND a
156
+ // no-org guard also precedes that throw → the early return plausibly
157
+ // owns the no-org path; the throw is unreachable when there is no org.
158
+ // → conform.
159
+ // "ambiguous" a typed-empty return precedes the throw but NO no-org guard is
160
+ // visible before it → we cannot establish the return dominates the
161
+ // no-org path (interleaved/complex control flow) → could-not-judge,
162
+ // preferred over a false green (and over a false red the throw might
163
+ // not deserve).
164
+ // "none" no typed-empty return precedes the throw → the throw IS the no-org
165
+ // behaviour; the existing verdict (violate) is unchanged.
166
+ // The SECOND dominated form (Eta #20 REVISE — the CRM `dashboard.ts` actual shape):
167
+ // the no-org branch is a VALUE returned from a helper CALL whose NAME marks the no-org
168
+ // case, guarded on that same value —
169
+ // `const empty = typedEmptyIfNoWorkspace(ctx.workspaceId, EMPTY_DASHBOARD_SUMMARY);
170
+ // if (empty) return empty;`
171
+ // then a later `requireWorkspaceId(...)` throw that only runs once a workspace IS
172
+ // present. The inline matcher (a `return { … }` object literal behind `if (!org…)`)
173
+ // misses this entirely. The widening is deliberately NARROW: the initializer must be a
174
+ // CALL whose name marks the no-org/empty case (never any `return <identifier>`), AND the
175
+ // guard must be on THAT bound value returning THAT value — the guard is bound to its
176
+ // control, so an unrelated `const x = f(); if (x) return x;` cannot dominate.
177
+ const NO_ORG_EMPTY_HELPER = /(?:typedEmpty|emptyIfNo|ifNoWorkspace|ifNoOrg|noWorkspace|noOrg|unauthorizedIf|whenNoOrg|whenNoWorkspace|emptyWhenNo)/i;
178
+ function namedValueDomination(before) {
179
+ // `const VAR = [await] [ns.]<helper-whose-name-marks-no-org>( …`
180
+ const decl = /const\s+([A-Za-z0-9_]+)\s*=\s*(?:await\s+)?(?:[A-Za-z0-9_]+\.)*([A-Za-z0-9_]+)\s*\(/g;
181
+ let m;
182
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop
183
+ while ((m = decl.exec(before)) !== null) {
184
+ const varName = m[1];
185
+ const helperName = m[2];
186
+ if (!NO_ORG_EMPTY_HELPER.test(helperName))
187
+ continue;
188
+ // guarded on THAT value, returning THAT value: `if ([!]VAR) [{] return VAR`
189
+ const guard = new RegExp(`if\\s*\\(\\s*!?\\s*${varName}\\s*\\)\\s*(?:\\{\\s*)?return\\s+${varName}\\b`);
190
+ if (guard.test(before))
191
+ return true;
192
+ }
193
+ return false;
194
+ }
195
+ function throwDomination(blockText) {
196
+ const t1 = ORG_THROW.exec(blockText);
197
+ const t2 = ORG_THROW_HELPER.exec(blockText);
198
+ const throwIdx = Math.min(t1 ? t1.index : Number.POSITIVE_INFINITY, t2 ? t2.index : Number.POSITIVE_INFINITY);
199
+ if (!Number.isFinite(throwIdx))
200
+ return "none";
201
+ const before = blockText.slice(0, throwIdx);
202
+ // Form B (CRM helper-returned value, guard bound to the value) — decisive on its own.
203
+ if (namedValueDomination(before))
204
+ return "dominated";
205
+ // Form A (inline typed-empty object literal + a no-org guard).
206
+ if (!TYPED_EMPTY_RETURN.test(before))
207
+ return "none";
208
+ return NO_ORG_GUARD.test(before) ? "dominated" : "ambiguous";
209
+ }
142
210
  /** delimit a function body from its opener at index i by matching braces. */
143
211
  function queryBlock(lines, i) {
144
212
  let depth = 0;
@@ -280,11 +348,36 @@ export function scanR50(files) {
280
348
  });
281
349
  }
282
350
  else if (throwsOnNoOrg) {
283
- scan.violate.push({
284
- rel: f.rel,
285
- n: block.lines[0].n,
286
- note: `${fn}: public query throws on the missing-org path (org-keyed ConvexError or a throw-on-no-org helper) with no documented contract${hasResolver ? " — a `withOrgScope` resolver is ALSO present, but a fail-closed resolver returning a typed empty scope does NOT rescue an explicit throw that still fires into the subscription (Eta #13 seam)" : ""}. A subscribing client receives an uncaught Server Error. Return a typed empty/unauthorized shape (do not throw), or document that the layout must not subscribe`,
287
- });
351
+ const dom = throwDomination(blockText);
352
+ if (dom === "dominated") {
353
+ // The no-org branch RETURNS a typed empty/unauthorized shape before
354
+ // the throw is reached, and a no-org guard owns that branch — the
355
+ // throw guards an org-PRESENT path and never fires on the no-org path
356
+ // a subscribing client can hit. Conform (dominated throw).
357
+ scan.conform.push({
358
+ rel: f.rel,
359
+ n: block.lines[0].n,
360
+ note: `${fn}: public query returns a typed empty/unauthorized shape on the no-org branch (an early no-org guard) BEFORE the org-throw is reachable — the throw guards an org-present path and never fires into the subscription on the no-org path. The subscribing client receives a shape, not an uncaught Server Error`,
361
+ });
362
+ }
363
+ else if (dom === "ambiguous") {
364
+ // A typed-empty return precedes the throw but no no-org guard is visible
365
+ // before it — whether the return dominates the no-org path (vs. sitting
366
+ // behind an unrelated condition while the throw still fires on no-org)
367
+ // is not statically decidable. Could-not-judge, never a silent green.
368
+ scan.cnj.push({
369
+ rel: f.rel,
370
+ n: block.lines[0].n,
371
+ note: `${fn}: a typed empty/unauthorized return precedes the org-throw but no no-org guard is visible ahead of the throw — whether the return dominates the no-org path or the throw still fires on it is not statically decidable; a human must judge (never a silent green)`,
372
+ });
373
+ }
374
+ else {
375
+ scan.violate.push({
376
+ rel: f.rel,
377
+ n: block.lines[0].n,
378
+ note: `${fn}: public query throws on the missing-org path (org-keyed ConvexError or a throw-on-no-org helper) with no documented contract${hasResolver ? " — a `withOrgScope` resolver is ALSO present, but a fail-closed resolver returning a typed empty scope does NOT rescue an explicit throw that still fires into the subscription (Eta #13 seam)" : ""}. A subscribing client receives an uncaught Server Error. Return a typed empty/unauthorized shape (do not throw), or document that the layout must not subscribe`,
379
+ });
380
+ }
288
381
  }
289
382
  else {
290
383
  // hasResolver, no throw, no marker — the fail-closed resolver is the
@@ -142,57 +142,128 @@ export function scanR20(files) {
142
142
  //
143
143
  // SOURCE READ: convex/credentials.ts issues a Bearer under a rate limit of
144
144
  // `maxPerWindow: 5` over `windowMs: 60_000` (lines 513-514), rejecting with a
145
- // `429` (line 523) — numeric rate + numeric window. PATTERN: a file that
146
- // references a rate-limit control (`rateLimit` / `maxPerWindow` / `windowMs`) is
147
- // conform iff BOTH a numeric window and a numeric max are present; a rate-limit
148
- // control whose bounds are only non-numeric placeholders (no numeric literal) is
149
- // the violation (unless a written exception is on file).
145
+ // `429` (line 523); mcp-server/server-http.ts declares the DCR `/register`
146
+ // limiter as the `REGISTER_RATE_LIMIT = 5` / `REGISTER_RATE_WINDOW_MS = 60_000`
147
+ // const pair (lines 297-298) — numeric rate + numeric window at ONE site.
148
+ //
149
+ // SITE-scoped, not file-scoped. Two defects the old file-wide scan carried:
150
+ // (1) FALSE POSITIVE — `referencesRate` was true for ANY file containing the
151
+ // substring `rateLimit`, so the schema TABLE NAME `credentialsRateLimits`
152
+ // (convex/schema.ts) dragged schema.ts into the scan, and the file-wide
153
+ // window pattern then matched the unrelated `2000` in
154
+ // `body: v.string(), // truncated to 2000 chars` (schema.ts:540) — a
155
+ // phantom R-23 violation on a v.string() field. A schema table declaration
156
+ // is NOT a rate-limit control, and a numeric elsewhere in the file is not
157
+ // "this control's window".
158
+ // (2) FILE→SITE leak — because window/max/exception were found file-wide, ONE
159
+ // `rate-limit exception:` marker anywhere conformed EVERY control in the
160
+ // file. The window was never bound to its own max/control.
161
+ //
162
+ // PATTERN: locate each CONTROL SITE (a local region declaring/enforcing a
163
+ // limiter — a `maxPerWindow:` key, a `*_RATE_LIMIT =` numeric const, a
164
+ // `per minute/second/hour` check, or a written-exception marker). A schema
165
+ // `defineTable(...)` block or a validator field (`v.number()`) is NEVER a
166
+ // control site. Each site is judged WITHIN its own local line span: a numeric
167
+ // max AND a numeric window there -> conform; else an exception marker bound to
168
+ // THAT site -> conform; else -> violate. A numeric/exception on a DIFFERENT
169
+ // site does not carry.
150
170
  // ---------------------------------------------------------------------------
151
- const RATE_REF = /ratelimit|maxperwindow|windowms|per\s+(minute|second|hour)/i;
152
- const NUMERIC_WINDOW = /windowms\s*:\s*\d|(\d[\d_]*)\s*(ms|_?000)\b|per\s+(minute|second|hour)/i;
153
- const NUMERIC_MAX = /maxperwindow\s*:\s*\d|\b\d+\s*(req|requests|per)\b/i;
171
+ const NUMERIC_WINDOW = /windowms\s*:\s*\d|\brate_?window\w*\s*=\s*\d|(\d[\d_]*)\s*(ms|_?000)\b|per\s+(minute|second|hour)/i;
172
+ const NUMERIC_MAX = /maxperwindow\s*:\s*\d|\brate_?limit\w*\s*=\s*\d|\b\d+\s*(req|requests|per)\b/i;
154
173
  const WRITTEN_EXCEPTION = /allow-unbounded|written exception|rate-limit exception/i;
155
- function referencesRate(f) {
156
- return f.lines.some((l) => code(l) && RATE_REF.test(l.raw));
174
+ // A limiter DECLARATION/enforcement, not a mere keyword mention:
175
+ // - `maxPerWindow:` — the per-window MAX key (the definitive throttle signal;
176
+ // a lone `windowMs` is only a duration and appears everywhere, so it is
177
+ // never an anchor by itself — only a window CONFIRMATION inside a site).
178
+ // - `*_RATE_LIMIT = <numeric>` — the const-pair form (server-http.ts).
179
+ // A `per minute/second/hour` phrase is NOT an anchor: it appears in
180
+ // human-readable rejection strings (`"...5 registrations per minute per IP."`)
181
+ // that are not control declarations. It survives only as a WINDOW confirmation
182
+ // inside a site already anchored by a real limiter declaration.
183
+ const RATE_CONTROL_DECL = /maxperwindow\s*:|\brate_?limit\w*\s*=\s*\d/i;
184
+ // A schema table declaration or a validator field is never a control SITE —
185
+ // this is what excludes `credentialsRateLimits: defineTable(...)` and
186
+ // `maxPerWindow: v.number()` from anchoring a site.
187
+ const SCHEMA_DECL = /definetable\s*\(|v\.\w+\s*\(/i;
188
+ // Local span geometry: consecutive anchors within MERGE_GAP lines collapse into
189
+ // ONE site (a const group / a single call's arg object); the site's judgement
190
+ // window is that anchor run padded by CONTEXT lines each side — never the file.
191
+ const CONTEXT = 3;
192
+ const MERGE_GAP = 3;
193
+ function isControlAnchor(raw) {
194
+ // An explicit written exception always marks a site, even on an odd line.
195
+ if (WRITTEN_EXCEPTION.test(raw))
196
+ return true;
197
+ // A schema table decl / validator field is never a control site.
198
+ if (SCHEMA_DECL.test(raw))
199
+ return false;
200
+ return RATE_CONTROL_DECL.test(raw);
157
201
  }
158
202
  export function scanR23(files) {
159
203
  const scan = emptyScan();
160
204
  for (const f of files) {
161
- if (!isJudgeable(f) || !referencesRate(f))
205
+ if (!isJudgeable(f))
162
206
  continue;
163
- let windowLine = 0;
164
- let maxLine = 0;
165
- let exception = 0;
166
- for (const l of f.lines) {
167
- if (!code(l))
168
- continue;
169
- if (!windowLine && NUMERIC_WINDOW.test(l.raw))
170
- windowLine = l.n;
171
- if (!maxLine && NUMERIC_MAX.test(l.raw))
172
- maxLine = l.n;
173
- if (!exception && WRITTEN_EXCEPTION.test(l.raw))
174
- exception = l.n;
175
- }
176
- if (windowLine && maxLine) {
177
- scan.conform.push({
178
- rel: f.rel,
179
- n: maxLine,
180
- note: `rate control declares a numeric max (line ${maxLine}) and window (line ${windowLine})`,
181
- });
207
+ // 1. Collect the code-line indices that anchor a control site.
208
+ const anchors = [];
209
+ for (let i = 0; i < f.lines.length; i++) {
210
+ const l = f.lines[i];
211
+ if (code(l) && isControlAnchor(l.raw))
212
+ anchors.push(i);
182
213
  }
183
- else if (exception) {
184
- scan.conform.push({
185
- rel: f.rel,
186
- n: exception,
187
- note: `rate bound absent but a written exception is on file (line ${exception})`,
188
- });
214
+ if (anchors.length === 0)
215
+ continue;
216
+ // 2. Merge adjacent anchors (gap <= MERGE_GAP) into one site.
217
+ const sites = [];
218
+ for (const idx of anchors) {
219
+ const open = sites[sites.length - 1];
220
+ if (open && idx - open.last <= MERGE_GAP) {
221
+ open.last = idx;
222
+ }
223
+ else {
224
+ sites.push({ first: idx, last: idx });
225
+ }
189
226
  }
190
- else {
191
- scan.violate.push({
192
- rel: f.rel,
193
- n: windowLine || maxLine || 1,
194
- note: "rate-limit control present but its bound is not numeric (placeholder) and no written exception on file",
195
- });
227
+ // 3. Judge each site WITHIN its own local span — never file-wide.
228
+ for (const site of sites) {
229
+ const from = Math.max(0, site.first - CONTEXT);
230
+ const to = Math.min(f.lines.length - 1, site.last + CONTEXT);
231
+ let windowLine = 0;
232
+ let maxLine = 0;
233
+ let exception = 0;
234
+ for (let j = from; j <= to; j++) {
235
+ const l = f.lines[j];
236
+ if (!code(l))
237
+ continue;
238
+ if (!windowLine && NUMERIC_WINDOW.test(l.raw))
239
+ windowLine = l.n;
240
+ if (!maxLine && NUMERIC_MAX.test(l.raw))
241
+ maxLine = l.n;
242
+ if (!exception && WRITTEN_EXCEPTION.test(l.raw))
243
+ exception = l.n;
244
+ }
245
+ const repLine = f.lines[site.first].n;
246
+ if (windowLine && maxLine) {
247
+ scan.conform.push({
248
+ rel: f.rel,
249
+ n: maxLine,
250
+ note: `rate control site declares a numeric max (line ${maxLine}) and window (line ${windowLine}) bound to the same site`,
251
+ });
252
+ }
253
+ else if (exception) {
254
+ scan.conform.push({
255
+ rel: f.rel,
256
+ n: exception,
257
+ note: `rate bound absent but a written exception is bound to THIS control site (line ${exception})`,
258
+ });
259
+ }
260
+ else {
261
+ scan.violate.push({
262
+ rel: f.rel,
263
+ n: windowLine || maxLine || repLine,
264
+ note: "rate-limit control site present but its bound is not numeric (placeholder) and no written exception is bound to this site",
265
+ });
266
+ }
196
267
  }
197
268
  }
198
269
  return scan;
@@ -13,6 +13,62 @@ const GATE_HELPER = /\b(?:require|ensure|assert)[A-Z][A-Za-z0-9_]*\s*\(/;
13
13
  // a TYPED unauthorized RESULT — the write returns a shape instead of throwing.
14
14
  const TYPED_UNAUTH_RETURN = /return\s+\{[^}]*\b(?:unauthorized|forbidden|noOrg|no_org|denied|ok\s*:\s*false)\b/i;
15
15
  const CONTRACT_MARKER = /write-contract:|client-must-not-write-before-org|isolation-contract:/i;
16
+ // A TYPED EMPTY / UNAUTHORIZED RETURN broad enough to catch the CRM no-org branch
17
+ // shape `return { items: [], unauthorized: true }` / `{ orgId: null }` — mirrors
18
+ // R-50's TYPED_EMPTY_RETURN. TYPED_UNAUTH_RETURN above matches the explicit
19
+ // unauthorized/denied vocabulary anywhere in the block (the write-returns-a-shape
20
+ // green); this broader form is used ONLY by the domination heuristic, to recognise a
21
+ // typed-empty return that lexically DOMINATES a later org-throw on the no-org branch.
22
+ const TYPED_EMPTY_RETURN = /return\s+\{[^}]*\b(?:unauthorized|forbidden|noOrg|no_org|denied|empty)\b|return\s+\{[^}]*\b(?:orgSlug|orgId)\s*:\s*null\b|return\s+\{\s*items\s*:\s*\[\s*\]|return\s+\{[^}]*\bok\s*:\s*false\b/i;
23
+ // A NO-ORG GUARD condition owning the no-org branch — used by the domination
24
+ // heuristic to establish that a typed-empty return preceding the org-throw plausibly
25
+ // owns the no-org path.
26
+ const NO_ORG_GUARD = /if\s*\(\s*!\s*(?:scope\.|ctx\.[A-Za-z0-9_.]*)?(?:org|orgId|orgSlug|workspaceId|workspace|identity)\b|if\s*\([^)]*\b(?:org|orgId|orgSlug|workspaceId)\s*(?:===|==)\s*null/i;
27
+ // DOMINATION heuristic (mirror of R-50 — a DOMINATED no-org throw). A bare public
28
+ // mutation whose handler RETURNS a typed empty/unauthorized shape on the no-org
29
+ // branch BEFORE it reaches the org-throw is NOT the two-absence violation: the throw
30
+ // guards an org-PRESENT path (the CRM shape `if (!org) return { items: [] }` at the
31
+ // top, then a later `requireWorkspaceId(...)` / `throw` reachable only once an org
32
+ // exists). Judged TEXTUALLY and CONSERVATIVELY — a real throw is never silently
33
+ // flipped to green:
34
+ // "dominated" a typed-empty return lexically precedes the first org-throw AND a
35
+ // no-org guard also precedes that throw → conform (dominated throw).
36
+ // "ambiguous" a typed-empty return precedes the throw but NO no-org guard is
37
+ // visible before it → could-not-judge (domination not established),
38
+ // never a silent green and never a false red.
39
+ // "none" no typed-empty return precedes the throw → unchanged (the existing
40
+ // two-absence violate / marker CNJ chain stands).
41
+ // Form B (Eta #20 REVISE — a no-org VALUE returned from a helper CALL whose NAME marks
42
+ // the no-org case, guarded on that same value: `const empty = typedEmptyIfNoWorkspace(…);
43
+ // if (empty) return empty;`). NARROW: the initializer is a call whose name marks no-org
44
+ // (never any `return <identifier>`), and the guard is bound to THAT value returning it.
45
+ const NO_ORG_EMPTY_HELPER = /(?:typedEmpty|emptyIfNo|ifNoWorkspace|ifNoOrg|noWorkspace|noOrg|unauthorizedIf|whenNoOrg|whenNoWorkspace|emptyWhenNo)/i;
46
+ function namedValueDomination(before) {
47
+ const decl = /const\s+([A-Za-z0-9_]+)\s*=\s*(?:await\s+)?(?:[A-Za-z0-9_]+\.)*([A-Za-z0-9_]+)\s*\(/g;
48
+ let m;
49
+ // biome-ignore lint/suspicious/noAssignInExpressions: standard regex-exec loop
50
+ while ((m = decl.exec(before)) !== null) {
51
+ if (!NO_ORG_EMPTY_HELPER.test(m[2]))
52
+ continue;
53
+ const guard = new RegExp(`if\\s*\\(\\s*!?\\s*${m[1]}\\s*\\)\\s*(?:\\{\\s*)?return\\s+${m[1]}\\b`);
54
+ if (guard.test(before))
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ function throwDomination(blockText) {
60
+ const t1 = ORG_THROW.exec(blockText);
61
+ const t2 = WRITE_GATE_HELPER.exec(blockText);
62
+ const throwIdx = Math.min(t1 ? t1.index : Number.POSITIVE_INFINITY, t2 ? t2.index : Number.POSITIVE_INFINITY);
63
+ if (!Number.isFinite(throwIdx))
64
+ return "none";
65
+ const before = blockText.slice(0, throwIdx);
66
+ if (namedValueDomination(before))
67
+ return "dominated";
68
+ if (!TYPED_EMPTY_RETURN.test(before))
69
+ return "none";
70
+ return NO_ORG_GUARD.test(before) ? "dominated" : "ambiguous";
71
+ }
16
72
  function discoverWriteBuilders(files) {
17
73
  const b = {
18
74
  publicMutation: new Set([BASE_PUBLIC_MUTATION]),
@@ -191,6 +247,27 @@ export function scanR51(files) {
191
247
  note: `${fn}: public mutation returns a typed unauthorized result on the no-org path — a pre-org client receives a shape it can render, not an uncaught Server Error`,
192
248
  });
193
249
  }
250
+ else if (throwsOnNoOrg && throwDomination(blockText) === "dominated") {
251
+ // The no-org branch RETURNS a typed empty/unauthorized shape (behind an
252
+ // early no-org guard) BEFORE the org-throw is reachable — the throw guards
253
+ // an org-PRESENT path and never fires on the no-org render path. Not the
254
+ // two-absence violation. Conform (dominated throw).
255
+ scan.conform.push({
256
+ rel: f.rel,
257
+ n: block.lines[0].n,
258
+ note: `${fn}: public mutation returns a typed empty/unauthorized shape on the no-org branch (an early no-org guard) BEFORE the org-throw is reachable — the throw guards an org-present path and never fires on the no-org render path. A pre-org client receives a shape, not an uncaught Server Error`,
259
+ });
260
+ }
261
+ else if (throwsOnNoOrg && throwDomination(blockText) === "ambiguous") {
262
+ // A typed-empty return precedes the throw but no no-org guard is visible
263
+ // ahead of it — whether the return dominates the no-org path is not
264
+ // statically decidable. Could-not-judge, never a silent green.
265
+ scan.cnj.push({
266
+ rel: f.rel,
267
+ n: block.lines[0].n,
268
+ note: `${fn}: a typed empty/unauthorized return precedes the org-throw but no no-org guard is visible ahead of the throw — whether the return dominates the no-org path or the throw still fires on it is not statically decidable; a human must judge (never a silent green)`,
269
+ });
270
+ }
194
271
  else if (hasMarker) {
195
272
  // A throwing write that CARRIES a write-contract marker: the marker is the
196
273
  // author's recorded claim that a client cannot reach this write pre-org —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/backend-doctor",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A conformance instrument that measures the VantagePeers backend against the pinned backend standard. The THIRD doctor built ON the @vantageos/mcp-doctor engine — never a third engine. It consumes runDoctor/renderTable and adds only detectors.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",