@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.
- package/dist/detectors/source/isolation.js +171 -13
- package/package.json +1 -1
|
@@ -1,10 +1,122 @@
|
|
|
1
1
|
import { code, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, srcDetector, } from "./_scan.js";
|
|
2
|
-
// A
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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",
|