@vantageos/backend-doctor 0.1.14 → 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 +91 -8
- package/dist/coverage-cli.js +8 -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/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.js +1 -7
- 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/cli.js
CHANGED
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
// DISTINCTLY. The target is a real directory so the engine's loadTree succeeds;
|
|
6
6
|
// the first-wave detectors ignore the tree and consult the oracle.
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
9
9
|
import { dirname, join, relative, resolve } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { renderTable, runDoctor } from "@vantageos/mcp-doctor";
|
|
12
|
+
import { parseArgs, USAGE } from "./args.js";
|
|
13
|
+
import { checkDistFreshness } from "./build-stamp.js";
|
|
14
|
+
import { CR1_ID, callerResolutionResult, callerResolutionSection, violations as crViolations, scanCallerResolution, } from "./caller-resolution.js";
|
|
12
15
|
import { coverage, coverageSummary } from "./coverage.js";
|
|
13
16
|
import { crudCoverageSection, extractReactRouterPaths, scanCrudCoverage, } from "./crud-coverage.js";
|
|
14
17
|
import { checkStandardReadable } from "./detectors/standard/_doc.js";
|
|
15
18
|
import { NOT_MECHANICALLY_CHECKABLE, notCheckableResults, } from "./not-checkable.js";
|
|
19
|
+
import { activateOracle, describeOracle, resolveOracle } from "./oracle.js";
|
|
16
20
|
import { REGISTRY } from "./registry.js";
|
|
17
21
|
import { writeReport } from "./report.js";
|
|
18
22
|
import { handlerSurfaceLine, handlerSurfaceStats, tally, tallyLine, verdict, } from "./tally.js";
|
|
23
|
+
import { describeScope, loadScopedTree, onScopedTree, resolveTarget, } from "./target.js";
|
|
19
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
25
|
/** `git -C <target> rev-parse HEAD`, or "not a git repo" when it is not one. */
|
|
21
26
|
function targetCommit(target) {
|
|
@@ -148,7 +153,49 @@ function probeRoutes(target) {
|
|
|
148
153
|
return { candidateDir, routePaths };
|
|
149
154
|
}
|
|
150
155
|
function main() {
|
|
151
|
-
|
|
156
|
+
// Refuse to judge from a dist/ compiled from other source than this checkout's
|
|
157
|
+
// (src/build-stamp.ts). Runs before anything is read, so no verdict is printed.
|
|
158
|
+
// Flags first: --version/--help answer without reading a target, and an unknown
|
|
159
|
+
// flag is a usage error — never a path handed to the filesystem (an `-v` used to
|
|
160
|
+
// become `<cwd>/-v` and die on an uncaught ENOENT stack).
|
|
161
|
+
const args = parseArgs(process.argv.slice(2));
|
|
162
|
+
if (args.kind === "version") {
|
|
163
|
+
console.log(toolVersion());
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
if (args.kind === "help") {
|
|
167
|
+
console.log(USAGE);
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
if (args.kind === "usage-error") {
|
|
171
|
+
console.error(`backend-doctor: ${args.message}\n\n${USAGE}`);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
const fresh = checkDistFreshness(here);
|
|
175
|
+
if (!fresh.ok) {
|
|
176
|
+
console.error(`backend-doctor: REFUSING TO RUN — ${fresh.reason}`);
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
const target = resolve(args.positional[0] ?? process.cwd());
|
|
180
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
181
|
+
console.error(`backend-doctor: target ${target} is not a readable directory — could not judge.\n\n${USAGE}`);
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
// ONE normalisation (src/target.ts): `convex` and `.` resolve to the same repo
|
|
185
|
+
// root, convex dir and file set, so every rule sees the same rels either way.
|
|
186
|
+
const scope = resolveTarget(target);
|
|
187
|
+
const tree = loadScopedTree(scope);
|
|
188
|
+
console.log(describeScope(scope, tree));
|
|
189
|
+
// The oracle is DERIVED from the target (never from this tool's own install
|
|
190
|
+
// directory) and the run says which file it used and where that came from.
|
|
191
|
+
// Absent => the oracle-backed rules refuse to judge (could-not-judge).
|
|
192
|
+
const oracleSel = resolveOracle(target, {
|
|
193
|
+
oracle: args.flags.oracle,
|
|
194
|
+
vrOracle: args.flags["vr-oracle"],
|
|
195
|
+
writerTier: args.flags["writer-tier"],
|
|
196
|
+
});
|
|
197
|
+
activateOracle(oracleSel);
|
|
198
|
+
console.log(describeOracle(oracleSel));
|
|
152
199
|
// Instrument self-check, BEFORE any judging: a backend-doctor run reads its
|
|
153
200
|
// pinned standard fixture as material. If that fixture is absent/unreadable the
|
|
154
201
|
// tool cannot judge — REFUSE with exit 2 (could-not-judge) naming what it could
|
|
@@ -163,18 +210,43 @@ function main() {
|
|
|
163
210
|
// pending detector) — so the tally's rule total equals the 48 derived rules and
|
|
164
211
|
// nothing is silently dropped.
|
|
165
212
|
const results = [
|
|
166
|
-
...runDoctor(
|
|
213
|
+
...runDoctor(scope.repoRoot, onScopedTree(tree, REGISTRY.map((e) => e.detector))),
|
|
167
214
|
...notCheckableResults(),
|
|
168
215
|
];
|
|
169
|
-
console.log(renderTable("backend-doctor (
|
|
216
|
+
console.log(renderTable("backend-doctor (oracle derived from target)", results));
|
|
170
217
|
// CRUD COVERAGE — an inventory/coverage report DIMENSION (not a numbered rule
|
|
171
218
|
// in the pinned 50-rule standard). Rows are EVERY defineTable table derived
|
|
172
219
|
// from the target schema at run time; the union of rows MUST equal the schema
|
|
173
220
|
// table set or the run is RED (fail-closed).
|
|
174
221
|
console.log("");
|
|
175
|
-
const crud = scanCrudCoverageForCli(target);
|
|
222
|
+
const crud = scanCrudCoverageForCli(scope.convexDir ?? target);
|
|
176
223
|
console.log(crudCoverageSection(crud));
|
|
177
224
|
const crudDefect = crud.status === "unreadable" || !crud.unionEqualsSchema;
|
|
225
|
+
// CALLER RESOLUTION (CR-1) — a DOCTOR-OWNED CRITICAL control, reported in its own
|
|
226
|
+
// section for the same reason CRUD coverage is: the pinned standard numbers
|
|
227
|
+
// R-1..R-51 and does not define it, so putting it in REGISTRY would make
|
|
228
|
+
// coverage.ts report a phantom rule and would forge the standard's pin. It is run
|
|
229
|
+
// through the engine's OWN loader (never a second tree traversal) and its
|
|
230
|
+
// violations drive the exit code — an unjustified NO-IDENTITY or a FAIL-OPEN
|
|
231
|
+
// registration is mechanical, so the gate refuses on it.
|
|
232
|
+
console.log("");
|
|
233
|
+
let crScan;
|
|
234
|
+
runDoctor(scope.repoRoot, onScopedTree(tree, [
|
|
235
|
+
{
|
|
236
|
+
id: CR1_ID,
|
|
237
|
+
run: (t) => {
|
|
238
|
+
crScan = scanCallerResolution(t);
|
|
239
|
+
return callerResolutionResult(crScan);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
]));
|
|
243
|
+
if (crScan === undefined) {
|
|
244
|
+
console.error("CR-1: the engine did not run the caller-resolution detector — refusing to report a surface that was never scanned.");
|
|
245
|
+
process.exit(2);
|
|
246
|
+
}
|
|
247
|
+
console.log(callerResolutionSection(crScan));
|
|
248
|
+
const crResult = callerResolutionResult(crScan);
|
|
249
|
+
const crDefect = crViolations(crScan).length > 0;
|
|
178
250
|
console.log("");
|
|
179
251
|
const cov = coverage();
|
|
180
252
|
console.log(coverageSummary(cov));
|
|
@@ -199,6 +271,7 @@ function main() {
|
|
|
199
271
|
// process rules, so its could-not-judge count is visible on its own and can be
|
|
200
272
|
// driven to 0 (Laurent: an instrument that cannot go red protects nothing).
|
|
201
273
|
console.log(handlerSurfaceLine(handlerSurfaceStats(results)));
|
|
274
|
+
console.log(`CALLER-RESOLUTION TALLY (CR-1, doctor-owned, outside the pinned rule set): state=${crResult.state} violations=${crViolations(crScan).length} | ${crResult.verdict.split("\n")[0]}`);
|
|
202
275
|
console.log(v.summary);
|
|
203
276
|
if (v.criticalViolations > 0) {
|
|
204
277
|
console.log(` critical violations: ${v.violations.critical.join(", ")}`);
|
|
@@ -210,13 +283,16 @@ function main() {
|
|
|
210
283
|
// the git facts are gathered HERE (a real process) and handed to the pure writer.
|
|
211
284
|
const ctx = {
|
|
212
285
|
target,
|
|
213
|
-
targetCommit: targetCommit(
|
|
286
|
+
targetCommit: targetCommit(scope.repoRoot),
|
|
214
287
|
toolVersion: toolVersion(),
|
|
215
288
|
dateISO: new Date().toISOString().slice(0, 10),
|
|
216
|
-
treeStatus: treeStatus(
|
|
289
|
+
treeStatus: treeStatus(scope.repoRoot),
|
|
290
|
+
oracle: describeOracle(oracleSel),
|
|
217
291
|
};
|
|
218
292
|
const reportDir = join(here, "..", "reports");
|
|
219
|
-
const path = writeReport(ctx, results, t, v, reportDir
|
|
293
|
+
const path = writeReport(ctx, results, t, v, reportDir, undefined, [
|
|
294
|
+
callerResolutionSection(crScan),
|
|
295
|
+
]);
|
|
220
296
|
console.log(`\nreport written: ${path}`);
|
|
221
297
|
if (!cov.ok) {
|
|
222
298
|
console.log("\nNOTE: coverage is INCOMPLETE — a rule is in neither bucket. See the summary above.");
|
|
@@ -229,6 +305,13 @@ function main() {
|
|
|
229
305
|
console.log("\nNOTE: CRUD coverage self-check FAILED — the rows do not cover every schema table (or the schema is unreadable). See the CRUD COVERAGE section above.");
|
|
230
306
|
process.exit(1);
|
|
231
307
|
}
|
|
308
|
+
// CR-1 is CRITICAL: a FAIL-OPEN or unjustified NO-IDENTITY public registration is
|
|
309
|
+
// a mechanical violation, so the gate refuses on it even when every numbered rule
|
|
310
|
+
// is satisfied. Reported BEFORE the numbered-rule exit so the reason is legible.
|
|
311
|
+
if (crDefect) {
|
|
312
|
+
console.log(`\nNOTE: CR-1 (caller resolution) FAILED — ${crViolations(crScan).length} public registration(s) serve or write data without resolving their caller. CRITICAL, no bypass. See the CALLER RESOLUTION section above; every member is named there.`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
232
315
|
process.exit(v.exitCode);
|
|
233
316
|
}
|
|
234
317
|
main();
|
package/dist/coverage-cli.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
// Prints the coverage proof over the TWO buckets — CHECKED and
|
|
3
3
|
// NOT_MECHANICALLY_CHECKABLE. Exits non-zero only if the union does not equal
|
|
4
4
|
// the derived rule ids (a rule in no bucket, in two, or a stale bucket entry).
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { checkDistFreshness } from "./build-stamp.js";
|
|
5
8
|
import { coverage, coverageSummary } from "./coverage.js";
|
|
6
9
|
import { NOT_MECHANICALLY_CHECKABLE } from "./not-checkable.js";
|
|
10
|
+
const fresh = checkDistFreshness(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
if (!fresh.ok) {
|
|
12
|
+
console.error(`backend-doctor --coverage: REFUSING TO RUN — ${fresh.reason}`);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
7
15
|
const r = coverage();
|
|
8
16
|
console.log(coverageSummary(r));
|
|
9
17
|
console.log("");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Detector } from "../engine-types.js";
|
|
2
2
|
import { type OracleRow } from "../oracle.js";
|
|
3
|
-
/**
|
|
3
|
+
/** The vendored day159 SECOND-backend (VantageRegistry) snapshot — a FIXTURE for
|
|
4
|
+
* this tool's tests, never a default (see oracle.ts resolveOracle).
|
|
4
5
|
* Vendored at pin a74a042b, blob b53e2626954a887303d11d944b6132a11dbd34f6. */
|
|
5
|
-
export declare const
|
|
6
|
+
export declare const FIXTURE_VR_ORACLE_PATH: string;
|
|
6
7
|
/** The vendored standard (the R-5 law) — the SAME pin coverage.ts parses. The
|
|
7
8
|
* §2 [shared] contract columns are DERIVED from this file, never hardcoded.
|
|
8
9
|
* Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
|
|
@@ -19,7 +20,8 @@ export interface Inventory {
|
|
|
19
20
|
/** Load any backend inventory CSV generically from its OWN header — the shared
|
|
20
21
|
* fields are DERIVED from these real headers, never a prose illustration. */
|
|
21
22
|
export declare function loadInventory(path: string): Inventory;
|
|
22
|
-
/** Memoized read of the
|
|
23
|
+
/** Memoized read of the VR inventory bound for THIS run (derived from the target).
|
|
24
|
+
* Throws OracleAbsentError when none was found. */
|
|
23
25
|
export declare function readVrInventory(): Inventory;
|
|
24
26
|
/** The VP inventory, adapted from the memoized VP oracle reader — the SAME rows
|
|
25
27
|
* the axis-2 detectors read, exposed with VP's real header. */
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Both inputs are IN HAND (VP oracle + VR oracle, both vendored + hash-checked).
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
|
-
import { dirname, join } from "node:path";
|
|
21
|
+
import { basename, dirname, join } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { ORACLE_COLUMNS, parseCsv, readOracle, } from "../oracle.js";
|
|
23
|
+
import { ORACLE_COLUMNS, parseCsv, activeOracle, oracleLabel, readOracle, refuseWithoutOracle, requireVrOracle, } from "../oracle.js";
|
|
24
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
/**
|
|
25
|
+
/** The vendored day159 SECOND-backend (VantageRegistry) snapshot — a FIXTURE for
|
|
26
|
+
* this tool's tests, never a default (see oracle.ts resolveOracle).
|
|
26
27
|
* Vendored at pin a74a042b, blob b53e2626954a887303d11d944b6132a11dbd34f6. */
|
|
27
|
-
export const
|
|
28
|
+
export const FIXTURE_VR_ORACLE_PATH = join(here, "..", "..", "fixtures", "vr-by-tool-day159.csv");
|
|
28
29
|
/** The vendored standard (the R-5 law) — the SAME pin coverage.ts parses. The
|
|
29
30
|
* §2 [shared] contract columns are DERIVED from this file, never hardcoded.
|
|
30
31
|
* Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
|
|
@@ -44,13 +45,17 @@ export function loadInventory(path) {
|
|
|
44
45
|
});
|
|
45
46
|
return { header, rows };
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
/** Memoized read of the
|
|
48
|
+
const vrCache = new Map();
|
|
49
|
+
/** Memoized read of the VR inventory bound for THIS run (derived from the target).
|
|
50
|
+
* Throws OracleAbsentError when none was found. */
|
|
49
51
|
export function readVrInventory() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const path = requireVrOracle().path;
|
|
53
|
+
const hit = vrCache.get(path);
|
|
54
|
+
if (hit)
|
|
55
|
+
return hit;
|
|
56
|
+
const inv = loadInventory(path);
|
|
57
|
+
vrCache.set(path, inv);
|
|
58
|
+
return inv;
|
|
54
59
|
}
|
|
55
60
|
/** The VP inventory, adapted from the memoized VP oracle reader — the SAME rows
|
|
56
61
|
* the axis-2 detectors read, exposed with VP's real header. */
|
|
@@ -161,13 +166,17 @@ export function compareBackends(vpProvider = readOracle, vrProvider = readVrInve
|
|
|
161
166
|
};
|
|
162
167
|
}
|
|
163
168
|
const R40_ID = "R-40 between-backend divergence";
|
|
164
|
-
|
|
169
|
+
function r40Anchor() {
|
|
170
|
+
const vr = activeOracle()?.vr;
|
|
171
|
+
const vrName = vr && vr.status === "found" ? basename(vr.path) : "ABSENT";
|
|
172
|
+
return `${oracleLabel()} vs oracle:${vrName}`;
|
|
173
|
+
}
|
|
165
174
|
/** Wrap the Axis-1 compare into a Detector. Providers are injectable for tests;
|
|
166
175
|
* production reads the two memoized real oracles. */
|
|
167
176
|
export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
168
177
|
return {
|
|
169
178
|
id: R40_ID,
|
|
170
|
-
run: () => {
|
|
179
|
+
run: () => refuseWithoutOracle(R40_ID, () => {
|
|
171
180
|
const cmp = compareBackends(vpProvider, vrProvider);
|
|
172
181
|
const { schema, sites } = cmp;
|
|
173
182
|
// the schema (column) divergence is itself a site — surfaced, never dropped.
|
|
@@ -188,7 +197,7 @@ export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
|
188
197
|
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
198
|
return {
|
|
190
199
|
id: R40_ID,
|
|
191
|
-
anchor:
|
|
200
|
+
anchor: r40Anchor(),
|
|
192
201
|
state: "present",
|
|
193
202
|
verdict: reason,
|
|
194
203
|
};
|
|
@@ -210,10 +219,10 @@ export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
|
210
219
|
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}.`;
|
|
211
220
|
return {
|
|
212
221
|
id: R40_ID,
|
|
213
|
-
anchor: `${
|
|
222
|
+
anchor: `${r40Anchor()} field=${sites[0]?.field ?? "schema"}`,
|
|
214
223
|
state: "non-conforming",
|
|
215
224
|
verdict: reason,
|
|
216
225
|
};
|
|
217
|
-
},
|
|
226
|
+
}),
|
|
218
227
|
};
|
|
219
228
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isInventoryRow, readOracle, } from "../oracle.js";
|
|
1
|
+
import { isInventoryRow, oracleLabel, readOracle, refuseWithoutOracle, } from "../oracle.js";
|
|
2
2
|
// A written justification token turns a flagged divergence from a DEFECT the
|
|
3
3
|
// author owes into a recorded, accepted divergence. It is the mechanical pole
|
|
4
4
|
// that replaced "a human decides whether the divergence is sound": the control
|
|
@@ -152,13 +152,12 @@ export function findDivergences(rows) {
|
|
|
152
152
|
return sites;
|
|
153
153
|
}
|
|
154
154
|
const R37_ID = "R-37 filled-neighbour divergence";
|
|
155
|
-
const R37_ANCHOR = "oracle:vp-by-tool-day158.csv";
|
|
156
155
|
/** Wrap the filled-neighbour compare into a Detector. `rowsProvider` is
|
|
157
156
|
* injectable for tests; production reads the memoized real oracle. */
|
|
158
157
|
export function makeR37(rowsProvider = readOracle) {
|
|
159
158
|
return {
|
|
160
159
|
id: R37_ID,
|
|
161
|
-
run: () => {
|
|
160
|
+
run: () => refuseWithoutOracle(R37_ID, () => {
|
|
162
161
|
const all = findDivergences(rowsProvider());
|
|
163
162
|
// MECHANICAL disposition: an unjustified divergence is the defect; a
|
|
164
163
|
// divergence carrying a written per-tool justification token is accepted.
|
|
@@ -173,7 +172,7 @@ export function makeR37(rowsProvider = readOracle) {
|
|
|
173
172
|
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}`;
|
|
174
173
|
return {
|
|
175
174
|
id: R37_ID,
|
|
176
|
-
anchor:
|
|
175
|
+
anchor: oracleLabel(),
|
|
177
176
|
state: "present",
|
|
178
177
|
verdict: reason,
|
|
179
178
|
};
|
|
@@ -185,10 +184,10 @@ export function makeR37(rowsProvider = readOracle) {
|
|
|
185
184
|
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}.`;
|
|
186
185
|
return {
|
|
187
186
|
id: R37_ID,
|
|
188
|
-
anchor:
|
|
187
|
+
anchor: `${oracleLabel()} table=${sites[0].table}`,
|
|
189
188
|
state: "non-conforming",
|
|
190
189
|
verdict: reason,
|
|
191
190
|
};
|
|
192
|
-
},
|
|
191
|
+
}),
|
|
193
192
|
};
|
|
194
193
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WriterTierTable } from "../oracle.js";
|
|
1
2
|
import { type RulePredicate } from "./shared.js";
|
|
2
3
|
/** Collapse a raw `surface` to one of the three canonical exposures, or null. */
|
|
3
4
|
export declare function collapseSurface(raw: string): string | null;
|
|
@@ -11,6 +12,8 @@ export declare const r4: RulePredicate;
|
|
|
11
12
|
export declare function collapseTier(raw: string): string | null;
|
|
12
13
|
export declare const r5: RulePredicate;
|
|
13
14
|
export declare const r9: RulePredicate;
|
|
15
|
+
export declare const WRITER_TIERS: ReadonlySet<string>;
|
|
16
|
+
export declare function makeR10(sideCar: () => WriterTierTable): RulePredicate;
|
|
14
17
|
export declare const r10: RulePredicate;
|
|
15
18
|
export declare const r12: RulePredicate;
|
|
16
19
|
export declare const r13: RulePredicate;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// The eleven per-row first-wave predicates. Each transcribes its §15 "The
|
|
2
|
+
// check" row and its §2 field mapping from the pinned standard, then applies
|
|
3
|
+
// that predicate to the oracle rows. Structural R-14 and R-38 live in
|
|
4
|
+
// structural.ts (they judge the inventory SCHEMA, not a row).
|
|
5
|
+
import { basename } from "node:path";
|
|
6
|
+
import { WRITER_TIER_NAME, readWriterTier } from "../oracle.js";
|
|
1
7
|
import { LIST_VERBS, WRITE_VERBS, isMcpPublic } from "./shared.js";
|
|
2
8
|
// ---- R-1 exposure declared ------------------------------------------------
|
|
3
9
|
// §2 line 32: exposure -> `surface` = `public` (MCP surface) / `internal` /
|
|
@@ -200,22 +206,81 @@ export const r9 = {
|
|
|
200
206
|
},
|
|
201
207
|
};
|
|
202
208
|
// ---- R-10 writer authority, a distinct resolved tier ----------------------
|
|
203
|
-
// §2 writer authority
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
// §2 line 36: writer authority is "a **distinct** tier from the same set, resolved
|
|
210
|
+
// from a verified identity"; §15 line 413: "writer tier present, distinct, one of
|
|
211
|
+
// five"; line 63: "Reader and writer are two fields". DISTINCT is a property of
|
|
212
|
+
// the FIELD, not of the value: the standard requires the writer tier to be
|
|
213
|
+
// RECORDED apart from the reader tier so a `read=public write=admin` divergence
|
|
214
|
+
// can be written — it nowhere forbids a tool whose writer tier happens to equal
|
|
215
|
+
// its reader tier (an org member reading and writing its own org's rows). So an
|
|
216
|
+
// equal VALUE conforms; a MERGED field (no separate record, or one cell naming
|
|
217
|
+
// several tiers) violates.
|
|
218
|
+
//
|
|
219
|
+
// Material: the writer-tier side-car (src/oracle.ts WRITER_TIER_NAME), joined on
|
|
220
|
+
// `outil`. Branches, per write row:
|
|
221
|
+
// side-car absent / unreadable -> cnj, naming the file(s) looked at
|
|
222
|
+
// no side-car row for the tool -> violate (tier absent)
|
|
223
|
+
// two rows with different tiers -> violate (merged)
|
|
224
|
+
// tier empty / `?` -> violate (unknown)
|
|
225
|
+
// tier naming several tiers (a/b, a+b) -> violate (merged)
|
|
226
|
+
// tier outside the five -> violate
|
|
227
|
+
// writer_gate empty or `mcp-scope:<t>` -> cnj: the tier was not derived from
|
|
228
|
+
// a gate on a write door, so "resolved from a verified identity" is not
|
|
229
|
+
// evidenced by the side-car (never a silent pass)
|
|
230
|
+
// otherwise -> conform
|
|
231
|
+
export const WRITER_TIERS = new Set([
|
|
232
|
+
"public",
|
|
233
|
+
"org-member",
|
|
234
|
+
"org-admin",
|
|
235
|
+
"fleet-internal",
|
|
236
|
+
"master",
|
|
237
|
+
]);
|
|
238
|
+
const UNKNOWN_TIER = new Set(["", "?", "unknown", "n/a", "tbd"]);
|
|
239
|
+
const MERGED_TIER = /[/+,|&]|\s(and|or|et|ou)\s/i;
|
|
240
|
+
export function makeR10(sideCar) {
|
|
241
|
+
return {
|
|
242
|
+
ruleId: "R-10",
|
|
243
|
+
id: "R-10 writer authority, distinct tier",
|
|
244
|
+
check: "writer tier recorded in its own field (side-car joined on `outil`), one of five, gated on a write door; equal-to-reader values conform (the field must be distinct, §2 line 36)",
|
|
245
|
+
classify: (r) => {
|
|
246
|
+
if (!WRITE_VERBS.has(r.by.crud))
|
|
247
|
+
return { kind: "na" };
|
|
248
|
+
const t = sideCar();
|
|
249
|
+
if (t.status === "absent")
|
|
250
|
+
return {
|
|
251
|
+
kind: "cnj",
|
|
252
|
+
note: `writer-tier side-car ${WRITER_TIER_NAME} ABSENT (${t.origin}; searched: ${t.searched.join(", ") || "<none>"}) — this oracle has write rows, so writer authority cannot be judged`,
|
|
253
|
+
};
|
|
254
|
+
if (t.status === "unreadable")
|
|
255
|
+
return { kind: "cnj", note: `writer-tier side-car ${t.path} unreadable — ${t.reason}` };
|
|
256
|
+
const file = basename(t.path);
|
|
257
|
+
const entries = t.byTool.get(r.by.outil) ?? [];
|
|
258
|
+
if (entries.length === 0)
|
|
259
|
+
return { kind: "violate", note: `no writer tier recorded for "${r.by.outil}" in ${file} — writer authority absent` };
|
|
260
|
+
const tiers = [...new Set(entries.map((e) => e.writer_tier))];
|
|
261
|
+
if (tiers.length > 1)
|
|
262
|
+
return {
|
|
263
|
+
kind: "violate",
|
|
264
|
+
note: `${file} records ${tiers.length} writer tiers for "${r.by.outil}" (${tiers.join(", ")}) — merged, not one tier`,
|
|
265
|
+
};
|
|
266
|
+
const tier = tiers[0];
|
|
267
|
+
if (UNKNOWN_TIER.has(tier.toLowerCase()))
|
|
268
|
+
return { kind: "violate", note: `writer_tier="${tier || "<empty>"}" for "${r.by.outil}" — unknown, not a tier` };
|
|
269
|
+
if (MERGED_TIER.test(tier))
|
|
270
|
+
return { kind: "violate", note: `writer_tier="${tier}" names several tiers — merged, not one tier` };
|
|
271
|
+
if (!WRITER_TIERS.has(tier))
|
|
272
|
+
return { kind: "violate", note: `writer_tier="${tier}" is none of the five tiers {${[...WRITER_TIERS].join(", ")}}` };
|
|
273
|
+
const gate = entries[0].writer_gate;
|
|
274
|
+
if (gate === "" || gate.startsWith("mcp-scope:"))
|
|
275
|
+
return {
|
|
276
|
+
kind: "cnj",
|
|
277
|
+
note: `writer_tier="${tier}" for "${r.by.outil}" rests on ${gate === "" ? "no writer_gate" : `the MCP-scope fallback "${gate}"`} — no write-door gate evidences resolution from a verified identity`,
|
|
278
|
+
};
|
|
279
|
+
return { kind: "conform" };
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export const r10 = makeR10(readWriterTier);
|
|
219
284
|
// ---- R-12 vector search declares namespace + topK -------------------------
|
|
220
285
|
// §2 vector boundary -> `search_mecanisme`. §15: real namespace + numeric topK.
|
|
221
286
|
export const r12 = {
|
package/dist/detectors/shared.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isInventoryRow, readOracle } from "../oracle.js";
|
|
1
|
+
import { isInventoryRow, oracleLabel, readOracle, refuseWithoutOracle, } from "../oracle.js";
|
|
2
2
|
/** Run a predicate over a row set, tallying per-kind. */
|
|
3
3
|
export function tally(spec, rows) {
|
|
4
4
|
const t = { conform: 0, violate: 0, cnj: 0, na: 0, violations: [], unjudged: [] };
|
|
@@ -52,7 +52,7 @@ export function evaluate(spec, rows) {
|
|
|
52
52
|
.join("; ")}. This is NOT a clean bill. ${breakdown}`;
|
|
53
53
|
return {
|
|
54
54
|
id: spec.id,
|
|
55
|
-
anchor: anchorFor(t.unjudged,
|
|
55
|
+
anchor: anchorFor(t.unjudged, oracleLabel()),
|
|
56
56
|
state: "could-not-judge",
|
|
57
57
|
verdict: reason,
|
|
58
58
|
reason,
|
|
@@ -65,14 +65,14 @@ export function evaluate(spec, rows) {
|
|
|
65
65
|
.join("; ");
|
|
66
66
|
return {
|
|
67
67
|
id: spec.id,
|
|
68
|
-
anchor: anchorFor(t.violations,
|
|
68
|
+
anchor: anchorFor(t.violations, oracleLabel()),
|
|
69
69
|
state: "non-conforming",
|
|
70
70
|
verdict: `${spec.ruleId}: ${t.violate} of ${applicable} applicable oracle rows VIOLATE (${spec.check}). ${breakdown} Examples: ${examples}.`,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
return {
|
|
74
74
|
id: spec.id,
|
|
75
|
-
anchor:
|
|
75
|
+
anchor: oracleLabel(),
|
|
76
76
|
state: "present",
|
|
77
77
|
verdict: `${spec.ruleId}: ${t.conform} applicable oracle rows conform (${spec.check}); 0 violations. ${breakdown}`,
|
|
78
78
|
};
|
|
@@ -85,7 +85,7 @@ export function evaluate(spec, rows) {
|
|
|
85
85
|
export function makeOracleDetector(spec, rowsProvider = readOracle) {
|
|
86
86
|
return {
|
|
87
87
|
id: spec.id,
|
|
88
|
-
run: () => evaluate(spec, rowsProvider()),
|
|
88
|
+
run: () => refuseWithoutOracle(spec.id, () => evaluate(spec, rowsProvider())),
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
// ---- small shared classifiers over oracle vocabulary --------------------
|
|
@@ -73,7 +73,15 @@ export function derivedStructures(files) {
|
|
|
73
73
|
if (FIELD_LINE.test(l.raw))
|
|
74
74
|
fields += 1;
|
|
75
75
|
const id = ID_REF.exec(l.raw);
|
|
76
|
-
|
|
76
|
+
// A SYSTEM table (`_storage`, `_scheduled_functions` — Convex reserves the
|
|
77
|
+
// leading underscore) is not a source a migration can backfill FROM: its
|
|
78
|
+
// rows are platform objects, not domain rows, and there is no
|
|
79
|
+
// `ctx.db.query("_storage")` that enumerates what the write path would
|
|
80
|
+
// have bound. A table whose only foreign key points at a system table
|
|
81
|
+
// (a TOFU ownership binding of uploaded blobs, kbUploads) is therefore
|
|
82
|
+
// not a derived structure in R-44's sense. The first NON-system FK is
|
|
83
|
+
// the source; a table with none is skipped.
|
|
84
|
+
if (id && !fkTarget && !id[1].startsWith("_"))
|
|
77
85
|
fkTarget = id[1];
|
|
78
86
|
if (HAS_INDEX.test(l.raw))
|
|
79
87
|
hasIndex = true;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
export type GuardHelperKind = "binds-arg" | "master-only";
|
|
3
|
+
export type HelperVerdict = {
|
|
4
|
+
verdict: "honoured";
|
|
5
|
+
} | {
|
|
6
|
+
verdict: "refused";
|
|
7
|
+
why: string;
|
|
8
|
+
} | {
|
|
9
|
+
verdict: "could-not-judge";
|
|
10
|
+
why: string;
|
|
11
|
+
};
|
|
12
|
+
/** A callee name, as seen from the function being judged, resolved to its
|
|
13
|
+
* definition in the tree (and to a resolver for names seen from THAT file). */
|
|
14
|
+
export type CallResolver = (name: string) => {
|
|
15
|
+
fn: ts.FunctionLikeDeclaration;
|
|
16
|
+
key: string;
|
|
17
|
+
resolve: CallResolver;
|
|
18
|
+
} | undefined;
|
|
19
|
+
/** Judge one helper function on its control flow. */
|
|
20
|
+
export declare function judgeHelperFunction(fn: ts.FunctionLikeDeclaration, kind: GuardHelperKind): HelperVerdict;
|
|
21
|
+
export interface FoundHelper {
|
|
22
|
+
name: string;
|
|
23
|
+
line: number;
|
|
24
|
+
exported: boolean;
|
|
25
|
+
judgement: HelperVerdict;
|
|
26
|
+
}
|
|
27
|
+
/** Every definition, in `source`, of a helper named in `kinds`. */
|
|
28
|
+
export declare function findHelperDefinitions(fileName: string, source: string, kinds: Readonly<Record<string, GuardHelperKind>>): FoundHelper[];
|
|
29
|
+
/** The arguments of each `await <helper>(…)` that is a DIRECT statement of the
|
|
30
|
+
* handler `name` (`export const name = mutation({ handler: … })`) and starts
|
|
31
|
+
* before `beforeLine`. A helper call nested in a branch, a try, or a closure of
|
|
32
|
+
* the handler is not a statement every path runs, so it is not returned.
|
|
33
|
+
* `null` = the handler's body could not be located in the file. */
|
|
34
|
+
export declare function topLevelHelperCalls(fileName: string, source: string, handlerName: string,
|
|
35
|
+
/** `null` = any identifier callee (judged by the caller on its body). */
|
|
36
|
+
helpers: ReadonlySet<string> | null, beforeLine: number): {
|
|
37
|
+
name: string;
|
|
38
|
+
args: string;
|
|
39
|
+
argNodes: readonly ts.Expression[];
|
|
40
|
+
line: number;
|
|
41
|
+
}[] | null;
|
|
42
|
+
export interface TopLevelFunction {
|
|
43
|
+
name: string;
|
|
44
|
+
line: number;
|
|
45
|
+
exported: boolean;
|
|
46
|
+
fn: ts.FunctionLikeDeclaration;
|
|
47
|
+
}
|
|
48
|
+
/** Every top-level function declaration and `const f = (…) => {…}` /
|
|
49
|
+
* `function (…) {…}` of a file. */
|
|
50
|
+
export declare function topLevelFunctions(fileName: string, source: string): TopLevelFunction[];
|
|
51
|
+
/** Judge a function as an IDENTITY RESOLVER (shape 1 above). */
|
|
52
|
+
export declare function judgeResolverFunction(fn: ts.FunctionLikeDeclaration): HelperVerdict;
|
|
53
|
+
export type ValidatorSummary = {
|
|
54
|
+
ok: false;
|
|
55
|
+
why: string;
|
|
56
|
+
} | {
|
|
57
|
+
ok: true;
|
|
58
|
+
/** every normal exit binds the target parameter (or is the master's) */
|
|
59
|
+
exitsBound: boolean;
|
|
60
|
+
/** every normal exit is the fleet master's */
|
|
61
|
+
exitsMaster: boolean;
|
|
62
|
+
/** no normal exit at all: it refuses every caller */
|
|
63
|
+
never: boolean;
|
|
64
|
+
};
|
|
65
|
+
export declare function summarizeValidator(fn: ts.FunctionLikeDeclaration, o: {
|
|
66
|
+
/** the parameter whose binding is asked; undefined = master-only asked */
|
|
67
|
+
boundIdx: number | undefined;
|
|
68
|
+
/** the parameter receiving the caller's verified scope, if any */
|
|
69
|
+
identityIdx: number | undefined;
|
|
70
|
+
/** the call's arguments (object literals become constants) */
|
|
71
|
+
callArgs: readonly ts.Expression[];
|
|
72
|
+
resolve: CallResolver;
|
|
73
|
+
key: string;
|
|
74
|
+
depth: number;
|
|
75
|
+
}): ValidatorSummary;
|
|
76
|
+
/** Judge a function as a REFUSING PREDICATE (shape 2 above): parameter
|
|
77
|
+
* `identityIdx` receives the caller's verified scope, `boundIdx` the value the
|
|
78
|
+
* caller then writes. */
|
|
79
|
+
export declare function judgePredicateFunction(fn: ts.FunctionLikeDeclaration, identityIdx: number, boundIdx: number): HelperVerdict;
|
|
80
|
+
/** What a mutation handler's own body declares and refuses before `beforeLine`. */
|
|
81
|
+
export interface HandlerGuards {
|
|
82
|
+
/** Top-level `const` locals: whether bound straight to an identity read, or
|
|
83
|
+
* to `await <callee>(…)` (the callee is then judged by the caller). */
|
|
84
|
+
consts: Map<string, {
|
|
85
|
+
identityRead: boolean;
|
|
86
|
+
awaitedCallee?: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** `if (!P(a, b, …)) { …throw }` statements: the predicate and its args. */
|
|
89
|
+
refusals: {
|
|
90
|
+
predicate: string;
|
|
91
|
+
args: {
|
|
92
|
+
text: string;
|
|
93
|
+
ident?: string;
|
|
94
|
+
memberBase?: string;
|
|
95
|
+
}[];
|
|
96
|
+
line: number;
|
|
97
|
+
}[];
|
|
98
|
+
}
|
|
99
|
+
/** Read the direct statements of handler `name` before `beforeLine`. `null`
|
|
100
|
+
* when the handler's body cannot be located. */
|
|
101
|
+
export declare function readHandlerGuards(fileName: string, source: string, handlerName: string, beforeLine: number): HandlerGuards | null;
|
|
102
|
+
/** A CONDITIONAL boundary value at a write site (`orgId: c ? a : b`): which arm
|
|
103
|
+
* reads the caller's `args`, and whether the condition is a positive
|
|
104
|
+
* `<local>.isMaster` test (the fleet carve-out, which may write what the
|
|
105
|
+
* master names). Read with the TypeScript parser on the write-site text. */
|
|
106
|
+
export interface ConditionalBoundary {
|
|
107
|
+
field: string;
|
|
108
|
+
masterBase?: string;
|
|
109
|
+
trueReadsArgs: boolean;
|
|
110
|
+
falseReadsArgs: boolean;
|
|
111
|
+
}
|
|
112
|
+
export declare function conditionalBoundaries(text: string, fields: ReadonlySet<string>): ConditionalBoundary[];
|