@vantageos/backend-doctor 0.1.13 → 0.2.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 +19 -2
- package/dist/args.d.ts +16 -0
- package/dist/args.js +54 -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 +1195 -0
- package/dist/cli.js +217 -10
- package/dist/coverage-cli.js +8 -0
- package/dist/crud-coverage.d.ts +81 -0
- package/dist/crud-coverage.js +375 -0
- package/dist/detectors/oracle-axis1.d.ts +5 -3
- package/dist/detectors/oracle-axis1.js +24 -15
- package/dist/detectors/oracle-axis2.js +5 -6
- package/dist/detectors/predicates.d.ts +3 -0
- package/dist/detectors/predicates.js +81 -16
- package/dist/detectors/shared.js +5 -5
- package/dist/detectors/source/_scan.js +6 -1
- package/dist/detectors/source/backfill.js +9 -1
- package/dist/detectors/source/guard-helper-flow.d.ts +112 -0
- package/dist/detectors/source/guard-helper-flow.js +1173 -0
- package/dist/detectors/source/identity.d.ts +51 -2
- package/dist/detectors/source/identity.js +815 -14
- package/dist/detectors/source/isolation.d.ts +11 -1
- package/dist/detectors/source/isolation.js +106 -25
- package/dist/detectors/source/jobs.js +165 -2
- package/dist/detectors/source/readbudget.d.ts +1 -0
- package/dist/detectors/source/readbudget.js +67 -4
- package/dist/detectors/source/security.d.ts +16 -0
- package/dist/detectors/source/security.js +225 -7
- package/dist/detectors/source/write-isolation.d.ts +8 -1
- package/dist/detectors/source/write-isolation.js +64 -30
- package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
- package/dist/detectors/standard/r46-returns-validator.js +45 -14
- package/dist/detectors/structural.js +14 -10
- package/dist/oracle.d.ts +92 -3
- package/dist/oracle.js +229 -12
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +48 -6
- package/dist/report.d.ts +11 -1
- package/dist/report.js +15 -2
- package/dist/state-rank.d.ts +5 -0
- package/dist/state-rank.js +9 -0
- package/dist/tally.d.ts +35 -0
- package/dist/tally.js +76 -8
- package/dist/target.d.ts +32 -0
- package/dist/target.js +173 -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/vp-writer-tier-1e4ab36.csv +55 -0
- package/package.json +2 -2
package/dist/oracle.d.ts
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { DetectorResult } from "./engine-types.js";
|
|
2
|
+
/** The vendored day158 snapshot. It is a FIXTURE for this tool's own tests and
|
|
3
|
+
* for an operator who names it explicitly with `--oracle`. It is NEVER a default:
|
|
4
|
+
* nothing in the run path reads it unless a caller passes this path in. */
|
|
5
|
+
export declare const FIXTURE_ORACLE_PATH: string;
|
|
6
|
+
export declare const ORACLE_DIR = ".backend-doctor";
|
|
7
|
+
export declare const ORACLE_NAME = "vp-by-tool.csv";
|
|
8
|
+
export declare const VR_ORACLE_NAME = "vr-by-tool.csv";
|
|
9
|
+
export interface OracleFile {
|
|
10
|
+
status: "found";
|
|
11
|
+
path: string;
|
|
12
|
+
/** where the path came from: "flag" or "convention:<dir>". */
|
|
13
|
+
origin: string;
|
|
14
|
+
}
|
|
15
|
+
export interface OracleMissing {
|
|
16
|
+
status: "absent";
|
|
17
|
+
/** every path that was looked at, in order. */
|
|
18
|
+
searched: string[];
|
|
19
|
+
origin: string;
|
|
20
|
+
}
|
|
21
|
+
export type OracleLocation = OracleFile | OracleMissing;
|
|
22
|
+
export interface OracleSelection {
|
|
23
|
+
target: string;
|
|
24
|
+
vp: OracleLocation;
|
|
25
|
+
vr: OracleLocation;
|
|
26
|
+
/** the writer-tier side-car R-10 reads (see WRITER_TIER_NAME). Optional so a
|
|
27
|
+
* caller that binds only the two oracles keeps working: when undefined, the
|
|
28
|
+
* side-car is derived beside the bound vp oracle at read time. */
|
|
29
|
+
writerTier?: OracleLocation;
|
|
30
|
+
}
|
|
31
|
+
/** The convention dirs for `target`, in search order, never above the repo root. */
|
|
32
|
+
export declare function oracleDirs(target: string): string[];
|
|
33
|
+
/** Derive the oracle for `target`. Pure over the filesystem; never reads this tool's own directory. */
|
|
34
|
+
export declare function resolveOracle(target: string, opts?: {
|
|
35
|
+
oracle?: string;
|
|
36
|
+
vrOracle?: string;
|
|
37
|
+
writerTier?: string;
|
|
38
|
+
}): OracleSelection;
|
|
39
|
+
export declare const WRITER_TIER_NAME = "vp-writer-tier.csv";
|
|
40
|
+
export declare const WRITER_TIER_COLUMNS: readonly ["outil", "writer_tier", "writer_gate"];
|
|
41
|
+
export interface WriterTierEntry {
|
|
42
|
+
/** 1-based data-row index in the side-car. */
|
|
43
|
+
n: number;
|
|
44
|
+
outil: string;
|
|
45
|
+
writer_tier: string;
|
|
46
|
+
writer_gate: string;
|
|
47
|
+
}
|
|
48
|
+
export type WriterTierTable = {
|
|
49
|
+
status: "found";
|
|
50
|
+
path: string;
|
|
51
|
+
byTool: Map<string, WriterTierEntry[]>;
|
|
52
|
+
} | {
|
|
53
|
+
status: "absent";
|
|
54
|
+
searched: string[];
|
|
55
|
+
origin: string;
|
|
56
|
+
} | {
|
|
57
|
+
status: "unreadable";
|
|
58
|
+
path: string;
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
/** Parse one side-car. A missing required column or a row whose cell count
|
|
62
|
+
* differs from the header makes the WHOLE file unreadable — never a partial pass. */
|
|
63
|
+
export declare function readWriterTierFile(path: string): WriterTierTable;
|
|
64
|
+
/** The side-car bound for THIS run. Throws OracleAbsentError when no vp oracle is
|
|
65
|
+
* bound (R-10 then refuses like every oracle-backed rule); otherwise returns the
|
|
66
|
+
* table, or an `absent` record naming every path that was looked at. */
|
|
67
|
+
export declare function readWriterTier(): WriterTierTable;
|
|
68
|
+
/** One human line naming which oracle files a run used and where each came from. */
|
|
69
|
+
export declare function describeOracle(sel: OracleSelection): string;
|
|
70
|
+
/** Raised when an oracle-backed rule is asked to judge and no oracle describes the tree. */
|
|
71
|
+
export declare class OracleAbsentError extends Error {
|
|
72
|
+
readonly which: "vp" | "vr";
|
|
73
|
+
readonly searched: string[];
|
|
74
|
+
constructor(which: "vp" | "vr", searched: string[], detail?: string);
|
|
75
|
+
}
|
|
76
|
+
/** Bind the oracle for this run. The CLI calls it once with the target's resolution. */
|
|
77
|
+
export declare function activateOracle(sel: OracleSelection | null): void;
|
|
78
|
+
/** The oracle currently bound, or null. */
|
|
79
|
+
export declare function activeOracle(): OracleSelection | null;
|
|
80
|
+
/** Path of the bound VP oracle, or throws OracleAbsentError. */
|
|
81
|
+
export declare function requireOracle(): OracleFile;
|
|
82
|
+
/** Path of the bound VR oracle, or throws OracleAbsentError. */
|
|
83
|
+
export declare function requireVrOracle(): OracleFile;
|
|
84
|
+
/** `oracle:<file>` naming the bound oracle in a result anchor, or `oracle:ABSENT`. */
|
|
85
|
+
export declare function oracleLabel(): string;
|
|
86
|
+
/** Run an oracle-backed detector body. An absent oracle is could-not-judge —
|
|
87
|
+
* never present, never non-conforming. Anything else propagates untouched. */
|
|
88
|
+
export declare function refuseWithoutOracle(id: string, body: () => DetectorResult): DetectorResult;
|
|
3
89
|
/** The eighteen audit columns, in file order. §2 of the standard maps contract
|
|
4
90
|
* fields onto these column names. */
|
|
5
91
|
export declare const ORACLE_COLUMNS: readonly ["table", "outil", "T2_verdict", "T2_composant_cible", "crud", "operation_detail", "search_mecanisme", "surface", "rbac_qui", "scope_enforcement", "namespace_ou_org", "tokens_estime_contexte", "doublon_alias", "rbac_coherence_table", "rbac_adjustment_needed", "table_purpose", "table_conserver_supprimer", "statut_suppression"];
|
|
@@ -23,7 +109,10 @@ export interface OracleRow {
|
|
|
23
109
|
export declare function parseCsv(text: string): string[][];
|
|
24
110
|
/** Build typed rows from raw parsed cells (header already removed). */
|
|
25
111
|
export declare function toRows(dataRows: string[][]): OracleRow[];
|
|
26
|
-
/** Memoized read of
|
|
112
|
+
/** Memoized read of one oracle CSV, keyed by path. */
|
|
113
|
+
export declare function readOracleFile(path: string): OracleRow[];
|
|
114
|
+
/** The rows of the oracle bound for THIS run (derived from the target).
|
|
115
|
+
* Throws OracleAbsentError when none is bound or none was found. */
|
|
27
116
|
export declare function readOracle(): OracleRow[];
|
|
28
117
|
/**
|
|
29
118
|
* A real inventory row is one tool/table entry. The audit's trailing `TOTAL`
|
package/dist/oracle.js
CHANGED
|
@@ -5,12 +5,222 @@
|
|
|
5
5
|
// The tree the engine's loadTree builds is .ts-only and does NOT contain this
|
|
6
6
|
// CSV, so every first-wave detector ignores `tree` and consults this memoized
|
|
7
7
|
// reader instead.
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import {
|
|
11
|
+
import { resolveTarget } from "./target.js";
|
|
11
12
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
/**
|
|
13
|
-
|
|
13
|
+
/** The vendored day158 snapshot. It is a FIXTURE for this tool's own tests and
|
|
14
|
+
* for an operator who names it explicitly with `--oracle`. It is NEVER a default:
|
|
15
|
+
* nothing in the run path reads it unless a caller passes this path in. */
|
|
16
|
+
export const FIXTURE_ORACLE_PATH = join(here, "..", "fixtures", "vp-by-tool-day158.csv");
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// ORACLE RESOLUTION — the oracle describes the TREE UNDER JUDGEMENT, so it is
|
|
19
|
+
// found from the target the CLI was given, never from this tool's install dir.
|
|
20
|
+
//
|
|
21
|
+
// Rule (first hit wins, every candidate is listed in the refusal when none hits):
|
|
22
|
+
// 1. an explicit path (`--oracle <file>` / `--vr-oracle <file>`). An explicit
|
|
23
|
+
// path that does not exist is a REFUSAL, never a silent fall-through.
|
|
24
|
+
// 2. `<convexDir>/.backend-doctor/<name>`, then `<repoRoot>/.backend-doctor/<name>`,
|
|
25
|
+
// then `<target>/.backend-doctor/<name>` when the argument is neither — where
|
|
26
|
+
// repoRoot/convexDir come from resolveTarget (src/target.ts), so `convex` and
|
|
27
|
+
// `.` search the SAME list. Nothing ABOVE the repository root is ever read: a
|
|
28
|
+
// `.backend-doctor/` in a parent directory describes some other tree.
|
|
29
|
+
// where <name> is `vp-by-tool.csv` (the target backend's own inventory) and
|
|
30
|
+
// `vr-by-tool.csv` (the second backend R-40 compares against).
|
|
31
|
+
// No oracle found => the oracle-backed rules return could-not-judge.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
export const ORACLE_DIR = ".backend-doctor";
|
|
34
|
+
export const ORACLE_NAME = "vp-by-tool.csv";
|
|
35
|
+
export const VR_ORACLE_NAME = "vr-by-tool.csv";
|
|
36
|
+
/** The convention dirs for `target`, in search order, never above the repo root. */
|
|
37
|
+
export function oracleDirs(target) {
|
|
38
|
+
const scope = resolveTarget(target);
|
|
39
|
+
const dirs = [
|
|
40
|
+
...(scope.convexDir ? [join(scope.convexDir, ORACLE_DIR)] : []),
|
|
41
|
+
join(scope.repoRoot, ORACLE_DIR),
|
|
42
|
+
join(scope.input, ORACLE_DIR),
|
|
43
|
+
];
|
|
44
|
+
return [...new Set(dirs)];
|
|
45
|
+
}
|
|
46
|
+
function locate(target, name, explicit) {
|
|
47
|
+
if (explicit !== undefined) {
|
|
48
|
+
const p = resolve(explicit);
|
|
49
|
+
return existsSync(p)
|
|
50
|
+
? { status: "found", path: p, origin: "flag" }
|
|
51
|
+
: { status: "absent", searched: [p], origin: "flag (explicit path does not exist)" };
|
|
52
|
+
}
|
|
53
|
+
const dirs = oracleDirs(target);
|
|
54
|
+
const searched = dirs.map((d) => join(d, name));
|
|
55
|
+
for (let i = 0; i < searched.length; i++) {
|
|
56
|
+
if (existsSync(searched[i])) {
|
|
57
|
+
return { status: "found", path: searched[i], origin: `convention:${dirs[i]}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { status: "absent", searched, origin: "convention" };
|
|
61
|
+
}
|
|
62
|
+
/** Derive the oracle for `target`. Pure over the filesystem; never reads this tool's own directory. */
|
|
63
|
+
export function resolveOracle(target, opts = {}) {
|
|
64
|
+
const t = resolve(target);
|
|
65
|
+
const vp = locate(t, ORACLE_NAME, opts.oracle);
|
|
66
|
+
return {
|
|
67
|
+
target: t,
|
|
68
|
+
vp,
|
|
69
|
+
vr: locate(t, VR_ORACLE_NAME, opts.vrOracle),
|
|
70
|
+
writerTier: locateWriterTier(vp, opts.writerTier),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// WRITER-TIER SIDE-CAR — R-10's material.
|
|
75
|
+
//
|
|
76
|
+
// §2 splits authority into reader (R-5) and writer (R-10). The oracle's eighteen
|
|
77
|
+
// columns carry ONE authority column (`rbac_qui`, the reader), and a 19th column
|
|
78
|
+
// would make every row of every 18-column oracle malformed under toRows (measured
|
|
79
|
+
// on vantage-peers: 108/108). So the writer tier travels in a SIDE-CAR beside the
|
|
80
|
+
// oracle, joined by tool name (`outil`). Header-addressed, so a producer may add
|
|
81
|
+
// columns; the three below are required.
|
|
82
|
+
//
|
|
83
|
+
// Resolution: an explicit `--writer-tier <file>` (missing => absent, never a
|
|
84
|
+
// fall-through), else `<dirname(vp oracle)>/vp-writer-tier.csv`. A side-car that
|
|
85
|
+
// is absent or unreadable makes every WRITE row could-not-judge, naming the file.
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
export const WRITER_TIER_NAME = "vp-writer-tier.csv";
|
|
88
|
+
export const WRITER_TIER_COLUMNS = ["outil", "writer_tier", "writer_gate"];
|
|
89
|
+
function locateWriterTier(vp, explicit) {
|
|
90
|
+
if (explicit !== undefined) {
|
|
91
|
+
const p = resolve(explicit);
|
|
92
|
+
return existsSync(p)
|
|
93
|
+
? { status: "found", path: p, origin: "flag" }
|
|
94
|
+
: { status: "absent", searched: [p], origin: "flag (explicit path does not exist)" };
|
|
95
|
+
}
|
|
96
|
+
if (vp.status === "absent")
|
|
97
|
+
return { status: "absent", searched: [], origin: "no vp oracle to sit beside" };
|
|
98
|
+
const p = join(dirname(vp.path), WRITER_TIER_NAME);
|
|
99
|
+
return existsSync(p)
|
|
100
|
+
? { status: "found", path: p, origin: `beside-oracle:${dirname(vp.path)}` }
|
|
101
|
+
: { status: "absent", searched: [p], origin: "beside-oracle" };
|
|
102
|
+
}
|
|
103
|
+
const writerCache = new Map();
|
|
104
|
+
/** Parse one side-car. A missing required column or a row whose cell count
|
|
105
|
+
* differs from the header makes the WHOLE file unreadable — never a partial pass. */
|
|
106
|
+
export function readWriterTierFile(path) {
|
|
107
|
+
const hit = writerCache.get(path);
|
|
108
|
+
if (hit)
|
|
109
|
+
return hit;
|
|
110
|
+
const raw = parseCsv(readFileSync(path, "utf8")).filter((r) => !(r.length === 1 && r[0] === ""));
|
|
111
|
+
const [header = [], ...data] = raw;
|
|
112
|
+
const cols = header.map((h) => h.trim());
|
|
113
|
+
const missing = WRITER_TIER_COLUMNS.filter((c) => !cols.includes(c));
|
|
114
|
+
let table;
|
|
115
|
+
if (missing.length > 0) {
|
|
116
|
+
table = { status: "unreadable", path, reason: `header lacks required column(s) ${missing.join(", ")}` };
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const bad = data.findIndex((cells) => cells.length !== cols.length);
|
|
120
|
+
if (bad !== -1) {
|
|
121
|
+
table = {
|
|
122
|
+
status: "unreadable",
|
|
123
|
+
path,
|
|
124
|
+
reason: `row ${bad + 1} has ${data[bad].length} cells, header has ${cols.length}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const at = (c) => cols.indexOf(c);
|
|
129
|
+
const byTool = new Map();
|
|
130
|
+
data.forEach((cells, i) => {
|
|
131
|
+
const e = {
|
|
132
|
+
n: i + 1,
|
|
133
|
+
outil: cells[at("outil")].trim(),
|
|
134
|
+
writer_tier: cells[at("writer_tier")].trim(),
|
|
135
|
+
writer_gate: cells[at("writer_gate")].trim(),
|
|
136
|
+
};
|
|
137
|
+
byTool.set(e.outil, [...(byTool.get(e.outil) ?? []), e]);
|
|
138
|
+
});
|
|
139
|
+
table = { status: "found", path, byTool };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
writerCache.set(path, table);
|
|
143
|
+
return table;
|
|
144
|
+
}
|
|
145
|
+
/** The side-car bound for THIS run. Throws OracleAbsentError when no vp oracle is
|
|
146
|
+
* bound (R-10 then refuses like every oracle-backed rule); otherwise returns the
|
|
147
|
+
* table, or an `absent` record naming every path that was looked at. */
|
|
148
|
+
export function readWriterTier() {
|
|
149
|
+
const vp = requireOracle();
|
|
150
|
+
const loc = active?.writerTier ?? locateWriterTier(vp, undefined);
|
|
151
|
+
if (loc.status === "absent")
|
|
152
|
+
return { status: "absent", searched: loc.searched, origin: loc.origin };
|
|
153
|
+
return readWriterTierFile(loc.path);
|
|
154
|
+
}
|
|
155
|
+
/** One human line naming which oracle files a run used and where each came from. */
|
|
156
|
+
export function describeOracle(sel) {
|
|
157
|
+
const one = (label, l) => l.status === "found"
|
|
158
|
+
? `${label}=${l.path} (from ${l.origin})`
|
|
159
|
+
: `${label}=ABSENT (${l.origin}; searched: ${l.searched.join(", ")})`;
|
|
160
|
+
const wt = sel.writerTier ? `; ${one("writer-tier", sel.writerTier)}` : "";
|
|
161
|
+
return `oracle for target ${sel.target}: ${one("vp", sel.vp)}; ${one("vr", sel.vr)}${wt}`;
|
|
162
|
+
}
|
|
163
|
+
/** Raised when an oracle-backed rule is asked to judge and no oracle describes the tree. */
|
|
164
|
+
export class OracleAbsentError extends Error {
|
|
165
|
+
which;
|
|
166
|
+
searched;
|
|
167
|
+
constructor(which, searched, detail) {
|
|
168
|
+
super(`no ${which === "vp" ? "target-backend" : "second-backend (VR)"} oracle describes this tree — ${detail ?? `searched: ${searched.join(", ")}`}`);
|
|
169
|
+
this.which = which;
|
|
170
|
+
this.searched = searched;
|
|
171
|
+
this.name = "OracleAbsentError";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let active = null;
|
|
175
|
+
/** Bind the oracle for this run. The CLI calls it once with the target's resolution. */
|
|
176
|
+
export function activateOracle(sel) {
|
|
177
|
+
active = sel;
|
|
178
|
+
}
|
|
179
|
+
/** The oracle currently bound, or null. */
|
|
180
|
+
export function activeOracle() {
|
|
181
|
+
return active;
|
|
182
|
+
}
|
|
183
|
+
function requireLocation(which) {
|
|
184
|
+
if (!active) {
|
|
185
|
+
throw new OracleAbsentError(which, [], "no target was bound (activateOracle never called), so no oracle was derived");
|
|
186
|
+
}
|
|
187
|
+
const l = active[which];
|
|
188
|
+
if (l.status === "absent")
|
|
189
|
+
throw new OracleAbsentError(which, l.searched);
|
|
190
|
+
return l;
|
|
191
|
+
}
|
|
192
|
+
/** Path of the bound VP oracle, or throws OracleAbsentError. */
|
|
193
|
+
export function requireOracle() {
|
|
194
|
+
return requireLocation("vp");
|
|
195
|
+
}
|
|
196
|
+
/** Path of the bound VR oracle, or throws OracleAbsentError. */
|
|
197
|
+
export function requireVrOracle() {
|
|
198
|
+
return requireLocation("vr");
|
|
199
|
+
}
|
|
200
|
+
/** `oracle:<file>` naming the bound oracle in a result anchor, or `oracle:ABSENT`. */
|
|
201
|
+
export function oracleLabel() {
|
|
202
|
+
const l = active?.vp;
|
|
203
|
+
return l && l.status === "found" ? `oracle:${basename(l.path)}` : "oracle:ABSENT";
|
|
204
|
+
}
|
|
205
|
+
/** Run an oracle-backed detector body. An absent oracle is could-not-judge —
|
|
206
|
+
* never present, never non-conforming. Anything else propagates untouched. */
|
|
207
|
+
export function refuseWithoutOracle(id, body) {
|
|
208
|
+
try {
|
|
209
|
+
return body();
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
if (!(e instanceof OracleAbsentError))
|
|
213
|
+
throw e;
|
|
214
|
+
const reason = `${id}: REFUSED TO JUDGE — ${e.message}. An absent oracle is neither a clean bill nor a violation; supply one with --oracle/--vr-oracle or place it under ${ORACLE_DIR}/.`;
|
|
215
|
+
return {
|
|
216
|
+
id,
|
|
217
|
+
anchor: `oracle:ABSENT (${e.which})`,
|
|
218
|
+
state: "could-not-judge",
|
|
219
|
+
verdict: reason,
|
|
220
|
+
reason,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
14
224
|
/** The eighteen audit columns, in file order. §2 of the standard maps contract
|
|
15
225
|
* fields onto these column names. */
|
|
16
226
|
export const ORACLE_COLUMNS = [
|
|
@@ -106,16 +316,23 @@ export function toRows(dataRows) {
|
|
|
106
316
|
return { n, cells, by };
|
|
107
317
|
});
|
|
108
318
|
}
|
|
109
|
-
|
|
110
|
-
/** Memoized read of
|
|
111
|
-
export function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
319
|
+
const cache = new Map();
|
|
320
|
+
/** Memoized read of one oracle CSV, keyed by path. */
|
|
321
|
+
export function readOracleFile(path) {
|
|
322
|
+
const hit = cache.get(path);
|
|
323
|
+
if (hit)
|
|
324
|
+
return hit;
|
|
325
|
+
const text = readFileSync(path, "utf8");
|
|
115
326
|
const raw = parseCsv(text).filter((r) => !(r.length === 1 && r[0] === ""));
|
|
116
327
|
const [, ...data] = raw; // drop header
|
|
117
|
-
|
|
118
|
-
|
|
328
|
+
const rows = toRows(data);
|
|
329
|
+
cache.set(path, rows);
|
|
330
|
+
return rows;
|
|
331
|
+
}
|
|
332
|
+
/** The rows of the oracle bound for THIS run (derived from the target).
|
|
333
|
+
* Throws OracleAbsentError when none is bound or none was found. */
|
|
334
|
+
export function readOracle() {
|
|
335
|
+
return readOracleFile(requireOracle().path);
|
|
119
336
|
}
|
|
120
337
|
/**
|
|
121
338
|
* A real inventory row is one tool/table entry. The audit's trailing `TOTAL`
|
package/dist/registry.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export interface RegistryEntry {
|
|
|
6
6
|
ruleId: string;
|
|
7
7
|
standardPin: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Merge two detectors that judge halves of the SAME rule into one result: the
|
|
11
|
+
* worst state wins (non-conforming > could-not-judge > absent > present, the
|
|
12
|
+
* tally's own ranking), the anchor is the deciding half's, the title is the LATER
|
|
13
|
+
* (refined) half's, and both halves' verdicts are kept — nothing is dropped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeHalves(ruleId: string, a: Detector, b: Detector): Detector;
|
|
9
16
|
export declare const REGISTRY: RegistryEntry[];
|
|
10
17
|
/** The rule ids this wave mechanically CHECKS — derived from the registry. */
|
|
11
18
|
export declare function checkedRuleIds(): string[];
|
package/dist/registry.js
CHANGED
|
@@ -20,8 +20,38 @@ import { R41, SIBLING_PIN } from "./detectors/standard/r41-frontier.js";
|
|
|
20
20
|
import { R46_STANDARD_PIN, STANDARD_SOURCE_DETECTORS, } from "./detectors/standard/r46-returns-validator.js";
|
|
21
21
|
import { STANDARD_DOC_DETECTORS } from "./detectors/standard/standard-doc.js";
|
|
22
22
|
import { makeR14, makeR38 } from "./detectors/structural.js";
|
|
23
|
+
import { STATE_RANK } from "./state-rank.js";
|
|
23
24
|
/** The standard both pins the whole wave against. */
|
|
24
25
|
export const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4bcd605656ceff52d4f1593a9176cd6b988471be (elpiarthera/ElPi-Corp)";
|
|
26
|
+
/**
|
|
27
|
+
* Merge two detectors that judge halves of the SAME rule into one result: the
|
|
28
|
+
* worst state wins (non-conforming > could-not-judge > absent > present, the
|
|
29
|
+
* tally's own ranking), the anchor is the deciding half's, the title is the LATER
|
|
30
|
+
* (refined) half's, and both halves' verdicts are kept — nothing is dropped.
|
|
31
|
+
*/
|
|
32
|
+
export function mergeHalves(ruleId, a, b) {
|
|
33
|
+
return {
|
|
34
|
+
id: b.id,
|
|
35
|
+
run: (tree) => {
|
|
36
|
+
const ra = a.run(tree);
|
|
37
|
+
const rb = b.run(tree);
|
|
38
|
+
const decider = STATE_RANK[ra.state] > STATE_RANK[rb.state] ? ra : rb;
|
|
39
|
+
const verdict = `${ruleId} (two halves, worst wins): [${ra.id}] ${ra.state} — ${ra.verdict} || [${rb.id}] ${rb.state} — ${rb.verdict}`;
|
|
40
|
+
const reasons = [ra, rb]
|
|
41
|
+
.filter((r) => r.state === "could-not-judge")
|
|
42
|
+
.map((r) => r.reason ?? r.verdict);
|
|
43
|
+
return {
|
|
44
|
+
id: b.id,
|
|
45
|
+
anchor: decider.anchor,
|
|
46
|
+
state: decider.state,
|
|
47
|
+
verdict,
|
|
48
|
+
...(decider.state === "could-not-judge"
|
|
49
|
+
? { reason: reasons.join(" || ") }
|
|
50
|
+
: {}),
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
25
55
|
export const REGISTRY = [
|
|
26
56
|
...PER_ROW_PREDICATES.map((spec) => ({
|
|
27
57
|
detector: makeOracleDetector(spec),
|
|
@@ -51,7 +81,7 @@ export const REGISTRY = [
|
|
|
51
81
|
{
|
|
52
82
|
detector: makeR40(),
|
|
53
83
|
ruleId: "R-40",
|
|
54
|
-
standardPin: `${STANDARD_PIN} + oracle
|
|
84
|
+
standardPin: `${STANDARD_PIN} + second-backend oracle derived from the target (vr-by-tool.csv / --vr-oracle)`,
|
|
55
85
|
},
|
|
56
86
|
// The twenty source-tree detectors (built all at once — the wave plan cancelled).
|
|
57
87
|
// Each reads the REAL convex/ source; R-6/R-16 also carry a live half. Their
|
|
@@ -121,11 +151,23 @@ export const REGISTRY = [
|
|
|
121
151
|
// VENDORED standard itself, performs its mechanical/static half in full, and
|
|
122
152
|
// returns could-not-judge naming the sites a human/gate verdict is owed on.
|
|
123
153
|
...STANDARD_DOC_DETECTORS,
|
|
124
|
-
].
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
].reduce((acc, e) => {
|
|
155
|
+
// ONE result per rule. Two modules carry an R-31 half (writesafety: the
|
|
156
|
+
// bulk-WRITE self-schedule; readbudget: the refined read+write budget), and
|
|
157
|
+
// both used to print as two R-31 rows under two different titles. They are
|
|
158
|
+
// merged into one row titled by the refined standard text (4bcd605 :266).
|
|
159
|
+
const prev = acc.find((x) => x.ruleId === e.ruleId);
|
|
160
|
+
if (prev) {
|
|
161
|
+
prev.detector = mergeHalves(e.ruleId, prev.detector, e.detector);
|
|
162
|
+
return acc;
|
|
163
|
+
}
|
|
164
|
+
acc.push({
|
|
165
|
+
detector: e.detector,
|
|
166
|
+
ruleId: e.ruleId,
|
|
167
|
+
standardPin: STANDARD_PIN,
|
|
168
|
+
});
|
|
169
|
+
return acc;
|
|
170
|
+
}, []),
|
|
129
171
|
// R-41 — the ONE fully-mechanical rule of the nine: it reads the backend
|
|
130
172
|
// standard's §9 frontier AND the four vendored MCP-family siblings, resolving
|
|
131
173
|
// every cited S-/C-/E-/A- id against its sibling's defined-id set. present when
|
package/dist/report.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ReportContext {
|
|
|
17
17
|
* bit a reviewer). Empty string means a clean tree.
|
|
18
18
|
*/
|
|
19
19
|
treeStatus: string;
|
|
20
|
+
/** which oracle files the run used and where each came from (describeOracle). Optional so older callers still compile. */
|
|
21
|
+
oracle?: string;
|
|
20
22
|
}
|
|
21
23
|
/** Derive the committed report path from the target basename + the run date. */
|
|
22
24
|
export declare function reportPath(target: string, dateISO: string, dir: string): string;
|
|
@@ -32,4 +34,12 @@ export declare function buildReport(ctx: ReportContext, results: DetectorResult[
|
|
|
32
34
|
* <dir> if needed. Returns the absolute path written. The caller (CLI) supplies the
|
|
33
35
|
* ReportContext (clock + git) and the computed tally/verdict.
|
|
34
36
|
*/
|
|
35
|
-
export declare function writeReport(ctx: ReportContext, results: DetectorResult[], t: Tally, v: Verdict, dir: string, severity?: Map<string, Severity
|
|
37
|
+
export declare function writeReport(ctx: ReportContext, results: DetectorResult[], t: Tally, v: Verdict, dir: string, severity?: Map<string, Severity>,
|
|
38
|
+
/**
|
|
39
|
+
* Doctor-owned sections appended VERBATIM after the rule tables, each in its
|
|
40
|
+
* own fence. CR-1 renders through here: its contract is that every member of
|
|
41
|
+
* every class is NAMED, and the truncation that hid thirteen of fourteen
|
|
42
|
+
* violations is the defect that contract exists not to repeat — so this
|
|
43
|
+
* carries the section's own text rather than re-deriving or summarising it.
|
|
44
|
+
*/
|
|
45
|
+
extraSections?: string[]): string;
|
package/dist/report.js
CHANGED
|
@@ -94,6 +94,7 @@ export function buildReport(ctx, results, t, v, severity = deriveSeverityMap())
|
|
|
94
94
|
- **target commit**: \`${ctx.targetCommit}\`
|
|
95
95
|
- **standard commit** (pinned): \`${STANDARD_SHA}\`
|
|
96
96
|
- **tool version**: \`${ctx.toolVersion}\`
|
|
97
|
+
- **oracle**: ${ctx.oracle ?? "(not recorded)"}
|
|
97
98
|
- **run date**: ${ctx.dateISO}
|
|
98
99
|
- **machine tally**: \`${tallyLine(t)}\`
|
|
99
100
|
- **verdict**: ${v.summary}
|
|
@@ -130,9 +131,21 @@ ${present.length === 0 ? "_none._" : present.join(", ")}
|
|
|
130
131
|
* <dir> if needed. Returns the absolute path written. The caller (CLI) supplies the
|
|
131
132
|
* ReportContext (clock + git) and the computed tally/verdict.
|
|
132
133
|
*/
|
|
133
|
-
export function writeReport(ctx, results, t, v, dir, severity = deriveSeverityMap()
|
|
134
|
+
export function writeReport(ctx, results, t, v, dir, severity = deriveSeverityMap(),
|
|
135
|
+
/**
|
|
136
|
+
* Doctor-owned sections appended VERBATIM after the rule tables, each in its
|
|
137
|
+
* own fence. CR-1 renders through here: its contract is that every member of
|
|
138
|
+
* every class is NAMED, and the truncation that hid thirteen of fourteen
|
|
139
|
+
* violations is the defect that contract exists not to repeat — so this
|
|
140
|
+
* carries the section's own text rather than re-deriving or summarising it.
|
|
141
|
+
*/
|
|
142
|
+
extraSections = []) {
|
|
134
143
|
mkdirSync(dir, { recursive: true });
|
|
135
144
|
const path = reportPath(ctx.target, ctx.dateISO, dir);
|
|
136
|
-
|
|
145
|
+
const body = [
|
|
146
|
+
buildReport(ctx, results, t, v, severity),
|
|
147
|
+
...extraSections.map((sec) => `\n\n\`\`\`\n${sec}\n\`\`\`\n`),
|
|
148
|
+
].join("");
|
|
149
|
+
writeFileSync(path, body, "utf8");
|
|
137
150
|
return path;
|
|
138
151
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { State } from "./engine-types.js";
|
|
2
|
+
/** The four states, worst highest — every reduction (tally, a merged rule) picks
|
|
3
|
+
* the worst. A module of its own so registry.ts can use it without a cycle
|
|
4
|
+
* through tally -> coverage -> registry. */
|
|
5
|
+
export declare const STATE_RANK: Record<State, number>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The four states, worst highest — every reduction (tally, a merged rule) picks
|
|
2
|
+
* the worst. A module of its own so registry.ts can use it without a cycle
|
|
3
|
+
* through tally -> coverage -> registry. */
|
|
4
|
+
export const STATE_RANK = {
|
|
5
|
+
"non-conforming": 3,
|
|
6
|
+
"could-not-judge": 2,
|
|
7
|
+
absent: 1,
|
|
8
|
+
present: 0,
|
|
9
|
+
};
|
package/dist/tally.d.ts
CHANGED
|
@@ -53,5 +53,40 @@ export interface Verdict {
|
|
|
53
53
|
* on an exit that means abstention.
|
|
54
54
|
*/
|
|
55
55
|
export declare function verdict(t: Tally, results: DetectorResult[], severity?: Map<string, Severity>): Verdict;
|
|
56
|
+
/**
|
|
57
|
+
* The HANDLER-SURFACE rules — the source/*-isolation detectors that judge the
|
|
58
|
+
* public query/mutation surface (R-50 read-isolation, R-51 write-isolation).
|
|
59
|
+
* Reported APART from the document/oracle/process rules (R-32, R-37, R-40,
|
|
60
|
+
* R-48, …) so the handler-surface could-not-judge count is visible on its own
|
|
61
|
+
* and can be driven to 0 without conflating it with honest human-gate CNJ.
|
|
62
|
+
* Laurent doctrine: an instrument that cannot go red protects nothing — the
|
|
63
|
+
* handler-surface CNJ must reach 0 except genuine parse-fail / marker-borne /
|
|
64
|
+
* ambiguous-domination sites.
|
|
65
|
+
*/
|
|
66
|
+
export declare const HANDLER_SURFACE_RULES: readonly string[];
|
|
67
|
+
export interface HandlerSurfaceStats {
|
|
68
|
+
byState: Record<State, number>;
|
|
69
|
+
/** rule-level state per handler-surface ruleId (worst across its results). */
|
|
70
|
+
ruleState: Map<string, State>;
|
|
71
|
+
/** the handler-surface could-not-judge RULE count — R-50/R-51 as rules abstain
|
|
72
|
+
* or they don't. This is 0 whenever both rules reach a verdict, EVEN when the
|
|
73
|
+
* underlying sites include abstaining ones — so it must never be printed alone. */
|
|
74
|
+
couldNotJudge: number;
|
|
75
|
+
/** the handler-surface could-not-judge SITE count — the sum of every rule's
|
|
76
|
+
* scan.cnj length, parsed from each result's `could-not-judge=N` breakdown. A
|
|
77
|
+
* non-conforming rule can still carry abstaining SITES (Eta REVISE blocker 2:
|
|
78
|
+
* the CRM run had rules-abstaining=0 while 8 sites abstained). Both are printed. */
|
|
79
|
+
sitesAbstaining: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The handler-surface slice of the tally, computed over HANDLER_SURFACE_RULES
|
|
83
|
+
* only. Reduces each rule to its worst result state (same reduction as `tally`)
|
|
84
|
+
* so a rule with any non-conforming result reads non-conforming, and surfaces a
|
|
85
|
+
* distinct handler-surface could-not-judge count the CLI prints separately.
|
|
86
|
+
*/
|
|
87
|
+
export declare function handlerSurfaceStats(results: DetectorResult[], handlerRules?: readonly string[]): HandlerSurfaceStats;
|
|
88
|
+
/** The one-line handler-surface tally string the CLI prints, apart from the
|
|
89
|
+
* document/oracle/process rules. */
|
|
90
|
+
export declare function handlerSurfaceLine(s: HandlerSurfaceStats): string;
|
|
56
91
|
/** The one-line machine tally string the CLI prints. */
|
|
57
92
|
export declare function tallyLine(t: Tally): string;
|