@vantageos/backend-doctor 0.1.6 → 0.1.8

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.
@@ -120,7 +120,9 @@ function discoverBuilders(files) {
120
120
  function definitionOpener(raw, set) {
121
121
  // longest builder names first so `authedQuery` wins over a `query` substring.
122
122
  const names = [...set].sort((a, z) => z.length - a.length);
123
- const alt = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
123
+ const alt = names
124
+ .map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
125
+ .join("|");
124
126
  const re = new RegExp(`(?:export\\s+)?const\\s+([A-Za-z0-9_]+)\\s*=\\s*(${alt})\\s*\\(`);
125
127
  const m = re.exec(raw);
126
128
  return m ? { name: m[1], builder: m[2] } : null;
@@ -234,6 +236,12 @@ export function scanR50(files) {
234
236
  // Pass 1: discover the tree's query/mutation builders so a customQuery wrapper
235
237
  // (authedQuery, orgQuery, …) is recognised, not just the bare `query` base.
236
238
  const builders = discoverBuilders(files);
239
+ // Public queries that matched a definition opener but exhibit NO org-throw, NO
240
+ // fail-closed resolver, and NO isolation-contract marker. Not gated subjects, so
241
+ // historically SKIPPED — which, when EVERY public query is such a bare read,
242
+ // produced a false "no public query matched" abstain. Recorded for the
243
+ // otherwise-empty branch below.
244
+ const ungated = [];
237
245
  // A customQuery wrapper whose base is neither query nor internalQuery leaves the
238
246
  // public-query surface unknowable — REFUSE (could-not-judge), never a silent green.
239
247
  for (const a of builders.ambiguous) {
@@ -321,16 +329,21 @@ export function scanR50(files) {
321
329
  // so a commented-out throw does not count). The contract marker is, by
322
330
  // definition, a COMMENT — so it is read from the full block text INCLUDING
323
331
  // comment lines, not the code-only blockText.
324
- const blockTextWithComments = block.lines
325
- .map((l) => l.raw)
326
- .join("\n");
332
+ const blockTextWithComments = block.lines.map((l) => l.raw).join("\n");
327
333
  const throwsOnNoOrg = ORG_THROW.test(blockText) || ORG_THROW_HELPER.test(blockText);
328
334
  const hasResolver = SAFE_RESOLVER.test(blockText);
329
335
  const hasMarker = CONTRACT_MARKER.test(blockTextWithComments);
330
336
  if (!throwsOnNoOrg && !hasResolver && !hasMarker) {
331
337
  // query does neither org-isolation throw nor fail-closed resolve nor
332
338
  // declare a contract — not R-50's subject (too ambiguous to judge
333
- // mechanically; never a violation).
339
+ // mechanically; never a violation). Recorded so a tree whose ENTIRE
340
+ // public-read surface is bare is judged (CNJ-with-sites) rather than
341
+ // falsely abstained as "no public query matched".
342
+ ungated.push({
343
+ rel: f.rel,
344
+ n: block.lines[0].n,
345
+ note: `${fn}: public query with no org-throw, no fail-closed resolver, and no isolation-contract marker — no org-isolation handling is visible in convex/, and whether a subscribing client can reach it before an org exists is not statically decidable; a reviewer must confirm this read isolates by org (never a silent clean bill)`,
346
+ });
334
347
  continue;
335
348
  }
336
349
  // PRECEDENCE (Eta #13 REVISE seam): the THROW wins over a fail-closed
@@ -390,6 +403,18 @@ export function scanR50(files) {
390
403
  }
391
404
  }
392
405
  }
406
+ // A tree whose ENTIRE public-read surface is ungated (no throw / resolver / marker
407
+ // on ANY query) leaves the scan otherwise empty. The legacy abstain then claimed
408
+ // "no public query matched" — FALSE when bare public queries exist, and a silent
409
+ // pass over a fully-ungated backend. Surface those reads as could-not-judge sites
410
+ // so the verdict is CNJ-with-sites, not a misleading empty abstain. Gated on "scan
411
+ // otherwise empty" so a tree that already produced subjects (VP: 20 conform) is
412
+ // byte-for-byte unchanged.
413
+ if (scan.conform.length + scan.violate.length + scan.cnj.length === 0 &&
414
+ ungated.length > 0) {
415
+ for (const u of ungated)
416
+ scan.cnj.push(u);
417
+ }
393
418
  return scan;
394
419
  }
395
420
  export const R50 = srcDetector("R-50", "R-50 org isolation on a reactively-subscribed read returns a typed empty/unauthorized result, never an uncaught throw", "a public query that resolves org via a fail-closed non-throwing path or documents its client contract — it never throws on the missing-org path a subscribing client cannot catch", scanR50, "R-50: no public query matched in the scanned tree — no reactive read to certify. NOT a clean bill.");
@@ -168,8 +168,8 @@ export function scanR20(files) {
168
168
  // THAT site -> conform; else -> violate. A numeric/exception on a DIFFERENT
169
169
  // site does not carry.
170
170
  // ---------------------------------------------------------------------------
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;
171
+ const NUMERIC_WINDOW = /windowms\s*:\s*\d|(?:^|[^A-Za-z])rate_?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|(?:^|[^A-Za-z])rate_?limit\w*\s*=\s*\d|\b\d+\s*(req|requests|per)\b/i;
173
173
  const WRITTEN_EXCEPTION = /allow-unbounded|written exception|rate-limit exception/i;
174
174
  // A limiter DECLARATION/enforcement, not a mere keyword mention:
175
175
  // - `maxPerWindow:` — the per-window MAX key (the definitive throttle signal;
@@ -180,7 +180,7 @@ const WRITTEN_EXCEPTION = /allow-unbounded|written exception|rate-limit exceptio
180
180
  // human-readable rejection strings (`"...5 registrations per minute per IP."`)
181
181
  // that are not control declarations. It survives only as a WINDOW confirmation
182
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;
183
+ const RATE_CONTROL_DECL = /maxperwindow\s*:|(?:^|[^A-Za-z])rate_?limit\w*\s*=\s*\d/i;
184
184
  // A schema table declaration or a validator field is never a control SITE —
185
185
  // this is what excludes `credentialsRateLimits: defineTable(...)` and
186
186
  // `maxPerWindow: v.number()` from anchoring a site.
@@ -141,7 +141,9 @@ function discoverWriteBuilders(files) {
141
141
  }
142
142
  function definitionOpener(raw, set) {
143
143
  const names = [...set].sort((a, z) => z.length - a.length);
144
- const alt = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
144
+ const alt = names
145
+ .map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
146
+ .join("|");
145
147
  const re = new RegExp(`(?:export\\s+)?const\\s+([A-Za-z0-9_]+)\\s*=\\s*(${alt})\\s*\\(`);
146
148
  const m = re.exec(raw);
147
149
  return m ? { name: m[1], builder: m[2] } : null;
@@ -169,6 +171,13 @@ function mutationBlock(lines, i) {
169
171
  export function scanR51(files) {
170
172
  const scan = emptyScan();
171
173
  const builders = discoverWriteBuilders(files);
174
+ // Public writes that matched a definition opener but exhibit NO org-throw, NO
175
+ // typed-unauthorized return, and NO write-contract marker. They are not gated
176
+ // subjects, so they were historically SKIPPED — which, when EVERY public write
177
+ // in the tree is such a bare write, produced a false "no public mutation
178
+ // matched" abstain (a silent pass over a fully-ungated public-write surface).
179
+ // Recorded here so the otherwise-empty branch below can surface them.
180
+ const ungated = [];
172
181
  for (const a of builders.ambiguous) {
173
182
  scan.cnj.push({
174
183
  rel: a.rel,
@@ -236,8 +245,17 @@ export function scanR51(files) {
236
245
  const throwsOnNoOrg = ORG_THROW.test(blockText) || WRITE_GATE_HELPER.test(blockText);
237
246
  const typedUnauth = TYPED_UNAUTH_RETURN.test(blockText);
238
247
  const hasMarker = CONTRACT_MARKER.test(blockAll);
239
- if (!throwsOnNoOrg && !typedUnauth && !hasMarker)
240
- continue; // not a subject
248
+ if (!throwsOnNoOrg && !typedUnauth && !hasMarker) {
249
+ // A bare public write with no visible org isolation. Not a gated
250
+ // subject — but recorded so a tree whose ENTIRE public-write surface
251
+ // is bare is judged (CNJ-with-sites) rather than falsely abstained.
252
+ ungated.push({
253
+ rel: f.rel,
254
+ n: block.lines[0].n,
255
+ note: `${fn}: public mutation with no org-throw, no typed-unauthorized return, and no write-contract marker — no org-isolation handling is visible in convex/, and whether a client can reach it before an org exists is not statically decidable; a reviewer must confirm this write isolates by org (never a silent clean bill)`,
256
+ });
257
+ continue; // not a gated subject
258
+ }
241
259
  if (typedUnauth) {
242
260
  // The write returns a shape on the no-org path — a pre-org client can render
243
261
  // it, not an uncaught Server Error. Green.
@@ -295,6 +313,19 @@ export function scanR51(files) {
295
313
  }
296
314
  }
297
315
  }
316
+ // A tree whose ENTIRE public-write surface is ungated (no throw / typed-unauth /
317
+ // marker on ANY write) leaves the scan otherwise empty. The legacy abstain then
318
+ // claimed "no public mutation matched" — FALSE when bare public writes exist, and
319
+ // a silent pass over a fully-ungated backend. Surface those writes as
320
+ // could-not-judge sites so the verdict is CNJ-with-sites, not a misleading empty
321
+ // abstain. Gated on "scan otherwise empty" so a tree that already produced
322
+ // subjects (VP: 7 marker CNJ) is byte-for-byte unchanged — the bare-write
323
+ // disposition in a mixed tree is out of this fix's scope.
324
+ if (scan.conform.length + scan.violate.length + scan.cnj.length === 0 &&
325
+ ungated.length > 0) {
326
+ for (const u of ungated)
327
+ scan.cnj.push(u);
328
+ }
298
329
  return scan;
299
330
  }
300
331
  export const R51 = srcDetector("R-51", "R-51 a public mutation returns a typed unauthorized result on the no-org path, never an uncaught throw a pre-org client shell cannot render", "a public write either returns a typed unauthorized result / documents its client contract, or a human confirms a pre-org client cannot reach it — it never throws NO_ORG on a path an ordinary render can hit", scanR51, "R-51: no public mutation matched in the scanned tree — no write to certify. NOT a clean bill.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/backend-doctor",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",