@vantageos/backend-doctor 0.1.0 → 0.1.2
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 +285 -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[];
|