@vantageos/backend-doctor 0.1.0 → 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/README.md +1 -1
- package/dist/cli.js +21 -5
- package/dist/coverage.d.ts +4 -3
- package/dist/coverage.js +5 -4
- package/dist/detectors/oracle-axis1.js +19 -21
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +63 -18
- package/dist/detectors/source/isolation.d.ts +6 -0
- package/dist/detectors/source/isolation.js +285 -0
- package/dist/detectors/standard/_doc.d.ts +16 -1
- package/dist/detectors/standard/_doc.js +29 -2
- package/dist/not-checkable.js +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +11 -1
- package/fixtures/backend-standard-4265226e.md +680 -0
- package/package.json +2 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { code, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, srcDetector, } from "./_scan.js";
|
|
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
|
+
}
|
|
120
|
+
// An ORG-ISOLATION THROW: a throw keyed on the caller having no org/identity, or a
|
|
121
|
+
// helper that throws on the missing-org path. This is the behaviour a subscribed
|
|
122
|
+
// client receives as an uncaught Server Error.
|
|
123
|
+
const ORG_THROW = /throw\b[^;]*\b(?:NO_ORG|ORG_REQUIRED|RBAC_DENIED|no org|missing org|organization is required|not in (?:an )?org)/i;
|
|
124
|
+
const ORG_THROW_HELPER = /\b(?:requireWorkspaceId|requireOrgId|requireOrgSlug|getOrgIdOrThrow|ensureCurrentOrg|ensureCurrent)\s*\(/;
|
|
125
|
+
// The NON-throwing, fail-closed org resolver (VP's `withOrgScope` returns a typed
|
|
126
|
+
// empty scope on no identity — never throws) OR a documented isolation contract
|
|
127
|
+
// OR an explicit typed-empty return on the no-org branch. Any of these means the
|
|
128
|
+
// subscribing client always receives a shape it can render.
|
|
129
|
+
const SAFE_RESOLVER = /\bwithOrgScope\s*\(/;
|
|
130
|
+
const CONTRACT_MARKER = /isolation-contract:|client-safe-empty|@client-must-not-subscribe/i;
|
|
131
|
+
/** delimit a function body from its opener at index i by matching braces. */
|
|
132
|
+
function queryBlock(lines, i) {
|
|
133
|
+
let depth = 0;
|
|
134
|
+
let seenOpen = false;
|
|
135
|
+
const body = [];
|
|
136
|
+
for (let j = i; j < lines.length; j++) {
|
|
137
|
+
const l = lines[j];
|
|
138
|
+
body.push(l);
|
|
139
|
+
for (const ch of l.raw) {
|
|
140
|
+
if (ch === "{") {
|
|
141
|
+
depth += 1;
|
|
142
|
+
seenOpen = true;
|
|
143
|
+
}
|
|
144
|
+
else if (ch === "}") {
|
|
145
|
+
depth -= 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (seenOpen && depth <= 0)
|
|
149
|
+
return { lines: body, unterminated: false };
|
|
150
|
+
}
|
|
151
|
+
return { lines: body, unterminated: seenOpen };
|
|
152
|
+
}
|
|
153
|
+
export function scanR50(files) {
|
|
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
|
+
}
|
|
193
|
+
for (const f of files) {
|
|
194
|
+
if (!isJudgeable(f))
|
|
195
|
+
continue;
|
|
196
|
+
const lines = f.lines;
|
|
197
|
+
for (let i = 0; i < lines.length; i++) {
|
|
198
|
+
if (!code(lines[i]))
|
|
199
|
+
continue;
|
|
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)
|
|
205
|
+
continue;
|
|
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))
|
|
211
|
+
continue;
|
|
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;
|
|
217
|
+
const block = queryBlock(lines, i);
|
|
218
|
+
i += block.lines.length - 1;
|
|
219
|
+
if (block.unterminated) {
|
|
220
|
+
scan.cnj.push({
|
|
221
|
+
rel: f.rel,
|
|
222
|
+
n: lines[Math.min(i, lines.length - 1)].n,
|
|
223
|
+
note: `${fn}: a public query block could not be delimited (unbalanced braces) — whether its no-org path throws or returns a typed empty is not statically decidable; a human must judge (never a silent green)`,
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const blockText = block.lines
|
|
228
|
+
.filter(code)
|
|
229
|
+
.map((l) => l.raw)
|
|
230
|
+
.join("\n");
|
|
231
|
+
// The org-throw and the resolver are CODE (read from executing lines only,
|
|
232
|
+
// so a commented-out throw does not count). The contract marker is, by
|
|
233
|
+
// definition, a COMMENT — so it is read from the full block text INCLUDING
|
|
234
|
+
// comment lines, not the code-only blockText.
|
|
235
|
+
const blockTextWithComments = block.lines
|
|
236
|
+
.map((l) => l.raw)
|
|
237
|
+
.join("\n");
|
|
238
|
+
const throwsOnNoOrg = ORG_THROW.test(blockText) || ORG_THROW_HELPER.test(blockText);
|
|
239
|
+
const hasResolver = SAFE_RESOLVER.test(blockText);
|
|
240
|
+
const hasMarker = CONTRACT_MARKER.test(blockTextWithComments);
|
|
241
|
+
if (!throwsOnNoOrg && !hasResolver && !hasMarker) {
|
|
242
|
+
// query does neither org-isolation throw nor fail-closed resolve nor
|
|
243
|
+
// declare a contract — not R-50's subject (too ambiguous to judge
|
|
244
|
+
// mechanically; never a violation).
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// PRECEDENCE (Eta #13 REVISE seam): the THROW wins over a fail-closed
|
|
248
|
+
// resolver. `withOrgScope` returning a typed empty scope is a property of
|
|
249
|
+
// the RESOLVER, not of the handler — a handler that calls withOrgScope AND
|
|
250
|
+
// then `throw`s on the no-org path still fires that throw into the
|
|
251
|
+
// subscription, so the resolver's presence must NOT clear it. Only a
|
|
252
|
+
// documented CONTRACT_MARKER — a CLAIM the reviewer checks, never a proof —
|
|
253
|
+
// clears a throwing handler (a declared "the layout must not subscribe").
|
|
254
|
+
if (hasMarker) {
|
|
255
|
+
scan.conform.push({
|
|
256
|
+
rel: f.rel,
|
|
257
|
+
n: block.lines[0].n,
|
|
258
|
+
note: `${fn}: public query carries a documented isolation-contract marker (a declared divergence — a CLAIM the reviewer verifies, not a proof of behaviour) — accepted, the layout is asserted not to subscribe on the no-org path`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else if (throwsOnNoOrg) {
|
|
262
|
+
scan.violate.push({
|
|
263
|
+
rel: f.rel,
|
|
264
|
+
n: block.lines[0].n,
|
|
265
|
+
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`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// hasResolver, no throw, no marker — the fail-closed resolver is the
|
|
270
|
+
// whole no-org handling; the subscribing client receives a typed shape.
|
|
271
|
+
scan.conform.push({
|
|
272
|
+
rel: f.rel,
|
|
273
|
+
n: block.lines[0].n,
|
|
274
|
+
note: `${fn}: public query resolves org via the fail-closed non-throwing resolver (\`withOrgScope\`) and does NOT throw on the no-org path — the subscribing client receives a typed shape, not an uncaught throw`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return scan;
|
|
280
|
+
}
|
|
281
|
+
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.");
|
|
282
|
+
/** In the form the orchestrator spreads into REGISTRY. */
|
|
283
|
+
export const SOURCE_ISOLATION_DETECTORS = [
|
|
284
|
+
{ detector: R50, ruleId: "R-50", standardPin: SOURCE_STANDARD_PIN },
|
|
285
|
+
];
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { Detector, DetectorResult } from "../../engine-types.js";
|
|
2
2
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
3
|
-
export declare const STANDARD_FILE = "backend-standard-
|
|
3
|
+
export declare const STANDARD_FILE = "backend-standard-4265226e.md";
|
|
4
4
|
export declare const VENDORED_STANDARD_PATH: string;
|
|
5
5
|
/** The standard all four are pinned against (short hash a9d7877b). */
|
|
6
6
|
export declare const STANDARD_DOC_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) \u00A714/\u00A711/\u00A710/\u00A73";
|
|
7
7
|
/** Read the vendored standard's raw text. Injectable for tests. */
|
|
8
8
|
export declare function readStandard(path?: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The instrument's own readability check. A backend-doctor run reads its pinned
|
|
11
|
+
* standard fixture as material; if that fixture is ABSENT or UNREADABLE the tool
|
|
12
|
+
* has nothing to judge against, and letting `readFileSync` throw exits the CLI
|
|
13
|
+
* with code 1 — indistinguishable from "a violation was found" (Eta #12 REVISE).
|
|
14
|
+
* This returns a REFUSAL the CLI maps to exit 2 (could-not-judge) with a line
|
|
15
|
+
* NAMING what it could not read, mirroring the fleet leak_guard.py form. `ok`
|
|
16
|
+
* means the instrument is readable; a run may then legitimately pass or fail a pole.
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkStandardReadable(path?: string): {
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
refusal: string;
|
|
23
|
+
};
|
|
9
24
|
export interface DocLine {
|
|
10
25
|
/** 1-based line number in the vendored file. */
|
|
11
26
|
n: number;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// detectors that read THE STANDARD ITSELF as their material — R-42, R-15, R-32,
|
|
3
3
|
// R-39. Unlike the source-tree detectors (which read convex/ via `tree.files`)
|
|
4
4
|
// and the oracle detectors (which read the CSV), these read the VENDORED standard
|
|
5
|
-
// fixture `fixtures/backend-standard-
|
|
5
|
+
// fixture `fixtures/backend-standard-4265226e.md` — the material at the pin. The
|
|
6
6
|
// engine's contract types are DERIVED from engine-types.ts, never re-spelled.
|
|
7
7
|
//
|
|
8
8
|
// THE LAW (the one that caught R-5): every predicate is derived from the ACTUAL
|
|
@@ -13,7 +13,7 @@ import { readFileSync } from "node:fs";
|
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
16
|
-
export const STANDARD_FILE = "backend-standard-
|
|
16
|
+
export const STANDARD_FILE = "backend-standard-4265226e.md";
|
|
17
17
|
/** The vendored standard at the pin — DATA on the same footing as the oracle CSV.
|
|
18
18
|
* Resolved locally (not via coverage.ts) so this module carries no import cycle
|
|
19
19
|
* with registry.ts. Same file coverage.ts's VENDORED_STANDARD_PATH points at. */
|
|
@@ -25,6 +25,33 @@ export const STANDARD_DOC_PIN = "analysis/vantagepeers/backend-standard/backend-
|
|
|
25
25
|
export function readStandard(path = VENDORED_STANDARD_PATH) {
|
|
26
26
|
return readFileSync(path, "utf8");
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The instrument's own readability check. A backend-doctor run reads its pinned
|
|
30
|
+
* standard fixture as material; if that fixture is ABSENT or UNREADABLE the tool
|
|
31
|
+
* has nothing to judge against, and letting `readFileSync` throw exits the CLI
|
|
32
|
+
* with code 1 — indistinguishable from "a violation was found" (Eta #12 REVISE).
|
|
33
|
+
* This returns a REFUSAL the CLI maps to exit 2 (could-not-judge) with a line
|
|
34
|
+
* NAMING what it could not read, mirroring the fleet leak_guard.py form. `ok`
|
|
35
|
+
* means the instrument is readable; a run may then legitimately pass or fail a pole.
|
|
36
|
+
*/
|
|
37
|
+
export function checkStandardReadable(path = VENDORED_STANDARD_PATH) {
|
|
38
|
+
try {
|
|
39
|
+
const text = readFileSync(path, "utf8");
|
|
40
|
+
if (text.trim().length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
refusal: `REFUSING TO JUDGE: standard fixture ${STANDARD_FILE} is EMPTY — the instrument cannot read its own pin (${path})`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
refusal: `REFUSING TO JUDGE: standard fixture ${STANDARD_FILE} is ABSENT or UNREADABLE — the instrument cannot read its own pin (${path})`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
28
55
|
/** Split the standard text into 1-based numbered lines. */
|
|
29
56
|
export function toLines(text) {
|
|
30
57
|
return text.split("\n").map((raw, i) => ({ n: i + 1, raw }));
|
package/dist/not-checkable.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// gentler word — it was emptied.
|
|
17
17
|
import { STANDARD_FILE } from "./detectors/standard/_doc.js";
|
|
18
18
|
export const NOT_MECHANICALLY_CHECKABLE = {
|
|
19
|
-
"R-48": "R-48 authority-totality detector pending — task k17chpr4kzp399j3s7r7edpm4s8cscbz; text pinned at
|
|
19
|
+
"R-48": "R-48 authority-totality detector pending — task k17chpr4kzp399j3s7r7edpm4s8cscbz; text pinned at 4265226e so coverage cannot pretend 47 is complete. No mechanical control yet; R-48 is enumerated and visible, never silently dropped.",
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* One `could-not-judge` DetectorResult per NOT_MECHANICALLY_CHECKABLE rule, so
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector } from "./engine-types.js";
|
|
2
2
|
/** The standard both pins the whole wave against. */
|
|
3
|
-
export declare const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @
|
|
3
|
+
export declare const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4265226eb6698c7dfbae3ced77df564e877908ad (elpiarthera/ElPi-Corp)";
|
|
4
4
|
export interface RegistryEntry {
|
|
5
5
|
detector: Detector;
|
|
6
6
|
ruleId: string;
|
package/dist/registry.js
CHANGED
|
@@ -8,6 +8,7 @@ import { makeOracleDetector } from "./detectors/shared.js";
|
|
|
8
8
|
import { SOURCE_BACKFILL_DETECTORS } from "./detectors/source/backfill.js";
|
|
9
9
|
import { SOURCE_CONTRACT_DETECTORS } from "./detectors/source/contract.js";
|
|
10
10
|
import { IDENTITY_DETECTORS } from "./detectors/source/identity.js";
|
|
11
|
+
import { SOURCE_ISOLATION_DETECTORS } from "./detectors/source/isolation.js";
|
|
11
12
|
import { SOURCE_JOBS_DETECTORS } from "./detectors/source/jobs.js";
|
|
12
13
|
import { SOURCE_LIFECYCLE_DETECTORS } from "./detectors/source/lifecycle.js";
|
|
13
14
|
import { SOURCE_READBUDGET_DETECTORS } from "./detectors/source/readbudget.js";
|
|
@@ -19,7 +20,7 @@ import { R46_STANDARD_PIN, STANDARD_SOURCE_DETECTORS, } from "./detectors/standa
|
|
|
19
20
|
import { STANDARD_DOC_DETECTORS } from "./detectors/standard/standard-doc.js";
|
|
20
21
|
import { makeR14, makeR38 } from "./detectors/structural.js";
|
|
21
22
|
/** The standard both pins the whole wave against. */
|
|
22
|
-
export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @
|
|
23
|
+
export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4265226eb6698c7dfbae3ced77df564e877908ad (elpiarthera/ElPi-Corp)";
|
|
23
24
|
export const REGISTRY = [
|
|
24
25
|
...PER_ROW_PREDICATES.map((spec) => ({
|
|
25
26
|
detector: makeOracleDetector(spec),
|
|
@@ -98,6 +99,15 @@ export const REGISTRY = [
|
|
|
98
99
|
// site (the human decides refusal vs a tighter index vs a declared bound),
|
|
99
100
|
// present on a wide scan that DOES refuse (the tasks.list SCAN_CAP shape).
|
|
100
101
|
...SOURCE_REFUSAL_DETECTORS,
|
|
102
|
+
// R-50 — org isolation on a reactively-subscribed read must be a TYPED EMPTY /
|
|
103
|
+
// UNAUTHORIZED RESULT, never an uncaught throw. Reads the convex source: a
|
|
104
|
+
// public `query` (subscribed via useQuery) whose no-org path throws an
|
|
105
|
+
// org-keyed ConvexError / a throw-on-no-org helper, with no fail-closed
|
|
106
|
+
// resolver (`withOrgScope`) or documented contract, hands the subscribing
|
|
107
|
+
// client an uncaught Server Error (the 2026-08-30 vCRM-Preview incident:
|
|
108
|
+
// workspaces:ensureCurrent + prefs:sidebar). Extends the R-11 isolation
|
|
109
|
+
// family; a mutation refusing an unauthorized write (R-16) is out of scope.
|
|
110
|
+
...SOURCE_ISOLATION_DETECTORS,
|
|
101
111
|
...SOURCE_CONTRACT_DETECTORS,
|
|
102
112
|
...IDENTITY_DETECTORS,
|
|
103
113
|
// The four STANDARD-DOCUMENT detectors (R-42/R-15/R-32/R-39): each reads the
|