@vantageos/backend-doctor 0.2.0 → 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 +48 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +19 -1
- package/dist/build-stamp.json +2 -2
- package/dist/caller-resolution.js +11 -8
- package/dist/cli.js +164 -15
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.js +5 -4
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +32 -16
- package/dist/detectors/predicates.js +12 -3
- package/dist/detectors/shared.js +3 -2
- 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 +62 -17
- 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 +22 -0
- package/dist/detectors/source/guard-helper-flow.js +49 -6
- 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 +8 -1
- package/dist/detectors/source/identity.js +364 -35
- package/dist/detectors/source/jobs.js +13 -2
- 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.js +291 -0
- 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.js +455 -26
- 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.js +3 -2
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +75 -43
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +11 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +27 -3
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +14 -6
- 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/rule-classes-2825122.json +228 -0
- package/package.json +1 -1
|
@@ -131,20 +131,33 @@ export function scanR18(files) {
|
|
|
131
131
|
// them into one mutation "would game the regex without changing atomicity"
|
|
132
132
|
// (okfBundleNode importOkfBundle shape).
|
|
133
133
|
// An action that would be flagged non-conforming may carry an `atomicity-exception:`
|
|
134
|
-
// marker naming EXACTLY ONE of those two shapes. Per Eta's three conditions
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
134
|
+
// marker naming EXACTLY ONE of those two shapes. Per Eta's three conditions
|
|
135
|
+
// (superseded on the disposition axis by task k179jnhx, §23 — judgement retired
|
|
136
|
+
// as an acceptance kind, 2026-09-02):
|
|
137
|
+
// COND 1 — originally "the disposition is COULD-NOT-JUDGE, never conform: a
|
|
138
|
+
// marker records a CLAIM that reaches a human reviewer" (Eta ruling
|
|
139
|
+
// 2026-08-31, mirroring the R-51 marker #141). The standard's §23 rewrite
|
|
140
|
+
// retires that disposition: a well-formed, COMPLETE claim (COND 2 + COND 3
|
|
141
|
+
// both satisfied — the shape named, and its checkable sub-conditions all
|
|
142
|
+
// present) IS the written justification the marker was always meant to
|
|
143
|
+
// record, exactly as JUSTIFIÉ:/JUSTIFIED: is elsewhere — so it now decides
|
|
144
|
+
// mechanically to PRESENT, never could-not-judge. An INCOMPLETE claim still
|
|
145
|
+
// buys nothing (COND 3, unchanged): the site stays non-conforming until the
|
|
146
|
+
// claim is well-formed.
|
|
147
|
+
// COND 2 — the marker MUST name which shape it claims, because the three are
|
|
148
|
+
// checked differently and the check must know which sub-conditions to ask for.
|
|
140
149
|
// COND 3 — the dangerous shape is (2): "unbounded loop, each write independently
|
|
141
150
|
// atomic" is a sentence anyone can write about a split invariant. So shape (2)
|
|
142
151
|
// is only honoured when the region ALSO names the loop (for/for-of/.forEach/.map)
|
|
143
152
|
// AND states the independence (no invariant spans two writes). A shape-(2) marker
|
|
144
153
|
// that names neither the loop nor the independence is INCOMPLETE — it does not buy
|
|
145
|
-
//
|
|
146
|
-
// A well-formed marker →
|
|
147
|
-
//
|
|
154
|
+
// present; the site stays non-conforming until the claim is well-formed.
|
|
155
|
+
// A well-formed, COMPLETE marker → present (the claim itself is the recorded
|
|
156
|
+
// justification; the control decides on the claim's structural completeness,
|
|
157
|
+
// never on a person's opinion of whether it is sound).
|
|
158
|
+
// No marker, an unknown shape, or an incomplete claim → non-conforming.
|
|
159
|
+
// could-not-judge is reserved for a site the instrument could not read (never
|
|
160
|
+
// reached here — every site below either parsed a marker or did not).
|
|
148
161
|
// ---------------------------------------------------------------------------
|
|
149
162
|
const ACTION_OPEN = /export\s+const\s+(\w+)\s*=\s*action\s*\(/;
|
|
150
163
|
const NEXT_EXPORT = /export\s+const\s+\w+\s*=/;
|
|
@@ -232,16 +245,16 @@ export function scanR29(files) {
|
|
|
232
245
|
? shapeReingest
|
|
233
246
|
: false;
|
|
234
247
|
if (shape && shapeComplete) {
|
|
235
|
-
// COND 1: a well-formed
|
|
236
|
-
//
|
|
237
|
-
scan.
|
|
248
|
+
// COND 1 (§23 rewrite, task k179jnhx): a well-formed, COMPLETE marker
|
|
249
|
+
// is itself the written justification — decided mechanically, present.
|
|
250
|
+
scan.conform.push({
|
|
238
251
|
rel: f.rel,
|
|
239
252
|
n: firstMut || openLine,
|
|
240
|
-
note: `action ${inAction} issues ${count} ctx.runMutation calls AND carries
|
|
253
|
+
note: `action ${inAction} issues ${count} ctx.runMutation calls AND carries a COMPLETE atomicity-exception marker claiming shape "${shape}" — ${shape === SHAPE_PER_ITEM_LOOP
|
|
241
254
|
? "the region names its loop and states the per-item writes are independently atomic (no invariant spans two)"
|
|
242
255
|
: shape === SHAPE_GATE_REINGEST
|
|
243
256
|
? "a leading gate runMutation, a supersede/dedup keyed on a stable namespace, and a recovering re-insert loop, with the convergence stated explicitly — a re-ingest of the same stable key converges on one current set"
|
|
244
|
-
: "exactly two runMutations, the first a gate (require*/assert*/bind*) preceding a single atomic write, which R-29's <=1 bar has no room for"}.
|
|
257
|
+
: "exactly two runMutations, the first a gate (require*/assert*/bind*) preceding a single atomic write, which R-29's <=1 bar has no room for"}. The marker's structural completeness IS the written justification (§23) — decided by the claim's presence, never a person's opinion of it`,
|
|
245
258
|
});
|
|
246
259
|
}
|
|
247
260
|
else if (shape === SHAPE_GATE_REINGEST) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector, DetectorResult } from "../../engine-types.js";
|
|
2
2
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
3
|
-
export declare const STANDARD_FILE = "backend-standard-
|
|
3
|
+
export declare const STANDARD_FILE = "backend-standard-2825122.md";
|
|
4
4
|
export declare const VENDORED_STANDARD_PATH: string;
|
|
5
5
|
/** The standard all four are pinned against (short hash a9d7877b). */
|
|
6
6
|
export declare const STANDARD_DOC_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) \u00A714/\u00A711/\u00A710/\u00A73";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// detectors that read THE STANDARD ITSELF as their material — R-42, R-15, R-32,
|
|
3
3
|
// R-39. Unlike the source-tree detectors (which read convex/ via `tree.files`)
|
|
4
4
|
// and the oracle detectors (which read the CSV), these read the VENDORED standard
|
|
5
|
-
// fixture `fixtures/backend-standard-
|
|
5
|
+
// fixture `fixtures/backend-standard-2825122.md` — the material at the pin. The
|
|
6
6
|
// engine's contract types are DERIVED from engine-types.ts, never re-spelled.
|
|
7
7
|
//
|
|
8
8
|
// THE LAW (the one that caught R-5): every predicate is derived from the ACTUAL
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { itemCount, withCount } from "../../violation-count.js";
|
|
15
16
|
/** The vendored-fixture basename used as the anchor file for every doc site. */
|
|
16
|
-
export const STANDARD_FILE = "backend-standard-
|
|
17
|
+
export const STANDARD_FILE = "backend-standard-2825122.md";
|
|
17
18
|
/** The vendored standard at the pin — DATA on the same footing as the oracle CSV.
|
|
18
19
|
* Resolved locally (not via coverage.ts) so this module carries no import cycle
|
|
19
20
|
* with registry.ts. Same file coverage.ts's VENDORED_STANDARD_PATH points at. */
|
|
@@ -136,12 +137,12 @@ export function decideDoc(ruleId, id, check, scan, emptyReason) {
|
|
|
136
137
|
.map((x) => `${STANDARD_FILE}:${x.n} (${x.note})`)
|
|
137
138
|
.join("; ");
|
|
138
139
|
if (nV > 0) {
|
|
139
|
-
return {
|
|
140
|
+
return withCount({
|
|
140
141
|
id,
|
|
141
142
|
anchor: `${STANDARD_FILE}:${scan.violate[0].n}`,
|
|
142
143
|
state: "non-conforming",
|
|
143
144
|
verdict: `${ruleId}: ${nV} STATIC violation(s) in the standard document (${check}). ${breakdown} Violations: ${fmt(scan.violate)}.${nC ? ` Conform: ${fmt(scan.conform)}.` : ""}`,
|
|
144
|
-
};
|
|
145
|
+
}, itemCount(nV));
|
|
145
146
|
}
|
|
146
147
|
if (nJ > 0) {
|
|
147
148
|
const reason = `${ruleId}: static half clean (${nC} conform); ${nJ} site(s) owe a human/gate verdict — ${fmt(scan.cnj)}. This is NOT a clean bill: GREEN is the gate half surfaced per site, not the detection running. ${breakdown}`;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { readFileSync } from "node:fs";
|
|
25
25
|
import { dirname, join } from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { itemCount, withCount } from "../../violation-count.js";
|
|
27
28
|
import { readStandard, STANDARD_FILE, toLines } from "./_doc.js";
|
|
28
29
|
/** prefix → { the sibling fixture basename, its recorded upstream blob hash }.
|
|
29
30
|
* The blob hash is the PINS.txt record — a vendored document that cannot be
|
|
@@ -154,12 +155,12 @@ export function decideR41(r) {
|
|
|
154
155
|
const deadList = r.dead
|
|
155
156
|
.map((c) => `${c.id} (cited ${site(c)}) not defined in ${SIBLING[c.prefix].file}`)
|
|
156
157
|
.join("; ");
|
|
157
|
-
return {
|
|
158
|
+
return withCount({
|
|
158
159
|
id: R41_ID,
|
|
159
160
|
anchor: site(r.dead[0]),
|
|
160
161
|
state: "non-conforming",
|
|
161
162
|
verdict: `R-41: ${r.dead.length} DEAD POINTER(S) — a cited frontier id resolves to no definition in its sibling (a dead pointer is a red, not a note). ${counts} Dead: ${deadList}. Cited: ${citedList}.`,
|
|
162
|
-
};
|
|
163
|
+
}, itemCount(r.dead.length));
|
|
163
164
|
}
|
|
164
165
|
return {
|
|
165
166
|
id: R41_ID,
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
// (non-conforming / could-not-judge / present / absent) — no fifth state,
|
|
38
38
|
// no second mechanism: the detector reads the tree the engine hands it.
|
|
39
39
|
import * as ts from "typescript";
|
|
40
|
+
import { siteCount, withCount } from "../../violation-count.js";
|
|
40
41
|
/** The standard this detector is pinned against (the re-pinned instrument pin). */
|
|
41
42
|
export const R46_STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) §16 R-46";
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
@@ -456,12 +457,12 @@ export function decideR46(cov) {
|
|
|
456
457
|
}
|
|
457
458
|
if (missing.length > 0) {
|
|
458
459
|
const first = missing[0];
|
|
459
|
-
return {
|
|
460
|
+
return withCount({
|
|
460
461
|
id: R46_ID,
|
|
461
462
|
anchor: `${first.rel}:${first.line}`,
|
|
462
463
|
state: "non-conforming",
|
|
463
464
|
verdict: `R-46: ${missing.length} returns validator(s) enumerate a stored document but OMIT a schema-declared field with NO written projection reason (${R46_CHECK}). ${breakdown}\n=== coverage inventory ===\n${inventory}\n=== end inventory ===`,
|
|
464
|
-
};
|
|
465
|
+
}, siteCount(missing));
|
|
465
466
|
}
|
|
466
467
|
if (cov.findings.length > 0) {
|
|
467
468
|
const first = cov.findings[0];
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { Detector } from "../engine-types.js";
|
|
2
|
-
import { type OracleRow } from "../oracle.js";
|
|
2
|
+
import { type OracleRow, type WriterTierTable } from "../oracle.js";
|
|
3
3
|
/**
|
|
4
4
|
* R-14 — inventory-schema parity, contract → inventory, one direction.
|
|
5
5
|
* A merged reader/writer authority column is "non-conforming by construction"
|
|
6
|
-
* (§12).
|
|
6
|
+
* (§12). Writer authority is a separate column only when the side-car declares it.
|
|
7
7
|
*/
|
|
8
|
-
export declare function makeR14(columns?: readonly string[]): Detector;
|
|
8
|
+
export declare function makeR14(columns?: readonly string[], rowsProvider?: () => OracleRow[], sideCar?: () => WriterTierTable): Detector;
|
|
9
9
|
/**
|
|
10
|
-
* R-38 — the completeness gate over the thirteen §2 fields.
|
|
11
|
-
*
|
|
12
|
-
* fields (both map to `rbac_qui`).
|
|
10
|
+
* R-38 — the completeness gate over the thirteen §2 fields. Writer authority is
|
|
11
|
+
* complete only when the side-car records a tier for every write tool.
|
|
13
12
|
*/
|
|
14
|
-
export declare function makeR38(columns?: readonly string[], rowsProvider?: () => OracleRow[]): Detector;
|
|
13
|
+
export declare function makeR38(columns?: readonly string[], rowsProvider?: () => OracleRow[], sideCar?: () => WriterTierTable): Detector;
|
|
@@ -1,54 +1,80 @@
|
|
|
1
1
|
// The two STRUCTURAL detectors — R-14 and R-38. They judge the inventory SCHEMA
|
|
2
|
-
// against the §2 contract, not a single row.
|
|
3
|
-
//
|
|
4
|
-
// (reader R-5, writer R-10)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
// against the §2 contract, not a single row.
|
|
3
|
+
//
|
|
4
|
+
// §2 splits authority into two contract fields (reader R-5, writer R-10). The
|
|
5
|
+
// oracle carries ONE authority column, `rbac_qui`; writer authority has a column
|
|
6
|
+
// of its own only through the writer-tier side-car (`writer_tier`, joined on
|
|
7
|
+
// `outil` — see resolveAuthoritySplit in src/contract.ts). So:
|
|
8
|
+
// side-car absent, or missing a write tool -> non-conforming (merged/omitted)
|
|
9
|
+
// side-car unreadable -> could-not-judge
|
|
10
|
+
// side-car complete over every write tool -> the split is declared
|
|
11
|
+
// An oracle alone is never green here: agreeing with a merged column would be
|
|
12
|
+
// certifying the defect the instrument exists to find.
|
|
13
|
+
import { CONTRACT_FIELDS, READER_AUTHORITY_COLUMN, resolveAuthoritySplit, SPLIT_WRITER_AUTHORITY_COLUMN, } from "../contract.js";
|
|
14
|
+
import { isInventoryRow, ORACLE_COLUMNS, oracleLabel, readOracle, readWriterTier, refuseWithoutOracle, requireOracle, } from "../oracle.js";
|
|
15
|
+
import { itemCount, withCount } from "../violation-count.js";
|
|
16
|
+
import { WRITE_VERBS } from "./shared.js";
|
|
9
17
|
const csvAnchor = () => `${oracleLabel()} col#9 rbac_qui`;
|
|
18
|
+
function splitFor(rows, sideCar) {
|
|
19
|
+
const writeTools = rows
|
|
20
|
+
.filter(isInventoryRow)
|
|
21
|
+
.filter((r) => WRITE_VERBS.has(r.by.crud))
|
|
22
|
+
.map((r) => r.by.outil);
|
|
23
|
+
return resolveAuthoritySplit(writeTools, sideCar());
|
|
24
|
+
}
|
|
25
|
+
function unjudgeable(id, reason) {
|
|
26
|
+
const verdict = `${id.split(" ")[0]}: could not judge — ${reason}.`;
|
|
27
|
+
return { id, anchor: csvAnchor(), state: "could-not-judge", verdict, reason: verdict };
|
|
28
|
+
}
|
|
10
29
|
/**
|
|
11
30
|
* R-14 — inventory-schema parity, contract → inventory, one direction.
|
|
12
31
|
* A merged reader/writer authority column is "non-conforming by construction"
|
|
13
|
-
* (§12).
|
|
32
|
+
* (§12). Writer authority is a separate column only when the side-car declares it.
|
|
14
33
|
*/
|
|
15
|
-
export function makeR14(columns = ORACLE_COLUMNS) {
|
|
34
|
+
export function makeR14(columns = ORACLE_COLUMNS, rowsProvider = readOracle, sideCar = readWriterTier) {
|
|
35
|
+
const id = "R-14 inventory-schema parity";
|
|
16
36
|
return {
|
|
17
|
-
id
|
|
37
|
+
id,
|
|
18
38
|
// The verdict is about the oracle's schema, so with no oracle bound there is
|
|
19
39
|
// no subject: refuse (could-not-judge), never a verdict on the tool's own constant.
|
|
20
|
-
run: () => refuseWithoutOracle(
|
|
40
|
+
run: () => refuseWithoutOracle(id, () => {
|
|
21
41
|
requireOracle();
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
const missing = CONTRACT_FIELDS.flatMap((f) => f.columns.filter((c) => !columns.includes(c)).map((c) => `${f.field}→${c}`));
|
|
43
|
+
const split = splitFor(rowsProvider(), sideCar);
|
|
44
|
+
if (split.kind === "unjudgeable")
|
|
45
|
+
return unjudgeable(id, split.reason);
|
|
46
|
+
if (split.kind === "merged" || missing.length > 0) {
|
|
47
|
+
return withCount({
|
|
48
|
+
id,
|
|
26
49
|
anchor: csvAnchor(),
|
|
27
50
|
state: "non-conforming",
|
|
28
|
-
verdict: "R-14:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
51
|
+
verdict: "R-14: §2 splits authority into two distinct contract fields — reader authority (R-5) " +
|
|
52
|
+
`and writer authority (R-10) — but the inventory's only authority column is \`${READER_AUTHORITY_COLUMN}\` (col#9)` +
|
|
53
|
+
(split.kind === "merged" ? `: ${split.reason}. ` : ". ") +
|
|
54
|
+
"A merged authority column is non-conforming by construction: the reader/writer divergence " +
|
|
55
|
+
"R-10 and R-7 exist to record cannot be written in it." +
|
|
56
|
+
(missing.length ? ` Also missing: ${missing.join(", ")}.` : ""),
|
|
57
|
+
}, itemCount(1));
|
|
33
58
|
}
|
|
34
59
|
return {
|
|
35
|
-
id
|
|
36
|
-
anchor: oracleLabel()
|
|
60
|
+
id,
|
|
61
|
+
anchor: `${oracleLabel()} + ${split.sideCar}`,
|
|
37
62
|
state: "present",
|
|
38
|
-
verdict:
|
|
63
|
+
verdict: `R-14: every §2 contract field present; reader authority is \`${READER_AUTHORITY_COLUMN}\`, writer authority is ` +
|
|
64
|
+
`\`${SPLIT_WRITER_AUTHORITY_COLUMN}\` in ${split.sideCar}, recorded for all ${split.writeTools} write tools.`,
|
|
39
65
|
};
|
|
40
66
|
}),
|
|
41
67
|
};
|
|
42
68
|
}
|
|
43
69
|
/**
|
|
44
|
-
* R-38 — the completeness gate over the thirteen §2 fields.
|
|
45
|
-
*
|
|
46
|
-
* fields (both map to `rbac_qui`).
|
|
70
|
+
* R-38 — the completeness gate over the thirteen §2 fields. Writer authority is
|
|
71
|
+
* complete only when the side-car records a tier for every write tool.
|
|
47
72
|
*/
|
|
48
|
-
export function makeR38(columns = ORACLE_COLUMNS, rowsProvider = readOracle) {
|
|
73
|
+
export function makeR38(columns = ORACLE_COLUMNS, rowsProvider = readOracle, sideCar = readWriterTier) {
|
|
74
|
+
const id = "R-38 thirteen-field completeness";
|
|
49
75
|
return {
|
|
50
|
-
id
|
|
51
|
-
run: () => refuseWithoutOracle(
|
|
76
|
+
id,
|
|
77
|
+
run: () => refuseWithoutOracle(id, () => {
|
|
52
78
|
requireOracle();
|
|
53
79
|
const colSet = new Set(columns);
|
|
54
80
|
const missing = [];
|
|
@@ -58,29 +84,35 @@ export function makeR38(columns = ORACLE_COLUMNS, rowsProvider = readOracle) {
|
|
|
58
84
|
missing.push(`${f.field}→${col}`);
|
|
59
85
|
}
|
|
60
86
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
const rows = rowsProvider();
|
|
88
|
+
const split = splitFor(rows, sideCar);
|
|
89
|
+
if (split.kind === "unjudgeable")
|
|
90
|
+
return unjudgeable(id, split.reason);
|
|
91
|
+
if (split.kind === "merged" || missing.length > 0) {
|
|
92
|
+
const uncovered = split.kind === "merged" ? new Set(split.uncovered) : new Set();
|
|
64
93
|
const sample = rows
|
|
65
|
-
.filter((r) =>
|
|
94
|
+
.filter((r) => isInventoryRow(r) && r.by.rbac_qui && (uncovered.size === 0 || uncovered.has(r.by.outil)))
|
|
66
95
|
.slice(0, 3)
|
|
67
96
|
.map((r) => `row#${r.n} "${r.by.outil || r.by.table}" rbac_qui="${r.by.rbac_qui}"`)
|
|
68
97
|
.join("; ");
|
|
69
|
-
return {
|
|
70
|
-
id
|
|
98
|
+
return withCount({
|
|
99
|
+
id,
|
|
71
100
|
anchor: csvAnchor(),
|
|
72
101
|
state: "non-conforming",
|
|
73
|
-
verdict:
|
|
74
|
-
`writer authority (R-10) are not two separate fields;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
102
|
+
verdict: "R-38: the oracle cannot satisfy the thirteen-field §2 contract — reader authority (R-5) and " +
|
|
103
|
+
`writer authority (R-10) are not two separate fields for every tool; writer authority falls back to ` +
|
|
104
|
+
`\`${READER_AUTHORITY_COLUMN}\` (col#9)` +
|
|
105
|
+
(split.kind === "merged" ? `: ${split.reason}.` : ".") +
|
|
106
|
+
(missing.length ? ` Also missing: ${missing.join(", ")}.` : "") +
|
|
107
|
+
` Evidence: ${sample}.`,
|
|
108
|
+
}, itemCount(1));
|
|
78
109
|
}
|
|
79
110
|
return {
|
|
80
|
-
id
|
|
81
|
-
anchor: oracleLabel()
|
|
111
|
+
id,
|
|
112
|
+
anchor: `${oracleLabel()} + ${split.sideCar}`,
|
|
82
113
|
state: "present",
|
|
83
|
-
verdict: "R-38: all thirteen §2 contract fields present per table
|
|
114
|
+
verdict: "R-38: all thirteen §2 contract fields present per table; writer authority is " +
|
|
115
|
+
`\`${SPLIT_WRITER_AUTHORITY_COLUMN}\` in ${split.sideCar} for all ${split.writeTools} write tools.`,
|
|
84
116
|
};
|
|
85
117
|
}),
|
|
86
118
|
};
|
package/dist/not-checkable.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DetectorResult } from "./engine-types.js";
|
|
1
2
|
/**
|
|
2
3
|
* The deferral bucket. R-1..R-47 each carry an executable control (registry.ts),
|
|
3
4
|
* so none is deferred to "a human decides". R-48 (authority-totality, the #130
|
|
@@ -9,7 +10,6 @@
|
|
|
9
10
|
* moves to registry.ts and this entry is removed. The export is consumed by
|
|
10
11
|
* coverage.ts / coverage-cli.ts / cli.ts.
|
|
11
12
|
*/
|
|
12
|
-
import type { DetectorResult } from "./engine-types.js";
|
|
13
13
|
export declare const NOT_MECHANICALLY_CHECKABLE: Record<string, string>;
|
|
14
14
|
/**
|
|
15
15
|
* One `could-not-judge` DetectorResult per NOT_MECHANICALLY_CHECKABLE rule, so
|
package/dist/not-checkable.js
CHANGED
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
// input is vendored. They were built, not relabelled. The bucket was not renamed to a
|
|
16
16
|
// gentler word — it was emptied.
|
|
17
17
|
import { STANDARD_FILE } from "./detectors/standard/_doc.js";
|
|
18
|
+
/**
|
|
19
|
+
* The deferral bucket. R-1..R-47 each carry an executable control (registry.ts),
|
|
20
|
+
* so none is deferred to "a human decides". R-48 (authority-totality, the #130
|
|
21
|
+
* class) was added to the standard at the 51ec7af1 re-pin AFTER its detector; its
|
|
22
|
+
* text is now pinned so coverage enumerates 48, but no detector exists yet — so it
|
|
23
|
+
* is reported here as NOT_MECHANICALLY_CHECKABLE with a written reason rather than
|
|
24
|
+
* silently omitted, which would let coverage pretend 47 is the whole document. The
|
|
25
|
+
* detector is task k17chpr4kzp399j3s7r7edpm4s8cscbz (after D5); when it lands, R-48
|
|
26
|
+
* moves to registry.ts and this entry is removed. The export is consumed by
|
|
27
|
+
* coverage.ts / coverage-cli.ts / cli.ts.
|
|
28
|
+
*/
|
|
18
29
|
export const NOT_MECHANICALLY_CHECKABLE = {
|
|
19
30
|
"R-48": "R-48 authority-totality detector pending — task k17chpr4kzp399j3s7r7edpm4s8cscbz; text pinned at 4bcd605 so coverage cannot pretend 47 is complete. No mechanical control yet; R-48 is enumerated and visible, never silently dropped.",
|
|
20
31
|
};
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector } from "./engine-types.js";
|
|
2
2
|
/** The standard both pins the whole wave against. */
|
|
3
|
-
export declare const STANDARD_PIN = "
|
|
3
|
+
export declare const STANDARD_PIN = "vantagepeers/backend-standard/backend-standard.md @ 2825122a3ab1e4ddb4b955cfb136b7e8ab04f8f3 (elpiarthera/ElPi-Corp, merge of PR #228 on origin/main, after #226 at 9f7d789)";
|
|
4
4
|
export interface RegistryEntry {
|
|
5
5
|
detector: Detector;
|
|
6
6
|
ruleId: string;
|
package/dist/registry.js
CHANGED
|
@@ -7,7 +7,9 @@ import { PER_ROW_PREDICATES } from "./detectors/predicates.js";
|
|
|
7
7
|
import { makeOracleDetector } from "./detectors/shared.js";
|
|
8
8
|
import { SOURCE_BACKFILL_DETECTORS } from "./detectors/source/backfill.js";
|
|
9
9
|
import { SOURCE_CONTRACT_DETECTORS } from "./detectors/source/contract.js";
|
|
10
|
+
import { SOURCE_CROSS_TENANT_WRITE_DETECTORS } from "./detectors/source/cross-tenant-write.js";
|
|
10
11
|
import { IDENTITY_DETECTORS } from "./detectors/source/identity.js";
|
|
12
|
+
import { SOURCE_IDENTITY_BY_ID_DETECTORS } from "./detectors/source/identity-by-id.js";
|
|
11
13
|
import { SOURCE_ISOLATION_DETECTORS } from "./detectors/source/isolation.js";
|
|
12
14
|
import { SOURCE_JOBS_DETECTORS } from "./detectors/source/jobs.js";
|
|
13
15
|
import { SOURCE_LIFECYCLE_DETECTORS } from "./detectors/source/lifecycle.js";
|
|
@@ -21,8 +23,9 @@ import { R46_STANDARD_PIN, STANDARD_SOURCE_DETECTORS, } from "./detectors/standa
|
|
|
21
23
|
import { STANDARD_DOC_DETECTORS } from "./detectors/standard/standard-doc.js";
|
|
22
24
|
import { makeR14, makeR38 } from "./detectors/structural.js";
|
|
23
25
|
import { STATE_RANK } from "./state-rank.js";
|
|
26
|
+
import { countOf, sumCounts, withCount } from "./violation-count.js";
|
|
24
27
|
/** The standard both pins the whole wave against. */
|
|
25
|
-
export const STANDARD_PIN = "
|
|
28
|
+
export const STANDARD_PIN = "vantagepeers/backend-standard/backend-standard.md @ 2825122a3ab1e4ddb4b955cfb136b7e8ab04f8f3 (elpiarthera/ElPi-Corp, merge of PR #228 on origin/main, after #226 at 9f7d789)";
|
|
26
29
|
/**
|
|
27
30
|
* Merge two detectors that judge halves of the SAME rule into one result: the
|
|
28
31
|
* worst state wins (non-conforming > could-not-judge > absent > present, the
|
|
@@ -40,7 +43,7 @@ export function mergeHalves(ruleId, a, b) {
|
|
|
40
43
|
const reasons = [ra, rb]
|
|
41
44
|
.filter((r) => r.state === "could-not-judge")
|
|
42
45
|
.map((r) => r.reason ?? r.verdict);
|
|
43
|
-
|
|
46
|
+
const merged = {
|
|
44
47
|
id: b.id,
|
|
45
48
|
anchor: decider.anchor,
|
|
46
49
|
state: decider.state,
|
|
@@ -49,6 +52,13 @@ export function mergeHalves(ruleId, a, b) {
|
|
|
49
52
|
? { reason: reasons.join(" || ") }
|
|
50
53
|
: {}),
|
|
51
54
|
};
|
|
55
|
+
// The per-rule count of a red merge is the sum over its RED halves — the
|
|
56
|
+
// halves whose `N site(s) VIOLATE` the merged verdict prints. An abstaining
|
|
57
|
+
// half contributes nothing; a red half with no count leaves it uncounted.
|
|
58
|
+
if (merged.state !== "non-conforming")
|
|
59
|
+
return merged;
|
|
60
|
+
const c = sumCounts([ra, rb].filter((r) => r.state === "non-conforming").map(countOf));
|
|
61
|
+
return c === undefined ? merged : withCount(merged, c);
|
|
52
62
|
},
|
|
53
63
|
};
|
|
54
64
|
}
|
|
@@ -61,7 +71,9 @@ export const REGISTRY = [
|
|
|
61
71
|
{ detector: makeR14(), ruleId: "R-14", standardPin: STANDARD_PIN },
|
|
62
72
|
{ detector: makeR38(), ruleId: "R-38", standardPin: STANDARD_PIN },
|
|
63
73
|
// Axis-2 coherence detectors (R-7/R-8/R-37): each performs its mechanical half
|
|
64
|
-
// IN FULL and
|
|
74
|
+
// IN FULL and decides on the written justification token's PRESENCE in the
|
|
75
|
+
// remediation cell (task k179jnhx, §23) — non-conforming without the token,
|
|
76
|
+
// present with it; could-not-judge only on a row the instrument cannot read.
|
|
65
77
|
{
|
|
66
78
|
detector: makeOracleDetector(r7),
|
|
67
79
|
ruleId: "R-7",
|
|
@@ -145,6 +157,18 @@ export const REGISTRY = [
|
|
|
145
157
|
// documents a client contract; a gated customMutation wrapper (writeMutation) is
|
|
146
158
|
// refused (could-not-judge) since client reachability is not visible from convex/.
|
|
147
159
|
...SOURCE_WRITE_ISOLATION_DETECTORS,
|
|
160
|
+
// R-52 — a write that reaches a row carrying no tenant value through a
|
|
161
|
+
// caller-choosable key (an argument, or a stored field the caller set
|
|
162
|
+
// through a public write) is dominated by a guard reading a server-stamped
|
|
163
|
+
// tenant. Reads schema.ts tenant columns and every public mutation/action
|
|
164
|
+
// handler on the AST (the vantage-peers 45e40c8 tasks.complete auto-link).
|
|
165
|
+
...SOURCE_CROSS_TENANT_WRITE_DETECTORS,
|
|
166
|
+
// R-53 — an actor and a target are identified by ID through
|
|
167
|
+
// @vantageos/cloud-identity: per public write door, the principal's
|
|
168
|
+
// source (a), the admission predicate (b), the unresolved branch (c);
|
|
169
|
+
// per helper / MCP gate, a local identity layer (d). Reads convex/ and
|
|
170
|
+
// mcp-server/src (the PR #1464 ten doors + org-less bulk_complete path).
|
|
171
|
+
...SOURCE_IDENTITY_BY_ID_DETECTORS,
|
|
148
172
|
...SOURCE_CONTRACT_DETECTORS,
|
|
149
173
|
...IDENTITY_DETECTORS,
|
|
150
174
|
// The four STANDARD-DOCUMENT detectors (R-42/R-15/R-32/R-39): each reads the
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { State } from "./engine-types.js";
|
|
2
|
+
export declare const RULE_CLASS_PATH: string;
|
|
3
|
+
export type GateClass = "mechanical" | "judgement";
|
|
4
|
+
export interface RuleClassFile {
|
|
5
|
+
standard_sha: string;
|
|
6
|
+
standard_kind_to_class: Record<string, string>;
|
|
7
|
+
rules: Record<string, {
|
|
8
|
+
class: string;
|
|
9
|
+
basis: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadRuleClassFile(path?: string): RuleClassFile;
|
|
13
|
+
/**
|
|
14
|
+
* The build kind the PINNED STANDARD itself states per rule: every row of the
|
|
15
|
+
* section 15 table (`| R-N | mechanical|judgement|process | … |`), then, for a rule the
|
|
16
|
+
* table does not list, the `*kind:* <word>` token inside that rule's own text (up
|
|
17
|
+
* to the next rule definition). A rule the standard classifies nowhere is absent
|
|
18
|
+
* from the map — classifyRules reports it as unclassified.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseStandardKinds(path?: string): Map<string, string>;
|
|
21
|
+
export interface Unclassified {
|
|
22
|
+
id: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Classification {
|
|
26
|
+
classOf: Map<string, GateClass>;
|
|
27
|
+
basisOf: Map<string, string>;
|
|
28
|
+
/** could-not-judge: rules the classification cannot place. Never defaulted. */
|
|
29
|
+
unclassified: Unclassified[];
|
|
30
|
+
/** sidecar entries naming no rule of this run — reported, a stale sidecar. */
|
|
31
|
+
stale: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Place every id in exactly one class, or name it unclassified with its reason.
|
|
35
|
+
* `ids` is the derived rule set PLUS the doctor-owned ids (CR-1). An R- id is
|
|
36
|
+
* cross-checked against the standard; a non-R id (CR-1) is not in the standard and
|
|
37
|
+
* stands on the sidecar alone.
|
|
38
|
+
*/
|
|
39
|
+
export declare function classifyRules(ids: readonly string[], file?: RuleClassFile, standardKinds?: Map<string, string>, standardSha?: string): Classification;
|
|
40
|
+
export interface MechanicalCount {
|
|
41
|
+
/** the gate's count, or `null` when it cannot be certified (could-not-judge). */
|
|
42
|
+
count: number | null;
|
|
43
|
+
mechanicalViolations: string[];
|
|
44
|
+
judgementViolations: string[];
|
|
45
|
+
/** could-not-judge rule states — NOT violations; reported, never folded into 0. */
|
|
46
|
+
mechanicalAbstentions: string[];
|
|
47
|
+
judgementAbstentions: string[];
|
|
48
|
+
unclassified: Unclassified[];
|
|
49
|
+
/** every reason the count is null (empty when it is an integer). */
|
|
50
|
+
cannotCertify: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Count the non-conforming rules whose class is mechanical. The count is `null`
|
|
54
|
+
* — never 0 — when any rule is unclassified (its violation, present or future,
|
|
55
|
+
* could be mechanical) or when the instrument names a self-defect that voids the
|
|
56
|
+
* run (`instrumentDefects`, e.g. incomplete coverage or a dirty judged tree).
|
|
57
|
+
*/
|
|
58
|
+
export declare function countMechanical(ruleState: ReadonlyMap<string, State>, classification: Classification, instrumentDefects?: readonly string[]): MechanicalCount;
|
|
59
|
+
/** The human line printed next to the VERDICT line. */
|
|
60
|
+
export declare function mechanicalLine(m: MechanicalCount): string;
|
|
61
|
+
/** The deploy hook's evidence schema, key for key and nothing else. */
|
|
62
|
+
export interface Evidence {
|
|
63
|
+
sha: string;
|
|
64
|
+
cli_commit: string;
|
|
65
|
+
convex_path: string;
|
|
66
|
+
exit_code: number;
|
|
67
|
+
checked: number;
|
|
68
|
+
total: number;
|
|
69
|
+
mechanical_violations: number | null;
|
|
70
|
+
/**
|
|
71
|
+
* Each RED mechanical rule -> the non-conforming count its report line prints
|
|
72
|
+
* (sites for a source rule, rows for an oracle rule). Its key count equals
|
|
73
|
+
* `mechanical_violations`; null whenever that is null or a red rule carries no
|
|
74
|
+
* count (src/violation-count.ts). Additive: the seven keys above are unchanged.
|
|
75
|
+
*/
|
|
76
|
+
mechanical_rule_counts: Record<string, number> | null;
|
|
77
|
+
/**
|
|
78
|
+
* Per site-based red mechanical rule: module -> count, where a convex/ file's
|
|
79
|
+
* module is its basename without extension and any other file's is its path
|
|
80
|
+
* without extension (`mcp-server/src/tools`). Values sum to that rule's
|
|
81
|
+
* `mechanical_rule_counts` entry. Row-based rules are absent. Null with the above.
|
|
82
|
+
*/
|
|
83
|
+
mechanical_rule_module_counts: Record<string, Record<string, number>> | null;
|
|
84
|
+
}
|
|
85
|
+
export declare function buildEvidence(e: Evidence): Evidence;
|