@vantageos/backend-doctor 0.1.2 → 0.1.4

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 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-4265226e.md` (49 rules — adds R-50, org isolation on a subscribed read is a typed empty result, never an uncaught throw; 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.
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
 
@@ -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 = "4265226eb6698c7dfbae3ced77df564e877908ad";
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 = "4265226eb6698c7dfbae3ced77df564e877908ad";
22
- export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-4265226e.md");
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-4265226e.md");
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
- b.gated.set(name, { rel: f.rel, n: l.n });
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,9 @@ 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;
131
142
  /** delimit a function body from its opener at index i by matching braces. */
132
143
  function queryBlock(lines, i) {
133
144
  let depth = 0;
@@ -164,13 +175,14 @@ export function scanR50(files) {
164
175
  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
176
  });
166
177
  }
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.)
178
+ // A GATED public customQuery builder: its customCtx runs an auth/org gate, so
179
+ // every subscribed query built from it inherits that gate's no-org disposition in
180
+ // the shared wrapper. That disposition is READABLE in the wrapper's own customCtx
181
+ // body (unlike a bare handler, where "does a client reach it pre-org" is not
182
+ // visible), so R-50 JUDGES the builder — never CNJ on this class: a wrapper that
183
+ // throws NO_ORG with no typed-empty return fires an uncaught Server Error into
184
+ // every subscription (non-conforming); a wrapper that returns a typed empty/
185
+ // unauthorized context on the no-org path hands the subscription a shape (conform).
174
186
  for (const [name, at] of builders.gated) {
175
187
  let deps = 0;
176
188
  for (const f of files) {
@@ -184,11 +196,20 @@ export function scanR50(files) {
184
196
  deps += 1;
185
197
  }
186
198
  }
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
- });
199
+ if (at.throws) {
200
+ scan.violate.push({
201
+ rel: at.rel,
202
+ n: at.n,
203
+ 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`,
204
+ });
205
+ }
206
+ else {
207
+ scan.conform.push({
208
+ rel: at.rel,
209
+ n: at.n,
210
+ 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`,
211
+ });
212
+ }
192
213
  }
193
214
  for (const f of files) {
194
215
  if (!isJudgeable(f))
@@ -0,0 +1,5 @@
1
+ import type { Detector } from "../../engine-types.js";
2
+ import { type Scan, type SourceDetectorEntry, type SrcFile } from "./_scan.js";
3
+ export declare function scanR51(files: SrcFile[]): Scan;
4
+ export declare const R51: Detector;
5
+ export declare const SOURCE_WRITE_ISOLATION_DETECTORS: SourceDetectorEntry[];
@@ -0,0 +1,226 @@
1
+ import { code, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, srcDetector, } from "./_scan.js";
2
+ const CUSTOM_MUTATION_OPEN = /(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*customMutation\s*\(/;
3
+ const CUSTOM_QUERY_DEF = /(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*customQuery\s*\(/;
4
+ const FIRST_IDENT = /([A-Za-z_][A-Za-z0-9_]*)/;
5
+ const BASE_PUBLIC_MUTATION = "mutation";
6
+ const BASE_INTERNAL_MUTATION = "internalMutation";
7
+ // A no-org throw a write inherits: an org-keyed ConvexError, or a write/org gate
8
+ // helper that throws when no org is resolved.
9
+ const ORG_THROW = /throw\b[^;]*\b(?:NO_ORG|ORG_REQUIRED|RBAC_DENIED|no org|missing org|organization is required|not in (?:an )?org)/i;
10
+ const WRITE_GATE_HELPER = /\b(?:requireWriteAccess|requireWorkspaceId|requireOrgId|getEffectiveWorkspaceId|getOrgIdOrThrow|ensureCurrentOrg)\s*\(/;
11
+ // a gate helper generally (any require*/ensure*/assert* access check in a wrapper).
12
+ const GATE_HELPER = /\b(?:require|ensure|assert)[A-Z][A-Za-z0-9_]*\s*\(/;
13
+ // a TYPED unauthorized RESULT — the write returns a shape instead of throwing.
14
+ const TYPED_UNAUTH_RETURN = /return\s+\{[^}]*\b(?:unauthorized|forbidden|noOrg|no_org|denied|ok\s*:\s*false)\b/i;
15
+ const CONTRACT_MARKER = /write-contract:|client-must-not-write-before-org|isolation-contract:/i;
16
+ function discoverWriteBuilders(files) {
17
+ const b = {
18
+ publicMutation: new Set([BASE_PUBLIC_MUTATION]),
19
+ nonSubject: new Set([
20
+ BASE_INTERNAL_MUTATION,
21
+ "query",
22
+ "internalQuery",
23
+ "action",
24
+ "internalAction",
25
+ "httpAction",
26
+ ]),
27
+ gated: new Map(),
28
+ ambiguous: [],
29
+ };
30
+ for (const f of files) {
31
+ if (!isJudgeable(f))
32
+ continue;
33
+ const codeLines = f.lines.filter(code);
34
+ const allLines = f.lines;
35
+ for (let i = 0; i < codeLines.length; i++) {
36
+ const l = codeLines[i];
37
+ // a customQuery wrapper is a READ builder — not R-51's subject.
38
+ if (CUSTOM_QUERY_DEF.test(l.raw))
39
+ continue;
40
+ const cm = CUSTOM_MUTATION_OPEN.exec(l.raw);
41
+ if (!cm)
42
+ continue;
43
+ const name = cm[1];
44
+ const afterOpen = l.raw.slice((cm.index ?? 0) + cm[0].length);
45
+ let base = FIRST_IDENT.exec(afterOpen)?.[1];
46
+ for (let j = i + 1; !base && j < codeLines.length && j <= i + 4; j++) {
47
+ base = FIRST_IDENT.exec(codeLines[j].raw)?.[1];
48
+ }
49
+ if (base === BASE_PUBLIC_MUTATION) {
50
+ b.publicMutation.add(name);
51
+ const codeRegion = codeLines
52
+ .slice(i, i + 10)
53
+ .map((x) => x.raw)
54
+ .join("\n");
55
+ const startAbs = allLines.indexOf(l);
56
+ const rawRegion = startAbs >= 0
57
+ ? allLines
58
+ .slice(Math.max(0, startAbs - 3), startAbs + 14)
59
+ .map((x) => x.raw)
60
+ .join("\n")
61
+ : codeRegion;
62
+ const gates = ORG_THROW.test(codeRegion) ||
63
+ WRITE_GATE_HELPER.test(codeRegion) ||
64
+ GATE_HELPER.test(codeRegion);
65
+ if (gates && !CONTRACT_MARKER.test(rawRegion)) {
66
+ // The wrapper's customCtx is readable — judge its no-org disposition:
67
+ // a typed unauthorized return means writes receive a shape (conform);
68
+ // otherwise the gate throws NO_ORG into every write (non-conforming).
69
+ const typedUnauth = TYPED_UNAUTH_RETURN.test(codeRegion);
70
+ b.gated.set(name, { rel: f.rel, n: l.n, throws: !typedUnauth });
71
+ }
72
+ }
73
+ else if (base === BASE_INTERNAL_MUTATION)
74
+ b.nonSubject.add(name);
75
+ else
76
+ b.ambiguous.push({
77
+ rel: f.rel,
78
+ n: l.n,
79
+ name,
80
+ base: base ?? "<unresolved>",
81
+ });
82
+ }
83
+ }
84
+ return b;
85
+ }
86
+ function definitionOpener(raw, set) {
87
+ const names = [...set].sort((a, z) => z.length - a.length);
88
+ const alt = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
89
+ const re = new RegExp(`(?:export\\s+)?const\\s+([A-Za-z0-9_]+)\\s*=\\s*(${alt})\\s*\\(`);
90
+ const m = re.exec(raw);
91
+ return m ? { name: m[1], builder: m[2] } : null;
92
+ }
93
+ function mutationBlock(lines, i) {
94
+ let depth = 0;
95
+ let seenOpen = false;
96
+ const body = [];
97
+ for (let j = i; j < lines.length; j++) {
98
+ const l = lines[j];
99
+ body.push(l);
100
+ for (const ch of l.raw) {
101
+ if (ch === "{") {
102
+ depth += 1;
103
+ seenOpen = true;
104
+ }
105
+ else if (ch === "}")
106
+ depth -= 1;
107
+ }
108
+ if (seenOpen && depth <= 0)
109
+ return { lines: body, unterminated: false };
110
+ }
111
+ return { lines: body, unterminated: seenOpen };
112
+ }
113
+ export function scanR51(files) {
114
+ const scan = emptyScan();
115
+ const builders = discoverWriteBuilders(files);
116
+ for (const a of builders.ambiguous) {
117
+ scan.cnj.push({
118
+ rel: a.rel,
119
+ n: a.n,
120
+ note: `write builder "${a.name}" = customMutation(${a.base}, …) — its base "${a.base}" resolves to neither \`mutation\` nor \`internalMutation\`, so whether definitions using it are public writes cannot be enumerated; R-51 refuses rather than certify a surface it cannot see`,
121
+ });
122
+ }
123
+ for (const [name, at] of builders.gated) {
124
+ let deps = 0;
125
+ for (const f of files) {
126
+ if (!isJudgeable(f))
127
+ continue;
128
+ for (const l of f.lines) {
129
+ if (!code(l))
130
+ continue;
131
+ if (definitionOpener(l.raw, new Set([name])))
132
+ deps += 1;
133
+ }
134
+ }
135
+ if (at.throws) {
136
+ scan.violate.push({
137
+ rel: at.rel,
138
+ n: at.n,
139
+ 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`,
140
+ });
141
+ }
142
+ else {
143
+ scan.conform.push({
144
+ rel: at.rel,
145
+ n: at.n,
146
+ 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`,
147
+ });
148
+ }
149
+ }
150
+ for (const f of files) {
151
+ if (!isJudgeable(f))
152
+ continue;
153
+ const lines = f.lines;
154
+ for (let i = 0; i < lines.length; i++) {
155
+ if (!code(lines[i]))
156
+ continue;
157
+ const opener = definitionOpener(lines[i].raw, builders.publicMutation);
158
+ if (!opener)
159
+ continue;
160
+ // a write built through a GATED customMutation wrapper is covered by the
161
+ // builder-level refusal above — skip the per-handler pass.
162
+ if (builders.gated.has(opener.builder))
163
+ continue;
164
+ const block = mutationBlock(lines, i);
165
+ i += block.lines.length - 1;
166
+ const fn = opener.name;
167
+ if (block.unterminated) {
168
+ scan.cnj.push({
169
+ rel: f.rel,
170
+ n: block.lines[0].n,
171
+ note: `${fn}: a public mutation block could not be delimited (unbalanced braces) — whether its no-org path throws or returns a typed unauthorized result is not statically decidable; a human must judge (never a silent PASS)`,
172
+ });
173
+ continue;
174
+ }
175
+ const blockText = block.lines
176
+ .filter(code)
177
+ .map((l) => l.raw)
178
+ .join("\n");
179
+ const blockAll = block.lines.map((l) => l.raw).join("\n");
180
+ const throwsOnNoOrg = ORG_THROW.test(blockText) || WRITE_GATE_HELPER.test(blockText);
181
+ const typedUnauth = TYPED_UNAUTH_RETURN.test(blockText);
182
+ const hasMarker = CONTRACT_MARKER.test(blockAll);
183
+ if (!throwsOnNoOrg && !typedUnauth && !hasMarker)
184
+ continue; // not a subject
185
+ if (typedUnauth) {
186
+ // The write returns a shape on the no-org path — a pre-org client can render
187
+ // it, not an uncaught Server Error. Green.
188
+ scan.conform.push({
189
+ rel: f.rel,
190
+ n: block.lines[0].n,
191
+ 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`,
192
+ });
193
+ }
194
+ else if (hasMarker) {
195
+ // A throwing write that CARRIES a write-contract marker: the marker is the
196
+ // author's recorded claim that a client cannot reach this write pre-org —
197
+ // a claim the reviewer verifies, not one convex/ can prove. could-not-judge
198
+ // (Eta #141 REVISE: CNJ is reserved for the marker-borne claim, not the
199
+ // bare accusation).
200
+ scan.cnj.push({
201
+ rel: f.rel,
202
+ n: block.lines[0].n,
203
+ 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`,
204
+ });
205
+ }
206
+ else {
207
+ // Bare throwing write: no typed-unauthorized return AND no write-contract
208
+ // marker — the two absences are both readable in convex/. The accusation
209
+ // stands (an uncaught Server Error on any pre-org render path) until the
210
+ // author returns a shape or records the unreachability claim. RED, never
211
+ // CNJ (Eta #141 REVISE: deleting this red deletes the only pressure to
212
+ // write the marker).
213
+ scan.violate.push({
214
+ rel: f.rel,
215
+ n: block.lines[0].n,
216
+ 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`,
217
+ });
218
+ }
219
+ }
220
+ }
221
+ return scan;
222
+ }
223
+ 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.");
224
+ export const SOURCE_WRITE_ISOLATION_DETECTORS = [
225
+ { detector: R51, ruleId: "R-51", standardPin: SOURCE_STANDARD_PIN },
226
+ ];
@@ -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-4265226e.md";
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-4265226e.md` — the material at the pin. The
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-4265226e.md";
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. */
@@ -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 4265226e so coverage cannot pretend 47 is complete. No mechanical control yet; R-48 is enumerated and visible, never silently dropped.",
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
@@ -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 @ 4265226eb6698c7dfbae3ced77df564e877908ad (elpiarthera/ElPi-Corp)";
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
@@ -14,13 +14,14 @@ import { SOURCE_LIFECYCLE_DETECTORS } from "./detectors/source/lifecycle.js";
14
14
  import { SOURCE_READBUDGET_DETECTORS } from "./detectors/source/readbudget.js";
15
15
  import { SOURCE_REFUSAL_DETECTORS } from "./detectors/source/refusal.js";
16
16
  import { SOURCE_SECURITY_DETECTORS } from "./detectors/source/security.js";
17
+ import { SOURCE_WRITE_ISOLATION_DETECTORS } from "./detectors/source/write-isolation.js";
17
18
  import { SOURCE_WRITESAFETY_DETECTORS } from "./detectors/source/writesafety.js";
18
19
  import { R41, SIBLING_PIN } from "./detectors/standard/r41-frontier.js";
19
20
  import { R46_STANDARD_PIN, STANDARD_SOURCE_DETECTORS, } from "./detectors/standard/r46-returns-validator.js";
20
21
  import { STANDARD_DOC_DETECTORS } from "./detectors/standard/standard-doc.js";
21
22
  import { makeR14, makeR38 } from "./detectors/structural.js";
22
23
  /** The standard both pins the whole wave against. */
23
- export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4265226eb6698c7dfbae3ced77df564e877908ad (elpiarthera/ElPi-Corp)";
24
+ export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4bcd605656ceff52d4f1593a9176cd6b988471be (elpiarthera/ElPi-Corp)";
24
25
  export const REGISTRY = [
25
26
  ...PER_ROW_PREDICATES.map((spec) => ({
26
27
  detector: makeOracleDetector(spec),
@@ -108,6 +109,12 @@ export const REGISTRY = [
108
109
  // workspaces:ensureCurrent + prefs:sidebar). Extends the R-11 isolation
109
110
  // family; a mutation refusing an unauthorized write (R-16) is out of scope.
110
111
  ...SOURCE_ISOLATION_DETECTORS,
112
+ // R-51 — the WRITE counterpart of R-50: a public mutation whose no-org path
113
+ // throws NO_ORG (requireWriteAccess/getEffectiveWorkspaceId) into a pre-org
114
+ // client shell is a VIOLATION unless it returns a typed unauthorised result or
115
+ // documents a client contract; a gated customMutation wrapper (writeMutation) is
116
+ // refused (could-not-judge) since client reachability is not visible from convex/.
117
+ ...SOURCE_WRITE_ISOLATION_DETECTORS,
111
118
  ...SOURCE_CONTRACT_DETECTORS,
112
119
  ...IDENTITY_DETECTORS,
113
120
  // The four STANDARD-DOCUMENT detectors (R-42/R-15/R-32/R-39): each reads the