@vantageos/backend-doctor 0.1.9 → 0.1.11

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.
@@ -94,8 +94,19 @@ export function scanR19(files) {
94
94
  const JOB_FILE = /recurring|monitor|sweep|poll|resolver|cleanup|digest|cron/i;
95
95
  const RUN_BOUND = /\.take\s*\(|_cap\b|\blimit\b/i;
96
96
  const OUTCOME_LOG = /console\.(log|error|warn|info)\s*\(/;
97
+ // ACTUAL job content — a Convex data operation, a scheduler/cron registration, or
98
+ // a scheduled-function handler. Eta ruling (2026-08-31): a file is a background-job
99
+ // SUBJECT only if its content is a job, never its FILENAME alone — otherwise a pure
100
+ // env/string helper whose basename merely contains "monitor" (errorMonitorGroupKey,
101
+ // errorMonitorKillSwitch, errorMonitorDeployWindow — no ctx.db, no scheduler, no
102
+ // cron) is scored as an unbounded job, and R-20 (unlike R-23) has no written-exception
103
+ // carve-out to clear the false positive. Tighten the subject, do not add a carve-out.
104
+ const JOB_CONTENT = /ctx\.db\.|\.collect\s*\(|\.paginate\s*\(|ctx\.scheduler\.|\bcron(s|Jobs)?\b|internalAction\s*\(|internalMutation\s*\(/i;
97
105
  function isJobFile(f) {
98
- return JOB_FILE.test(base(f.rel));
106
+ if (!JOB_FILE.test(base(f.rel)))
107
+ return false;
108
+ // require job CONTENT on an executing line, not a filename match alone.
109
+ return f.lines.some((l) => code(l) && JOB_CONTENT.test(l.raw));
99
110
  }
100
111
  export function scanR20(files) {
101
112
  const scan = emptyScan();
@@ -116,10 +116,56 @@ export function scanR18(files) {
116
116
  // atomic invariant (the bundle imported wholesale) is implied — flagged
117
117
  // non-conforming (judgement: a human confirms the invariant truly needs
118
118
  // atomicity). An action with ≤1 runMutation conforms.
119
+ //
120
+ // WRITTEN-EXCEPTION CARVE-OUT (Eta R-29 ruling, task k173kmwbr1r8x2h7bkk7xek4z18dhtqj,
121
+ // approved 2026-08-31 — mirrors the R-23 rate-adjacent carve-out #1248/#21 and the
122
+ // R-51 write-contract marker disposition #141): the <=1-runMutation bar has NO room
123
+ // for two shapes that are legitimately more than one runMutation yet carry no cross-
124
+ // write invariant to violate —
125
+ // (1) pre-io-gate-then-single-atomic-write: a security/existence GATE runMutation
126
+ // that must precede an I/O read (e.g. ctx.storage.get) followed by ONE atomic
127
+ // write — the gate is not part of the write's transaction and cannot be folded
128
+ // into it (kb.ts storeDocumentChunked shape); and
129
+ // (2) independent-per-item-loop-writes: a loop whose per-item writes are each
130
+ // independently atomic, where NO invariant spans two of them, so collapsing
131
+ // them into one mutation "would game the regex without changing atomicity"
132
+ // (okfBundleNode importOkfBundle shape).
133
+ // An action that would be flagged non-conforming may carry an `atomicity-exception:`
134
+ // marker naming EXACTLY ONE of those two shapes. Per Eta's three conditions:
135
+ // COND 1 — the disposition is COULD-NOT-JUDGE, never conform. A marker records a
136
+ // CLAIM that reaches a human reviewer; it never closes the rule green (same as
137
+ // the R-51 marker, #141).
138
+ // COND 2 — the marker MUST name which shape it claims, because the two are checked
139
+ // differently and the reviewer must know which question to ask.
140
+ // COND 3 — the dangerous shape is (2): "unbounded loop, each write independently
141
+ // atomic" is a sentence anyone can write about a split invariant. So shape (2)
142
+ // is only honoured when the region ALSO names the loop (for/for-of/.forEach/.map)
143
+ // AND states the independence (no invariant spans two writes). A shape-(2) marker
144
+ // that names neither the loop nor the independence is INCOMPLETE — it does not buy
145
+ // could-not-judge; the site stays non-conforming until the claim is well-formed.
146
+ // A well-formed marker → could-not-judge (the reviewer verifies the per-site claim).
147
+ // No marker, an unknown shape, or an incomplete shape-(2) claim → non-conforming.
119
148
  // ---------------------------------------------------------------------------
120
149
  const ACTION_OPEN = /export\s+const\s+(\w+)\s*=\s*action\s*\(/;
121
150
  const NEXT_EXPORT = /export\s+const\s+\w+\s*=/;
122
151
  const RUN_MUTATION = /ctx\.runMutation\s*\(/;
152
+ // The carve-out marker + the two shape names it may claim (COND 2).
153
+ const ATOMICITY_MARKER = /atomicity-exception:\s*(pre-io-gate-then-single-atomic-write|independent-per-item-loop-writes)/i;
154
+ const SHAPE_PER_ITEM_LOOP = "independent-per-item-loop-writes";
155
+ const SHAPE_PRE_IO_GATE = "pre-io-gate-then-single-atomic-write";
156
+ // Shape-(2) sub-conditions (COND 3): the region must NAME the loop and STATE the
157
+ // write independence — neither is inferable, both are required of the claim.
158
+ const LOOP_NAMED = /\bfor\s*\(|\bfor\s+(?:const|let|var|await)\b|\.forEach\s*\(|\.map\s*\(|\bwhile\s*\(/;
159
+ const INDEPENDENCE_STATED = /no\s+invariant\s+spans|invariant\s+spans\s+no|writes?\s+are\s+independent|independently\s+atomic|no\s+cross-write\s+invariant/i;
160
+ // Shape-(1) sub-condition (Eta REVISE #26, 2026-08-31): "a pre-I/O gate mutation
161
+ // precedes a single atomic write" describes itself CHECKABLY — it is EXACTLY TWO
162
+ // ctx.runMutation calls, the FIRST a gate (require*/assert*/ensure*/bind*). A shape-1
163
+ // marker on anything else (three calls, an unbounded loop, a first call that is not a
164
+ // gate) is INCOMPLETE and stays non-conforming, exactly as an incomplete shape-2 does
165
+ // — otherwise eleven words in a comment remove a real violation from the #1250 prod
166
+ // gate (abstentions do not block; mechanical_violations is the sole driver), and the
167
+ // mechanical count that gate rests on is no longer honest.
168
+ const GATE_CALLEE = /\b(require|assert|ensure|bind)\w*/i;
123
169
  export function scanR29(files) {
124
170
  const scan = emptyScan();
125
171
  for (const f of files) {
@@ -129,15 +175,63 @@ export function scanR29(files) {
129
175
  let openLine = 0;
130
176
  let count = 0;
131
177
  let firstMut = 0;
178
+ let firstMutCallee = "";
179
+ let calleeNeedsMore = false;
180
+ let region = "";
132
181
  const flush = () => {
133
182
  if (!inAction)
134
183
  return;
135
184
  if (count > 1) {
136
- scan.violate.push({
137
- rel: f.rel,
138
- n: firstMut || openLine,
139
- note: `action ${inAction} orchestrates ${count} ctx.runMutation calls — several mutations across a race window, not one atomic transaction`,
140
- });
185
+ const marker = ATOMICITY_MARKER.exec(region);
186
+ const shape = marker?.[1]?.toLowerCase();
187
+ // Shape (1) is honoured ONLY as exactly two runMutations whose first is a
188
+ // gate call; shape (2) only when the loop is named and independence stated.
189
+ const shapeGate = count === 2 && GATE_CALLEE.test(firstMutCallee);
190
+ const shapeComplete = shape === SHAPE_PER_ITEM_LOOP
191
+ ? LOOP_NAMED.test(region) && INDEPENDENCE_STATED.test(region)
192
+ : shape === SHAPE_PRE_IO_GATE
193
+ ? shapeGate
194
+ : false;
195
+ if (shape && shapeComplete) {
196
+ // COND 1: a well-formed marker yields COULD-NOT-JUDGE, never conform —
197
+ // the claim is deferred to the reviewer, who verifies it per site.
198
+ scan.cnj.push({
199
+ rel: f.rel,
200
+ n: firstMut || openLine,
201
+ note: `action ${inAction} issues ${count} ctx.runMutation calls AND carries an atomicity-exception marker claiming shape "${shape}" — ${shape === SHAPE_PER_ITEM_LOOP
202
+ ? "the region names its loop and states the per-item writes are independently atomic (no invariant spans two)"
203
+ : "exactly two runMutations, the first a gate (require*/assert*/ensure*/bind*) preceding a single atomic write, which R-29's <=1 bar has no room for"}. R-29 refuses (a reviewer verifies the claim per site) — never a silent green (Eta ruling 2026-08-31)`,
204
+ });
205
+ }
206
+ else if (shape === SHAPE_PRE_IO_GATE) {
207
+ // Eta REVISE #26: shape (1) claimed but NOT exactly-two-with-a-gate-first
208
+ // — an incomplete claim does NOT buy could-not-judge.
209
+ scan.violate.push({
210
+ rel: f.rel,
211
+ n: firstMut || openLine,
212
+ note: `action ${inAction} orchestrates ${count} ctx.runMutation calls with an atomicity-exception "${SHAPE_PRE_IO_GATE}" marker that is INCOMPLETE — this shape is EXACTLY TWO runMutations whose FIRST is a gate (require*/assert*/ensure*/bind*); here ${count !== 2
213
+ ? `there are ${count}, not two`
214
+ : "the first runMutation is not a gate call"}. A gate-then-single-write is checkable; a marker cannot remove a >2-call or loop site from the gate`,
215
+ });
216
+ }
217
+ else if (shape === SHAPE_PER_ITEM_LOOP) {
218
+ // COND 3: shape (2) claimed but the loop and/or independence is not stated
219
+ // — an incomplete claim does NOT buy could-not-judge.
220
+ scan.violate.push({
221
+ rel: f.rel,
222
+ n: firstMut || openLine,
223
+ note: `action ${inAction} orchestrates ${count} ctx.runMutation calls with an atomicity-exception "${SHAPE_PER_ITEM_LOOP}" marker that is INCOMPLETE — the per-item-loop shape must NAME the loop (for/for-of/.forEach/.map) AND STATE the write independence (no invariant spans two writes). Complete the claim or split the invariant into one mutation`,
224
+ });
225
+ }
226
+ else {
227
+ scan.violate.push({
228
+ rel: f.rel,
229
+ n: firstMut || openLine,
230
+ note: `action ${inAction} orchestrates ${count} ctx.runMutation calls — several mutations across a race window, not one atomic transaction${marker === null && /atomicity-exception:/i.test(region)
231
+ ? " (an atomicity-exception marker is present but names no known shape — it must name pre-io-gate-then-single-atomic-write or independent-per-item-loop-writes)"
232
+ : ""}`,
233
+ });
234
+ }
141
235
  }
142
236
  else if (count === 1) {
143
237
  scan.conform.push({
@@ -148,8 +242,6 @@ export function scanR29(files) {
148
242
  }
149
243
  };
150
244
  for (const l of f.lines) {
151
- if (!code(l))
152
- continue;
153
245
  const open = ACTION_OPEN.exec(l.raw);
154
246
  if (open) {
155
247
  flush();
@@ -157,17 +249,41 @@ export function scanR29(files) {
157
249
  openLine = l.n;
158
250
  count = 0;
159
251
  firstMut = 0;
252
+ firstMutCallee = "";
253
+ calleeNeedsMore = false;
254
+ region = l.raw;
160
255
  continue;
161
256
  }
162
- if (inAction && NEXT_EXPORT.test(l.raw) && !ACTION_OPEN.test(l.raw)) {
257
+ if (inAction && code(l) && NEXT_EXPORT.test(l.raw) && !ACTION_OPEN.test(l.raw)) {
163
258
  flush();
164
259
  inAction = "";
260
+ region = "";
261
+ continue;
262
+ }
263
+ if (!inAction)
165
264
  continue;
265
+ // Accumulate the WHOLE region (comments included) so the marker — which
266
+ // lives in a comment — is visible; only count runMutation on code lines.
267
+ region += `\n${l.raw}`;
268
+ // The callee of the FIRST runMutation identifies the gate (shape 1). It may
269
+ // sit on the same line (`ctx.runMutation(internal.kb.assertX, {`) or wrap to
270
+ // the next code line (`const id = await ctx.runMutation(\n internal.kb.assertX,`);
271
+ // grab the paren-tail here and one more code line if the tail was empty.
272
+ if (code(l) && calleeNeedsMore) {
273
+ firstMutCallee += ` ${l.raw.trim()}`;
274
+ calleeNeedsMore = false;
166
275
  }
167
- if (inAction && RUN_MUTATION.test(l.raw)) {
276
+ if (code(l) && RUN_MUTATION.test(l.raw)) {
168
277
  count++;
169
- if (!firstMut)
278
+ if (!firstMut) {
170
279
  firstMut = l.n;
280
+ const tail = l.raw.slice(l.raw.search(RUN_MUTATION)).replace(RUN_MUTATION, "");
281
+ firstMutCallee = tail;
282
+ // If the paren-tail carries no identifier yet, the callee wrapped — take
283
+ // the next code line too.
284
+ if (!/\w/.test(tail))
285
+ calleeNeedsMore = true;
286
+ }
171
287
  }
172
288
  }
173
289
  flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/backend-doctor",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",