@vantageos/backend-doctor 0.1.3 → 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.
- package/README.md +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.js +1 -1
- package/dist/detectors/source/isolation.js +133 -19
- package/dist/detectors/source/write-isolation.js +122 -17
- package/dist/detectors/standard/_doc.d.ts +1 -1
- package/dist/detectors/standard/_doc.js +2 -2
- package/dist/not-checkable.js +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +1 -1
- package/fixtures/backend-standard-4bcd605.md +684 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ A `could-not-judge` rule is an **abstention** — a human verdict is owed, never
|
|
|
81
81
|
|
|
82
82
|
Every input is pinned and, where it is a vendored document, proven byte-identical to its upstream git blob (an un-hashable vendored doc is an authority granted to oneself).
|
|
83
83
|
|
|
84
|
-
**The single authority for which standard fixture is active is the loader constant** `STANDARD_FILE` in [`src/detectors/standard/_doc.ts`](./src/detectors/standard/_doc.ts) — currently `backend-standard-
|
|
84
|
+
**The single authority for which standard fixture is active is the loader constant** `STANDARD_FILE` in [`src/detectors/standard/_doc.ts`](./src/detectors/standard/_doc.ts) — currently `backend-standard-4bcd605.md` (50 rules — R-50/R-51, org isolation on a subscribed read / a pre-org write is a typed empty/unauthorised result, never an uncaught throw; at this pin a gated `customQuery`/`customMutation` wrapper is JUDGED red/green from its readable `customCtx`, not could-not-judge; canonical source: ElPi-Corp `analysis/vantagepeers/backend-standard/backend-standard.md`). Everything else is provenance, NOT the active pin: `PINS.txt` and any other `fixtures/backend-standard-<sha>.md` still in the tree (e.g. the superseded `backend-standard-a9d7877b.md`, 47 rules — it lacks R-48) are kept only to show what was scored before. If any of them disagrees with `STANDARD_FILE`, `STANDARD_FILE` wins. Read the active pin from `STANDARD_FILE`, never from `PINS.txt` or a number typed elsewhere — following a stale `PINS.txt` would silently score the superseded 47-rule document and never surface R-48. See [`PINS.txt`](./PINS.txt) for the engine, backend snapshot, and MCP-family doc + VR inventory pins.
|
|
85
85
|
|
|
86
86
|
## Discipline (how a detector is proven)
|
|
87
87
|
|
package/dist/coverage.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
|
|
5
5
|
* as a written-justification-token control (§23) — the paired standard PR the four
|
|
6
6
|
* decisive detectors below track. Still 48 rules; the pin moves forward with them. */
|
|
7
|
-
export declare const STANDARD_SHA = "
|
|
7
|
+
export declare const STANDARD_SHA = "4bcd605656ceff52d4f1593a9176cd6b988471be";
|
|
8
8
|
export declare const VENDORED_STANDARD_PATH: string;
|
|
9
9
|
/**
|
|
10
10
|
* Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
|
package/dist/coverage.js
CHANGED
|
@@ -18,8 +18,8 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
18
18
|
* canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
|
|
19
19
|
* as a written-justification-token control (§23) — the paired standard PR the four
|
|
20
20
|
* decisive detectors below track. Still 48 rules; the pin moves forward with them. */
|
|
21
|
-
export const STANDARD_SHA = "
|
|
22
|
-
export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-
|
|
21
|
+
export const STANDARD_SHA = "4bcd605656ceff52d4f1593a9176cd6b988471be";
|
|
22
|
+
export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-4bcd605.md");
|
|
23
23
|
/**
|
|
24
24
|
* Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
|
|
25
25
|
* never typed, never a git show against a local repo. Reads the `**R-NN`
|
|
@@ -29,7 +29,7 @@ export const VR_ORACLE_PATH = join(here, "..", "..", "fixtures", "vr-by-tool-day
|
|
|
29
29
|
* §2 [shared] contract columns are DERIVED from this file, never hardcoded.
|
|
30
30
|
* Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
|
|
31
31
|
* import cycle. */
|
|
32
|
-
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-
|
|
32
|
+
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-4bcd605.md");
|
|
33
33
|
/** Load any backend inventory CSV generically from its OWN header — the shared
|
|
34
34
|
* fields are DERIVED from these real headers, never a prose illustration. */
|
|
35
35
|
export function loadInventory(path) {
|
|
@@ -91,8 +91,16 @@ function discoverBuilders(files) {
|
|
|
91
91
|
const gates = ORG_THROW.test(codeRegion) ||
|
|
92
92
|
ORG_THROW_HELPER.test(codeRegion) ||
|
|
93
93
|
GATE_HELPER.test(codeRegion);
|
|
94
|
-
if (gates && !CONTRACT_MARKER.test(rawRegion))
|
|
95
|
-
|
|
94
|
+
if (gates && !CONTRACT_MARKER.test(rawRegion)) {
|
|
95
|
+
// The wrapper's customCtx body is READABLE — judge its no-org
|
|
96
|
+
// disposition: a typed empty/unauthorized return means the
|
|
97
|
+
// subscription receives a shape (conform); otherwise the gate
|
|
98
|
+
// throws NO_ORG into every subscribed query built from it
|
|
99
|
+
// (non-conforming). No CNJ on this class — the wrapper source is
|
|
100
|
+
// visible, so R-50 judges it.
|
|
101
|
+
const typedEmpty = TYPED_EMPTY_RETURN.test(codeRegion);
|
|
102
|
+
b.gated.set(name, { rel: f.rel, n: l.n, throws: !typedEmpty });
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
else if (base === BASE_INTERNAL_QUERY)
|
|
98
106
|
b.nonSubject.add(name);
|
|
@@ -128,6 +136,77 @@ const ORG_THROW_HELPER = /\b(?:requireWorkspaceId|requireOrgId|requireOrgSlug|ge
|
|
|
128
136
|
// subscribing client always receives a shape it can render.
|
|
129
137
|
const SAFE_RESOLVER = /\bwithOrgScope\s*\(/;
|
|
130
138
|
const CONTRACT_MARKER = /isolation-contract:|client-safe-empty|@client-must-not-subscribe/i;
|
|
139
|
+
// A TYPED EMPTY / UNAUTHORIZED RETURN in a wrapper's customCtx (or a handler): the
|
|
140
|
+
// no-org path returns a shape the subscribing client can render instead of throwing.
|
|
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
|
+
}
|
|
131
210
|
/** delimit a function body from its opener at index i by matching braces. */
|
|
132
211
|
function queryBlock(lines, i) {
|
|
133
212
|
let depth = 0;
|
|
@@ -164,13 +243,14 @@ export function scanR50(files) {
|
|
|
164
243
|
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
244
|
});
|
|
166
245
|
}
|
|
167
|
-
// A GATED public customQuery builder: its customCtx runs an auth/org gate
|
|
168
|
-
//
|
|
169
|
-
// wrapper
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
246
|
+
// A GATED public customQuery builder: its customCtx runs an auth/org gate, so
|
|
247
|
+
// every subscribed query built from it inherits that gate's no-org disposition in
|
|
248
|
+
// the shared wrapper. That disposition is READABLE in the wrapper's own customCtx
|
|
249
|
+
// body (unlike a bare handler, where "does a client reach it pre-org" is not
|
|
250
|
+
// visible), so R-50 JUDGES the builder — never CNJ on this class: a wrapper that
|
|
251
|
+
// throws NO_ORG with no typed-empty return fires an uncaught Server Error into
|
|
252
|
+
// every subscription (non-conforming); a wrapper that returns a typed empty/
|
|
253
|
+
// unauthorized context on the no-org path hands the subscription a shape (conform).
|
|
174
254
|
for (const [name, at] of builders.gated) {
|
|
175
255
|
let deps = 0;
|
|
176
256
|
for (const f of files) {
|
|
@@ -184,11 +264,20 @@ export function scanR50(files) {
|
|
|
184
264
|
deps += 1;
|
|
185
265
|
}
|
|
186
266
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
267
|
+
if (at.throws) {
|
|
268
|
+
scan.violate.push({
|
|
269
|
+
rel: at.rel,
|
|
270
|
+
n: at.n,
|
|
271
|
+
note: `customQuery builder "${name}" gates in its shared customCtx (an auth/org require*/throw) and returns NO typed-empty context on the no-org path — the throw fires into the ${deps} subscribed query(ies) that route through it as an uncaught Server Error the client cannot catch. Return a typed empty/unauthorized context from the wrapper's customCtx on no-org, or document an isolation-contract marker on the builder`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
scan.conform.push({
|
|
276
|
+
rel: at.rel,
|
|
277
|
+
n: at.n,
|
|
278
|
+
note: `customQuery builder "${name}" gates in its shared customCtx but returns a typed empty/unauthorized context on the no-org path (no throw) — the ${deps} subscribed query(ies) built from it receive a shape the client can render, not an uncaught throw`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
192
281
|
}
|
|
193
282
|
for (const f of files) {
|
|
194
283
|
if (!isJudgeable(f))
|
|
@@ -259,11 +348,36 @@ export function scanR50(files) {
|
|
|
259
348
|
});
|
|
260
349
|
}
|
|
261
350
|
else if (throwsOnNoOrg) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
}
|
|
267
381
|
}
|
|
268
382
|
else {
|
|
269
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]),
|
|
@@ -62,8 +118,13 @@ function discoverWriteBuilders(files) {
|
|
|
62
118
|
const gates = ORG_THROW.test(codeRegion) ||
|
|
63
119
|
WRITE_GATE_HELPER.test(codeRegion) ||
|
|
64
120
|
GATE_HELPER.test(codeRegion);
|
|
65
|
-
if (gates && !CONTRACT_MARKER.test(rawRegion))
|
|
66
|
-
|
|
121
|
+
if (gates && !CONTRACT_MARKER.test(rawRegion)) {
|
|
122
|
+
// The wrapper's customCtx is readable — judge its no-org disposition:
|
|
123
|
+
// a typed unauthorized return means writes receive a shape (conform);
|
|
124
|
+
// otherwise the gate throws NO_ORG into every write (non-conforming).
|
|
125
|
+
const typedUnauth = TYPED_UNAUTH_RETURN.test(codeRegion);
|
|
126
|
+
b.gated.set(name, { rel: f.rel, n: l.n, throws: !typedUnauth });
|
|
127
|
+
}
|
|
67
128
|
}
|
|
68
129
|
else if (base === BASE_INTERNAL_MUTATION)
|
|
69
130
|
b.nonSubject.add(name);
|
|
@@ -127,11 +188,20 @@ export function scanR51(files) {
|
|
|
127
188
|
deps += 1;
|
|
128
189
|
}
|
|
129
190
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
if (at.throws) {
|
|
192
|
+
scan.violate.push({
|
|
193
|
+
rel: at.rel,
|
|
194
|
+
n: at.n,
|
|
195
|
+
note: `customMutation builder "${name}" gates every write built from it in its shared customCtx (requireWriteAccess/an org gate that throws) and returns NO typed-unauthorized result on the no-org path — the throw fires into the ${deps} public write(s) that route through it as an uncaught Server Error a pre-org client shell cannot render. Return a typed unauthorized result from the wrapper's customCtx on no-org, or document a write-contract marker on the builder asserting a client cannot reach these pre-org`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
scan.conform.push({
|
|
200
|
+
rel: at.rel,
|
|
201
|
+
n: at.n,
|
|
202
|
+
note: `customMutation builder "${name}" gates in its shared customCtx but returns a typed unauthorized result on the no-org path (no throw) — the ${deps} public write(s) built from it receive a shape a pre-org client can render, not an uncaught Server Error`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
135
205
|
}
|
|
136
206
|
for (const f of files) {
|
|
137
207
|
if (!isJudgeable(f))
|
|
@@ -168,24 +238,59 @@ export function scanR51(files) {
|
|
|
168
238
|
const hasMarker = CONTRACT_MARKER.test(blockAll);
|
|
169
239
|
if (!throwsOnNoOrg && !typedUnauth && !hasMarker)
|
|
170
240
|
continue; // not a subject
|
|
171
|
-
if (
|
|
241
|
+
if (typedUnauth) {
|
|
242
|
+
// The write returns a shape on the no-org path — a pre-org client can render
|
|
243
|
+
// it, not an uncaught Server Error. Green.
|
|
244
|
+
scan.conform.push({
|
|
245
|
+
rel: f.rel,
|
|
246
|
+
n: block.lines[0].n,
|
|
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`,
|
|
248
|
+
});
|
|
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).
|
|
172
255
|
scan.conform.push({
|
|
173
256
|
rel: f.rel,
|
|
174
257
|
n: block.lines[0].n,
|
|
175
|
-
note: `${fn}: public mutation returns a typed unauthorized
|
|
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`,
|
|
176
259
|
});
|
|
177
260
|
}
|
|
178
|
-
else if (throwsOnNoOrg) {
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// It therefore REFUSES (could-not-judge) naming the write — never a silent
|
|
183
|
-
// PASS, and never a false `violate` on a server-side R-16 refusal. Pi's
|
|
184
|
-
// spec: "doctor refuses (CNJ) when it cannot judge the client; CNJ != vert".
|
|
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.
|
|
185
265
|
scan.cnj.push({
|
|
186
266
|
rel: f.rel,
|
|
187
267
|
n: block.lines[0].n,
|
|
188
|
-
note: `${fn}:
|
|
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
|
+
}
|
|
271
|
+
else if (hasMarker) {
|
|
272
|
+
// A throwing write that CARRIES a write-contract marker: the marker is the
|
|
273
|
+
// author's recorded claim that a client cannot reach this write pre-org —
|
|
274
|
+
// a claim the reviewer verifies, not one convex/ can prove. could-not-judge
|
|
275
|
+
// (Eta #141 REVISE: CNJ is reserved for the marker-borne claim, not the
|
|
276
|
+
// bare accusation).
|
|
277
|
+
scan.cnj.push({
|
|
278
|
+
rel: f.rel,
|
|
279
|
+
n: block.lines[0].n,
|
|
280
|
+
note: `${fn}: public mutation throws on the missing-org path AND carries a write-contract marker — the marker records a claim that a client cannot reach this write before an org exists, which convex/ cannot verify. R-51 refuses (a reviewer checks the marker's claim) — never a silent green`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// Bare throwing write: no typed-unauthorized return AND no write-contract
|
|
285
|
+
// marker — the two absences are both readable in convex/. The accusation
|
|
286
|
+
// stands (an uncaught Server Error on any pre-org render path) until the
|
|
287
|
+
// author returns a shape or records the unreachability claim. RED, never
|
|
288
|
+
// CNJ (Eta #141 REVISE: deleting this red deletes the only pressure to
|
|
289
|
+
// write the marker).
|
|
290
|
+
scan.violate.push({
|
|
291
|
+
rel: f.rel,
|
|
292
|
+
n: block.lines[0].n,
|
|
293
|
+
note: `${fn}: public mutation throws on the missing-org path (org-keyed ConvexError or requireWriteAccess/requireWorkspaceId/getEffectiveWorkspaceId) with NO typed-unauthorized return and NO write-contract marker — both absences readable in convex/. A pre-org client shell issuing this write receives an uncaught Server Error. Return a typed unauthorized result, or record a write-contract marker if a client cannot reach it pre-org`,
|
|
189
294
|
});
|
|
190
295
|
}
|
|
191
296
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
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-4bcd605.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";
|
|
@@ -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-4bcd605.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-4bcd605.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. */
|
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 4bcd605 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 @ 4bcd605656ceff52d4f1593a9176cd6b988471be (elpiarthera/ElPi-Corp)";
|
|
4
4
|
export interface RegistryEntry {
|
|
5
5
|
detector: Detector;
|
|
6
6
|
ruleId: string;
|
package/dist/registry.js
CHANGED
|
@@ -21,7 +21,7 @@ import { R46_STANDARD_PIN, STANDARD_SOURCE_DETECTORS, } from "./detectors/standa
|
|
|
21
21
|
import { STANDARD_DOC_DETECTORS } from "./detectors/standard/standard-doc.js";
|
|
22
22
|
import { makeR14, makeR38 } from "./detectors/structural.js";
|
|
23
23
|
/** The standard both pins the whole wave against. */
|
|
24
|
-
export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @
|
|
24
|
+
export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4bcd605656ceff52d4f1593a9176cd6b988471be (elpiarthera/ElPi-Corp)";
|
|
25
25
|
export const REGISTRY = [
|
|
26
26
|
...PER_ROW_PREDICATES.map((spec) => ({
|
|
27
27
|
detector: makeOracleDetector(spec),
|