@vantageos/backend-doctor 0.1.14 → 0.3.0
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 +66 -5
- package/dist/args.d.ts +16 -0
- package/dist/args.js +72 -0
- package/dist/build-stamp.d.ts +13 -0
- package/dist/build-stamp.js +76 -0
- package/dist/build-stamp.json +4 -0
- package/dist/caller-resolution.d.ts +222 -0
- package/dist/caller-resolution.js +1198 -0
- package/dist/cli.js +249 -17
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage-cli.js +8 -0
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.d.ts +5 -3
- package/dist/detectors/oracle-axis1.js +28 -18
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +37 -22
- package/dist/detectors/predicates.d.ts +3 -0
- package/dist/detectors/predicates.js +93 -19
- package/dist/detectors/shared.js +8 -7
- package/dist/detectors/source/_ast.d.ts +92 -0
- package/dist/detectors/source/_ast.js +353 -0
- package/dist/detectors/source/_scan.js +4 -3
- package/dist/detectors/source/backfill.js +71 -18
- package/dist/detectors/source/contract.js +21 -13
- package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
- package/dist/detectors/source/cross-tenant-write.js +1408 -0
- package/dist/detectors/source/guard-helper-flow.d.ts +134 -0
- package/dist/detectors/source/guard-helper-flow.js +1216 -0
- package/dist/detectors/source/identity-by-id.d.ts +80 -0
- package/dist/detectors/source/identity-by-id.js +1783 -0
- package/dist/detectors/source/identity.d.ts +58 -2
- package/dist/detectors/source/identity.js +1158 -28
- package/dist/detectors/source/isolation.d.ts +11 -1
- package/dist/detectors/source/isolation.js +106 -25
- package/dist/detectors/source/jobs.js +178 -4
- package/dist/detectors/source/lifecycle.js +14 -2
- package/dist/detectors/source/local-identity.d.ts +26 -0
- package/dist/detectors/source/local-identity.js +661 -0
- package/dist/detectors/source/package-principal.d.ts +9 -0
- package/dist/detectors/source/package-principal.js +177 -0
- package/dist/detectors/source/r6-shapes.d.ts +20 -0
- package/dist/detectors/source/r6-shapes.js +285 -0
- package/dist/detectors/source/readbudget.d.ts +1 -0
- package/dist/detectors/source/readbudget.js +358 -4
- package/dist/detectors/source/recipient-by-id.d.ts +40 -0
- package/dist/detectors/source/recipient-by-id.js +1108 -0
- package/dist/detectors/source/refusal.d.ts +14 -0
- package/dist/detectors/source/refusal.js +75 -2
- package/dist/detectors/source/security.d.ts +16 -0
- package/dist/detectors/source/security.js +680 -33
- package/dist/detectors/source/write-isolation.d.ts +8 -1
- package/dist/detectors/source/write-isolation.js +64 -30
- package/dist/detectors/source/writesafety.js +27 -14
- package/dist/detectors/standard/_doc.d.ts +1 -1
- package/dist/detectors/standard/_doc.js +5 -4
- package/dist/detectors/standard/r41-frontier.js +3 -2
- package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
- package/dist/detectors/standard/r46-returns-validator.js +48 -16
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +84 -48
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +11 -0
- package/dist/oracle.d.ts +92 -3
- package/dist/oracle.js +229 -12
- package/dist/registry.d.ts +8 -1
- package/dist/registry.js +74 -8
- package/dist/report.d.ts +11 -1
- package/dist/report.js +15 -2
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/state-rank.d.ts +5 -0
- package/dist/state-rank.js +9 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +15 -13
- package/dist/target.d.ts +32 -0
- package/dist/target.js +173 -0
- package/dist/violation-count.d.ts +48 -0
- package/dist/violation-count.js +94 -0
- package/fixtures/backend-standard-2825122.md +722 -0
- package/fixtures/backend-standard-4dcd3e0.md +722 -0
- package/fixtures/cr1-classes/_generated/api.d.ts +24 -0
- package/fixtures/cr1-classes/surface.ts +206 -0
- package/fixtures/cr1-empty/nothing.ts +1 -0
- package/fixtures/cr1-refusal-on-resolved/_generated/api.d.ts +24 -0
- package/fixtures/cr1-refusal-on-resolved/doors.ts +180 -0
- package/fixtures/cr1-refusal-on-resolved/lib/auth.ts +104 -0
- package/fixtures/r34-vp-auth-30e821b/auth-805-812.ts.txt +8 -0
- package/fixtures/r34-vp-auth-30e821b/auth-878-890.ts.txt +13 -0
- package/fixtures/rule-classes-2825122.json +228 -0
- package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
- package/package.json +2 -2
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Detector } from "../../engine-types.js";
|
|
2
|
-
import { type Scan, type SourceDetectorEntry, type SrcFile } from "./_scan.js";
|
|
2
|
+
import { type Scan, type Site, type SourceDetectorEntry, type SrcFile } from "./_scan.js";
|
|
3
|
+
/** R-51's scan plus the public writes found OUTSIDE its scope (no no-org throw,
|
|
4
|
+
* no typed-unauthorised return, no marker). The list never reaches a verdict. */
|
|
5
|
+
export interface R51Scan {
|
|
6
|
+
scan: Scan;
|
|
7
|
+
outOfScope: Site[];
|
|
8
|
+
}
|
|
3
9
|
export declare function scanR51(files: SrcFile[]): Scan;
|
|
10
|
+
export declare function scanR51Detailed(files: SrcFile[]): R51Scan;
|
|
4
11
|
export declare const R51: Detector;
|
|
5
12
|
export declare const SOURCE_WRITE_ISOLATION_DETECTORS: SourceDetectorEntry[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SCOPE_REFUSED_RETURN } from "./isolation.js";
|
|
2
|
+
import { code, decide, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, } from "./_scan.js";
|
|
2
3
|
const CUSTOM_MUTATION_OPEN = /(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*customMutation\s*\(/;
|
|
3
4
|
const CUSTOM_QUERY_DEF = /(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*=\s*customQuery\s*\(/;
|
|
4
5
|
const FIRST_IDENT = /([A-Za-z_][A-Za-z0-9_]*)/;
|
|
@@ -6,7 +7,26 @@ const BASE_PUBLIC_MUTATION = "mutation";
|
|
|
6
7
|
const BASE_INTERNAL_MUTATION = "internalMutation";
|
|
7
8
|
// A no-org throw a write inherits: an org-keyed ConvexError, or a write/org gate
|
|
8
9
|
// helper that throws when no org is resolved.
|
|
9
|
-
|
|
10
|
+
//
|
|
11
|
+
// `RBAC_DENIED` is NOT in this vocabulary, and that is the standard's own line, not
|
|
12
|
+
// a convenience. The pinned standard (backend-standard-4bcd605.md :652) says "a
|
|
13
|
+
// mutation that refuses an unauthorised write via `requireOrgAdmin` throwing
|
|
14
|
+
// `RBAC_DENIED` is **correct**", and R-51's own text (:655-656) keeps R-16's
|
|
15
|
+
// `{code,field/scope,message}` refusal of a chosen unauthorised action correct and
|
|
16
|
+
// names R-51's throw as NO_ORG (an org-keyed ConvexError or
|
|
17
|
+
// requireWriteAccess/requireWorkspaceId/getEffectiveWorkspaceId). Counting every
|
|
18
|
+
// RBAC_DENIED as a no-org throw contradicted both lines (isolation.ts header
|
|
19
|
+
// :16-19 said CORRECT while this regex said RED). An RBAC_DENIED raised BECAUSE the
|
|
20
|
+
// org is missing is still caught — by ORG_KEYED_GUARDED_THROW below, which reads
|
|
21
|
+
// the CONDITION, not the code word.
|
|
22
|
+
const ORG_THROW = /throw\b[^;]*\b(?:NO_ORG|ORG_REQUIRED|no org|missing org|organization is required|not in (?:an )?org)/i;
|
|
23
|
+
// A throw (any code) whose governing `if` is an ORG-keyed absence test — the
|
|
24
|
+
// no-org path itself, whatever the error string says:
|
|
25
|
+
// if (!scope.isMaster && scope.orgSlug === null) { throw new ConvexError("RBAC_DENIED…") }
|
|
26
|
+
// The condition must test an org/workspace field for absence; an identity-absence
|
|
27
|
+
// test (`if (!identity)`) is a caller with no credential at all, not the signed-in
|
|
28
|
+
// pre-org shell R-51 protects, and is R-16's refusal.
|
|
29
|
+
const ORG_KEYED_GUARDED_THROW = /if\s*\((?:[^()]|\([^()]*\))*?(?:!\s*(?:[A-Za-z_$][\w$]*\.)?(?:org|orgId|orgSlug|workspaceId|workspace)\b|\b(?:org|orgId|orgSlug|workspaceId)\s*(?:===|==)\s*(?:null|undefined))(?:[^()]|\([^()]*\))*\)\s*\{?\s*throw\b/i;
|
|
10
30
|
const WRITE_GATE_HELPER = /\b(?:requireWriteAccess|requireWorkspaceId|requireOrgId|getEffectiveWorkspaceId|getOrgIdOrThrow|ensureCurrentOrg)\s*\(/;
|
|
11
31
|
// a gate helper generally (any require*/ensure*/assert* access check in a wrapper).
|
|
12
32
|
const GATE_HELPER = /\b(?:require|ensure|assert)[A-Z][A-Za-z0-9_]*\s*\(/;
|
|
@@ -59,12 +79,17 @@ function namedValueDomination(before) {
|
|
|
59
79
|
function throwDomination(blockText) {
|
|
60
80
|
const t1 = ORG_THROW.exec(blockText);
|
|
61
81
|
const t2 = WRITE_GATE_HELPER.exec(blockText);
|
|
62
|
-
const
|
|
82
|
+
const t3 = ORG_KEYED_GUARDED_THROW.exec(blockText);
|
|
83
|
+
const throwIdx = Math.min(t1 ? t1.index : Number.POSITIVE_INFINITY, t2 ? t2.index : Number.POSITIVE_INFINITY, t3 ? t3.index : Number.POSITIVE_INFINITY);
|
|
63
84
|
if (!Number.isFinite(throwIdx))
|
|
64
85
|
return "none";
|
|
65
86
|
const before = blockText.slice(0, throwIdx);
|
|
66
87
|
if (namedValueDomination(before))
|
|
67
88
|
return "dominated";
|
|
89
|
+
// Form C — the VP pre-org `if (scope.refused) return …` typed value (shared
|
|
90
|
+
// definition with R-50, isolation.ts SCOPE_REFUSED_RETURN).
|
|
91
|
+
if (SCOPE_REFUSED_RETURN.test(before))
|
|
92
|
+
return "dominated";
|
|
68
93
|
if (!TYPED_EMPTY_RETURN.test(before))
|
|
69
94
|
return "none";
|
|
70
95
|
return NO_ORG_GUARD.test(before) ? "dominated" : "ambiguous";
|
|
@@ -169,6 +194,9 @@ function mutationBlock(lines, i) {
|
|
|
169
194
|
return { lines: body, unterminated: seenOpen };
|
|
170
195
|
}
|
|
171
196
|
export function scanR51(files) {
|
|
197
|
+
return scanR51Detailed(files).scan;
|
|
198
|
+
}
|
|
199
|
+
export function scanR51Detailed(files) {
|
|
172
200
|
const scan = emptyScan();
|
|
173
201
|
const builders = discoverWriteBuilders(files);
|
|
174
202
|
// Public writes that matched a definition opener but exhibit NO org-throw, NO
|
|
@@ -242,19 +270,28 @@ export function scanR51(files) {
|
|
|
242
270
|
.map((l) => l.raw)
|
|
243
271
|
.join("\n");
|
|
244
272
|
const blockAll = block.lines.map((l) => l.raw).join("\n");
|
|
245
|
-
const throwsOnNoOrg = ORG_THROW.test(blockText) ||
|
|
273
|
+
const throwsOnNoOrg = ORG_THROW.test(blockText) ||
|
|
274
|
+
WRITE_GATE_HELPER.test(blockText) ||
|
|
275
|
+
ORG_KEYED_GUARDED_THROW.test(blockText);
|
|
246
276
|
const typedUnauth = TYPED_UNAUTH_RETURN.test(blockText);
|
|
247
277
|
const hasMarker = CONTRACT_MARKER.test(blockAll);
|
|
248
278
|
if (!throwsOnNoOrg && !typedUnauth && !hasMarker) {
|
|
249
|
-
// A
|
|
250
|
-
//
|
|
251
|
-
//
|
|
279
|
+
// A public write that neither throws on the no-org path, nor returns a
|
|
280
|
+
// typed unauthorised result, nor carries a marker is NOT an R-51
|
|
281
|
+
// subject. The pinned standard's red (backend-standard-4bcd605.md :656,
|
|
282
|
+
// clause b) is "a bare public mutation whose no-org path THROWS" with
|
|
283
|
+
// the two readable absences; a write with no no-org throw cannot
|
|
284
|
+
// crash a pre-org shell, which is the whole of R-51. Whether such a
|
|
285
|
+
// write resolves its caller at all is CR-1's control
|
|
286
|
+
// (src/caller-resolution.ts), which drives the exit code — the
|
|
287
|
+
// instrument still goes red on an unguarded write, under the control
|
|
288
|
+
// whose question it is. Counted only to word the empty abstain.
|
|
252
289
|
ungated.push({
|
|
253
290
|
rel: f.rel,
|
|
254
291
|
n: block.lines[0].n,
|
|
255
|
-
note: `${fn}: public mutation with no org
|
|
292
|
+
note: `${fn}: public mutation with no no-org throw, no typed-unauthorized return, no write-contract marker — not an R-51 subject (standard 4bcd605 :656 red requires a no-org throw); caller resolution is CR-1's control`,
|
|
256
293
|
});
|
|
257
|
-
continue; // not a
|
|
294
|
+
continue; // not a subject
|
|
258
295
|
}
|
|
259
296
|
if (typedUnauth) {
|
|
260
297
|
// The write returns a shape on the no-org path — a pre-org client can render
|
|
@@ -295,7 +332,7 @@ export function scanR51(files) {
|
|
|
295
332
|
scan.cnj.push({
|
|
296
333
|
rel: f.rel,
|
|
297
334
|
n: block.lines[0].n,
|
|
298
|
-
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`,
|
|
335
|
+
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. Why R-50 accepts its marker and R-51 does not: the pinned standard decides it per rule — R-50's green includes a query that "documents a client contract" (4bcd605 :653), while R-51 lists "a write … that carries a write-contract marker whose unreachability claim the detector cannot verify" under could-not-judge, never green (4bcd605 :656)`,
|
|
299
336
|
});
|
|
300
337
|
}
|
|
301
338
|
else {
|
|
@@ -313,27 +350,24 @@ export function scanR51(files) {
|
|
|
313
350
|
}
|
|
314
351
|
}
|
|
315
352
|
}
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
// could-not-judge sites so the verdict is CNJ-with-sites, not a misleading empty
|
|
321
|
-
// abstain. Gated on "scan otherwise empty" so a tree that already produced
|
|
322
|
-
// subjects (VP: 7 marker CNJ) is byte-for-byte unchanged — the bare-write
|
|
323
|
-
// disposition in a mixed tree is out of this fix's scope.
|
|
324
|
-
// An UNMARKED bare public write (no throw / typed-unauth / marker) is
|
|
325
|
-
// NON-CONFORMING, never an abstention — whether the scan is otherwise empty (a
|
|
326
|
-
// fully-unmarked public-write surface) OR mixed (bare writes alongside
|
|
327
|
-
// gated/marker subjects). Laurent doctrine: "an instrument that cannot go red
|
|
328
|
-
// protects nothing; CNJ is for UNREADABLE input only." A bare write is fully
|
|
329
|
-
// READABLE — its two absences (no isolation, no contract) are the defect, not a
|
|
330
|
-
// could-not-read. Push to violate; the author's exit is a fail-closed path, a
|
|
331
|
-
// typed unauthorized return, or a recorded write-contract marker.
|
|
332
|
-
for (const u of ungated)
|
|
333
|
-
scan.violate.push(u);
|
|
334
|
-
return scan;
|
|
353
|
+
// Unmarked public writes are NOT pushed anywhere (standard 4bcd605 :656 — the red
|
|
354
|
+
// requires a no-org throw). Until 0.1.16 they were pushed to `violate`; that
|
|
355
|
+
// obligation is CR-1's now (a write resolving no caller is a CR-1 violation).
|
|
356
|
+
return { scan, outOfScope: ungated };
|
|
335
357
|
}
|
|
336
|
-
|
|
358
|
+
const R51_ID = "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";
|
|
359
|
+
const R51_CHECK = "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";
|
|
360
|
+
export const R51 = {
|
|
361
|
+
id: R51_ID,
|
|
362
|
+
run: (tree) => {
|
|
363
|
+
const { scan, outOfScope } = scanR51Detailed(tree.files);
|
|
364
|
+
const n = outOfScope.length;
|
|
365
|
+
const empty = n > 0
|
|
366
|
+
? `R-51: no R-51 subject in the scanned tree — ${n} public mutation(s) matched, none throws on the no-org path, returns a typed unauthorized result, or carries a write-contract marker (out of scope per standard 4bcd605 :656; caller resolution is CR-1's control). NOT a clean bill.`
|
|
367
|
+
: "R-51: no public mutation matched in the scanned tree — no write to certify. NOT a clean bill.";
|
|
368
|
+
return decide("R-51", R51_ID, R51_CHECK, scan, empty);
|
|
369
|
+
},
|
|
370
|
+
};
|
|
337
371
|
export const SOURCE_WRITE_ISOLATION_DETECTORS = [
|
|
338
372
|
{ detector: R51, ruleId: "R-51", standardPin: SOURCE_STANDARD_PIN },
|
|
339
373
|
];
|
|
@@ -131,20 +131,33 @@ export function scanR18(files) {
|
|
|
131
131
|
// them into one mutation "would game the regex without changing atomicity"
|
|
132
132
|
// (okfBundleNode importOkfBundle shape).
|
|
133
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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
134
|
+
// marker naming EXACTLY ONE of those two shapes. Per Eta's three conditions
|
|
135
|
+
// (superseded on the disposition axis by task k179jnhx, §23 — judgement retired
|
|
136
|
+
// as an acceptance kind, 2026-09-02):
|
|
137
|
+
// COND 1 — originally "the disposition is COULD-NOT-JUDGE, never conform: a
|
|
138
|
+
// marker records a CLAIM that reaches a human reviewer" (Eta ruling
|
|
139
|
+
// 2026-08-31, mirroring the R-51 marker #141). The standard's §23 rewrite
|
|
140
|
+
// retires that disposition: a well-formed, COMPLETE claim (COND 2 + COND 3
|
|
141
|
+
// both satisfied — the shape named, and its checkable sub-conditions all
|
|
142
|
+
// present) IS the written justification the marker was always meant to
|
|
143
|
+
// record, exactly as JUSTIFIÉ:/JUSTIFIED: is elsewhere — so it now decides
|
|
144
|
+
// mechanically to PRESENT, never could-not-judge. An INCOMPLETE claim still
|
|
145
|
+
// buys nothing (COND 3, unchanged): the site stays non-conforming until the
|
|
146
|
+
// claim is well-formed.
|
|
147
|
+
// COND 2 — the marker MUST name which shape it claims, because the three are
|
|
148
|
+
// checked differently and the check must know which sub-conditions to ask for.
|
|
140
149
|
// COND 3 — the dangerous shape is (2): "unbounded loop, each write independently
|
|
141
150
|
// atomic" is a sentence anyone can write about a split invariant. So shape (2)
|
|
142
151
|
// is only honoured when the region ALSO names the loop (for/for-of/.forEach/.map)
|
|
143
152
|
// AND states the independence (no invariant spans two writes). A shape-(2) marker
|
|
144
153
|
// that names neither the loop nor the independence is INCOMPLETE — it does not buy
|
|
145
|
-
//
|
|
146
|
-
// A well-formed marker →
|
|
147
|
-
//
|
|
154
|
+
// present; the site stays non-conforming until the claim is well-formed.
|
|
155
|
+
// A well-formed, COMPLETE marker → present (the claim itself is the recorded
|
|
156
|
+
// justification; the control decides on the claim's structural completeness,
|
|
157
|
+
// never on a person's opinion of whether it is sound).
|
|
158
|
+
// No marker, an unknown shape, or an incomplete claim → non-conforming.
|
|
159
|
+
// could-not-judge is reserved for a site the instrument could not read (never
|
|
160
|
+
// reached here — every site below either parsed a marker or did not).
|
|
148
161
|
// ---------------------------------------------------------------------------
|
|
149
162
|
const ACTION_OPEN = /export\s+const\s+(\w+)\s*=\s*action\s*\(/;
|
|
150
163
|
const NEXT_EXPORT = /export\s+const\s+\w+\s*=/;
|
|
@@ -232,16 +245,16 @@ export function scanR29(files) {
|
|
|
232
245
|
? shapeReingest
|
|
233
246
|
: false;
|
|
234
247
|
if (shape && shapeComplete) {
|
|
235
|
-
// COND 1: a well-formed
|
|
236
|
-
//
|
|
237
|
-
scan.
|
|
248
|
+
// COND 1 (§23 rewrite, task k179jnhx): a well-formed, COMPLETE marker
|
|
249
|
+
// is itself the written justification — decided mechanically, present.
|
|
250
|
+
scan.conform.push({
|
|
238
251
|
rel: f.rel,
|
|
239
252
|
n: firstMut || openLine,
|
|
240
|
-
note: `action ${inAction} issues ${count} ctx.runMutation calls AND carries
|
|
253
|
+
note: `action ${inAction} issues ${count} ctx.runMutation calls AND carries a COMPLETE atomicity-exception marker claiming shape "${shape}" — ${shape === SHAPE_PER_ITEM_LOOP
|
|
241
254
|
? "the region names its loop and states the per-item writes are independently atomic (no invariant spans two)"
|
|
242
255
|
: shape === SHAPE_GATE_REINGEST
|
|
243
256
|
? "a leading gate runMutation, a supersede/dedup keyed on a stable namespace, and a recovering re-insert loop, with the convergence stated explicitly — a re-ingest of the same stable key converges on one current set"
|
|
244
|
-
: "exactly two runMutations, the first a gate (require*/assert*/bind*) preceding a single atomic write, which R-29's <=1 bar has no room for"}.
|
|
257
|
+
: "exactly two runMutations, the first a gate (require*/assert*/bind*) preceding a single atomic write, which R-29's <=1 bar has no room for"}. The marker's structural completeness IS the written justification (§23) — decided by the claim's presence, never a person's opinion of it`,
|
|
245
258
|
});
|
|
246
259
|
}
|
|
247
260
|
else if (shape === SHAPE_GATE_REINGEST) {
|
|
@@ -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-2825122.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-2825122.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
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { itemCount, withCount } from "../../violation-count.js";
|
|
15
16
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
16
|
-
export const STANDARD_FILE = "backend-standard-
|
|
17
|
+
export const STANDARD_FILE = "backend-standard-2825122.md";
|
|
17
18
|
/** The vendored standard at the pin — DATA on the same footing as the oracle CSV.
|
|
18
19
|
* Resolved locally (not via coverage.ts) so this module carries no import cycle
|
|
19
20
|
* with registry.ts. Same file coverage.ts's VENDORED_STANDARD_PATH points at. */
|
|
@@ -136,12 +137,12 @@ export function decideDoc(ruleId, id, check, scan, emptyReason) {
|
|
|
136
137
|
.map((x) => `${STANDARD_FILE}:${x.n} (${x.note})`)
|
|
137
138
|
.join("; ");
|
|
138
139
|
if (nV > 0) {
|
|
139
|
-
return {
|
|
140
|
+
return withCount({
|
|
140
141
|
id,
|
|
141
142
|
anchor: `${STANDARD_FILE}:${scan.violate[0].n}`,
|
|
142
143
|
state: "non-conforming",
|
|
143
144
|
verdict: `${ruleId}: ${nV} STATIC violation(s) in the standard document (${check}). ${breakdown} Violations: ${fmt(scan.violate)}.${nC ? ` Conform: ${fmt(scan.conform)}.` : ""}`,
|
|
144
|
-
};
|
|
145
|
+
}, itemCount(nV));
|
|
145
146
|
}
|
|
146
147
|
if (nJ > 0) {
|
|
147
148
|
const reason = `${ruleId}: static half clean (${nC} conform); ${nJ} site(s) owe a human/gate verdict — ${fmt(scan.cnj)}. This is NOT a clean bill: GREEN is the gate half surfaced per site, not the detection running. ${breakdown}`;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { readFileSync } from "node:fs";
|
|
25
25
|
import { dirname, join } from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { itemCount, withCount } from "../../violation-count.js";
|
|
27
28
|
import { readStandard, STANDARD_FILE, toLines } from "./_doc.js";
|
|
28
29
|
/** prefix → { the sibling fixture basename, its recorded upstream blob hash }.
|
|
29
30
|
* The blob hash is the PINS.txt record — a vendored document that cannot be
|
|
@@ -154,12 +155,12 @@ export function decideR41(r) {
|
|
|
154
155
|
const deadList = r.dead
|
|
155
156
|
.map((c) => `${c.id} (cited ${site(c)}) not defined in ${SIBLING[c.prefix].file}`)
|
|
156
157
|
.join("; ");
|
|
157
|
-
return {
|
|
158
|
+
return withCount({
|
|
158
159
|
id: R41_ID,
|
|
159
160
|
anchor: site(r.dead[0]),
|
|
160
161
|
state: "non-conforming",
|
|
161
162
|
verdict: `R-41: ${r.dead.length} DEAD POINTER(S) — a cited frontier id resolves to no definition in its sibling (a dead pointer is a red, not a note). ${counts} Dead: ${deadList}. Cited: ${citedList}.`,
|
|
162
|
-
};
|
|
163
|
+
}, itemCount(r.dead.length));
|
|
163
164
|
}
|
|
164
165
|
return {
|
|
165
166
|
id: R41_ID,
|
|
@@ -23,6 +23,14 @@ export interface DocValidatorMatch {
|
|
|
23
23
|
/** 1-based line span of the enclosing export — where a projection reason lives. */
|
|
24
24
|
exportStartLine: number;
|
|
25
25
|
exportEndLine: number;
|
|
26
|
+
/** 1-based line spans of every SHARED CONSTANT the export's `returns:` resolved
|
|
27
|
+
* through to reach this object (`returns: externalOpenReturns` →
|
|
28
|
+
* `const externalOpenReturns = v.object({…})`). A `returns-projection:` reason
|
|
29
|
+
* written on that constant governs every export that uses it. */
|
|
30
|
+
constSpans: {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
}[];
|
|
26
34
|
}
|
|
27
35
|
export declare function extractDocMatches(name: string, text: string): DocValidatorMatch[];
|
|
28
36
|
export declare function projectionReason(rawLines: string[], startLine: number, endLine: number): string | undefined;
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
// (non-conforming / could-not-judge / present / absent) — no fifth state,
|
|
38
38
|
// no second mechanism: the detector reads the tree the engine hands it.
|
|
39
39
|
import * as ts from "typescript";
|
|
40
|
+
import { siteCount, withCount } from "../../violation-count.js";
|
|
40
41
|
/** The standard this detector is pinned against (the re-pinned instrument pin). */
|
|
41
42
|
export const R46_STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) §16 R-46";
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
@@ -160,17 +161,35 @@ export function extractDocMatches(name, text) {
|
|
|
160
161
|
const matches = [];
|
|
161
162
|
const lineOf = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
162
163
|
const constMap = new Map();
|
|
164
|
+
const constSpan = new Map();
|
|
163
165
|
function collectConsts(node) {
|
|
164
166
|
if (ts.isVariableStatement(node)) {
|
|
165
167
|
for (const decl of node.declarationList.declarations) {
|
|
166
168
|
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
167
169
|
constMap.set(decl.name.text, decl.initializer);
|
|
170
|
+
constSpan.set(decl.name.text, {
|
|
171
|
+
start: lineOf(node.getStart(sf)),
|
|
172
|
+
end: lineOf(node.getEnd()),
|
|
173
|
+
});
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
ts.forEachChild(node, collectConsts);
|
|
172
178
|
}
|
|
173
179
|
collectConsts(sf);
|
|
180
|
+
/** The span of every const an expression resolves THROUGH (the chain a
|
|
181
|
+
* `returns: X` takes to reach its validator). */
|
|
182
|
+
function constChain(expr) {
|
|
183
|
+
const out = [];
|
|
184
|
+
let cur = expr;
|
|
185
|
+
for (let d = 0; d <= 10 && ts.isIdentifier(cur) && constMap.has(cur.text); d++) {
|
|
186
|
+
const span = constSpan.get(cur.text);
|
|
187
|
+
if (span)
|
|
188
|
+
out.push(span);
|
|
189
|
+
cur = constMap.get(cur.text);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
174
193
|
function resolve(expr, depth = 0) {
|
|
175
194
|
if (depth > 10)
|
|
176
195
|
return expr;
|
|
@@ -179,10 +198,11 @@ export function extractDocMatches(name, text) {
|
|
|
179
198
|
}
|
|
180
199
|
return expr;
|
|
181
200
|
}
|
|
182
|
-
function walkForDocObjects(expr, out, exportName, alias, span, depth = 0) {
|
|
201
|
+
function walkForDocObjects(expr, out, exportName, alias, span, depth = 0, consts = []) {
|
|
183
202
|
if (depth > 20)
|
|
184
203
|
return;
|
|
185
204
|
const nextAlias = ts.isIdentifier(expr) ? expr.text : alias;
|
|
205
|
+
const chain = [...consts, ...constChain(expr)];
|
|
186
206
|
const resolved = resolve(expr);
|
|
187
207
|
if (ts.isCallExpression(resolved) &&
|
|
188
208
|
ts.isPropertyAccessExpression(resolved.expression)) {
|
|
@@ -207,11 +227,12 @@ export function extractDocMatches(name, text) {
|
|
|
207
227
|
line: lineOf(objArg.getStart(sf)),
|
|
208
228
|
exportStartLine: span.start,
|
|
209
229
|
exportEndLine: span.end,
|
|
230
|
+
constSpans: chain,
|
|
210
231
|
});
|
|
211
232
|
}
|
|
212
233
|
for (const prop of objArg.properties) {
|
|
213
234
|
if (ts.isPropertyAssignment(prop)) {
|
|
214
|
-
walkForDocObjects(prop.initializer, out, exportName, undefined, span, depth + 1);
|
|
235
|
+
walkForDocObjects(prop.initializer, out, exportName, undefined, span, depth + 1, chain);
|
|
215
236
|
}
|
|
216
237
|
}
|
|
217
238
|
}
|
|
@@ -219,7 +240,7 @@ export function extractDocMatches(name, text) {
|
|
|
219
240
|
}
|
|
220
241
|
if (method === "union" || method === "array" || method === "optional") {
|
|
221
242
|
for (const arg of resolved.arguments) {
|
|
222
|
-
walkForDocObjects(arg, out, exportName, nextAlias, span, depth + 1);
|
|
243
|
+
walkForDocObjects(arg, out, exportName, nextAlias, span, depth + 1, chain);
|
|
223
244
|
}
|
|
224
245
|
return;
|
|
225
246
|
}
|
|
@@ -348,18 +369,29 @@ export function scanR46(files) {
|
|
|
348
369
|
}
|
|
349
370
|
// include the contiguous leading-comment block above the export — the
|
|
350
371
|
// natural place a `returns-projection:` reason is written for the read path.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
372
|
+
// The same holds for every SHARED CONSTANT the export's `returns:` resolves
|
|
373
|
+
// through: a reason written on `const externalOpenReturns = v.object(…)`
|
|
374
|
+
// is the author's written reason for every export that returns it. A
|
|
375
|
+
// reason on an UNRELATED const (one this export does not resolve through)
|
|
376
|
+
// governs nothing here.
|
|
377
|
+
const withLead = (start) => {
|
|
378
|
+
let lead = start;
|
|
379
|
+
while (lead > 1) {
|
|
380
|
+
const prev = (rawLines[lead - 2] ?? "").trim();
|
|
381
|
+
if (prev.startsWith("//") ||
|
|
382
|
+
prev.startsWith("*") ||
|
|
383
|
+
prev.startsWith("/*")) {
|
|
384
|
+
lead -= 1;
|
|
385
|
+
}
|
|
386
|
+
else
|
|
387
|
+
break;
|
|
358
388
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
389
|
+
return lead;
|
|
390
|
+
};
|
|
391
|
+
const reason = projectionReason(rawLines, withLead(m.exportStartLine), m.exportEndLine) ??
|
|
392
|
+
m.constSpans
|
|
393
|
+
.map((c) => projectionReason(rawLines, withLead(c.start), c.end))
|
|
394
|
+
.find((r) => r !== undefined);
|
|
363
395
|
if (reason) {
|
|
364
396
|
inventory.push({
|
|
365
397
|
rel: f.rel,
|
|
@@ -425,12 +457,12 @@ export function decideR46(cov) {
|
|
|
425
457
|
}
|
|
426
458
|
if (missing.length > 0) {
|
|
427
459
|
const first = missing[0];
|
|
428
|
-
return {
|
|
460
|
+
return withCount({
|
|
429
461
|
id: R46_ID,
|
|
430
462
|
anchor: `${first.rel}:${first.line}`,
|
|
431
463
|
state: "non-conforming",
|
|
432
464
|
verdict: `R-46: ${missing.length} returns validator(s) enumerate a stored document but OMIT a schema-declared field with NO written projection reason (${R46_CHECK}). ${breakdown}\n=== coverage inventory ===\n${inventory}\n=== end inventory ===`,
|
|
433
|
-
};
|
|
465
|
+
}, siteCount(missing));
|
|
434
466
|
}
|
|
435
467
|
if (cov.findings.length > 0) {
|
|
436
468
|
const first = cov.findings[0];
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { Detector } from "../engine-types.js";
|
|
2
|
-
import { type OracleRow } from "../oracle.js";
|
|
2
|
+
import { type OracleRow, type WriterTierTable } from "../oracle.js";
|
|
3
3
|
/**
|
|
4
4
|
* R-14 — inventory-schema parity, contract → inventory, one direction.
|
|
5
5
|
* A merged reader/writer authority column is "non-conforming by construction"
|
|
6
|
-
* (§12).
|
|
6
|
+
* (§12). Writer authority is a separate column only when the side-car declares it.
|
|
7
7
|
*/
|
|
8
|
-
export declare function makeR14(columns?: readonly string[]): Detector;
|
|
8
|
+
export declare function makeR14(columns?: readonly string[], rowsProvider?: () => OracleRow[], sideCar?: () => WriterTierTable): Detector;
|
|
9
9
|
/**
|
|
10
|
-
* R-38 — the completeness gate over the thirteen §2 fields.
|
|
11
|
-
*
|
|
12
|
-
* fields (both map to `rbac_qui`).
|
|
10
|
+
* R-38 — the completeness gate over the thirteen §2 fields. Writer authority is
|
|
11
|
+
* complete only when the side-car records a tier for every write tool.
|
|
13
12
|
*/
|
|
14
|
-
export declare function makeR38(columns?: readonly string[], rowsProvider?: () => OracleRow[]): Detector;
|
|
13
|
+
export declare function makeR38(columns?: readonly string[], rowsProvider?: () => OracleRow[], sideCar?: () => WriterTierTable): Detector;
|