@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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
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",
|