@vantageos/backend-doctor 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +21 -5
- package/dist/coverage.d.ts +4 -3
- package/dist/coverage.js +5 -4
- package/dist/detectors/oracle-axis1.js +19 -21
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +63 -18
- package/dist/detectors/source/isolation.d.ts +6 -0
- package/dist/detectors/source/isolation.js +127 -0
- package/dist/detectors/standard/_doc.d.ts +16 -1
- package/dist/detectors/standard/_doc.js +29 -2
- package/dist/not-checkable.js +1 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +11 -1
- package/fixtures/backend-standard-4265226e.md +680 -0
- package/package.json +2 -2
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-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.
|
|
85
85
|
|
|
86
86
|
## Discipline (how a detector is proven)
|
|
87
87
|
|
package/dist/cli.js
CHANGED
|
@@ -8,17 +8,20 @@ import { execFileSync } from "node:child_process";
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { dirname, join, resolve } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import {
|
|
12
|
-
import { REGISTRY } from "./registry.js";
|
|
11
|
+
import { renderTable, runDoctor } from "@vantageos/mcp-doctor";
|
|
13
12
|
import { coverage, coverageSummary } from "./coverage.js";
|
|
13
|
+
import { checkStandardReadable } from "./detectors/standard/_doc.js";
|
|
14
14
|
import { NOT_MECHANICALLY_CHECKABLE, notCheckableResults } from "./not-checkable.js";
|
|
15
|
-
import {
|
|
15
|
+
import { REGISTRY } from "./registry.js";
|
|
16
16
|
import { writeReport } from "./report.js";
|
|
17
|
+
import { tally, tallyLine, verdict } from "./tally.js";
|
|
17
18
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
/** `git -C <target> rev-parse HEAD`, or "not a git repo" when it is not one. */
|
|
19
20
|
function targetCommit(target) {
|
|
20
21
|
try {
|
|
21
|
-
return execFileSync("git", ["-C", target, "rev-parse", "HEAD"], {
|
|
22
|
+
return execFileSync("git", ["-C", target, "rev-parse", "HEAD"], {
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
}).trim();
|
|
22
25
|
}
|
|
23
26
|
catch {
|
|
24
27
|
return "not a git repo";
|
|
@@ -41,6 +44,15 @@ function toolVersion() {
|
|
|
41
44
|
}
|
|
42
45
|
function main() {
|
|
43
46
|
const target = resolve(process.argv[2] ?? process.cwd());
|
|
47
|
+
// Instrument self-check, BEFORE any judging: a backend-doctor run reads its
|
|
48
|
+
// pinned standard fixture as material. If that fixture is absent/unreadable the
|
|
49
|
+
// tool cannot judge — REFUSE with exit 2 (could-not-judge) naming what it could
|
|
50
|
+
// not read, never exit 1 (which reads as "a violation was found"). Eta #12 REVISE.
|
|
51
|
+
const readable = checkStandardReadable();
|
|
52
|
+
if (!readable.ok) {
|
|
53
|
+
console.error(readable.refusal);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
44
56
|
// Every derived rule carries EXACTLY ONE result: the executable detectors plus
|
|
45
57
|
// one could-not-judge abstention per NOT_MECHANICALLY_CHECKABLE rule (R-48's
|
|
46
58
|
// pending detector) — so the tally's rule total equals the 48 derived rules and
|
|
@@ -54,7 +66,7 @@ function main() {
|
|
|
54
66
|
const cov = coverage();
|
|
55
67
|
console.log(coverageSummary(cov));
|
|
56
68
|
console.log("");
|
|
57
|
-
console.log(`NOT MECHANICALLY CHECKABLE (${cov.notCheckable.length}) —
|
|
69
|
+
console.log(`NOT MECHANICALLY CHECKABLE (${cov.notCheckable.length}) — NAMED, OWNED gaps (detector owned by another task), NOT 'a human decides'; NOT a clean bill:`);
|
|
58
70
|
for (const id of cov.notCheckable) {
|
|
59
71
|
console.log(` ${id} — ${NOT_MECHANICALLY_CHECKABLE[id]}`);
|
|
60
72
|
}
|
|
@@ -62,6 +74,10 @@ function main() {
|
|
|
62
74
|
// DERIVED here (no human counts rows) and the verdict maps to a TRUTHFUL exit
|
|
63
75
|
// code — >=1 critical or a hard violation exits 1, a human-verdict-owed
|
|
64
76
|
// abstention exits 2, approve exits 0. See src/tally.ts + src/severity.ts.
|
|
77
|
+
// The tally is over ALL 48 derived rules: the executable detectors' results
|
|
78
|
+
// PLUS the one could-not-judge abstention per NOT_MECHANICALLY_CHECKABLE rule
|
|
79
|
+
// (R-48) appended above — so `results` already carries exactly one result per
|
|
80
|
+
// rule and `tally` reconciles against deriveRuleIds() (48), nothing dropped.
|
|
65
81
|
const t = tally(results);
|
|
66
82
|
const v = verdict(t, results);
|
|
67
83
|
console.log("");
|
package/dist/coverage.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** The standard is VENDORED at the pin as a fixture — DATA at 51ec7af1, a
|
|
2
2
|
* comparison target on the same footing as the oracle CSV. deriveRuleIds no
|
|
3
3
|
* longer shells out to any local repo, so it is CI-portable. Re-pinned from the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
4
|
+
* canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
|
|
5
|
+
* as a written-justification-token control (§23) — the paired standard PR the four
|
|
6
|
+
* decisive detectors below track. Still 48 rules; the pin moves forward with them. */
|
|
7
|
+
export declare const STANDARD_SHA = "4265226eb6698c7dfbae3ced77df564e877908ad";
|
|
7
8
|
export declare const VENDORED_STANDARD_PATH: string;
|
|
8
9
|
/**
|
|
9
10
|
* Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
|
package/dist/coverage.js
CHANGED
|
@@ -15,10 +15,11 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
15
15
|
/** The standard is VENDORED at the pin as a fixture — DATA at 51ec7af1, a
|
|
16
16
|
* comparison target on the same footing as the oracle CSV. deriveRuleIds no
|
|
17
17
|
* longer shells out to any local repo, so it is CI-portable. Re-pinned from the
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
export const
|
|
18
|
+
* canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
|
|
19
|
+
* as a written-justification-token control (§23) — the paired standard PR the four
|
|
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");
|
|
22
23
|
/**
|
|
23
24
|
* Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
|
|
24
25
|
* never typed, never a git show against a local repo. Reads the `**R-NN`
|
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
// Axis-2 (divergence WITHIN one backend) is R-37; Axis-1 (BETWEEN two backends)
|
|
7
7
|
// is R-40 — it fires the moment a second backend's inventory exists.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// a
|
|
14
|
-
//
|
|
9
|
+
// The instrument performs the mechanical compare IN FULL — it names every
|
|
10
|
+
// concrete between-backend divergence site (which shared field, which value on
|
|
11
|
+
// VP vs which on VR) — and DECIDES: a divergence with no written justification
|
|
12
|
+
// token is `non-conforming` (the two backends owe either agreement or a recorded
|
|
13
|
+
// justification); a run where every divergence carries a written justification
|
|
14
|
+
// token, or where the two agree on every shared field, is `present`. The
|
|
15
|
+
// disposition is the token's PRESENCE, never a human's opinion of its soundness.
|
|
16
|
+
// could-not-judge is reserved for an unreadable oracle (a malformed VR/VP CSV),
|
|
17
|
+
// never for "a person should weigh the divergence".
|
|
15
18
|
//
|
|
16
|
-
// Both inputs are IN HAND (VP oracle + VR oracle, both vendored + hash-checked)
|
|
17
|
-
// so this is NOT the input-absent could-not-judge — it is the human-verdict-owed
|
|
18
|
-
// state, the acceptance property of a comparative judgement rule.
|
|
19
|
+
// Both inputs are IN HAND (VP oracle + VR oracle, both vendored + hash-checked).
|
|
19
20
|
import { readFileSync } from "node:fs";
|
|
20
21
|
import { dirname, join } from "node:path";
|
|
21
22
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,7 +29,7 @@ export const VR_ORACLE_PATH = join(here, "..", "..", "fixtures", "vr-by-tool-day
|
|
|
28
29
|
* §2 [shared] contract columns are DERIVED from this file, never hardcoded.
|
|
29
30
|
* Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
|
|
30
31
|
* import cycle. */
|
|
31
|
-
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-
|
|
32
|
+
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-4265226e.md");
|
|
32
33
|
/** Load any backend inventory CSV generically from its OWN header — the shared
|
|
33
34
|
* fields are DERIVED from these real headers, never a prose illustration. */
|
|
34
35
|
export function loadInventory(path) {
|
|
@@ -176,22 +177,20 @@ export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
|
176
177
|
if (schema.vrOnlyColumns.length)
|
|
177
178
|
schemaBits.push(`VR-only column(s): ${schema.vrOnlyColumns.join(", ")}`);
|
|
178
179
|
const schemaFinding = schemaBits.length
|
|
179
|
-
? `Between-schema divergence (R-14-class): ${schemaBits.join("; ")} — a shared field absent on one backend
|
|
180
|
+
? `Between-schema divergence (R-14-class): ${schemaBits.join("; ")} — a shared field absent on one backend, with no written justification recorded; non-conforming until the schema difference carries a written justification.`
|
|
180
181
|
: "No between-schema column divergence.";
|
|
181
182
|
const totalValueDivergences = sites.reduce((n, s) => n + s.vpOnly.length + s.vrOnly.length, 0);
|
|
182
183
|
const breakdown = `[shared-field-sites=${sites.length} value-divergences=${totalValueDivergences} schema-col-divergences=${schema.vpOnlyColumns.length + schema.vrOnlyColumns.length}]`;
|
|
183
184
|
if (sites.length === 0 && schemaBits.length === 0) {
|
|
184
|
-
// The two backends agree on every shared field's vocabulary
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
const reason = `R-40: the VP and VR inventories agree on every shared field's closed vocabulary and share every column; the mechanical between-backend compare is exhausted with no divergence. Confirming the contract is human. ${breakdown}`;
|
|
185
|
+
// The two backends agree on every shared field's vocabulary and share
|
|
186
|
+
// every column — a decisive PASS. The disposition is mechanical (nothing
|
|
187
|
+
// diverges, so no justification is owed); the instrument does not abstain.
|
|
188
|
+
const reason = `R-40: the VP and VR inventories agree on every shared field's closed vocabulary and share every column; the mechanical between-backend compare is exhausted with no divergence. Present — decided by the compare, not a human verdict. ${breakdown}`;
|
|
189
189
|
return {
|
|
190
190
|
id: R40_ID,
|
|
191
191
|
anchor: R40_ANCHOR,
|
|
192
|
-
state: "
|
|
192
|
+
state: "present",
|
|
193
193
|
verdict: reason,
|
|
194
|
-
reason,
|
|
195
194
|
};
|
|
196
195
|
}
|
|
197
196
|
const examples = sites
|
|
@@ -208,13 +207,12 @@ export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
|
208
207
|
return `${s.field}: VP-only {${vp || "—"}} vs VR-only {${vr || "—"}}`;
|
|
209
208
|
})
|
|
210
209
|
.join("; ");
|
|
211
|
-
const reason = `R-40: ${sites.length} shared field(s) diverge BETWEEN VP and VR on their closed vocabulary — the mechanical compare names WHICH FIELD and WHICH value on VP vs VR
|
|
210
|
+
const reason = `R-40: ${sites.length} shared field(s) diverge BETWEEN VP and VR on their closed vocabulary — the mechanical compare names WHICH FIELD and WHICH value on VP vs VR, and none carries a written between-backend justification; non-conforming until each divergence is reconciled or carries a recorded justification. ${schemaFinding} ${breakdown} Field sites: ${examples}.`;
|
|
212
211
|
return {
|
|
213
212
|
id: R40_ID,
|
|
214
213
|
anchor: `${R40_ANCHOR} field=${sites[0]?.field ?? "schema"}`,
|
|
215
|
-
state: "
|
|
214
|
+
state: "non-conforming",
|
|
216
215
|
verdict: reason,
|
|
217
|
-
reason,
|
|
218
216
|
};
|
|
219
217
|
},
|
|
220
218
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Detector } from "../engine-types.js";
|
|
2
2
|
import { type OracleRow } from "../oracle.js";
|
|
3
3
|
import type { RulePredicate } from "./shared.js";
|
|
4
|
+
export declare function hasJustificationToken(r: OracleRow): boolean;
|
|
4
5
|
export declare const r7: RulePredicate;
|
|
5
6
|
export declare const r8: RulePredicate;
|
|
6
7
|
export interface DivergenceSite {
|
|
@@ -10,6 +11,11 @@ export interface DivergenceSite {
|
|
|
10
11
|
valA: string;
|
|
11
12
|
toolB: string;
|
|
12
13
|
valB: string;
|
|
14
|
+
/** MECHANICAL disposition: the divergence carries a written per-tool
|
|
15
|
+
* justification token on at least one of the two diverging rows. A justified
|
|
16
|
+
* divergence is `present`; an unjustified one is `non-conforming`. This is the
|
|
17
|
+
* pole that replaced "a human judges whether the justification is sound". */
|
|
18
|
+
justified: boolean;
|
|
13
19
|
}
|
|
14
20
|
/**
|
|
15
21
|
* Group the inventory rows by table and, per shared field, name ONE diverging
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { isInventoryRow, readOracle, } from "../oracle.js";
|
|
2
|
+
// A written justification token turns a flagged divergence from a DEFECT the
|
|
3
|
+
// author owes into a recorded, accepted divergence. It is the mechanical pole
|
|
4
|
+
// that replaced "a human decides whether the divergence is sound": the control
|
|
5
|
+
// reads whether the token is PRESENT in the inventory (the `rbac_adjustment_needed`
|
|
6
|
+
// remediation cell), never whether a person judges it sound. `CORRIGER`/`REVOIR`
|
|
7
|
+
// are remediation-owed markers (R-28) — a fix still owed, NOT a justification —
|
|
8
|
+
// so they do not satisfy this predicate; only an explicit JUSTIFIÉ/JUSTIFIED/
|
|
9
|
+
// STATELESS/ACCEPTED token does.
|
|
10
|
+
export function hasJustificationToken(r) {
|
|
11
|
+
const cell = (r.by.rbac_adjustment_needed ?? "").trim();
|
|
12
|
+
// Anchored on the DOCUMENTED shape: the standard writes the token with a
|
|
13
|
+
// trailing colon (`JUSTIFIÉ:` / `JUSTIFIED:` / `STATELESS:`), at a word
|
|
14
|
+
// boundary. Two failures a bare substring match let through, both closed here:
|
|
15
|
+
// • a LEADING-letter negation — "UNJUSTIFIED" contains "JUSTIFIED", so the
|
|
16
|
+
// leading `(?<![A-Za-zÀ-ÿ-])` (not preceded by a letter OR a hyphen — a
|
|
17
|
+
// hyphen-joined negation like French `non-justifié:` clears a letter-only
|
|
18
|
+
// lookbehind, Eta #12 round-2) rejects it; and R-37's own output prints
|
|
19
|
+
// "N UNJUSTIFIED divergence(s)".
|
|
20
|
+
// • a token with NO colon — "not justified", "justified by design" — cleared
|
|
21
|
+
// the guard though the standard requires the colon; the trailing `\s*:`
|
|
22
|
+
// now rejects it. Only the recorded token form (`TOKEN:`) is a justification.
|
|
23
|
+
// No trailing \b before the colon: an accented final letter (É in JUSTIFIÉ) is
|
|
24
|
+
// not an ASCII word char, so \b would not match after it — the colon is the anchor.
|
|
25
|
+
return /(?<![A-Za-zÀ-ÿ-])(JUSTIFI(É|E|ED)|STATELESS|ACCEPT(É|E|ED)|WONTFIX)\s*:/i.test(cell);
|
|
26
|
+
}
|
|
2
27
|
// ---- R-7 read/write coherence divergence (INCOHERENT* table-aggregate flag) -
|
|
3
28
|
// §18: "`INCOHERENT*` flag (mechanical); divergence justified (human)". The
|
|
4
29
|
// oracle's `rbac_coherence_table` carries the audit's table-aggregate coherence
|
|
@@ -9,14 +34,23 @@ import { isInventoryRow, readOracle, } from "../oracle.js";
|
|
|
9
34
|
export const r7 = {
|
|
10
35
|
ruleId: "R-7",
|
|
11
36
|
id: "R-7 read/write coherence divergence",
|
|
12
|
-
check: "row carries an `INCOHERENT*` coherence flag
|
|
37
|
+
check: "row carries an `INCOHERENT*` coherence flag with NO written justification token in its remediation cell → non-conforming; a `COHERENT*` verdict, or an `INCOHERENT*` carrying a `JUSTIFIÉ:`/`JUSTIFIED:` token, → present (the machine decides on the token's PRESENCE, never on a person's opinion of its soundness)",
|
|
13
38
|
classify: (r) => {
|
|
14
39
|
const v = r.by.rbac_coherence_table;
|
|
15
|
-
if (/^INCOHERENT/i.test(v))
|
|
40
|
+
if (/^INCOHERENT/i.test(v)) {
|
|
41
|
+
// The disposition is MECHANICAL: an incoherence publishes only with a
|
|
42
|
+
// written justification token recorded in the inventory (the same
|
|
43
|
+
// remediation cell R-28 governs). No token → non-conforming; the row owes
|
|
44
|
+
// a fix or a written justification, and asserting neither is the defect.
|
|
45
|
+
// could-not-judge is NOT emitted here — it is reserved for a row the
|
|
46
|
+
// parser could not read (handled upstream by the malformed branch).
|
|
47
|
+
if (hasJustificationToken(r))
|
|
48
|
+
return { kind: "conform" };
|
|
16
49
|
return {
|
|
17
|
-
kind: "
|
|
18
|
-
note: `rbac_coherence_table="${v}" — a read/write incoherence
|
|
50
|
+
kind: "violate",
|
|
51
|
+
note: `rbac_coherence_table="${v}" — a read/write incoherence with no written justification token (JUSTIFIÉ:/JUSTIFIED:) in rbac_adjustment_needed; it owes a fix or a recorded justification`,
|
|
19
52
|
};
|
|
53
|
+
}
|
|
20
54
|
if (/^COHERENT/i.test(v))
|
|
21
55
|
return { kind: "conform" };
|
|
22
56
|
return { kind: "na" }; // no coherence verdict recorded on this row (e.g. n/a)
|
|
@@ -30,7 +64,7 @@ export const r7 = {
|
|
|
30
64
|
export const r8 = {
|
|
31
65
|
ruleId: "R-8",
|
|
32
66
|
id: "R-8 zero scope check on a data op",
|
|
33
|
-
check: "empty/`AUCUN` scope enforcement on a row carrying a data op
|
|
67
|
+
check: "empty/`AUCUN` scope enforcement on a row carrying a data op, with NO written stateless/read-nothing justification token → non-conforming; a genuine scope check, or a no-scope row carrying a `JUSTIFIÉ:`/`STATELESS:` token, → present (the machine decides on the token's PRESENCE, never on a person's opinion of soundness)",
|
|
34
68
|
classify: (r) => {
|
|
35
69
|
const scope = r.by.scope_enforcement.trim();
|
|
36
70
|
const crud = r.by.crud.trim();
|
|
@@ -38,11 +72,18 @@ export const r8 = {
|
|
|
38
72
|
if (!hasDataOp)
|
|
39
73
|
return { kind: "na" }; // no data op — R-8 does not apply
|
|
40
74
|
const noScope = scope === "" || /AUCUN/i.test(scope);
|
|
41
|
-
if (noScope)
|
|
75
|
+
if (noScope) {
|
|
76
|
+
// MECHANICAL disposition: a zero-scope data op publishes only with an
|
|
77
|
+
// explicit written stateless/read-nothing justification token in its
|
|
78
|
+
// remediation cell. No token → non-conforming. could-not-judge is reserved
|
|
79
|
+
// for an unreadable row, never for "a person should weigh soundness".
|
|
80
|
+
if (hasJustificationToken(r) || /STATELESS/i.test(scope))
|
|
81
|
+
return { kind: "conform" };
|
|
42
82
|
return {
|
|
43
|
-
kind: "
|
|
44
|
-
note: `scope_enforcement="${scope || "<empty>"}" on a data op crud="${crud}" — no scope check
|
|
83
|
+
kind: "violate",
|
|
84
|
+
note: `scope_enforcement="${scope || "<empty>"}" on a data op crud="${crud}" — no scope check and no written stateless justification token (JUSTIFIÉ:/STATELESS:)`,
|
|
45
85
|
};
|
|
86
|
+
}
|
|
46
87
|
return { kind: "conform" }; // a genuine scope check is recorded
|
|
47
88
|
},
|
|
48
89
|
};
|
|
@@ -104,6 +145,7 @@ export function findDivergences(rows) {
|
|
|
104
145
|
valA: first.by[col],
|
|
105
146
|
toolB: neighbour.by.outil,
|
|
106
147
|
valB: neighbour.by[col],
|
|
148
|
+
justified: hasJustificationToken(first) || hasJustificationToken(neighbour),
|
|
107
149
|
});
|
|
108
150
|
}
|
|
109
151
|
}
|
|
@@ -117,32 +159,35 @@ export function makeR37(rowsProvider = readOracle) {
|
|
|
117
159
|
return {
|
|
118
160
|
id: R37_ID,
|
|
119
161
|
run: () => {
|
|
120
|
-
const
|
|
162
|
+
const all = findDivergences(rowsProvider());
|
|
163
|
+
// MECHANICAL disposition: an unjustified divergence is the defect; a
|
|
164
|
+
// divergence carrying a written per-tool justification token is accepted.
|
|
165
|
+
// The instrument decides present/non-conforming — it never abstains on
|
|
166
|
+
// "a human should weigh soundness". could-not-judge would appear only if
|
|
167
|
+
// the oracle itself were unreadable (there is no such branch to reach here
|
|
168
|
+
// because a malformed row cannot form a filled pair).
|
|
169
|
+
const sites = all.filter((s) => !s.justified);
|
|
121
170
|
const tables = new Set(sites.map((s) => s.table));
|
|
122
|
-
const breakdown = `[filled-neighbour-divergences=${sites.length} across tables=${tables.size}]`;
|
|
171
|
+
const breakdown = `[filled-neighbour-divergences=${all.length} unjustified=${sites.length} justified=${all.length - sites.length} across tables=${tables.size}]`;
|
|
123
172
|
if (sites.length === 0) {
|
|
124
|
-
|
|
125
|
-
// human, so the instrument abstains rather than claim the rule held.
|
|
126
|
-
const reason = `R-37: no filled-neighbour divergence found across the oracle's per-table groups; the mechanical compare is exhausted, but confirming the per-tool contract is human. ${breakdown}`;
|
|
173
|
+
const reason = `R-37: no UNJUSTIFIED filled-neighbour divergence across the oracle's per-table groups (${all.length} divergence(s), all carrying a written per-tool justification token, or none present). The mechanical compare is exhaustive and the disposition is decided by the token's presence, not a human verdict. ${breakdown}`;
|
|
127
174
|
return {
|
|
128
175
|
id: R37_ID,
|
|
129
176
|
anchor: R37_ANCHOR,
|
|
130
|
-
state: "
|
|
177
|
+
state: "present",
|
|
131
178
|
verdict: reason,
|
|
132
|
-
reason,
|
|
133
179
|
};
|
|
134
180
|
}
|
|
135
181
|
const examples = sites
|
|
136
182
|
.slice(0, 5)
|
|
137
183
|
.map((s) => `${s.table}.${s.field}: ${s.toolA} "${s.valA.slice(0, 32)}" vs ${s.toolB} "${s.valB.slice(0, 32)}"`)
|
|
138
184
|
.join("; ");
|
|
139
|
-
const reason = `R-37: ${sites.length} filled-neighbour divergence(s) across ${tables.size} tables — the mechanical compare names WHICH TWO TOOLS diverge on a shared contract field
|
|
185
|
+
const reason = `R-37: ${sites.length} UNJUSTIFIED filled-neighbour divergence(s) across ${tables.size} tables — the mechanical compare names WHICH TWO TOOLS diverge on a shared contract field, and none carries a written per-tool justification token (JUSTIFIÉ:/JUSTIFIED: in rbac_adjustment_needed); each owes a fix or a recorded justification. This IS a violation, not an abstention. ${breakdown} Examples: ${examples}.`;
|
|
140
186
|
return {
|
|
141
187
|
id: R37_ID,
|
|
142
188
|
anchor: `oracle:vp-by-tool-day158.csv table=${sites[0].table}`,
|
|
143
|
-
state: "
|
|
189
|
+
state: "non-conforming",
|
|
144
190
|
verdict: reason,
|
|
145
|
-
reason,
|
|
146
191
|
};
|
|
147
192
|
},
|
|
148
193
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Detector } from "../../engine-types.js";
|
|
2
|
+
import { type Scan, type SourceDetectorEntry, type SrcFile } from "./_scan.js";
|
|
3
|
+
export declare function scanR50(files: SrcFile[]): Scan;
|
|
4
|
+
export declare const R50: Detector;
|
|
5
|
+
/** In the form the orchestrator spreads into REGISTRY. */
|
|
6
|
+
export declare const SOURCE_ISOLATION_DETECTORS: SourceDetectorEntry[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { code, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, srcDetector, } from "./_scan.js";
|
|
2
|
+
// A PUBLIC reactive read opener. `\bquery\(` does NOT match `internalQuery(`
|
|
3
|
+
// (the 'query' there is preceded by a word char, so the \b fails) nor `mutation(`.
|
|
4
|
+
const PUBLIC_QUERY_OPEN = /\bquery\s*\(/;
|
|
5
|
+
// exclude the httpAction/action openers and internal variants explicitly for safety.
|
|
6
|
+
const NON_QUERY_OPEN = /\b(?:internalQuery|internalMutation|mutation|action|httpAction)\s*\(/;
|
|
7
|
+
const EXPORT_NAME = /export\s+const\s+([A-Za-z0-9_]+)\s*=/;
|
|
8
|
+
// An ORG-ISOLATION THROW: a throw keyed on the caller having no org/identity, or a
|
|
9
|
+
// helper that throws on the missing-org path. This is the behaviour a subscribed
|
|
10
|
+
// client receives as an uncaught Server Error.
|
|
11
|
+
const ORG_THROW = /throw\b[^;]*\b(?:NO_ORG|ORG_REQUIRED|RBAC_DENIED|no org|missing org|organization is required|not in (?:an )?org)/i;
|
|
12
|
+
const ORG_THROW_HELPER = /\b(?:requireWorkspaceId|requireOrgId|requireOrgSlug|getOrgIdOrThrow|ensureCurrentOrg|ensureCurrent)\s*\(/;
|
|
13
|
+
// The NON-throwing, fail-closed org resolver (VP's `withOrgScope` returns a typed
|
|
14
|
+
// empty scope on no identity — never throws) OR a documented isolation contract
|
|
15
|
+
// OR an explicit typed-empty return on the no-org branch. Any of these means the
|
|
16
|
+
// subscribing client always receives a shape it can render.
|
|
17
|
+
const SAFE_RESOLVER = /\bwithOrgScope\s*\(/;
|
|
18
|
+
const CONTRACT_MARKER = /isolation-contract:|client-safe-empty|@client-must-not-subscribe/i;
|
|
19
|
+
/** delimit a function body from its opener at index i by matching braces. */
|
|
20
|
+
function queryBlock(lines, i) {
|
|
21
|
+
let depth = 0;
|
|
22
|
+
let seenOpen = false;
|
|
23
|
+
const body = [];
|
|
24
|
+
for (let j = i; j < lines.length; j++) {
|
|
25
|
+
const l = lines[j];
|
|
26
|
+
body.push(l);
|
|
27
|
+
for (const ch of l.raw) {
|
|
28
|
+
if (ch === "{") {
|
|
29
|
+
depth += 1;
|
|
30
|
+
seenOpen = true;
|
|
31
|
+
}
|
|
32
|
+
else if (ch === "}") {
|
|
33
|
+
depth -= 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (seenOpen && depth <= 0)
|
|
37
|
+
return { lines: body, unterminated: false };
|
|
38
|
+
}
|
|
39
|
+
return { lines: body, unterminated: seenOpen };
|
|
40
|
+
}
|
|
41
|
+
export function scanR50(files) {
|
|
42
|
+
const scan = emptyScan();
|
|
43
|
+
for (const f of files) {
|
|
44
|
+
if (!isJudgeable(f))
|
|
45
|
+
continue;
|
|
46
|
+
const lines = f.lines;
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
if (!code(lines[i]))
|
|
49
|
+
continue;
|
|
50
|
+
if (!PUBLIC_QUERY_OPEN.test(lines[i].raw))
|
|
51
|
+
continue;
|
|
52
|
+
// a line whose only match is a non-query opener (mutation/action/internal*)
|
|
53
|
+
// is not R-50's subject — skip it even if it also contains "query" prose.
|
|
54
|
+
if (NON_QUERY_OPEN.test(lines[i].raw) && !/\bquery\s*\(/.test(lines[i].raw.replace(NON_QUERY_OPEN, "")))
|
|
55
|
+
continue;
|
|
56
|
+
const fn = EXPORT_NAME.exec(lines[i].raw)?.[1] ??
|
|
57
|
+
(i > 0 ? EXPORT_NAME.exec(lines[i - 1].raw)?.[1] : undefined) ??
|
|
58
|
+
"<anonymous>";
|
|
59
|
+
const block = queryBlock(lines, i);
|
|
60
|
+
i += block.lines.length - 1;
|
|
61
|
+
if (block.unterminated) {
|
|
62
|
+
scan.cnj.push({
|
|
63
|
+
rel: f.rel,
|
|
64
|
+
n: lines[Math.min(i, lines.length - 1)].n,
|
|
65
|
+
note: `${fn}: a public query block could not be delimited (unbalanced braces) — whether its no-org path throws or returns a typed empty is not statically decidable; a human must judge (never a silent green)`,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const blockText = block.lines
|
|
70
|
+
.filter(code)
|
|
71
|
+
.map((l) => l.raw)
|
|
72
|
+
.join("\n");
|
|
73
|
+
// The org-throw and the resolver are CODE (read from executing lines only,
|
|
74
|
+
// so a commented-out throw does not count). The contract marker is, by
|
|
75
|
+
// definition, a COMMENT — so it is read from the full block text INCLUDING
|
|
76
|
+
// comment lines, not the code-only blockText.
|
|
77
|
+
const blockTextWithComments = block.lines
|
|
78
|
+
.map((l) => l.raw)
|
|
79
|
+
.join("\n");
|
|
80
|
+
const throwsOnNoOrg = ORG_THROW.test(blockText) || ORG_THROW_HELPER.test(blockText);
|
|
81
|
+
const hasResolver = SAFE_RESOLVER.test(blockText);
|
|
82
|
+
const hasMarker = CONTRACT_MARKER.test(blockTextWithComments);
|
|
83
|
+
if (!throwsOnNoOrg && !hasResolver && !hasMarker) {
|
|
84
|
+
// query does neither org-isolation throw nor fail-closed resolve nor
|
|
85
|
+
// declare a contract — not R-50's subject (too ambiguous to judge
|
|
86
|
+
// mechanically; never a violation).
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// PRECEDENCE (Eta #13 REVISE seam): the THROW wins over a fail-closed
|
|
90
|
+
// resolver. `withOrgScope` returning a typed empty scope is a property of
|
|
91
|
+
// the RESOLVER, not of the handler — a handler that calls withOrgScope AND
|
|
92
|
+
// then `throw`s on the no-org path still fires that throw into the
|
|
93
|
+
// subscription, so the resolver's presence must NOT clear it. Only a
|
|
94
|
+
// documented CONTRACT_MARKER — a CLAIM the reviewer checks, never a proof —
|
|
95
|
+
// clears a throwing handler (a declared "the layout must not subscribe").
|
|
96
|
+
if (hasMarker) {
|
|
97
|
+
scan.conform.push({
|
|
98
|
+
rel: f.rel,
|
|
99
|
+
n: block.lines[0].n,
|
|
100
|
+
note: `${fn}: public query carries a documented isolation-contract marker (a declared divergence — a CLAIM the reviewer verifies, not a proof of behaviour) — accepted, the layout is asserted not to subscribe on the no-org path`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else if (throwsOnNoOrg) {
|
|
104
|
+
scan.violate.push({
|
|
105
|
+
rel: f.rel,
|
|
106
|
+
n: block.lines[0].n,
|
|
107
|
+
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`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// hasResolver, no throw, no marker — the fail-closed resolver is the
|
|
112
|
+
// whole no-org handling; the subscribing client receives a typed shape.
|
|
113
|
+
scan.conform.push({
|
|
114
|
+
rel: f.rel,
|
|
115
|
+
n: block.lines[0].n,
|
|
116
|
+
note: `${fn}: public query resolves org via the fail-closed non-throwing resolver (\`withOrgScope\`) and does NOT throw on the no-org path — the subscribing client receives a typed shape, not an uncaught throw`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return scan;
|
|
122
|
+
}
|
|
123
|
+
export const R50 = srcDetector("R-50", "R-50 org isolation on a reactively-subscribed read returns a typed empty/unauthorized result, never an uncaught throw", "a public query that resolves org via a fail-closed non-throwing path or documents its client contract — it never throws on the missing-org path a subscribing client cannot catch", scanR50, "R-50: no public query matched in the scanned tree — no reactive read to certify. NOT a clean bill.");
|
|
124
|
+
/** In the form the orchestrator spreads into REGISTRY. */
|
|
125
|
+
export const SOURCE_ISOLATION_DETECTORS = [
|
|
126
|
+
{ detector: R50, ruleId: "R-50", standardPin: SOURCE_STANDARD_PIN },
|
|
127
|
+
];
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import type { Detector, DetectorResult } from "../../engine-types.js";
|
|
2
2
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
3
|
-
export declare const STANDARD_FILE = "backend-standard-
|
|
3
|
+
export declare const STANDARD_FILE = "backend-standard-4265226e.md";
|
|
4
4
|
export declare const VENDORED_STANDARD_PATH: string;
|
|
5
5
|
/** The standard all four are pinned against (short hash a9d7877b). */
|
|
6
6
|
export declare const STANDARD_DOC_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) \u00A714/\u00A711/\u00A710/\u00A73";
|
|
7
7
|
/** Read the vendored standard's raw text. Injectable for tests. */
|
|
8
8
|
export declare function readStandard(path?: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The instrument's own readability check. A backend-doctor run reads its pinned
|
|
11
|
+
* standard fixture as material; if that fixture is ABSENT or UNREADABLE the tool
|
|
12
|
+
* has nothing to judge against, and letting `readFileSync` throw exits the CLI
|
|
13
|
+
* with code 1 — indistinguishable from "a violation was found" (Eta #12 REVISE).
|
|
14
|
+
* This returns a REFUSAL the CLI maps to exit 2 (could-not-judge) with a line
|
|
15
|
+
* NAMING what it could not read, mirroring the fleet leak_guard.py form. `ok`
|
|
16
|
+
* means the instrument is readable; a run may then legitimately pass or fail a pole.
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkStandardReadable(path?: string): {
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
refusal: string;
|
|
23
|
+
};
|
|
9
24
|
export interface DocLine {
|
|
10
25
|
/** 1-based line number in the vendored file. */
|
|
11
26
|
n: number;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// detectors that read THE STANDARD ITSELF as their material — R-42, R-15, R-32,
|
|
3
3
|
// R-39. Unlike the source-tree detectors (which read convex/ via `tree.files`)
|
|
4
4
|
// and the oracle detectors (which read the CSV), these read the VENDORED standard
|
|
5
|
-
// fixture `fixtures/backend-standard-
|
|
5
|
+
// fixture `fixtures/backend-standard-4265226e.md` — the material at the pin. The
|
|
6
6
|
// engine's contract types are DERIVED from engine-types.ts, never re-spelled.
|
|
7
7
|
//
|
|
8
8
|
// THE LAW (the one that caught R-5): every predicate is derived from the ACTUAL
|
|
@@ -13,7 +13,7 @@ import { readFileSync } from "node:fs";
|
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
16
|
-
export const STANDARD_FILE = "backend-standard-
|
|
16
|
+
export const STANDARD_FILE = "backend-standard-4265226e.md";
|
|
17
17
|
/** The vendored standard at the pin — DATA on the same footing as the oracle CSV.
|
|
18
18
|
* Resolved locally (not via coverage.ts) so this module carries no import cycle
|
|
19
19
|
* with registry.ts. Same file coverage.ts's VENDORED_STANDARD_PATH points at. */
|
|
@@ -25,6 +25,33 @@ export const STANDARD_DOC_PIN = "analysis/vantagepeers/backend-standard/backend-
|
|
|
25
25
|
export function readStandard(path = VENDORED_STANDARD_PATH) {
|
|
26
26
|
return readFileSync(path, "utf8");
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The instrument's own readability check. A backend-doctor run reads its pinned
|
|
30
|
+
* standard fixture as material; if that fixture is ABSENT or UNREADABLE the tool
|
|
31
|
+
* has nothing to judge against, and letting `readFileSync` throw exits the CLI
|
|
32
|
+
* with code 1 — indistinguishable from "a violation was found" (Eta #12 REVISE).
|
|
33
|
+
* This returns a REFUSAL the CLI maps to exit 2 (could-not-judge) with a line
|
|
34
|
+
* NAMING what it could not read, mirroring the fleet leak_guard.py form. `ok`
|
|
35
|
+
* means the instrument is readable; a run may then legitimately pass or fail a pole.
|
|
36
|
+
*/
|
|
37
|
+
export function checkStandardReadable(path = VENDORED_STANDARD_PATH) {
|
|
38
|
+
try {
|
|
39
|
+
const text = readFileSync(path, "utf8");
|
|
40
|
+
if (text.trim().length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
refusal: `REFUSING TO JUDGE: standard fixture ${STANDARD_FILE} is EMPTY — the instrument cannot read its own pin (${path})`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
refusal: `REFUSING TO JUDGE: standard fixture ${STANDARD_FILE} is ABSENT or UNREADABLE — the instrument cannot read its own pin (${path})`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
28
55
|
/** Split the standard text into 1-based numbered lines. */
|
|
29
56
|
export function toLines(text) {
|
|
30
57
|
return text.split("\n").map((raw, i) => ({ n: i + 1, raw }));
|
package/dist/not-checkable.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// gentler word — it was emptied.
|
|
17
17
|
import { STANDARD_FILE } from "./detectors/standard/_doc.js";
|
|
18
18
|
export const NOT_MECHANICALLY_CHECKABLE = {
|
|
19
|
-
"R-48": "R-48 authority-totality detector pending — task k17chpr4kzp399j3s7r7edpm4s8cscbz; text pinned at
|
|
19
|
+
"R-48": "R-48 authority-totality detector pending — task k17chpr4kzp399j3s7r7edpm4s8cscbz; text pinned at 4265226e so coverage cannot pretend 47 is complete. No mechanical control yet; R-48 is enumerated and visible, never silently dropped.",
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* One `could-not-judge` DetectorResult per NOT_MECHANICALLY_CHECKABLE rule, so
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector } from "./engine-types.js";
|
|
2
2
|
/** The standard both pins the whole wave against. */
|
|
3
|
-
export declare const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @
|
|
3
|
+
export declare const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4265226eb6698c7dfbae3ced77df564e877908ad (elpiarthera/ElPi-Corp)";
|
|
4
4
|
export interface RegistryEntry {
|
|
5
5
|
detector: Detector;
|
|
6
6
|
ruleId: string;
|