@vantageos/backend-doctor 0.1.4 → 0.1.5

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
@@ -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.5",
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",