@vantageos/backend-doctor 0.1.14 → 0.3.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 +66 -5
- package/dist/args.d.ts +16 -0
- package/dist/args.js +72 -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 +1198 -0
- package/dist/cli.js +249 -17
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage-cli.js +8 -0
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.d.ts +5 -3
- package/dist/detectors/oracle-axis1.js +28 -18
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +37 -22
- package/dist/detectors/predicates.d.ts +3 -0
- package/dist/detectors/predicates.js +93 -19
- package/dist/detectors/shared.js +8 -7
- package/dist/detectors/source/_ast.d.ts +92 -0
- package/dist/detectors/source/_ast.js +353 -0
- package/dist/detectors/source/_scan.js +4 -3
- package/dist/detectors/source/backfill.js +71 -18
- package/dist/detectors/source/contract.js +21 -13
- package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
- package/dist/detectors/source/cross-tenant-write.js +1408 -0
- package/dist/detectors/source/guard-helper-flow.d.ts +134 -0
- package/dist/detectors/source/guard-helper-flow.js +1216 -0
- package/dist/detectors/source/identity-by-id.d.ts +80 -0
- package/dist/detectors/source/identity-by-id.js +1783 -0
- package/dist/detectors/source/identity.d.ts +58 -2
- package/dist/detectors/source/identity.js +1158 -28
- package/dist/detectors/source/isolation.d.ts +11 -1
- package/dist/detectors/source/isolation.js +106 -25
- package/dist/detectors/source/jobs.js +178 -4
- package/dist/detectors/source/lifecycle.js +14 -2
- package/dist/detectors/source/local-identity.d.ts +26 -0
- package/dist/detectors/source/local-identity.js +661 -0
- package/dist/detectors/source/package-principal.d.ts +9 -0
- package/dist/detectors/source/package-principal.js +177 -0
- package/dist/detectors/source/r6-shapes.d.ts +20 -0
- package/dist/detectors/source/r6-shapes.js +285 -0
- package/dist/detectors/source/readbudget.d.ts +1 -0
- package/dist/detectors/source/readbudget.js +358 -4
- package/dist/detectors/source/recipient-by-id.d.ts +40 -0
- package/dist/detectors/source/recipient-by-id.js +1108 -0
- package/dist/detectors/source/refusal.d.ts +14 -0
- package/dist/detectors/source/refusal.js +75 -2
- package/dist/detectors/source/security.d.ts +16 -0
- package/dist/detectors/source/security.js +680 -33
- package/dist/detectors/source/write-isolation.d.ts +8 -1
- package/dist/detectors/source/write-isolation.js +64 -30
- package/dist/detectors/source/writesafety.js +27 -14
- package/dist/detectors/standard/_doc.d.ts +1 -1
- package/dist/detectors/standard/_doc.js +5 -4
- package/dist/detectors/standard/r41-frontier.js +3 -2
- package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
- package/dist/detectors/standard/r46-returns-validator.js +48 -16
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +84 -48
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +11 -0
- package/dist/oracle.d.ts +92 -3
- package/dist/oracle.js +229 -12
- package/dist/registry.d.ts +8 -1
- package/dist/registry.js +74 -8
- package/dist/report.d.ts +11 -1
- package/dist/report.js +15 -2
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/state-rank.d.ts +5 -0
- package/dist/state-rank.js +9 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +15 -13
- package/dist/target.d.ts +32 -0
- package/dist/target.js +173 -0
- package/dist/violation-count.d.ts +48 -0
- package/dist/violation-count.js +94 -0
- package/fixtures/backend-standard-2825122.md +722 -0
- package/fixtures/backend-standard-4dcd3e0.md +722 -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/rule-classes-2825122.json +228 -0
- package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,17 +5,25 @@
|
|
|
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, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } 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 {
|
|
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";
|
|
15
|
+
import { coverage, coverageSummary, deriveRuleIds } from "./coverage.js";
|
|
13
16
|
import { crudCoverageSection, extractReactRouterPaths, scanCrudCoverage, } from "./crud-coverage.js";
|
|
17
|
+
import { CR2_ID, localIdentityResult, localIdentitySection, scanLocalIdentity, } from "./detectors/source/local-identity.js";
|
|
14
18
|
import { checkStandardReadable } from "./detectors/standard/_doc.js";
|
|
15
19
|
import { NOT_MECHANICALLY_CHECKABLE, notCheckableResults, } from "./not-checkable.js";
|
|
20
|
+
import { activateOracle, describeOracle, resolveOracle } from "./oracle.js";
|
|
16
21
|
import { REGISTRY } from "./registry.js";
|
|
17
22
|
import { writeReport } from "./report.js";
|
|
23
|
+
import { buildEvidence, classifyRules, countMechanical, mechanicalLine, } from "./rule-class.js";
|
|
18
24
|
import { handlerSurfaceLine, handlerSurfaceStats, tally, tallyLine, verdict, } from "./tally.js";
|
|
25
|
+
import { describeScope, loadScopedTree, onScopedTree, resolveTarget, } from "./target.js";
|
|
26
|
+
import { mechanicalRuleCounts, mechanicalRuleCountsLine, } from "./violation-count.js";
|
|
19
27
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
28
|
/** `git -C <target> rev-parse HEAD`, or "not a git repo" when it is not one. */
|
|
21
29
|
function targetCommit(target) {
|
|
@@ -39,6 +47,41 @@ function treeStatus(target) {
|
|
|
39
47
|
return "not a git repo";
|
|
40
48
|
}
|
|
41
49
|
}
|
|
50
|
+
/** The instrument's own identity for the evidence file: the backend-doctor
|
|
51
|
+
* checkout's HEAD when this module runs from its own git checkout (`+dirty` when
|
|
52
|
+
* src/ or fixtures/ differ from it), else the published package version. Never the
|
|
53
|
+
* HEAD of a repository that merely CONTAINS an installed copy. */
|
|
54
|
+
function cliCommit() {
|
|
55
|
+
const root = resolve(here, "..");
|
|
56
|
+
try {
|
|
57
|
+
const top = execFileSync("git", ["-C", root, "rev-parse", "--show-toplevel"], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
60
|
+
}).trim();
|
|
61
|
+
if (resolve(top) !== root)
|
|
62
|
+
throw new Error("not this checkout");
|
|
63
|
+
const head = execFileSync("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
}).trim();
|
|
66
|
+
const dirty = execFileSync("git", ["-C", root, "status", "--porcelain", "--", "src", "fixtures"], { encoding: "utf8" }).trim();
|
|
67
|
+
return dirty ? `${head}+dirty` : head;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return `npm @vantageos/backend-doctor@${toolVersion()}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** `git -C <dir> <args>` trimmed, or undefined when git refuses. */
|
|
74
|
+
function gitOut(dir, args) {
|
|
75
|
+
try {
|
|
76
|
+
return execFileSync("git", ["-C", dir, ...args], {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
79
|
+
}).trim();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
42
85
|
function toolVersion() {
|
|
43
86
|
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
|
|
44
87
|
return pkg.version;
|
|
@@ -148,7 +191,49 @@ function probeRoutes(target) {
|
|
|
148
191
|
return { candidateDir, routePaths };
|
|
149
192
|
}
|
|
150
193
|
function main() {
|
|
151
|
-
|
|
194
|
+
// Refuse to judge from a dist/ compiled from other source than this checkout's
|
|
195
|
+
// (src/build-stamp.ts). Runs before anything is read, so no verdict is printed.
|
|
196
|
+
// Flags first: --version/--help answer without reading a target, and an unknown
|
|
197
|
+
// flag is a usage error — never a path handed to the filesystem (an `-v` used to
|
|
198
|
+
// become `<cwd>/-v` and die on an uncaught ENOENT stack).
|
|
199
|
+
const args = parseArgs(process.argv.slice(2));
|
|
200
|
+
if (args.kind === "version") {
|
|
201
|
+
console.log(toolVersion());
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
if (args.kind === "help") {
|
|
205
|
+
console.log(USAGE);
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
if (args.kind === "usage-error") {
|
|
209
|
+
console.error(`backend-doctor: ${args.message}\n\n${USAGE}`);
|
|
210
|
+
process.exit(2);
|
|
211
|
+
}
|
|
212
|
+
const fresh = checkDistFreshness(here);
|
|
213
|
+
if (!fresh.ok) {
|
|
214
|
+
console.error(`backend-doctor: REFUSING TO RUN — ${fresh.reason}`);
|
|
215
|
+
process.exit(2);
|
|
216
|
+
}
|
|
217
|
+
const target = resolve(args.positional[0] ?? process.cwd());
|
|
218
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
219
|
+
console.error(`backend-doctor: target ${target} is not a readable directory — could not judge.\n\n${USAGE}`);
|
|
220
|
+
process.exit(2);
|
|
221
|
+
}
|
|
222
|
+
// ONE normalisation (src/target.ts): `convex` and `.` resolve to the same repo
|
|
223
|
+
// root, convex dir and file set, so every rule sees the same rels either way.
|
|
224
|
+
const scope = resolveTarget(target);
|
|
225
|
+
const tree = loadScopedTree(scope);
|
|
226
|
+
console.log(describeScope(scope, tree));
|
|
227
|
+
// The oracle is DERIVED from the target (never from this tool's own install
|
|
228
|
+
// directory) and the run says which file it used and where that came from.
|
|
229
|
+
// Absent => the oracle-backed rules refuse to judge (could-not-judge).
|
|
230
|
+
const oracleSel = resolveOracle(target, {
|
|
231
|
+
oracle: args.flags.oracle,
|
|
232
|
+
vrOracle: args.flags["vr-oracle"],
|
|
233
|
+
writerTier: args.flags["writer-tier"],
|
|
234
|
+
});
|
|
235
|
+
activateOracle(oracleSel);
|
|
236
|
+
console.log(describeOracle(oracleSel));
|
|
152
237
|
// Instrument self-check, BEFORE any judging: a backend-doctor run reads its
|
|
153
238
|
// pinned standard fixture as material. If that fixture is absent/unreadable the
|
|
154
239
|
// tool cannot judge — REFUSE with exit 2 (could-not-judge) naming what it could
|
|
@@ -163,18 +248,64 @@ function main() {
|
|
|
163
248
|
// pending detector) — so the tally's rule total equals the 48 derived rules and
|
|
164
249
|
// nothing is silently dropped.
|
|
165
250
|
const results = [
|
|
166
|
-
...runDoctor(
|
|
251
|
+
...runDoctor(scope.repoRoot, onScopedTree(tree, REGISTRY.map((e) => e.detector))),
|
|
167
252
|
...notCheckableResults(),
|
|
168
253
|
];
|
|
169
|
-
console.log(renderTable("backend-doctor (
|
|
254
|
+
console.log(renderTable("backend-doctor (oracle derived from target)", results));
|
|
170
255
|
// CRUD COVERAGE — an inventory/coverage report DIMENSION (not a numbered rule
|
|
171
256
|
// in the pinned 50-rule standard). Rows are EVERY defineTable table derived
|
|
172
257
|
// from the target schema at run time; the union of rows MUST equal the schema
|
|
173
258
|
// table set or the run is RED (fail-closed).
|
|
174
259
|
console.log("");
|
|
175
|
-
const crud = scanCrudCoverageForCli(target);
|
|
260
|
+
const crud = scanCrudCoverageForCli(scope.convexDir ?? target);
|
|
176
261
|
console.log(crudCoverageSection(crud));
|
|
177
262
|
const crudDefect = crud.status === "unreadable" || !crud.unionEqualsSchema;
|
|
263
|
+
// CALLER RESOLUTION (CR-1) — a DOCTOR-OWNED CRITICAL control, reported in its own
|
|
264
|
+
// section for the same reason CRUD coverage is: the pinned standard numbers
|
|
265
|
+
// R-1..R-51 and does not define it, so putting it in REGISTRY would make
|
|
266
|
+
// coverage.ts report a phantom rule and would forge the standard's pin. It is run
|
|
267
|
+
// through the engine's OWN loader (never a second tree traversal) and its
|
|
268
|
+
// violations drive the exit code — an unjustified NO-IDENTITY or a FAIL-OPEN
|
|
269
|
+
// registration is mechanical, so the gate refuses on it.
|
|
270
|
+
console.log("");
|
|
271
|
+
let crScan;
|
|
272
|
+
runDoctor(scope.repoRoot, onScopedTree(tree, [
|
|
273
|
+
{
|
|
274
|
+
id: CR1_ID,
|
|
275
|
+
run: (t) => {
|
|
276
|
+
crScan = scanCallerResolution(t);
|
|
277
|
+
return callerResolutionResult(crScan);
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
]));
|
|
281
|
+
if (crScan === undefined) {
|
|
282
|
+
console.error("CR-1: the engine did not run the caller-resolution detector — refusing to report a surface that was never scanned.");
|
|
283
|
+
process.exit(2);
|
|
284
|
+
}
|
|
285
|
+
console.log(callerResolutionSection(crScan));
|
|
286
|
+
const crResult = callerResolutionResult(crScan);
|
|
287
|
+
const crDefect = crViolations(crScan).length > 0;
|
|
288
|
+
// LOCAL IDENTITY (CR-2) — DOCTOR-OWNED mechanical control, same footing as
|
|
289
|
+
// CR-1: no application defines its own identity primitive; identity lives
|
|
290
|
+
// only in @vantageos/cloud-identity. Ceiling 0 — any violation exits non-zero.
|
|
291
|
+
console.log("");
|
|
292
|
+
let liScan;
|
|
293
|
+
runDoctor(scope.repoRoot, onScopedTree(tree, [
|
|
294
|
+
{
|
|
295
|
+
id: CR2_ID,
|
|
296
|
+
run: (t) => {
|
|
297
|
+
liScan = scanLocalIdentity(t.files);
|
|
298
|
+
return localIdentityResult(liScan);
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
]));
|
|
302
|
+
if (liScan === undefined) {
|
|
303
|
+
console.error("CR-2: the engine did not run the local-identity detector — refusing to report a surface that was never scanned.");
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
console.log(localIdentitySection(liScan));
|
|
307
|
+
const liResult = localIdentityResult(liScan);
|
|
308
|
+
const liDefect = liScan.violate.length > 0;
|
|
178
309
|
console.log("");
|
|
179
310
|
const cov = coverage();
|
|
180
311
|
console.log(coverageSummary(cov));
|
|
@@ -199,36 +330,137 @@ function main() {
|
|
|
199
330
|
// process rules, so its could-not-judge count is visible on its own and can be
|
|
200
331
|
// driven to 0 (Laurent: an instrument that cannot go red protects nothing).
|
|
201
332
|
console.log(handlerSurfaceLine(handlerSurfaceStats(results)));
|
|
333
|
+
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]}`);
|
|
334
|
+
console.log(`LOCAL-IDENTITY TALLY (CR-2, doctor-owned, outside the pinned rule set): state=${liResult.state} violations=${liScan.violate.length} exempt=${liScan.exempt.length} could-not-judge=${liScan.cnj.length}`);
|
|
202
335
|
console.log(v.summary);
|
|
203
336
|
if (v.criticalViolations > 0) {
|
|
204
337
|
console.log(` critical violations: ${v.violations.critical.join(", ")}`);
|
|
205
338
|
}
|
|
206
339
|
if (v.criticalAbstentions > 0) {
|
|
207
|
-
console.log(` critical abstentions (
|
|
340
|
+
console.log(` critical abstentions (not decided; each rule names why): ${v.abstentions.critical.join(", ")}`);
|
|
341
|
+
}
|
|
342
|
+
// The process exit code, decided ONCE so the evidence file records the code the
|
|
343
|
+
// process actually exits with. Order matches the NOTE lines printed below.
|
|
344
|
+
let exitCode = v.exitCode;
|
|
345
|
+
if (!cov.ok || crudDefect || crDefect || liDefect)
|
|
346
|
+
exitCode = 1;
|
|
347
|
+
// CR-2 abstaining (an unparsable file) is never a pass.
|
|
348
|
+
if (exitCode === 0 && liResult.state === "could-not-judge")
|
|
349
|
+
exitCode = 2;
|
|
350
|
+
// MECHANICAL VIOLATIONS — the deploy gate's only refusal driver. The class of
|
|
351
|
+
// each rule is DATA (fixtures/rule-classes-<pin>.json, cross-checked against the
|
|
352
|
+
// pinned standard in src/rule-class.ts). Instrument self-defects and an
|
|
353
|
+
// unclassified rule make the count null (could-not-judge), never 0.
|
|
354
|
+
const convexPath = scope.convexDir ?? target;
|
|
355
|
+
const repoOfConvex = scope.convexDir
|
|
356
|
+
? dirname(scope.convexDir)
|
|
357
|
+
: scope.repoRoot;
|
|
358
|
+
const instrumentDefects = [];
|
|
359
|
+
if (!cov.ok)
|
|
360
|
+
instrumentDefects.push(coverageSummary(cov));
|
|
361
|
+
if (crudDefect)
|
|
362
|
+
instrumentDefects.push("CRUD coverage self-check failed (rows do not cover every schema table, or the schema is unreadable)");
|
|
363
|
+
// CR1_ID is the detector's full label; its rule id is the leading `CR-N` token.
|
|
364
|
+
const cr1Rule = /^CR-\d+/.exec(CR1_ID)?.[0] ?? CR1_ID;
|
|
365
|
+
const cr2Rule = /^CR-\d+/.exec(CR2_ID)?.[0] ?? CR2_ID;
|
|
366
|
+
const classification = classifyRules([...deriveRuleIds(), cr1Rule, cr2Rule]);
|
|
367
|
+
const ruleStates = new Map(t.ruleState);
|
|
368
|
+
ruleStates.set(cr1Rule, crResult.state);
|
|
369
|
+
ruleStates.set(cr2Rule, liResult.state);
|
|
370
|
+
const evidencePath = args.flags["json-evidence"];
|
|
371
|
+
let evidenceSha;
|
|
372
|
+
if (evidencePath !== undefined) {
|
|
373
|
+
evidenceSha = gitOut(repoOfConvex, ["rev-parse", "HEAD"]);
|
|
374
|
+
const dirty = gitOut(convexPath, [
|
|
375
|
+
"status",
|
|
376
|
+
"--porcelain",
|
|
377
|
+
"--untracked-files=all",
|
|
378
|
+
"--",
|
|
379
|
+
".",
|
|
380
|
+
]);
|
|
381
|
+
if (evidenceSha !== undefined && dirty === undefined)
|
|
382
|
+
instrumentDefects.push(`git status of ${convexPath} failed — whether the judged tree equals HEAD cannot be shown`);
|
|
383
|
+
else if (dirty)
|
|
384
|
+
instrumentDefects.push(`the judged convex tree differs from HEAD ${evidenceSha?.slice(0, 12)} (${dirty.split("\n").length} path(s) modified or untracked) — the count would describe bytes the sha does not name`);
|
|
385
|
+
}
|
|
386
|
+
const mech = countMechanical(ruleStates, classification, instrumentDefects);
|
|
387
|
+
if (mech.count === null && exitCode === 0)
|
|
388
|
+
exitCode = 2;
|
|
389
|
+
console.log(mechanicalLine(mech));
|
|
390
|
+
// Per red mechanical rule, the count its report line printed — read from the
|
|
391
|
+
// result the detector returned (src/violation-count.ts), never from the text.
|
|
392
|
+
const ruleCounts = mechanicalRuleCounts([...results, crResult, liResult], mech);
|
|
393
|
+
console.log(mechanicalRuleCountsLine(ruleCounts));
|
|
394
|
+
if (classification.stale.length > 0) {
|
|
395
|
+
console.log(` STALE SIDECAR ENTRIES (name no rule of this run): ${classification.stale.join(", ")}`);
|
|
208
396
|
}
|
|
209
397
|
// The run WRITES its report to a FILE, ALWAYS — not behind a flag. The clock and
|
|
210
398
|
// the git facts are gathered HERE (a real process) and handed to the pure writer.
|
|
211
399
|
const ctx = {
|
|
212
400
|
target,
|
|
213
|
-
targetCommit: targetCommit(
|
|
401
|
+
targetCommit: targetCommit(scope.repoRoot),
|
|
214
402
|
toolVersion: toolVersion(),
|
|
215
403
|
dateISO: new Date().toISOString().slice(0, 10),
|
|
216
|
-
treeStatus: treeStatus(
|
|
404
|
+
treeStatus: treeStatus(scope.repoRoot),
|
|
405
|
+
oracle: describeOracle(oracleSel),
|
|
217
406
|
};
|
|
218
407
|
const reportDir = join(here, "..", "reports");
|
|
219
|
-
const path = writeReport(ctx, results, t, v, reportDir
|
|
408
|
+
const path = writeReport(ctx, results, t, v, reportDir, undefined, [
|
|
409
|
+
callerResolutionSection(crScan),
|
|
410
|
+
localIdentitySection(liScan),
|
|
411
|
+
mechanicalLine(mech),
|
|
412
|
+
mechanicalRuleCountsLine(ruleCounts),
|
|
413
|
+
]);
|
|
220
414
|
console.log(`\nreport written: ${path}`);
|
|
415
|
+
// The deploy-gate evidence, keyed to the sha of the repository holding the
|
|
416
|
+
// judged convex dir. No sha, no file: evidence that names no commit certifies
|
|
417
|
+
// nothing, so the run refuses (exit 2) rather than write one.
|
|
418
|
+
if (evidencePath !== undefined) {
|
|
419
|
+
if (evidenceSha === undefined) {
|
|
420
|
+
console.error(`backend-doctor: --json-evidence REFUSED — \`git -C ${repoOfConvex} rev-parse HEAD\` failed, so the evidence would name no commit. No evidence file written.`);
|
|
421
|
+
process.exit(2);
|
|
422
|
+
}
|
|
423
|
+
const evidence = buildEvidence({
|
|
424
|
+
sha: evidenceSha,
|
|
425
|
+
cli_commit: cliCommit(),
|
|
426
|
+
convex_path: convexPath,
|
|
427
|
+
exit_code: exitCode,
|
|
428
|
+
checked: cov.checked.length,
|
|
429
|
+
total: cov.ruleIds.length,
|
|
430
|
+
mechanical_violations: mech.count,
|
|
431
|
+
mechanical_rule_counts: ruleCounts.counts,
|
|
432
|
+
mechanical_rule_module_counts: ruleCounts.moduleCounts,
|
|
433
|
+
});
|
|
434
|
+
let out = resolve(evidencePath);
|
|
435
|
+
// A trailing slash names a directory even before it exists.
|
|
436
|
+
if (/[/\\]$/.test(evidencePath))
|
|
437
|
+
mkdirSync(out, { recursive: true });
|
|
438
|
+
if (existsSync(out) && statSync(out).isDirectory())
|
|
439
|
+
out = join(out, `backend-doctor-${evidenceSha}.json`);
|
|
440
|
+
else
|
|
441
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
442
|
+
writeFileSync(out, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
443
|
+
console.log(`evidence written: ${out}`);
|
|
444
|
+
}
|
|
221
445
|
if (!cov.ok) {
|
|
222
446
|
console.log("\nNOTE: coverage is INCOMPLETE — a rule is in neither bucket. See the summary above.");
|
|
223
|
-
process.exit(1);
|
|
224
447
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
448
|
+
else if (crudDefect) {
|
|
449
|
+
// The CRUD coverage self-check is fail-closed: an unreadable schema or a rows
|
|
450
|
+
// union that does not cover the schema tables is a DOCTOR DEFECT → RED, never a
|
|
451
|
+
// silent pass. (ABSENT cells are honest reported gaps, NOT a defect.)
|
|
229
452
|
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
|
-
process.exit(1);
|
|
231
453
|
}
|
|
232
|
-
|
|
454
|
+
else if (crDefect) {
|
|
455
|
+
// CR-1 is CRITICAL: a FAIL-OPEN or unjustified NO-IDENTITY public registration
|
|
456
|
+
// is a mechanical violation, so the gate refuses on it even when every numbered
|
|
457
|
+
// rule is satisfied.
|
|
458
|
+
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.`);
|
|
459
|
+
}
|
|
460
|
+
if (liDefect) {
|
|
461
|
+
// Reported independently of the NOTE chain above: CR-2 is its own gate.
|
|
462
|
+
console.log(`\nNOTE: CR-2 (local identity) FAILED — ${liScan.violate.length} site(s) define a local identity primitive instead of calling @vantageos/cloud-identity. Ceiling 0. See the LOCAL IDENTITY section above; every site is named there.`);
|
|
463
|
+
}
|
|
464
|
+
process.exit(exitCode);
|
|
233
465
|
}
|
|
234
466
|
main();
|
package/dist/contract.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type OracleColumn, type WriterTierTable } from "./oracle.js";
|
|
2
2
|
export interface ContractField {
|
|
3
3
|
/** the §2 contract field name. */
|
|
4
4
|
field: string;
|
|
@@ -9,10 +9,28 @@ export interface ContractField {
|
|
|
9
9
|
}
|
|
10
10
|
/** The thirteen §2 contract fields (R-38 completeness gate operates on this). */
|
|
11
11
|
export declare const CONTRACT_FIELDS: ContractField[];
|
|
12
|
-
/**
|
|
13
|
-
* authority. The oracle merges both onto `rbac_qui`: the structural defect. */
|
|
12
|
+
/** Reader authority (R-5) lives in the oracle's own `rbac_qui` column. */
|
|
14
13
|
export declare const READER_AUTHORITY_COLUMN: OracleColumn;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/** Without a side-car, writer authority (R-10) can only fall back to the SAME
|
|
15
|
+
* column — the merge §12 calls non-conforming by construction. */
|
|
16
|
+
export declare const MERGED_WRITER_AUTHORITY_COLUMN: OracleColumn;
|
|
17
|
+
/** The side-car column that carries writer authority apart from `rbac_qui`. */
|
|
18
|
+
export declare const SPLIT_WRITER_AUTHORITY_COLUMN: "writer_tier";
|
|
19
|
+
export type AuthoritySplit = {
|
|
20
|
+
kind: "split";
|
|
21
|
+
sideCar: string;
|
|
22
|
+
writeTools: number;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "merged";
|
|
25
|
+
reason: string;
|
|
26
|
+
uncovered: string[];
|
|
27
|
+
} | {
|
|
28
|
+
kind: "unjudgeable";
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Resolve where writer authority lives for this oracle.
|
|
33
|
+
* @param writeTools the `outil` of every inventory row whose verb writes.
|
|
34
|
+
* @param sideCar the bound writer-tier side-car.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveAuthoritySplit(writeTools: readonly string[], sideCar: WriterTierTable): AuthoritySplit;
|
package/dist/contract.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
// The §2 per-table contract, transcribed from the pinned standard
|
|
2
|
+
// (backend-standard.md @ a9d7877b, read by command — never from memory).
|
|
3
|
+
//
|
|
4
|
+
// §2 splits authority into TWO distinct contract fields — reader authority (R-5)
|
|
5
|
+
// and writer authority (R-10). The oracle's eighteen columns carry ONE authority
|
|
6
|
+
// column, `rbac_qui`. An oracle alone is therefore merged, and R-14 calls a merged
|
|
7
|
+
// authority column "non-conforming by construction". An oracle declares the split
|
|
8
|
+
// through its writer-tier side-car (see resolveAuthoritySplit below).
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
import { WRITER_TIER_NAME } from "./oracle.js";
|
|
1
11
|
/** The thirteen §2 contract fields (R-38 completeness gate operates on this). */
|
|
2
12
|
export const CONTRACT_FIELDS = [
|
|
3
13
|
{ field: "exposure", columns: ["surface"], rule: "R-1" },
|
|
4
14
|
{ field: "operation verb", columns: ["crud"], rule: "R-2" },
|
|
5
15
|
{ field: "reader authority", columns: ["rbac_qui"], rule: "R-5" },
|
|
16
|
+
// §2's table maps writer authority onto `rbac_qui` too; that mapping IS the
|
|
17
|
+
// merge. The split column is the side-car's `writer_tier` (resolveAuthoritySplit).
|
|
6
18
|
{ field: "writer authority", columns: ["rbac_qui"], rule: "R-10" },
|
|
7
19
|
{ field: "retrieval mechanism", columns: ["search_mecanisme"], rule: "R-4" },
|
|
8
20
|
{ field: "pagination", columns: ["search_mecanisme"], rule: "R-3" },
|
|
@@ -18,12 +30,45 @@ export const CONTRACT_FIELDS = [
|
|
|
18
30
|
rule: "R-13/R-27",
|
|
19
31
|
},
|
|
20
32
|
];
|
|
21
|
-
/**
|
|
22
|
-
* authority. The oracle merges both onto `rbac_qui`: the structural defect. */
|
|
33
|
+
/** Reader authority (R-5) lives in the oracle's own `rbac_qui` column. */
|
|
23
34
|
export const READER_AUTHORITY_COLUMN = "rbac_qui";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
/** Without a side-car, writer authority (R-10) can only fall back to the SAME
|
|
36
|
+
* column — the merge §12 calls non-conforming by construction. */
|
|
37
|
+
export const MERGED_WRITER_AUTHORITY_COLUMN = "rbac_qui";
|
|
38
|
+
/** The side-car column that carries writer authority apart from `rbac_qui`. */
|
|
39
|
+
export const SPLIT_WRITER_AUTHORITY_COLUMN = "writer_tier";
|
|
40
|
+
const UNKNOWN_WRITER_TIER = new Set(["", "?", "unknown", "n/a", "tbd"]);
|
|
41
|
+
/**
|
|
42
|
+
* Resolve where writer authority lives for this oracle.
|
|
43
|
+
* @param writeTools the `outil` of every inventory row whose verb writes.
|
|
44
|
+
* @param sideCar the bound writer-tier side-car.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAuthoritySplit(writeTools, sideCar) {
|
|
47
|
+
if (sideCar.status === "absent") {
|
|
48
|
+
return {
|
|
49
|
+
kind: "merged",
|
|
50
|
+
reason: `writer-tier side-car ${WRITER_TIER_NAME} ABSENT (${sideCar.origin}; searched: ${sideCar.searched.join(", ") || "<none>"}), so writer authority has no column of its own`,
|
|
51
|
+
uncovered: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (sideCar.status === "unreadable") {
|
|
55
|
+
return {
|
|
56
|
+
kind: "unjudgeable",
|
|
57
|
+
reason: `writer-tier side-car ${sideCar.path} unreadable — ${sideCar.reason}; whether \`${SPLIT_WRITER_AUTHORITY_COLUMN}\` declares writer authority cannot be read`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const tools = [...new Set(writeTools)];
|
|
61
|
+
const uncovered = tools.filter((t) => {
|
|
62
|
+
const entries = sideCar.byTool.get(t) ?? [];
|
|
63
|
+
return !entries.some((e) => !UNKNOWN_WRITER_TIER.has(e.writer_tier.trim().toLowerCase()));
|
|
64
|
+
});
|
|
65
|
+
const file = basename(sideCar.path);
|
|
66
|
+
if (uncovered.length > 0) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "merged",
|
|
69
|
+
reason: `${file} records no \`${SPLIT_WRITER_AUTHORITY_COLUMN}\` for ${uncovered.length} write tool(s) of ${tools.length} (${uncovered.join(", ")}), so for those tools writer authority is omitted`,
|
|
70
|
+
uncovered,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { kind: "split", sideCar: file, writeTools: tools.length };
|
|
29
74
|
}
|
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("");
|
package/dist/coverage.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
|
|
5
5
|
* as a written-justification-token control (§23) — the paired standard PR the four
|
|
6
6
|
* decisive detectors below track. Still 48 rules; the pin moves forward with them. */
|
|
7
|
-
export declare const STANDARD_SHA = "
|
|
7
|
+
export declare const STANDARD_SHA = "2825122a3ab1e4ddb4b955cfb136b7e8ab04f8f3";
|
|
8
8
|
export declare const VENDORED_STANDARD_PATH: string;
|
|
9
9
|
/**
|
|
10
10
|
* Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
|
package/dist/coverage.js
CHANGED
|
@@ -18,8 +18,8 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
18
18
|
* canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
|
|
19
19
|
* as a written-justification-token control (§23) — the paired standard PR the four
|
|
20
20
|
* decisive detectors below track. Still 48 rules; the pin moves forward with them. */
|
|
21
|
-
export const STANDARD_SHA = "
|
|
22
|
-
export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-
|
|
21
|
+
export const STANDARD_SHA = "2825122a3ab1e4ddb4b955cfb136b7e8ab04f8f3";
|
|
22
|
+
export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-2825122.md");
|
|
23
23
|
/**
|
|
24
24
|
* Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
|
|
25
25
|
* never typed, never a git show against a local repo. Reads the `**R-NN`
|
|
@@ -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,18 +18,20 @@
|
|
|
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 { activeOracle, ORACLE_COLUMNS, oracleLabel, parseCsv, readOracle, refuseWithoutOracle, requireVrOracle, } from "../oracle.js";
|
|
24
|
+
import { itemCount, withCount } from "../violation-count.js";
|
|
24
25
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
/**
|
|
26
|
+
/** The vendored day159 SECOND-backend (VantageRegistry) snapshot — a FIXTURE for
|
|
27
|
+
* this tool's tests, never a default (see oracle.ts resolveOracle).
|
|
26
28
|
* Vendored at pin a74a042b, blob b53e2626954a887303d11d944b6132a11dbd34f6. */
|
|
27
|
-
export const
|
|
29
|
+
export const FIXTURE_VR_ORACLE_PATH = join(here, "..", "..", "fixtures", "vr-by-tool-day159.csv");
|
|
28
30
|
/** The vendored standard (the R-5 law) — the SAME pin coverage.ts parses. The
|
|
29
31
|
* §2 [shared] contract columns are DERIVED from this file, never hardcoded.
|
|
30
32
|
* Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
|
|
31
33
|
* import cycle. */
|
|
32
|
-
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-
|
|
34
|
+
export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-2825122.md");
|
|
33
35
|
/** Load any backend inventory CSV generically from its OWN header — the shared
|
|
34
36
|
* fields are DERIVED from these real headers, never a prose illustration. */
|
|
35
37
|
export function loadInventory(path) {
|
|
@@ -44,13 +46,17 @@ export function loadInventory(path) {
|
|
|
44
46
|
});
|
|
45
47
|
return { header, rows };
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
/** Memoized read of the
|
|
49
|
+
const vrCache = new Map();
|
|
50
|
+
/** Memoized read of the VR inventory bound for THIS run (derived from the target).
|
|
51
|
+
* Throws OracleAbsentError when none was found. */
|
|
49
52
|
export function readVrInventory() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const path = requireVrOracle().path;
|
|
54
|
+
const hit = vrCache.get(path);
|
|
55
|
+
if (hit)
|
|
56
|
+
return hit;
|
|
57
|
+
const inv = loadInventory(path);
|
|
58
|
+
vrCache.set(path, inv);
|
|
59
|
+
return inv;
|
|
54
60
|
}
|
|
55
61
|
/** The VP inventory, adapted from the memoized VP oracle reader — the SAME rows
|
|
56
62
|
* the axis-2 detectors read, exposed with VP's real header. */
|
|
@@ -161,13 +167,17 @@ export function compareBackends(vpProvider = readOracle, vrProvider = readVrInve
|
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
169
|
const R40_ID = "R-40 between-backend divergence";
|
|
164
|
-
|
|
170
|
+
function r40Anchor() {
|
|
171
|
+
const vr = activeOracle()?.vr;
|
|
172
|
+
const vrName = vr && vr.status === "found" ? basename(vr.path) : "ABSENT";
|
|
173
|
+
return `${oracleLabel()} vs oracle:${vrName}`;
|
|
174
|
+
}
|
|
165
175
|
/** Wrap the Axis-1 compare into a Detector. Providers are injectable for tests;
|
|
166
176
|
* production reads the two memoized real oracles. */
|
|
167
177
|
export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
168
178
|
return {
|
|
169
179
|
id: R40_ID,
|
|
170
|
-
run: () => {
|
|
180
|
+
run: () => refuseWithoutOracle(R40_ID, () => {
|
|
171
181
|
const cmp = compareBackends(vpProvider, vrProvider);
|
|
172
182
|
const { schema, sites } = cmp;
|
|
173
183
|
// the schema (column) divergence is itself a site — surfaced, never dropped.
|
|
@@ -188,7 +198,7 @@ export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
|
188
198
|
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
199
|
return {
|
|
190
200
|
id: R40_ID,
|
|
191
|
-
anchor:
|
|
201
|
+
anchor: r40Anchor(),
|
|
192
202
|
state: "present",
|
|
193
203
|
verdict: reason,
|
|
194
204
|
};
|
|
@@ -208,12 +218,12 @@ export function makeR40(vpProvider = readOracle, vrProvider = readVrInventory) {
|
|
|
208
218
|
})
|
|
209
219
|
.join("; ");
|
|
210
220
|
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
|
-
return {
|
|
221
|
+
return withCount({
|
|
212
222
|
id: R40_ID,
|
|
213
|
-
anchor: `${
|
|
223
|
+
anchor: `${r40Anchor()} field=${sites[0]?.field ?? "schema"}`,
|
|
214
224
|
state: "non-conforming",
|
|
215
225
|
verdict: reason,
|
|
216
|
-
};
|
|
217
|
-
},
|
|
226
|
+
}, itemCount(sites.length));
|
|
227
|
+
}),
|
|
218
228
|
};
|
|
219
229
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { Detector } from "../engine-types.js";
|
|
2
2
|
import { type OracleRow } from "../oracle.js";
|
|
3
3
|
import type { RulePredicate } from "./shared.js";
|
|
4
|
+
export declare const JUSTIFICATION_TOKEN_RE: RegExp;
|
|
5
|
+
/** The oracle-row form: reads the row's remediation cell (`rbac_adjustment_needed`). */
|
|
4
6
|
export declare function hasJustificationToken(r: OracleRow): boolean;
|
|
7
|
+
/** The raw-text form — for a SOURCE-scan site with no oracle row: a nearby code
|
|
8
|
+
* comment window is that site's "remediation cell". Same regex, same word-
|
|
9
|
+
* boundary/negation guards; never a second matcher. */
|
|
10
|
+
export declare function hasJustificationTokenInText(text: string): boolean;
|
|
5
11
|
export declare const r7: RulePredicate;
|
|
6
12
|
export declare const r8: RulePredicate;
|
|
7
13
|
export interface DivergenceSite {
|