@vantageos/backend-doctor 0.1.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 +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +94 -0
- package/dist/contract.d.ts +18 -0
- package/dist/contract.js +29 -0
- package/dist/coverage-cli.d.ts +2 -0
- package/dist/coverage-cli.js +17 -0
- package/dist/coverage.d.ts +32 -0
- package/dist/coverage.js +77 -0
- package/dist/detectors/oracle-axis1.d.ts +86 -0
- package/dist/detectors/oracle-axis1.js +221 -0
- package/dist/detectors/oracle-axis2.d.ts +22 -0
- package/dist/detectors/oracle-axis2.js +149 -0
- package/dist/detectors/predicates.d.ts +19 -0
- package/dist/detectors/predicates.js +334 -0
- package/dist/detectors/shared.d.ts +56 -0
- package/dist/detectors/shared.js +103 -0
- package/dist/detectors/source/_scan.d.ts +43 -0
- package/dist/detectors/source/_scan.js +82 -0
- package/dist/detectors/source/backfill.d.ts +27 -0
- package/dist/detectors/source/backfill.js +211 -0
- package/dist/detectors/source/contract.d.ts +33 -0
- package/dist/detectors/source/contract.js +385 -0
- package/dist/detectors/source/identity.d.ts +47 -0
- package/dist/detectors/source/identity.js +410 -0
- package/dist/detectors/source/jobs.d.ts +10 -0
- package/dist/detectors/source/jobs.js +209 -0
- package/dist/detectors/source/lifecycle.d.ts +6 -0
- package/dist/detectors/source/lifecycle.js +147 -0
- package/dist/detectors/source/readbudget.d.ts +6 -0
- package/dist/detectors/source/readbudget.js +133 -0
- package/dist/detectors/source/refusal.d.ts +6 -0
- package/dist/detectors/source/refusal.js +125 -0
- package/dist/detectors/source/security.d.ts +37 -0
- package/dist/detectors/source/security.js +392 -0
- package/dist/detectors/source/writesafety.d.ts +14 -0
- package/dist/detectors/source/writesafety.js +345 -0
- package/dist/detectors/standard/_doc.d.ts +66 -0
- package/dist/detectors/standard/_doc.js +152 -0
- package/dist/detectors/standard/r41-frontier.d.ts +58 -0
- package/dist/detectors/standard/r41-frontier.js +176 -0
- package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
- package/dist/detectors/standard/r46-returns-validator.js +474 -0
- package/dist/detectors/standard/standard-doc.d.ts +12 -0
- package/dist/detectors/standard/standard-doc.js +271 -0
- package/dist/detectors/structural.d.ts +14 -0
- package/dist/detectors/structural.js +83 -0
- package/dist/engine-types.d.ts +10 -0
- package/dist/engine-types.js +1 -0
- package/dist/not-checkable.d.ts +22 -0
- package/dist/not-checkable.js +37 -0
- package/dist/oracle.d.ts +33 -0
- package/dist/oracle.js +133 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +140 -0
- package/dist/report.d.ts +35 -0
- package/dist/report.js +138 -0
- package/dist/severity.d.ts +30 -0
- package/dist/severity.js +121 -0
- package/dist/tally.d.ts +57 -0
- package/dist/tally.js +152 -0
- package/dist/trigger.d.ts +47 -0
- package/dist/trigger.js +143 -0
- package/fixtures/backend-standard-51ec7af1.md +648 -0
- package/fixtures/backend-standard-a9d7877b.md +629 -0
- package/fixtures/mcp-app-standard-a74a042b.md +279 -0
- package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
- package/fixtures/mcp-server-standard-a74a042b.md +434 -0
- package/fixtures/mcp-standard-a74a042b.md +354 -0
- package/fixtures/vp-1200-green/.pin +1 -0
- package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
- package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
- package/fixtures/vp-1200-red/.pin +1 -0
- package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
- package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
- package/fixtures/vp-by-tool-day158.csv +190 -0
- package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
- package/fixtures/vp-convex/.pin +1 -0
- package/fixtures/vp-convex/bun.lock +783 -0
- package/fixtures/vp-convex/convex/auth.config.ts +8 -0
- package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
- package/fixtures/vp-convex/convex/convex.config.ts +14 -0
- package/fixtures/vp-convex/convex/credentials.ts +591 -0
- package/fixtures/vp-convex/convex/crons.ts +72 -0
- package/fixtures/vp-convex/convex/dashboard.ts +199 -0
- package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
- package/fixtures/vp-convex/convex/issues.ts +544 -0
- package/fixtures/vp-convex/convex/kb.ts +305 -0
- package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
- package/fixtures/vp-convex/convex/kbShared.ts +56 -0
- package/fixtures/vp-convex/convex/licenses.ts +443 -0
- package/fixtures/vp-convex/convex/memories.ts +406 -0
- package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
- package/fixtures/vp-convex/convex/messages.ts +971 -0
- package/fixtures/vp-convex/convex/migrations.ts +239 -0
- package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
- package/fixtures/vp-convex/convex/oauth.ts +1608 -0
- package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
- package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
- package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
- package/fixtures/vp-convex/convex/schema.ts +1229 -0
- package/fixtures/vp-convex/convex/tasks.ts +2782 -0
- package/fixtures/vp-r31-green/.pin +1 -0
- package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
- package/fixtures/vp-r31-red/.pin +1 -0
- package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
- package/fixtures/vr-by-tool-day159.csv +89 -0
- package/package.json +39 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// AXIS-1 between-backend divergence — R-40. §18: "two backends' shared field
|
|
2
|
+
// compared (mechanical); a divergence's written justification sound (human)."
|
|
3
|
+
// Prose (~line 570): two backends governed by this standard fill the same
|
|
4
|
+
// contract field from the same closed vocabulary; a divergence between two
|
|
5
|
+
// backends on a shared field publishes only with a WRITTEN JUSTIFICATION.
|
|
6
|
+
// Axis-2 (divergence WITHIN one backend) is R-37; Axis-1 (BETWEEN two backends)
|
|
7
|
+
// is R-40 — it fires the moment a second backend's inventory exists.
|
|
8
|
+
//
|
|
9
|
+
// This is a JUDGEMENT rule with a mechanical half. Like R-7/R-8/R-37 the
|
|
10
|
+
// instrument performs the mechanical half IN FULL — it names every concrete
|
|
11
|
+
// between-backend divergence site (which shared field, which value on VP vs
|
|
12
|
+
// which on VR) — and returns `could-not-judge`: whether each divergence carries
|
|
13
|
+
// a SOUND written justification is the human half. It never claims `present`
|
|
14
|
+
// (a clean bill) nor `absent` (does-not-apply) while real divergences exist.
|
|
15
|
+
//
|
|
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
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { ORACLE_COLUMNS, parseCsv, readOracle, } from "../oracle.js";
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
/** Absolute path to the pinned SECOND-backend (VantageRegistry) oracle fixture.
|
|
25
|
+
* Vendored at pin a74a042b, blob b53e2626954a887303d11d944b6132a11dbd34f6. */
|
|
26
|
+
export const VR_ORACLE_PATH = join(here, "..", "..", "fixtures", "vr-by-tool-day159.csv");
|
|
27
|
+
/** The vendored standard (the R-5 law) — the SAME pin coverage.ts parses. The
|
|
28
|
+
* §2 [shared] contract columns are DERIVED from this file, never hardcoded.
|
|
29
|
+
* Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
|
|
30
|
+
* import cycle. */
|
|
31
|
+
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-51ec7af1.md");
|
|
32
|
+
/** Load any backend inventory CSV generically from its OWN header — the shared
|
|
33
|
+
* fields are DERIVED from these real headers, never a prose illustration. */
|
|
34
|
+
export function loadInventory(path) {
|
|
35
|
+
const raw = parseCsv(readFileSync(path, "utf8")).filter((r) => !(r.length === 1 && r[0] === ""));
|
|
36
|
+
const header = (raw[0] ?? []).map((h) => h.trim());
|
|
37
|
+
const rows = raw.slice(1).map((cells) => {
|
|
38
|
+
const by = {};
|
|
39
|
+
header.forEach((h, i) => {
|
|
40
|
+
by[h] = (cells[i] ?? "").trim();
|
|
41
|
+
});
|
|
42
|
+
return by;
|
|
43
|
+
});
|
|
44
|
+
return { header, rows };
|
|
45
|
+
}
|
|
46
|
+
let vrCache = null;
|
|
47
|
+
/** Memoized read of the pinned VR inventory. */
|
|
48
|
+
export function readVrInventory() {
|
|
49
|
+
if (vrCache)
|
|
50
|
+
return vrCache;
|
|
51
|
+
vrCache = loadInventory(VR_ORACLE_PATH);
|
|
52
|
+
return vrCache;
|
|
53
|
+
}
|
|
54
|
+
/** The VP inventory, adapted from the memoized VP oracle reader — the SAME rows
|
|
55
|
+
* the axis-2 detectors read, exposed with VP's real header. */
|
|
56
|
+
export function vpInventory(rowsProvider = readOracle) {
|
|
57
|
+
return {
|
|
58
|
+
header: [...ORACLE_COLUMNS],
|
|
59
|
+
rows: rowsProvider()
|
|
60
|
+
.filter((r) => !r.malformed)
|
|
61
|
+
.map((r) => ({ ...r.by })),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** A real inventory row — the audit's trailing `TOTAL` aggregate is not a tool. */
|
|
65
|
+
function isInventoryEntry(by) {
|
|
66
|
+
if (by.table === "TOTAL")
|
|
67
|
+
return false;
|
|
68
|
+
if ((by.outil ?? "") === "" &&
|
|
69
|
+
(by.crud ?? "") === "" &&
|
|
70
|
+
(by.surface ?? "") === "")
|
|
71
|
+
return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
/** a value that actually FILLS the field — a blank or `n/a` is an omission, not a
|
|
75
|
+
* populated vocabulary member, so it never sources a between-backend divergence. */
|
|
76
|
+
function isFilled(v) {
|
|
77
|
+
const t = (v ?? "").trim().toLowerCase();
|
|
78
|
+
return t !== "" && t !== "n/a";
|
|
79
|
+
}
|
|
80
|
+
/** The SHARED FIELDS: the columns present in BOTH backends' real headers —
|
|
81
|
+
* computed as the intersection, never hardcoded. Used for the STRUCTURAL
|
|
82
|
+
* (schema) compare; the VALUE compare is narrowed to the §2 [shared] contract
|
|
83
|
+
* columns (see `sharedContractColumns`). */
|
|
84
|
+
export function sharedFields(vp, vr) {
|
|
85
|
+
const vrSet = new Set(vr);
|
|
86
|
+
return vp.filter((c) => vrSet.has(c));
|
|
87
|
+
}
|
|
88
|
+
/** The §2 CONTRACT columns the standard marks **[shared]** — DERIVED by parsing
|
|
89
|
+
* the vendored standard's §2 table (the R-5 law), never hardcoded. A [shared]
|
|
90
|
+
* row binds a contract field to its `audit column`, wrapped in backticks in the
|
|
91
|
+
* table's second cell. The five [shared] fields (operation verb, reader
|
|
92
|
+
* authority, writer authority, retrieval mechanism, pagination) resolve to the
|
|
93
|
+
* three audit columns `crud`, `rbac_qui`, `search_mecanisme` — reader/writer
|
|
94
|
+
* authority both live in the single merged `rbac_qui` (itself the R-14 defect),
|
|
95
|
+
* and retrieval mechanism + pagination both live in `search_mecanisme`.
|
|
96
|
+
*
|
|
97
|
+
* The R-40 VALUE compare runs ONLY over these closed-vocabulary columns. Free
|
|
98
|
+
* identifiers — `table`, `outil` (row identity) and `T2_verdict`,
|
|
99
|
+
* `T2_composant_cible` (audit bookkeeping) — are NOT vocabularies and are NEVER
|
|
100
|
+
* sited: comparing them counts row-identity noise as divergence and buries the
|
|
101
|
+
* one finding R-40 exists to catch. */
|
|
102
|
+
export function sharedContractColumns(standardPath = VENDORED_STANDARD_PATH) {
|
|
103
|
+
const text = readFileSync(standardPath, "utf8");
|
|
104
|
+
const cols = [];
|
|
105
|
+
for (const line of text.split("\n")) {
|
|
106
|
+
if (!line.trim().startsWith("|"))
|
|
107
|
+
continue;
|
|
108
|
+
const cells = line.split("|").map((c) => c.trim());
|
|
109
|
+
const contractCell = cells[1] ?? "";
|
|
110
|
+
if (!contractCell.includes("[shared]"))
|
|
111
|
+
continue;
|
|
112
|
+
const m = /`([^`]+)`/.exec(cells[2] ?? "");
|
|
113
|
+
if (m)
|
|
114
|
+
cols.push(m[1]);
|
|
115
|
+
}
|
|
116
|
+
return [...new Set(cols)];
|
|
117
|
+
}
|
|
118
|
+
export function schemaDivergence(vp, vr) {
|
|
119
|
+
const vpSet = new Set(vp);
|
|
120
|
+
const vrSet = new Set(vr);
|
|
121
|
+
return {
|
|
122
|
+
vpOnlyColumns: vp.filter((c) => !vrSet.has(c)),
|
|
123
|
+
vrOnlyColumns: vr.filter((c) => !vpSet.has(c)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** A field is a divergence SITE iff one backend fills it with a vocabulary
|
|
127
|
+
* member the other never uses. */
|
|
128
|
+
export function isSite(d) {
|
|
129
|
+
return d.vpOnly.length > 0 || d.vrOnly.length > 0;
|
|
130
|
+
}
|
|
131
|
+
/** Compare two inventories on each §2 [shared] contract field present in BOTH
|
|
132
|
+
* real headers. The scope is the CLOSED-VOCABULARY columns the standard marks
|
|
133
|
+
* [shared] (`crud`, `rbac_qui`, `search_mecanisme`) intersected with the two
|
|
134
|
+
* real headers — never all shared columns (that counts `table`/`outil`/`T2_*`
|
|
135
|
+
* free identifiers as vocabularies). Returns the per-field vocabulary comparison
|
|
136
|
+
* for every in-scope field (sites and agreeing fields alike) so a caller can
|
|
137
|
+
* prove BOTH poles: an agreeing value is not a site, a diverging value is. */
|
|
138
|
+
export function compareFields(vp, vr) {
|
|
139
|
+
const contract = new Set(sharedContractColumns());
|
|
140
|
+
const fields = sharedFields(vp.header, vr.header).filter((c) => contract.has(c));
|
|
141
|
+
const vpRows = vp.rows.filter(isInventoryEntry);
|
|
142
|
+
const vrRows = vr.rows.filter(isInventoryEntry);
|
|
143
|
+
return fields.map((field) => {
|
|
144
|
+
const vpVals = new Set(vpRows.map((r) => r[field]).filter((v) => v !== undefined && isFilled(v)));
|
|
145
|
+
const vrVals = new Set(vrRows.map((r) => r[field]).filter((v) => v !== undefined && isFilled(v)));
|
|
146
|
+
const shared = [...vpVals].filter((v) => vrVals.has(v)).sort();
|
|
147
|
+
const vpOnly = [...vpVals].filter((v) => !vrVals.has(v)).sort();
|
|
148
|
+
const vrOnly = [...vrVals].filter((v) => !vpVals.has(v)).sort();
|
|
149
|
+
return { field, shared, vpOnly, vrOnly };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export function compareBackends(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
153
|
+
const vp = vpInventory(vpProvider);
|
|
154
|
+
const vr = vrProvider();
|
|
155
|
+
const fields = compareFields(vp, vr);
|
|
156
|
+
return {
|
|
157
|
+
schema: schemaDivergence(vp.header, vr.header),
|
|
158
|
+
fields,
|
|
159
|
+
sites: fields.filter(isSite),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const R40_ID = "R-40 between-backend divergence";
|
|
163
|
+
const R40_ANCHOR = "oracle:vp-by-tool-day158.csv vs oracle:vr-by-tool-day159.csv";
|
|
164
|
+
/** Wrap the Axis-1 compare into a Detector. Providers are injectable for tests;
|
|
165
|
+
* production reads the two memoized real oracles. */
|
|
166
|
+
export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
167
|
+
return {
|
|
168
|
+
id: R40_ID,
|
|
169
|
+
run: () => {
|
|
170
|
+
const cmp = compareBackends(vpProvider, vrProvider);
|
|
171
|
+
const { schema, sites } = cmp;
|
|
172
|
+
// the schema (column) divergence is itself a site — surfaced, never dropped.
|
|
173
|
+
const schemaBits = [];
|
|
174
|
+
if (schema.vpOnlyColumns.length)
|
|
175
|
+
schemaBits.push(`VP-only column(s): ${schema.vpOnlyColumns.join(", ")}`);
|
|
176
|
+
if (schema.vrOnlyColumns.length)
|
|
177
|
+
schemaBits.push(`VR-only column(s): ${schema.vrOnlyColumns.join(", ")}`);
|
|
178
|
+
const schemaFinding = schemaBits.length
|
|
179
|
+
? `Between-schema divergence (R-14-class): ${schemaBits.join("; ")} — a shared field absent on one backend; a human must judge whether the schema difference is justified.`
|
|
180
|
+
: "No between-schema column divergence.";
|
|
181
|
+
const totalValueDivergences = sites.reduce((n, s) => n + s.vpOnly.length + s.vrOnly.length, 0);
|
|
182
|
+
const breakdown = `[shared-field-sites=${sites.length} value-divergences=${totalValueDivergences} schema-col-divergences=${schema.vpOnlyColumns.length + schema.vrOnlyColumns.length}]`;
|
|
183
|
+
if (sites.length === 0 && schemaBits.length === 0) {
|
|
184
|
+
// The two backends agree on every shared field's vocabulary — still not a
|
|
185
|
+
// clean bill: R-40's disposition (is each divergence justified?) is human,
|
|
186
|
+
// and here there is simply nothing owed. The instrument abstains rather
|
|
187
|
+
// than claim the rule held.
|
|
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}`;
|
|
189
|
+
return {
|
|
190
|
+
id: R40_ID,
|
|
191
|
+
anchor: R40_ANCHOR,
|
|
192
|
+
state: "could-not-judge",
|
|
193
|
+
verdict: reason,
|
|
194
|
+
reason,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const examples = sites
|
|
198
|
+
.slice(0, 5)
|
|
199
|
+
.map((s) => {
|
|
200
|
+
const vp = s.vpOnly
|
|
201
|
+
.slice(0, 2)
|
|
202
|
+
.map((v) => `"${v.slice(0, 28)}"`)
|
|
203
|
+
.join(", ");
|
|
204
|
+
const vr = s.vrOnly
|
|
205
|
+
.slice(0, 2)
|
|
206
|
+
.map((v) => `"${v.slice(0, 28)}"`)
|
|
207
|
+
.join(", ");
|
|
208
|
+
return `${s.field}: VP-only {${vp || "—"}} vs VR-only {${vr || "—"}}`;
|
|
209
|
+
})
|
|
210
|
+
.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; a human must judge whether each between-backend divergence carries a SOUND written justification. This is NOT a clean bill. ${schemaFinding} ${breakdown} Field sites: ${examples}.`;
|
|
212
|
+
return {
|
|
213
|
+
id: R40_ID,
|
|
214
|
+
anchor: `${R40_ANCHOR} field=${sites[0]?.field ?? "schema"}`,
|
|
215
|
+
state: "could-not-judge",
|
|
216
|
+
verdict: reason,
|
|
217
|
+
reason,
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Detector } from "../engine-types.js";
|
|
2
|
+
import { type OracleRow } from "../oracle.js";
|
|
3
|
+
import type { RulePredicate } from "./shared.js";
|
|
4
|
+
export declare const r7: RulePredicate;
|
|
5
|
+
export declare const r8: RulePredicate;
|
|
6
|
+
export interface DivergenceSite {
|
|
7
|
+
table: string;
|
|
8
|
+
field: string;
|
|
9
|
+
toolA: string;
|
|
10
|
+
valA: string;
|
|
11
|
+
toolB: string;
|
|
12
|
+
valB: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Group the inventory rows by table and, per shared field, name ONE diverging
|
|
16
|
+
* filled pair (the two tools whose filled values differ). The `n/a` pseudo-table
|
|
17
|
+
* groups unrelated tools and has no per-table contract, so it is excluded.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findDivergences(rows: OracleRow[]): DivergenceSite[];
|
|
20
|
+
/** Wrap the filled-neighbour compare into a Detector. `rowsProvider` is
|
|
21
|
+
* injectable for tests; production reads the memoized real oracle. */
|
|
22
|
+
export declare function makeR37(rowsProvider?: () => OracleRow[]): Detector;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { isInventoryRow, readOracle, } from "../oracle.js";
|
|
2
|
+
// ---- R-7 read/write coherence divergence (INCOHERENT* table-aggregate flag) -
|
|
3
|
+
// §18: "`INCOHERENT*` flag (mechanical); divergence justified (human)". The
|
|
4
|
+
// oracle's `rbac_coherence_table` carries the audit's table-aggregate coherence
|
|
5
|
+
// verdict; 105 rows are flagged `INCOHERENT...` (Finding 6). MECHANICAL: find
|
|
6
|
+
// every flagged row. HUMAN: whether each incoherence carries a written
|
|
7
|
+
// justification. So a flagged row is `cnj` (a human verdict is owed on it),
|
|
8
|
+
// never `violate` (which would usurp the human disposition) and never a pass.
|
|
9
|
+
export const r7 = {
|
|
10
|
+
ruleId: "R-7",
|
|
11
|
+
id: "R-7 read/write coherence divergence",
|
|
12
|
+
check: "row carries an `INCOHERENT*` coherence flag (mechanical); whether the read/write divergence is justified is human",
|
|
13
|
+
classify: (r) => {
|
|
14
|
+
const v = r.by.rbac_coherence_table;
|
|
15
|
+
if (/^INCOHERENT/i.test(v))
|
|
16
|
+
return {
|
|
17
|
+
kind: "cnj",
|
|
18
|
+
note: `rbac_coherence_table="${v}" — a read/write incoherence; a human must record whether this divergence is justified or the tool corrected`,
|
|
19
|
+
};
|
|
20
|
+
if (/^COHERENT/i.test(v))
|
|
21
|
+
return { kind: "conform" };
|
|
22
|
+
return { kind: "na" }; // no coherence verdict recorded on this row (e.g. n/a)
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
// ---- R-8 zero scope check on a data op (empty / `AUCUN`) --------------------
|
|
26
|
+
// §18: "empty/`AUCUN` + a data op (mechanical); justification sound (human)".
|
|
27
|
+
// MATERIAL: 5 rows carry `scope_enforcement=AUCUN(public_no_scope_check)` while
|
|
28
|
+
// performing a data op. MECHANICAL: find every such row. HUMAN: whether the
|
|
29
|
+
// stateless (no-scope) justification is sound. A matched row is `cnj`.
|
|
30
|
+
export const r8 = {
|
|
31
|
+
ruleId: "R-8",
|
|
32
|
+
id: "R-8 zero scope check on a data op",
|
|
33
|
+
check: "empty/`AUCUN` scope enforcement on a row carrying a data op (mechanical); whether the stateless justification is sound is human",
|
|
34
|
+
classify: (r) => {
|
|
35
|
+
const scope = r.by.scope_enforcement.trim();
|
|
36
|
+
const crud = r.by.crud.trim();
|
|
37
|
+
const hasDataOp = crud !== "" && crud.toLowerCase() !== "n/a";
|
|
38
|
+
if (!hasDataOp)
|
|
39
|
+
return { kind: "na" }; // no data op — R-8 does not apply
|
|
40
|
+
const noScope = scope === "" || /AUCUN/i.test(scope);
|
|
41
|
+
if (noScope)
|
|
42
|
+
return {
|
|
43
|
+
kind: "cnj",
|
|
44
|
+
note: `scope_enforcement="${scope || "<empty>"}" on a data op crud="${crud}" — no scope check; a human must judge whether the stateless justification is sound`,
|
|
45
|
+
};
|
|
46
|
+
return { kind: "conform" }; // a genuine scope check is recorded
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// ---- R-37 per-table filled-neighbour divergence ----------------------------
|
|
50
|
+
// §17.5 detector spec: "For each table, group its tool rows and, for every
|
|
51
|
+
// shared contract field (§2: exposure, verb effect-class, reader/writer
|
|
52
|
+
// authority, retrieval mechanism, isolation locus, boundary key), flag any two
|
|
53
|
+
// tools whose FILLED values differ." MECHANICAL: the filled-neighbour compare,
|
|
54
|
+
// naming WHICH TWO TOOLS diverge (the attribution R-7's lossy union label
|
|
55
|
+
// cannot carry). HUMAN: whether each divergence's per-tool justification is
|
|
56
|
+
// sound. §2 field → oracle column, using the ACTUAL column names.
|
|
57
|
+
const SHARED_FIELDS = [
|
|
58
|
+
{ field: "exposure", col: "surface" },
|
|
59
|
+
{ field: "verb effect-class", col: "crud" },
|
|
60
|
+
{ field: "reader/writer authority", col: "rbac_qui" },
|
|
61
|
+
{ field: "retrieval mechanism", col: "search_mecanisme" },
|
|
62
|
+
{ field: "isolation locus", col: "scope_enforcement" },
|
|
63
|
+
{ field: "boundary key", col: "namespace_ou_org" },
|
|
64
|
+
];
|
|
65
|
+
/** a value that actually FILLS the field — a blank or `n/a` is an omission
|
|
66
|
+
* (R-38's concern), not a filled neighbour, so it never sources a divergence. */
|
|
67
|
+
function isFilled(v) {
|
|
68
|
+
const t = v.trim().toLowerCase();
|
|
69
|
+
return t !== "" && t !== "n/a";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Group the inventory rows by table and, per shared field, name ONE diverging
|
|
73
|
+
* filled pair (the two tools whose filled values differ). The `n/a` pseudo-table
|
|
74
|
+
* groups unrelated tools and has no per-table contract, so it is excluded.
|
|
75
|
+
*/
|
|
76
|
+
export function findDivergences(rows) {
|
|
77
|
+
const inv = rows.filter(isInventoryRow);
|
|
78
|
+
const byTable = new Map();
|
|
79
|
+
for (const r of inv) {
|
|
80
|
+
const t = r.by.table;
|
|
81
|
+
if (t === "" || t === "n/a")
|
|
82
|
+
continue; // not a real per-table contract
|
|
83
|
+
const g = byTable.get(t);
|
|
84
|
+
if (g)
|
|
85
|
+
g.push(r);
|
|
86
|
+
else
|
|
87
|
+
byTable.set(t, [r]);
|
|
88
|
+
}
|
|
89
|
+
const sites = [];
|
|
90
|
+
for (const [table, group] of byTable) {
|
|
91
|
+
if (group.length < 2)
|
|
92
|
+
continue; // a single tool has no neighbour
|
|
93
|
+
for (const { field, col } of SHARED_FIELDS) {
|
|
94
|
+
const filled = group.filter((r) => isFilled(r.by[col]));
|
|
95
|
+
if (filled.length < 2)
|
|
96
|
+
continue;
|
|
97
|
+
const first = filled[0];
|
|
98
|
+
const neighbour = filled.find((r) => r.by[col] !== first.by[col]);
|
|
99
|
+
if (neighbour)
|
|
100
|
+
sites.push({
|
|
101
|
+
table,
|
|
102
|
+
field,
|
|
103
|
+
toolA: first.by.outil,
|
|
104
|
+
valA: first.by[col],
|
|
105
|
+
toolB: neighbour.by.outil,
|
|
106
|
+
valB: neighbour.by[col],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return sites;
|
|
111
|
+
}
|
|
112
|
+
const R37_ID = "R-37 filled-neighbour divergence";
|
|
113
|
+
const R37_ANCHOR = "oracle:vp-by-tool-day158.csv";
|
|
114
|
+
/** Wrap the filled-neighbour compare into a Detector. `rowsProvider` is
|
|
115
|
+
* injectable for tests; production reads the memoized real oracle. */
|
|
116
|
+
export function makeR37(rowsProvider = readOracle) {
|
|
117
|
+
return {
|
|
118
|
+
id: R37_ID,
|
|
119
|
+
run: () => {
|
|
120
|
+
const sites = findDivergences(rowsProvider());
|
|
121
|
+
const tables = new Set(sites.map((s) => s.table));
|
|
122
|
+
const breakdown = `[filled-neighbour-divergences=${sites.length} across tables=${tables.size}]`;
|
|
123
|
+
if (sites.length === 0) {
|
|
124
|
+
// No divergence anywhere — still not a clean bill: R-37's disposition is
|
|
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}`;
|
|
127
|
+
return {
|
|
128
|
+
id: R37_ID,
|
|
129
|
+
anchor: R37_ANCHOR,
|
|
130
|
+
state: "could-not-judge",
|
|
131
|
+
verdict: reason,
|
|
132
|
+
reason,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const examples = sites
|
|
136
|
+
.slice(0, 5)
|
|
137
|
+
.map((s) => `${s.table}.${s.field}: ${s.toolA} "${s.valA.slice(0, 32)}" vs ${s.toolB} "${s.valB.slice(0, 32)}"`)
|
|
138
|
+
.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; a human must judge whether each per-tool justification is sound. This is NOT a clean bill. ${breakdown} Examples: ${examples}.`;
|
|
140
|
+
return {
|
|
141
|
+
id: R37_ID,
|
|
142
|
+
anchor: `oracle:vp-by-tool-day158.csv table=${sites[0].table}`,
|
|
143
|
+
state: "could-not-judge",
|
|
144
|
+
verdict: reason,
|
|
145
|
+
reason,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RulePredicate } from "./shared.js";
|
|
2
|
+
/** Collapse a raw `surface` to one of the three canonical exposures, or null. */
|
|
3
|
+
export declare function collapseSurface(raw: string): string | null;
|
|
4
|
+
export declare const r1: RulePredicate;
|
|
5
|
+
export declare const r2: RulePredicate;
|
|
6
|
+
export declare const r3: RulePredicate;
|
|
7
|
+
export declare const r4: RulePredicate;
|
|
8
|
+
/** Collapse a raw `rbac_qui` value to one of the five canonical tiers, or null
|
|
9
|
+
* if it maps to none. `fleet_interne` → fleet-internal; every `toutes_orgs(...)`
|
|
10
|
+
* variant → an org tier (the documented collapse, standard line 61). */
|
|
11
|
+
export declare function collapseTier(raw: string): string | null;
|
|
12
|
+
export declare const r5: RulePredicate;
|
|
13
|
+
export declare const r9: RulePredicate;
|
|
14
|
+
export declare const r10: RulePredicate;
|
|
15
|
+
export declare const r12: RulePredicate;
|
|
16
|
+
export declare const r13: RulePredicate;
|
|
17
|
+
export declare const r27: RulePredicate;
|
|
18
|
+
export declare const r28: RulePredicate;
|
|
19
|
+
export declare const PER_ROW_PREDICATES: RulePredicate[];
|