@vantageos/backend-doctor 0.2.0 → 0.4.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 +57 -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/credential-hygiene.d.ts +11 -0
- package/dist/detectors/source/credential-hygiene.js +619 -0
- 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 +666 -0
- package/dist/detectors/source/narrow-then-bound.d.ts +11 -0
- package/dist/detectors/source/narrow-then-bound.js +571 -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/service-account-claim.d.ts +15 -0
- package/dist/detectors/source/service-account-claim.js +491 -0
- 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 +12 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +55 -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/backend-standard-bacc54a.md +785 -0
- package/fixtures/rule-classes-bacc54a.json +244 -0
- package/package.json +1 -1
|
@@ -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,8 +15,20 @@
|
|
|
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.",
|
|
31
|
+
"R-57": "R-57 no local identity primitive is MEASURED, under the doctor-owned id CR-2 (src/detectors/source/local-identity.ts, PR #61): the standard at bacc54a numbers as R-57 the rule CR-2 already enforces. Its count is reported once, under the key CR-2, because the vantage-peers deploy-gate baseline reads CR-2; reporting it under both ids would count one defect as two rules. Renaming CR-2 to R-57 in the evidence output is a follow-up that must move that baseline in the same delivery. R-57 is enumerated and visible here, never silently dropped, and never reported present.",
|
|
20
32
|
};
|
|
21
33
|
/**
|
|
22
34
|
* One `could-not-judge` DetectorResult per NOT_MECHANICALLY_CHECKABLE rule, so
|
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 @ bacc54a53d5f6f9f20e5254908e2b46807902faa (elpiarthera/ElPi-Corp main; the door-audit rules R-54..R-57, merged as ElPi-Corp #239 = 7d7df89 before the 2026-10-10 history rewrite, same blob 85d8530e)";
|
|
4
4
|
export interface RegistryEntry {
|
|
5
5
|
detector: Detector;
|
|
6
6
|
ruleId: string;
|
package/dist/registry.js
CHANGED
|
@@ -7,13 +7,18 @@ 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_CREDENTIAL_HYGIENE_DETECTORS } from "./detectors/source/credential-hygiene.js";
|
|
11
|
+
import { SOURCE_CROSS_TENANT_WRITE_DETECTORS } from "./detectors/source/cross-tenant-write.js";
|
|
10
12
|
import { IDENTITY_DETECTORS } from "./detectors/source/identity.js";
|
|
13
|
+
import { SOURCE_IDENTITY_BY_ID_DETECTORS } from "./detectors/source/identity-by-id.js";
|
|
11
14
|
import { SOURCE_ISOLATION_DETECTORS } from "./detectors/source/isolation.js";
|
|
12
15
|
import { SOURCE_JOBS_DETECTORS } from "./detectors/source/jobs.js";
|
|
16
|
+
import { SOURCE_NARROW_THEN_BOUND_DETECTORS } from "./detectors/source/narrow-then-bound.js";
|
|
13
17
|
import { SOURCE_LIFECYCLE_DETECTORS } from "./detectors/source/lifecycle.js";
|
|
14
18
|
import { SOURCE_READBUDGET_DETECTORS } from "./detectors/source/readbudget.js";
|
|
15
19
|
import { SOURCE_REFUSAL_DETECTORS } from "./detectors/source/refusal.js";
|
|
16
20
|
import { SOURCE_SECURITY_DETECTORS } from "./detectors/source/security.js";
|
|
21
|
+
import { SOURCE_SERVICE_ACCOUNT_CLAIM_DETECTORS } from "./detectors/source/service-account-claim.js";
|
|
17
22
|
import { SOURCE_WRITE_ISOLATION_DETECTORS } from "./detectors/source/write-isolation.js";
|
|
18
23
|
import { SOURCE_WRITESAFETY_DETECTORS } from "./detectors/source/writesafety.js";
|
|
19
24
|
import { R41, SIBLING_PIN } from "./detectors/standard/r41-frontier.js";
|
|
@@ -21,8 +26,9 @@ import { R46_STANDARD_PIN, STANDARD_SOURCE_DETECTORS, } from "./detectors/standa
|
|
|
21
26
|
import { STANDARD_DOC_DETECTORS } from "./detectors/standard/standard-doc.js";
|
|
22
27
|
import { makeR14, makeR38 } from "./detectors/structural.js";
|
|
23
28
|
import { STATE_RANK } from "./state-rank.js";
|
|
29
|
+
import { countOf, sumCounts, withCount } from "./violation-count.js";
|
|
24
30
|
/** The standard both pins the whole wave against. */
|
|
25
|
-
export const STANDARD_PIN = "
|
|
31
|
+
export const STANDARD_PIN = "vantagepeers/backend-standard/backend-standard.md @ bacc54a53d5f6f9f20e5254908e2b46807902faa (elpiarthera/ElPi-Corp main; the door-audit rules R-54..R-57, merged as ElPi-Corp #239 = 7d7df89 before the 2026-10-10 history rewrite, same blob 85d8530e)";
|
|
26
32
|
/**
|
|
27
33
|
* Merge two detectors that judge halves of the SAME rule into one result: the
|
|
28
34
|
* worst state wins (non-conforming > could-not-judge > absent > present, the
|
|
@@ -40,7 +46,7 @@ export function mergeHalves(ruleId, a, b) {
|
|
|
40
46
|
const reasons = [ra, rb]
|
|
41
47
|
.filter((r) => r.state === "could-not-judge")
|
|
42
48
|
.map((r) => r.reason ?? r.verdict);
|
|
43
|
-
|
|
49
|
+
const merged = {
|
|
44
50
|
id: b.id,
|
|
45
51
|
anchor: decider.anchor,
|
|
46
52
|
state: decider.state,
|
|
@@ -49,6 +55,13 @@ export function mergeHalves(ruleId, a, b) {
|
|
|
49
55
|
? { reason: reasons.join(" || ") }
|
|
50
56
|
: {}),
|
|
51
57
|
};
|
|
58
|
+
// The per-rule count of a red merge is the sum over its RED halves — the
|
|
59
|
+
// halves whose `N site(s) VIOLATE` the merged verdict prints. An abstaining
|
|
60
|
+
// half contributes nothing; a red half with no count leaves it uncounted.
|
|
61
|
+
if (merged.state !== "non-conforming")
|
|
62
|
+
return merged;
|
|
63
|
+
const c = sumCounts([ra, rb].filter((r) => r.state === "non-conforming").map(countOf));
|
|
64
|
+
return c === undefined ? merged : withCount(merged, c);
|
|
52
65
|
},
|
|
53
66
|
};
|
|
54
67
|
}
|
|
@@ -61,7 +74,9 @@ export const REGISTRY = [
|
|
|
61
74
|
{ detector: makeR14(), ruleId: "R-14", standardPin: STANDARD_PIN },
|
|
62
75
|
{ detector: makeR38(), ruleId: "R-38", standardPin: STANDARD_PIN },
|
|
63
76
|
// Axis-2 coherence detectors (R-7/R-8/R-37): each performs its mechanical half
|
|
64
|
-
// IN FULL and
|
|
77
|
+
// IN FULL and decides on the written justification token's PRESENCE in the
|
|
78
|
+
// remediation cell (task k179jnhx, §23) — non-conforming without the token,
|
|
79
|
+
// present with it; could-not-judge only on a row the instrument cannot read.
|
|
65
80
|
{
|
|
66
81
|
detector: makeOracleDetector(r7),
|
|
67
82
|
ruleId: "R-7",
|
|
@@ -145,8 +160,45 @@ export const REGISTRY = [
|
|
|
145
160
|
// documents a client contract; a gated customMutation wrapper (writeMutation) is
|
|
146
161
|
// refused (could-not-judge) since client reachability is not visible from convex/.
|
|
147
162
|
...SOURCE_WRITE_ISOLATION_DETECTORS,
|
|
163
|
+
// R-52 — a write that reaches a row carrying no tenant value through a
|
|
164
|
+
// caller-choosable key (an argument, or a stored field the caller set
|
|
165
|
+
// through a public write) is dominated by a guard reading a server-stamped
|
|
166
|
+
// tenant. Reads schema.ts tenant columns and every public mutation/action
|
|
167
|
+
// handler on the AST (the vantage-peers 45e40c8 tasks.complete auto-link).
|
|
168
|
+
...SOURCE_CROSS_TENANT_WRITE_DETECTORS,
|
|
169
|
+
// R-53 — an actor and a target are identified by ID through
|
|
170
|
+
// @vantageos/cloud-identity: per public write door, the principal's
|
|
171
|
+
// source (a), the admission predicate (b), the unresolved branch (c);
|
|
172
|
+
// per helper / MCP gate, a local identity layer (d). Reads convex/ and
|
|
173
|
+
// mcp-server/src (the PR #1464 ten doors + org-less bulk_complete path).
|
|
174
|
+
...SOURCE_IDENTITY_BY_ID_DETECTORS,
|
|
148
175
|
...SOURCE_CONTRACT_DETECTORS,
|
|
149
176
|
...IDENTITY_DETECTORS,
|
|
177
|
+
// R-54 — a door the fleet service account can reach acts only inside an
|
|
178
|
+
// organisation the transport verified for this call. Reads the convex source:
|
|
179
|
+
// a public door that resolves its caller through the organisation-scope
|
|
180
|
+
// resolver and declares no claim argument (A), or declares one but never
|
|
181
|
+
// refuses the claimless service-account call (B), is red; a claim-bound door
|
|
182
|
+
// and the three listed exits are present; an unreadable caller resolution is
|
|
183
|
+
// could-not-judge. The door audit's cause 1 (standard bacc54a §4 R-54).
|
|
184
|
+
...SOURCE_SERVICE_ACCOUNT_CLAIM_DETECTORS,
|
|
185
|
+
// R-55 — a read narrows by every predicate before it bounds the result, and an
|
|
186
|
+
// aggregate is computed over the narrowed set. Reads the convex source: a
|
|
187
|
+
// bounded read (.take / .paginate over a DB query, or a re-bounding .slice)
|
|
188
|
+
// whose rows are narrowed AFTER the bound by a caller-dependent predicate is
|
|
189
|
+
// red; a fill loop or a refusal on a full window between the bound and the
|
|
190
|
+
// narrowing is listed as covered. An unbounded .collect() is R-11 / R-47's.
|
|
191
|
+
// The door audit's cause 4 (standard bacc54a §4 R-55).
|
|
192
|
+
...SOURCE_NARROW_THEN_BOUND_DETECTORS,
|
|
193
|
+
// R-56 — credentials at rest, refresh tokens, bearers and verification tokens.
|
|
194
|
+
// Reads the convex source: a credential-named column stored without a digest or
|
|
195
|
+
// as the digest of a chosen secret (1); a refresh-token table read by hash with
|
|
196
|
+
// no consuming door (2); a bearer table with no revocation door (3a) or a mint
|
|
197
|
+
// that neither revokes prior bearers nor caps them (3b); a request-supplied
|
|
198
|
+
// commit identifier matched by substring without a full-length check (4) or a
|
|
199
|
+
// response echoing stored content (4e). The door audit's cause 5
|
|
200
|
+
// (standard bacc54a §4 R-56).
|
|
201
|
+
...SOURCE_CREDENTIAL_HYGIENE_DETECTORS,
|
|
150
202
|
// The four STANDARD-DOCUMENT detectors (R-42/R-15/R-32/R-39): each reads the
|
|
151
203
|
// VENDORED standard itself, performs its mechanical/static half in full, and
|
|
152
204
|
// returns could-not-judge naming the sites a human/gate verdict is owed on.
|
|
@@ -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;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// The GATE CLASS of every rule — mechanical or judgement — and the one count the
|
|
2
|
+
// deploy gate refuses on: `mechanical_violations`.
|
|
3
|
+
//
|
|
4
|
+
// The vantage-peers deploy hook (`enforce-backend-doctor-before-deploy.py`) reads
|
|
5
|
+
// `qa/backend-doctor-<sha>.json` and refuses ONLY on mechanical non-conformance,
|
|
6
|
+
// never on a judgement/process rule. This module is where that distinction is made,
|
|
7
|
+
// and it is made from DATA, never from TS logic:
|
|
8
|
+
//
|
|
9
|
+
// - the per-rule class lives in the sidecar `fixtures/rule-classes-<pin>.json`
|
|
10
|
+
// (one entry per numbered rule plus CR-1, each with a one-phrase basis);
|
|
11
|
+
// - every R- entry is CROSS-CHECKED against the pinned standard's own build kind
|
|
12
|
+
// (the section 15 "Build kind" column, and the `*kind:*` token of a Detector spec
|
|
13
|
+
// for a rule the table does not list — R-50/R-51), through the sidecar's
|
|
14
|
+
// `standard_kind_to_class` vocabulary (`process` -> judgement);
|
|
15
|
+
// - a rule with no entry, an entry outside the two classes, an entry with no
|
|
16
|
+
// basis, a rule the standard gives no build kind, or a sidecar class that
|
|
17
|
+
// DISAGREES with the standard is UNCLASSIFIED — a could-not-judge. It is never
|
|
18
|
+
// defaulted to either class; the count it would feed becomes `null`.
|
|
19
|
+
//
|
|
20
|
+
// COUNT GRANULARITY — by RULE, the same granularity as the TALLY and the VERDICT
|
|
21
|
+
// line: a rule whose reduced state is `non-conforming` is one violation, however
|
|
22
|
+
// many sites it names. CR-1 (doctor-owned, outside the pinned rule set) counts as
|
|
23
|
+
// one rule.
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { STANDARD_SHA, VENDORED_STANDARD_PATH } from "./coverage.js";
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
export const RULE_CLASS_PATH = join(here, "..", "fixtures", "rule-classes-bacc54a.json");
|
|
30
|
+
const GATE_CLASSES = new Set(["mechanical", "judgement"]);
|
|
31
|
+
export function loadRuleClassFile(path = RULE_CLASS_PATH) {
|
|
32
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The build kind the PINNED STANDARD itself states per rule: every row of the
|
|
36
|
+
* section 15 table (`| R-N | mechanical|judgement|process | … |`), then, for a rule the
|
|
37
|
+
* table does not list, the `*kind:* <word>` token inside that rule's own text (up
|
|
38
|
+
* to the next rule definition). A rule the standard classifies nowhere is absent
|
|
39
|
+
* from the map — classifyRules reports it as unclassified.
|
|
40
|
+
*/
|
|
41
|
+
export function parseStandardKinds(path = VENDORED_STANDARD_PATH) {
|
|
42
|
+
const blob = readFileSync(path, "utf8");
|
|
43
|
+
const out = new Map();
|
|
44
|
+
const start = blob.indexOf("## 15. Build column");
|
|
45
|
+
if (start !== -1) {
|
|
46
|
+
const after = blob.indexOf("\n## ", start + 1);
|
|
47
|
+
const section = blob.slice(start, after === -1 ? undefined : after);
|
|
48
|
+
for (const line of section.split("\n")) {
|
|
49
|
+
const m = /^\|\s*(R-\d+)\s*\|\s*(mechanical|judgement|process)\s*\|/.exec(line);
|
|
50
|
+
if (m)
|
|
51
|
+
out.set(m[1], m[2]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Detector-spec `*kind:*` for rules outside the table (none at bacc54a: the table lists all 56; kept for a future rule defined outside it).
|
|
55
|
+
const lines = blob.split("\n");
|
|
56
|
+
let current;
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const def = /^\*\*(R-\d+)\b/.exec(line);
|
|
59
|
+
if (def)
|
|
60
|
+
current = def[1];
|
|
61
|
+
if (current === undefined || out.has(current))
|
|
62
|
+
continue;
|
|
63
|
+
const k = /\*kind:\*\s*(mechanical|judgement|process)\b/.exec(line);
|
|
64
|
+
if (k)
|
|
65
|
+
out.set(current, k[1]);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Place every id in exactly one class, or name it unclassified with its reason.
|
|
71
|
+
* `ids` is the derived rule set PLUS the doctor-owned ids (CR-1). An R- id is
|
|
72
|
+
* cross-checked against the standard; a non-R id (CR-1) is not in the standard and
|
|
73
|
+
* stands on the sidecar alone.
|
|
74
|
+
*/
|
|
75
|
+
export function classifyRules(ids, file = loadRuleClassFile(), standardKinds = parseStandardKinds(), standardSha = STANDARD_SHA) {
|
|
76
|
+
const classOf = new Map();
|
|
77
|
+
const basisOf = new Map();
|
|
78
|
+
const unclassified = [];
|
|
79
|
+
const pinMismatch = file.standard_sha !== standardSha
|
|
80
|
+
? `sidecar is pinned to standard ${String(file.standard_sha).slice(0, 12)}, the instrument to ${standardSha.slice(0, 12)}`
|
|
81
|
+
: undefined;
|
|
82
|
+
for (const id of ids) {
|
|
83
|
+
if (pinMismatch) {
|
|
84
|
+
unclassified.push({ id, reason: pinMismatch });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const entry = file.rules?.[id];
|
|
88
|
+
if (entry === undefined) {
|
|
89
|
+
unclassified.push({ id, reason: "no class in the sidecar" });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!GATE_CLASSES.has(entry.class)) {
|
|
93
|
+
unclassified.push({
|
|
94
|
+
id,
|
|
95
|
+
reason: `sidecar class ${JSON.stringify(entry.class)} is neither mechanical nor judgement`,
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (typeof entry.basis !== "string" || entry.basis.trim() === "") {
|
|
100
|
+
unclassified.push({
|
|
101
|
+
id,
|
|
102
|
+
reason: "sidecar class carries no basis phrase",
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (/^R-\d+$/.test(id)) {
|
|
107
|
+
const kind = standardKinds.get(id);
|
|
108
|
+
if (kind === undefined) {
|
|
109
|
+
unclassified.push({
|
|
110
|
+
id,
|
|
111
|
+
reason: "the pinned standard states no build kind for this rule",
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const expected = file.standard_kind_to_class?.[kind];
|
|
116
|
+
if (expected !== entry.class) {
|
|
117
|
+
unclassified.push({
|
|
118
|
+
id,
|
|
119
|
+
reason: `sidecar says ${entry.class}, the pinned standard says ${kind} (-> ${expected ?? "no mapping"}) — CONFLICT`,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
classOf.set(id, entry.class);
|
|
125
|
+
basisOf.set(id, entry.basis);
|
|
126
|
+
}
|
|
127
|
+
const wanted = new Set(ids);
|
|
128
|
+
const stale = Object.keys(file.rules ?? {}).filter((id) => !wanted.has(id));
|
|
129
|
+
return { classOf, basisOf, unclassified, stale };
|
|
130
|
+
}
|
|
131
|
+
const byRuleId = (a, b) => {
|
|
132
|
+
const prefix = (s) => s.replace(/\d+$/, "");
|
|
133
|
+
const n = (s) => Number(/\d+$/.exec(s)?.[0] ?? 0);
|
|
134
|
+
return prefix(a).localeCompare(prefix(b)) || n(a) - n(b);
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Count the non-conforming rules whose class is mechanical. The count is `null`
|
|
138
|
+
* — never 0 — when any rule is unclassified (its violation, present or future,
|
|
139
|
+
* could be mechanical) or when the instrument names a self-defect that voids the
|
|
140
|
+
* run (`instrumentDefects`, e.g. incomplete coverage or a dirty judged tree).
|
|
141
|
+
*/
|
|
142
|
+
export function countMechanical(ruleState, classification, instrumentDefects = []) {
|
|
143
|
+
const out = {
|
|
144
|
+
count: null,
|
|
145
|
+
mechanicalViolations: [],
|
|
146
|
+
judgementViolations: [],
|
|
147
|
+
mechanicalAbstentions: [],
|
|
148
|
+
judgementAbstentions: [],
|
|
149
|
+
unclassified: classification.unclassified,
|
|
150
|
+
cannotCertify: [...instrumentDefects],
|
|
151
|
+
};
|
|
152
|
+
const unclassifiedIds = new Set(classification.unclassified.map((u) => u.id));
|
|
153
|
+
for (const [id, state] of ruleState) {
|
|
154
|
+
const cls = classification.classOf.get(id);
|
|
155
|
+
if (cls === undefined) {
|
|
156
|
+
if (!unclassifiedIds.has(id)) {
|
|
157
|
+
out.unclassified = [
|
|
158
|
+
...out.unclassified,
|
|
159
|
+
{
|
|
160
|
+
id,
|
|
161
|
+
reason: "a result exists for a rule the classification never saw",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
unclassifiedIds.add(id);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (state === "non-conforming") {
|
|
169
|
+
(cls === "mechanical"
|
|
170
|
+
? out.mechanicalViolations
|
|
171
|
+
: out.judgementViolations).push(id);
|
|
172
|
+
}
|
|
173
|
+
else if (state === "could-not-judge") {
|
|
174
|
+
(cls === "mechanical"
|
|
175
|
+
? out.mechanicalAbstentions
|
|
176
|
+
: out.judgementAbstentions).push(id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const list of [
|
|
180
|
+
out.mechanicalViolations,
|
|
181
|
+
out.judgementViolations,
|
|
182
|
+
out.mechanicalAbstentions,
|
|
183
|
+
out.judgementAbstentions,
|
|
184
|
+
])
|
|
185
|
+
list.sort(byRuleId);
|
|
186
|
+
if (out.unclassified.length > 0) {
|
|
187
|
+
out.cannotCertify.push(`${out.unclassified.length} rule(s) unclassified: ${out.unclassified.map((u) => `${u.id} (${u.reason})`).join("; ")}`);
|
|
188
|
+
}
|
|
189
|
+
if (out.cannotCertify.length === 0)
|
|
190
|
+
out.count = out.mechanicalViolations.length;
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
/** The human line printed next to the VERDICT line. */
|
|
194
|
+
export function mechanicalLine(m) {
|
|
195
|
+
const head = m.count === null
|
|
196
|
+
? "MECHANICAL VIOLATIONS: COULD-NOT-JUDGE (no count certified; the evidence file records null)"
|
|
197
|
+
: `MECHANICAL VIOLATIONS: ${m.count}`;
|
|
198
|
+
const list = (ids) => (ids.length ? ids.join(", ") : "none");
|
|
199
|
+
const lines = [
|
|
200
|
+
`${head} — mechanical [${list(m.mechanicalViolations)}]; judgement violations (printed, not counted) [${list(m.judgementViolations)}]; abstentions (not violations, not 0): mechanical [${list(m.mechanicalAbstentions)}], judgement [${list(m.judgementAbstentions)}]`,
|
|
201
|
+
];
|
|
202
|
+
for (const why of m.cannotCertify)
|
|
203
|
+
lines.push(` CANNOT CERTIFY: ${why}`);
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
export function buildEvidence(e) {
|
|
207
|
+
return {
|
|
208
|
+
sha: e.sha,
|
|
209
|
+
cli_commit: e.cli_commit,
|
|
210
|
+
convex_path: e.convex_path,
|
|
211
|
+
exit_code: e.exit_code,
|
|
212
|
+
checked: e.checked,
|
|
213
|
+
total: e.total,
|
|
214
|
+
mechanical_violations: e.mechanical_violations,
|
|
215
|
+
mechanical_rule_counts: e.mechanical_rule_counts,
|
|
216
|
+
mechanical_rule_module_counts: e.mechanical_rule_module_counts,
|
|
217
|
+
};
|
|
218
|
+
}
|