@vantageos/backend-doctor 0.1.1 → 0.1.2

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.
@@ -1,10 +1,122 @@
1
1
  import { code, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, srcDetector, } from "./_scan.js";
2
- // A PUBLIC reactive read opener. `\bquery\(` does NOT match `internalQuery(`
3
- // (the 'query' there is preceded by a word char, so the \b fails) nor `mutation(`.
4
- const PUBLIC_QUERY_OPEN = /\bquery\s*\(/;
5
- // exclude the httpAction/action openers and internal variants explicitly for safety.
6
- const NON_QUERY_OPEN = /\b(?:internalQuery|internalMutation|mutation|action|httpAction)\s*\(/;
7
- const EXPORT_NAME = /export\s+const\s+([A-Za-z0-9_]+)\s*=/;
2
+ // A public reactive read is DEFINED, not read. R-50 governs a query DEFINITION —
3
+ // `export const NAME = <builder>({ ... })` — never a `ctx.db.query(...)` table READ
4
+ // inside a handler (the pre-fix opener `/\bquery\(/` matched `.query(` reads
5
+ // incidentally, naming a helper's internal read as the site while missing the real
6
+ // subscribed queries — Eta k170q03p). The builder is `query` OR a `customQuery`
7
+ // wrapper the tree declares (the standard Convex idiom: `export const authedQuery =
8
+ // customQuery(query, ...)`, then `export const summary = authedQuery({...})`). A
9
+ // rule about subscribed reads that matches only the bare `query` builder is blind on
10
+ // most real Convex codebases, so R-50 DISCOVERS the builders first, and REFUSES
11
+ // (could-not-judge naming the file) when a builder's base cannot be resolved rather
12
+ // than return a green obtained by not looking.
13
+ // `export const X = customQuery(` — the wrapper opener. The BASE identifier may sit
14
+ // on the SAME line or a FOLLOWING one (the idiom often breaks the call across lines:
15
+ // `customQuery(\n query,\n …)`), so the base is resolved by a short look-ahead, not
16
+ // captured here.
17
+ const CUSTOM_QUERY_OPEN = /(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*customQuery\s*\(/;
18
+ // the first identifier token — used to read the BASE after a `customQuery(` opener.
19
+ const FIRST_IDENT = /([A-Za-z_][A-Za-z0-9_]*)/;
20
+ // `export const X = customMutation(...)` — a write builder, never an R-50 subject.
21
+ const CUSTOM_MUTATION_DEF = /(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*customMutation\s*\(/;
22
+ // the base builders the platform ships. `query` is the only public read base.
23
+ const BASE_PUBLIC_QUERY = "query";
24
+ const BASE_INTERNAL_QUERY = "internalQuery";
25
+ // An auth/org GATE helper a customQuery customCtx runs before the handler — a
26
+ // require*/ensure*/assert* access check (requireReadAccess, requireWorkspaceId,
27
+ // ensureCurrentOrg, …). Its presence in the wrapper means the wrapper, not the
28
+ // handler, owns the no-org disposition of every query built from it.
29
+ const GATE_HELPER = /\b(?:require|ensure|assert)[A-Z][A-Za-z0-9_]*\s*\(/;
30
+ /** Discover the query/mutation builders a tree declares BEFORE classifying any
31
+ * definition — `query` is always a public read base; a `customQuery(query,…)`
32
+ * wrapper is public; a `customQuery(internalQuery,…)` wrapper is non-subject; any
33
+ * other base is ambiguous (refuse). */
34
+ function discoverBuilders(files) {
35
+ const b = {
36
+ publicQuery: new Set([BASE_PUBLIC_QUERY]),
37
+ nonSubject: new Set([
38
+ BASE_INTERNAL_QUERY,
39
+ "mutation",
40
+ "internalMutation",
41
+ "action",
42
+ "internalAction",
43
+ "httpAction",
44
+ ]),
45
+ gated: new Map(),
46
+ ambiguous: [],
47
+ };
48
+ for (const f of files) {
49
+ if (!isJudgeable(f))
50
+ continue;
51
+ const codeLines = f.lines.filter(code);
52
+ // full-block text (comments included) for the marker check on a builder region.
53
+ const allLines = f.lines;
54
+ for (let i = 0; i < codeLines.length; i++) {
55
+ const l = codeLines[i];
56
+ const cm = CUSTOM_MUTATION_DEF.exec(l.raw);
57
+ if (cm) {
58
+ b.nonSubject.add(cm[1]);
59
+ continue;
60
+ }
61
+ const cq = CUSTOM_QUERY_OPEN.exec(l.raw);
62
+ if (!cq)
63
+ continue;
64
+ const name = cq[1];
65
+ // the BASE is the first identifier AFTER `customQuery(` — on this line's
66
+ // remainder, else on the following code lines (the call is often wrapped).
67
+ const afterOpen = l.raw.slice((cq.index ?? 0) + cq[0].length);
68
+ let base = FIRST_IDENT.exec(afterOpen)?.[1];
69
+ for (let j = i + 1; !base && j < codeLines.length && j <= i + 4; j++) {
70
+ base = FIRST_IDENT.exec(codeLines[j].raw)?.[1];
71
+ }
72
+ if (base === BASE_PUBLIC_QUERY) {
73
+ b.publicQuery.add(name);
74
+ // Does the wrapper's customCtx run an auth/org gate that can throw, with
75
+ // no documented contract? The builder region is the opener line plus the
76
+ // following few CODE lines (the customCtx body is short). The marker is a
77
+ // comment, so check it against the raw region INCLUDING comments.
78
+ const codeRegion = codeLines
79
+ .slice(i, i + 10)
80
+ .map((x) => x.raw)
81
+ .join("\n");
82
+ const startAbs = allLines.indexOf(l);
83
+ // include up to 3 lines ABOVE the opener: a builder's isolation-contract
84
+ // marker is conventionally the comment immediately above its declaration.
85
+ const rawRegion = startAbs >= 0
86
+ ? allLines
87
+ .slice(Math.max(0, startAbs - 3), startAbs + 14)
88
+ .map((x) => x.raw)
89
+ .join("\n")
90
+ : codeRegion;
91
+ const gates = ORG_THROW.test(codeRegion) ||
92
+ ORG_THROW_HELPER.test(codeRegion) ||
93
+ GATE_HELPER.test(codeRegion);
94
+ if (gates && !CONTRACT_MARKER.test(rawRegion))
95
+ b.gated.set(name, { rel: f.rel, n: l.n });
96
+ }
97
+ else if (base === BASE_INTERNAL_QUERY)
98
+ b.nonSubject.add(name);
99
+ else
100
+ b.ambiguous.push({
101
+ rel: f.rel,
102
+ n: l.n,
103
+ name,
104
+ base: base ?? "<unresolved>",
105
+ });
106
+ }
107
+ }
108
+ return b;
109
+ }
110
+ /** `export const NAME = <builder>(` for a builder in `set`. A DEFINITION opener,
111
+ * never a `.query(` read (there is no `const NAME =` before a `ctx.db.query(`). */
112
+ function definitionOpener(raw, set) {
113
+ // longest builder names first so `authedQuery` wins over a `query` substring.
114
+ const names = [...set].sort((a, z) => z.length - a.length);
115
+ const alt = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
116
+ const re = new RegExp(`(?:export\\s+)?const\\s+([A-Za-z0-9_]+)\\s*=\\s*(${alt})\\s*\\(`);
117
+ const m = re.exec(raw);
118
+ return m ? { name: m[1], builder: m[2] } : null;
119
+ }
8
120
  // An ORG-ISOLATION THROW: a throw keyed on the caller having no org/identity, or a
9
121
  // helper that throws on the missing-org path. This is the behaviour a subscribed
10
122
  // client receives as an uncaught Server Error.
@@ -40,6 +152,44 @@ function queryBlock(lines, i) {
40
152
  }
41
153
  export function scanR50(files) {
42
154
  const scan = emptyScan();
155
+ // Pass 1: discover the tree's query/mutation builders so a customQuery wrapper
156
+ // (authedQuery, orgQuery, …) is recognised, not just the bare `query` base.
157
+ const builders = discoverBuilders(files);
158
+ // A customQuery wrapper whose base is neither query nor internalQuery leaves the
159
+ // public-query surface unknowable — REFUSE (could-not-judge), never a silent green.
160
+ for (const a of builders.ambiguous) {
161
+ scan.cnj.push({
162
+ rel: a.rel,
163
+ n: a.n,
164
+ note: `builder "${a.name}" = customQuery(${a.base}, …) — its base "${a.base}" resolves to neither \`query\` (public) nor \`internalQuery\`, so whether definitions using it are subscribed queries cannot be enumerated; R-50 refuses rather than certify a surface it cannot see (never a silent green)`,
165
+ });
166
+ }
167
+ // A GATED public customQuery builder: its customCtx runs an auth/org gate that can
168
+ // throw, so every subscribed query built from it inherits that throw in the shared
169
+ // wrapper — invisible to a per-handler scan. REFUSE per builder (could-not-judge),
170
+ // naming it and counting its dependent subscribed queries, so a customQuery
171
+ // codebase never reads "R-50 clean" by the opener simply not looking into the
172
+ // wrapper. (The remedy a human confirms: the wrapper returns a typed empty/unauth
173
+ // context on the no-org path, or documents an isolation-contract on the builder.)
174
+ for (const [name, at] of builders.gated) {
175
+ let deps = 0;
176
+ for (const f of files) {
177
+ if (!isJudgeable(f))
178
+ continue;
179
+ for (const l of f.lines) {
180
+ if (!code(l))
181
+ continue;
182
+ const o = definitionOpener(l.raw, new Set([name]));
183
+ if (o)
184
+ deps += 1;
185
+ }
186
+ }
187
+ scan.cnj.push({
188
+ rel: at.rel,
189
+ n: at.n,
190
+ note: `customQuery builder "${name}" gates every query built from it in its customCtx (an auth/org require*/throw), and ${deps} subscribed query(ies) route through it — whether that wrapper THROWS into a subscription or returns a typed empty/unauthorized context on the no-org path is not decidable from the query surface. R-50 refuses (a human confirms the wrapper's no-org shape, or the builder carries an isolation-contract marker) — never a silent green obtained by not looking into the wrapper`,
191
+ });
192
+ }
43
193
  for (const f of files) {
44
194
  if (!isJudgeable(f))
45
195
  continue;
@@ -47,15 +197,23 @@ export function scanR50(files) {
47
197
  for (let i = 0; i < lines.length; i++) {
48
198
  if (!code(lines[i]))
49
199
  continue;
50
- if (!PUBLIC_QUERY_OPEN.test(lines[i].raw))
200
+ // R-50's subject is a public-query DEFINITION: `const NAME = <publicBuilder>(`.
201
+ // This never matches a `ctx.db.query(...)` read (no `const NAME =` there) nor
202
+ // an internal/mutation/action definition (those builders are not in the set).
203
+ const opener = definitionOpener(lines[i].raw, builders.publicQuery);
204
+ if (!opener)
51
205
  continue;
52
- // a line whose only match is a non-query opener (mutation/action/internal*)
53
- // is not R-50's subject — skip it even if it also contains "query" prose.
54
- if (NON_QUERY_OPEN.test(lines[i].raw) && !/\bquery\s*\(/.test(lines[i].raw.replace(NON_QUERY_OPEN, "")))
206
+ // A query declared through a GATED customQuery builder inherits its no-org
207
+ // disposition from the shared customCtx wrapper (an auth/org gate that can
208
+ // throw), NOT from this handler — so a per-handler scan cannot see it. The
209
+ // builder-level refusal below covers these; skip the per-handler pass here.
210
+ if (builders.gated.has(opener.builder))
55
211
  continue;
56
- const fn = EXPORT_NAME.exec(lines[i].raw)?.[1] ??
57
- (i > 0 ? EXPORT_NAME.exec(lines[i - 1].raw)?.[1] : undefined) ??
58
- "<anonymous>";
212
+ // exclude the builder DEFINITION line itself (`const authedQuery = query(`?
213
+ // no — customQuery defs use `customQuery(`, filtered above; but guard a
214
+ // `const x = query(` that is a re-export, not a `query({config})`): require
215
+ // the block to open a config object, handled by queryBlock below).
216
+ const fn = opener.name;
59
217
  const block = queryBlock(lines, i);
60
218
  i += block.lines.length - 1;
61
219
  if (block.unterminated) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/backend-doctor",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",