@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/target.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// TARGET RESOLUTION — the run judges the same backend whichever path names it.
|
|
2
|
+
//
|
|
3
|
+
// The defect this closes: the engine's loadTree computes every file's `rel`
|
|
4
|
+
// relative to the path the CLI was given, so the SAME repository produced two
|
|
5
|
+
// different trees under the two documented invocations:
|
|
6
|
+
// - `backend-doctor <repo>/convex` → rels `messages.ts` (no `convex/` segment)
|
|
7
|
+
// - `backend-doctor <repo>` → rels `convex/messages.ts`, PLUS every other
|
|
8
|
+
// `.ts` file in the repository, including archived copies such as
|
|
9
|
+
// `_archive-v0.2/<shell>/convex/*.ts` that a `(^|/)convex/` predicate then
|
|
10
|
+
// judged as live backend source.
|
|
11
|
+
// Detectors keyed on the `convex/` prefix skipped every file in the first form and
|
|
12
|
+
// over-counted in the second; R-36 looked for the lockfile beside `convex/`; CR-1
|
|
13
|
+
// looked for `_generated/api.d.ts` beside the repository root. Measured on vCRM
|
|
14
|
+
// d47a6d4: R-16 2 vs 50 violations, R-6 could-not-judge vs 19 violations.
|
|
15
|
+
//
|
|
16
|
+
// The fix is ONE normalisation at the entry, never a per-detector patch:
|
|
17
|
+
// repoRoot nearest ancestor-or-self holding package.json or .git (a `convex`
|
|
18
|
+
// dir itself is never the repo root, even when it holds a package.json)
|
|
19
|
+
// convexDir the `convex` segment the argument names, else <repoRoot>/convex
|
|
20
|
+
// scanRoots convexDir and <repoRoot>/mcp-server/src (the two source surfaces the
|
|
21
|
+
// detectors' own predicates name), each loaded by the ENGINE's loader
|
|
22
|
+
// rel relative to repoRoot, so every convex file carries `convex/`
|
|
23
|
+
// tree.root repoRoot, so repository co-artifacts (lockfile, workflows) are found
|
|
24
|
+
// Both invocations therefore reduce to the same (repoRoot, convexDir, files) triple.
|
|
25
|
+
import { existsSync, statSync } from "node:fs";
|
|
26
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
27
|
+
import { runDoctor } from "@vantageos/mcp-doctor";
|
|
28
|
+
function isDir(p) {
|
|
29
|
+
try {
|
|
30
|
+
return statSync(p).isDirectory();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function hasRepoMarker(dir) {
|
|
37
|
+
return existsSync(join(dir, "package.json")) || existsSync(join(dir, ".git"));
|
|
38
|
+
}
|
|
39
|
+
/** The `convex` dir the argument is (or sits inside), found walking UP from it and
|
|
40
|
+
* stopping at the first repository marker — so a repository that merely lives
|
|
41
|
+
* under some `/…/convex/…` host path is never mistaken for a convex dir. */
|
|
42
|
+
function namedConvexDir(p) {
|
|
43
|
+
for (let dir = p;; dir = dirname(dir)) {
|
|
44
|
+
if (basename(dir) === "convex")
|
|
45
|
+
return dir;
|
|
46
|
+
if (hasRepoMarker(dir) || dirname(dir) === dir)
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Resolve a CLI argument to the backend it names. Pure over the filesystem. */
|
|
51
|
+
export function resolveTarget(arg) {
|
|
52
|
+
const input = resolve(arg);
|
|
53
|
+
const named = namedConvexDir(input);
|
|
54
|
+
const start = named ? dirname(named) : input;
|
|
55
|
+
let repoRoot = start;
|
|
56
|
+
let repoRootBy = "no marker (argument used as-is)";
|
|
57
|
+
for (let dir = start;; dir = dirname(dir)) {
|
|
58
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
59
|
+
repoRoot = dir;
|
|
60
|
+
repoRootBy = "package.json";
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (existsSync(join(dir, ".git"))) {
|
|
64
|
+
repoRoot = dir;
|
|
65
|
+
repoRootBy = ".git";
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (dirname(dir) === dir)
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const convexDir = named && isDir(named)
|
|
72
|
+
? named
|
|
73
|
+
: isDir(join(repoRoot, "convex"))
|
|
74
|
+
? join(repoRoot, "convex")
|
|
75
|
+
: undefined;
|
|
76
|
+
const scanRoots = [];
|
|
77
|
+
if (convexDir) {
|
|
78
|
+
scanRoots.push(convexDir);
|
|
79
|
+
const mcpSrc = join(repoRoot, "mcp-server", "src");
|
|
80
|
+
if (isDir(mcpSrc))
|
|
81
|
+
scanRoots.push(mcpSrc);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// No convex dir anywhere: judge exactly what was named, never a guess.
|
|
85
|
+
scanRoots.push(input);
|
|
86
|
+
}
|
|
87
|
+
return { input, repoRoot, repoRootBy, convexDir, scanRoots };
|
|
88
|
+
}
|
|
89
|
+
/** Load one directory through the ENGINE's own loader (never a second traversal
|
|
90
|
+
* with its own skip list): a capture detector receives the tree runDoctor built. */
|
|
91
|
+
function engineTree(root) {
|
|
92
|
+
let captured;
|
|
93
|
+
runDoctor(root, [
|
|
94
|
+
{
|
|
95
|
+
id: "target-capture",
|
|
96
|
+
run: (tree) => {
|
|
97
|
+
captured = tree;
|
|
98
|
+
return {
|
|
99
|
+
id: "target-capture",
|
|
100
|
+
anchor: root,
|
|
101
|
+
state: "present",
|
|
102
|
+
verdict: "captured",
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
if (captured === undefined) {
|
|
108
|
+
throw new Error(`backend-doctor: the engine loaded no tree for ${root} — refusing to judge a surface that was never read`);
|
|
109
|
+
}
|
|
110
|
+
return captured;
|
|
111
|
+
}
|
|
112
|
+
/** The ONE tree every detector judges: the scan roots' files, `rel` relative to
|
|
113
|
+
* the repository root, `root` = the repository root. */
|
|
114
|
+
export function loadScopedTree(scope) {
|
|
115
|
+
const files = [];
|
|
116
|
+
let packageJson = null;
|
|
117
|
+
for (const root of scope.scanRoots) {
|
|
118
|
+
const t = engineTree(root);
|
|
119
|
+
packageJson ??= t.packageJson;
|
|
120
|
+
for (const f of t.files) {
|
|
121
|
+
const rel = relative(scope.repoRoot, f.path);
|
|
122
|
+
files.push({ ...f, rel: rel === "" ? basename(f.path) : rel });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { root: scope.repoRoot, files, packageJson };
|
|
126
|
+
}
|
|
127
|
+
/** Files scanned per top-level directory (two segments under the repo root), so a
|
|
128
|
+
* reader sees WHAT was judged — an empty or surprising surface is visible. */
|
|
129
|
+
export function coverageInventory(tree) {
|
|
130
|
+
const inv = new Map();
|
|
131
|
+
for (const f of tree.files) {
|
|
132
|
+
const parts = f.rel.split("/");
|
|
133
|
+
const key = parts.length > 2
|
|
134
|
+
? `${parts[0]}/${parts[1]}`
|
|
135
|
+
: parts.length === 2
|
|
136
|
+
? parts[0]
|
|
137
|
+
: ".";
|
|
138
|
+
inv.set(key, (inv.get(key) ?? 0) + 1);
|
|
139
|
+
}
|
|
140
|
+
return new Map([...inv.entries()].sort((a, b) => a[0].localeCompare(b[0])));
|
|
141
|
+
}
|
|
142
|
+
export function describeScope(scope, tree) {
|
|
143
|
+
const lines = [
|
|
144
|
+
`TARGET: ${scope.input}`,
|
|
145
|
+
` repo root: ${scope.repoRoot} (by ${scope.repoRootBy})`,
|
|
146
|
+
` convex dir: ${scope.convexDir ?? "NONE — no convex/ dir under the repo root; judging the argument as given"}`,
|
|
147
|
+
`COVERAGE INVENTORY (files scanned per dir, rel to repo root; ${tree.files.length} total):`,
|
|
148
|
+
];
|
|
149
|
+
const inv = coverageInventory(tree);
|
|
150
|
+
if (inv.size === 0)
|
|
151
|
+
lines.push(" scanned NOTHING — every source rule below abstains on an empty scan");
|
|
152
|
+
for (const [dir, n] of inv)
|
|
153
|
+
lines.push(` scanned ${dir}: ${n}`);
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
/** Wrap detectors so each judges the scoped tree instead of the engine's
|
|
157
|
+
* per-argument tree. runDoctor still owns the loud-fail contract. */
|
|
158
|
+
export function onScopedTree(tree, detectors) {
|
|
159
|
+
return detectors.map((d) => ({ ...d, run: () => d.run(tree) }));
|
|
160
|
+
}
|
|
161
|
+
/** Absolute convex dir for a scoped tree (tree.root = repo root) or for a tree
|
|
162
|
+
* rooted AT a convex dir (direct detector tests). `prefix` is what the tree's
|
|
163
|
+
* rels carry in front of a convex-relative path. */
|
|
164
|
+
export function convexRootOf(tree) {
|
|
165
|
+
if (existsSync(join(tree.root, "_generated")) ||
|
|
166
|
+
basename(tree.root) === "convex") {
|
|
167
|
+
return { dir: tree.root, prefix: "" };
|
|
168
|
+
}
|
|
169
|
+
const dir = join(tree.root, "convex");
|
|
170
|
+
if (isDir(dir))
|
|
171
|
+
return { dir, prefix: "convex/" };
|
|
172
|
+
return { dir: tree.root, prefix: "" };
|
|
173
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DetectorResult } from "./engine-types.js";
|
|
2
|
+
import type { MechanicalCount } from "./rule-class.js";
|
|
3
|
+
export interface ViolationCount {
|
|
4
|
+
/** the non-conforming count the verdict line prints. */
|
|
5
|
+
count: number;
|
|
6
|
+
/**
|
|
7
|
+
* per module (see moduleOf) — present for a site-based count, absent for a
|
|
8
|
+
* row- or finding-based one; the values always sum to `count`.
|
|
9
|
+
*/
|
|
10
|
+
modules?: Record<string, number>;
|
|
11
|
+
}
|
|
12
|
+
/** Attach `c` to `r` and return `r` (so a producer can `return withCount({...}, c)`). */
|
|
13
|
+
export declare function withCount<R extends DetectorResult>(r: R, c: ViolationCount): R;
|
|
14
|
+
export declare function countOf(r: DetectorResult): ViolationCount | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The module a site's file belongs to. A convex/ file is its basename without
|
|
17
|
+
* extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`). A file
|
|
18
|
+
* outside convex/ (R-53 also reads `mcp-server/src`) keeps its path without
|
|
19
|
+
* extension (`mcp-server/src/tools.ts` -> `mcp-server/src/tools`): it can never
|
|
20
|
+
* collide with a convex basename, and every site has a module, so the split
|
|
21
|
+
* always sums to the count.
|
|
22
|
+
*/
|
|
23
|
+
export declare function moduleOf(rel: string): string;
|
|
24
|
+
/** A site-based count: one item per site, split by module. */
|
|
25
|
+
export declare function siteCount(sites: readonly {
|
|
26
|
+
rel: string;
|
|
27
|
+
}[]): ViolationCount;
|
|
28
|
+
/** A row- or finding-based count: no file, so no module split. */
|
|
29
|
+
export declare function itemCount(n: number): ViolationCount;
|
|
30
|
+
/** Sum the counts of several non-conforming results of one rule (merged halves). */
|
|
31
|
+
export declare function sumCounts(parts: readonly (ViolationCount | undefined)[]): ViolationCount | undefined;
|
|
32
|
+
export interface MechanicalRuleCounts {
|
|
33
|
+
/** red mechanical rule id -> its non-conforming count; null when not certifiable. */
|
|
34
|
+
counts: Record<string, number> | null;
|
|
35
|
+
/** red mechanical rule id -> module -> count, for site-based rules. */
|
|
36
|
+
moduleCounts: Record<string, Record<string, number>> | null;
|
|
37
|
+
/** red mechanical rules with no attached count (the reason `counts` is null). */
|
|
38
|
+
uncounted: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One entry per rule in `mech.mechanicalViolations`: the sum of the attached
|
|
42
|
+
* counts of that rule's non-conforming results (the tally reduces a rule to its
|
|
43
|
+
* worst result; when several results of a rule are red, each one's items are
|
|
44
|
+
* printed and counted). Null when `mech.count` is null or any red rule is uncounted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mechanicalRuleCounts(results: readonly DetectorResult[], mech: MechanicalCount): MechanicalRuleCounts;
|
|
47
|
+
/** The human line printed under MECHANICAL VIOLATIONS. */
|
|
48
|
+
export declare function mechanicalRuleCountsLine(m: MechanicalRuleCounts): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const COUNTS = new WeakMap();
|
|
2
|
+
/** Attach `c` to `r` and return `r` (so a producer can `return withCount({...}, c)`). */
|
|
3
|
+
export function withCount(r, c) {
|
|
4
|
+
COUNTS.set(r, c);
|
|
5
|
+
return r;
|
|
6
|
+
}
|
|
7
|
+
export function countOf(r) {
|
|
8
|
+
return COUNTS.get(r);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The module a site's file belongs to. A convex/ file is its basename without
|
|
12
|
+
* extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`). A file
|
|
13
|
+
* outside convex/ (R-53 also reads `mcp-server/src`) keeps its path without
|
|
14
|
+
* extension (`mcp-server/src/tools.ts` -> `mcp-server/src/tools`): it can never
|
|
15
|
+
* collide with a convex basename, and every site has a module, so the split
|
|
16
|
+
* always sums to the count.
|
|
17
|
+
*/
|
|
18
|
+
export function moduleOf(rel) {
|
|
19
|
+
const noExt = (p) => p.replace(/\.[^./]+$/, "");
|
|
20
|
+
if (/(^|\/)convex\//.test(rel))
|
|
21
|
+
return noExt(rel.split("/").pop() ?? rel);
|
|
22
|
+
return noExt(rel.replace(/^\.\//, ""));
|
|
23
|
+
}
|
|
24
|
+
/** A site-based count: one item per site, split by module. */
|
|
25
|
+
export function siteCount(sites) {
|
|
26
|
+
const modules = {};
|
|
27
|
+
for (const s of sites) {
|
|
28
|
+
const m = moduleOf(s.rel);
|
|
29
|
+
modules[m] = (modules[m] ?? 0) + 1;
|
|
30
|
+
}
|
|
31
|
+
return { count: sites.length, modules: sortKeys(modules) };
|
|
32
|
+
}
|
|
33
|
+
/** A row- or finding-based count: no file, so no module split. */
|
|
34
|
+
export function itemCount(n) {
|
|
35
|
+
return { count: n };
|
|
36
|
+
}
|
|
37
|
+
/** Sum the counts of several non-conforming results of one rule (merged halves). */
|
|
38
|
+
export function sumCounts(parts) {
|
|
39
|
+
if (parts.length === 0 || parts.some((p) => p === undefined))
|
|
40
|
+
return undefined;
|
|
41
|
+
let count = 0;
|
|
42
|
+
let modules = {};
|
|
43
|
+
for (const p of parts) {
|
|
44
|
+
count += p.count;
|
|
45
|
+
if (p.modules === undefined || modules === undefined)
|
|
46
|
+
modules = undefined;
|
|
47
|
+
else
|
|
48
|
+
for (const [k, v] of Object.entries(p.modules))
|
|
49
|
+
modules[k] = (modules[k] ?? 0) + v;
|
|
50
|
+
}
|
|
51
|
+
return modules === undefined
|
|
52
|
+
? { count }
|
|
53
|
+
: { count, modules: sortKeys(modules) };
|
|
54
|
+
}
|
|
55
|
+
function sortKeys(o) {
|
|
56
|
+
return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
|
|
57
|
+
}
|
|
58
|
+
const ruleOf = (r) => /^((?:R|CR)-\d+)\b/.exec(r.id)?.[1];
|
|
59
|
+
/**
|
|
60
|
+
* One entry per rule in `mech.mechanicalViolations`: the sum of the attached
|
|
61
|
+
* counts of that rule's non-conforming results (the tally reduces a rule to its
|
|
62
|
+
* worst result; when several results of a rule are red, each one's items are
|
|
63
|
+
* printed and counted). Null when `mech.count` is null or any red rule is uncounted.
|
|
64
|
+
*/
|
|
65
|
+
export function mechanicalRuleCounts(results, mech) {
|
|
66
|
+
if (mech.count === null)
|
|
67
|
+
return { counts: null, moduleCounts: null, uncounted: [] };
|
|
68
|
+
const counts = {};
|
|
69
|
+
const moduleCounts = {};
|
|
70
|
+
const uncounted = [];
|
|
71
|
+
for (const id of mech.mechanicalViolations) {
|
|
72
|
+
const red = results.filter((r) => ruleOf(r) === id && r.state === "non-conforming");
|
|
73
|
+
const c = sumCounts(red.map(countOf));
|
|
74
|
+
if (c === undefined) {
|
|
75
|
+
uncounted.push(id);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
counts[id] = c.count;
|
|
79
|
+
if (c.modules !== undefined)
|
|
80
|
+
moduleCounts[id] = c.modules;
|
|
81
|
+
}
|
|
82
|
+
if (uncounted.length > 0)
|
|
83
|
+
return { counts: null, moduleCounts: null, uncounted };
|
|
84
|
+
return { counts, moduleCounts, uncounted };
|
|
85
|
+
}
|
|
86
|
+
/** The human line printed under MECHANICAL VIOLATIONS. */
|
|
87
|
+
export function mechanicalRuleCountsLine(m) {
|
|
88
|
+
if (m.counts === null)
|
|
89
|
+
return m.uncounted.length > 0
|
|
90
|
+
? `MECHANICAL RULE COUNTS: COULD-NOT-JUDGE — no count attached for ${m.uncounted.join(", ")} (the evidence file records null)`
|
|
91
|
+
: "MECHANICAL RULE COUNTS: COULD-NOT-JUDGE (the mechanical count is not certified; the evidence file records null)";
|
|
92
|
+
const entries = Object.entries(m.counts);
|
|
93
|
+
return `MECHANICAL RULE COUNTS: ${entries.length ? entries.map(([id, n]) => `${id} ${n}`).join(", ") : "none"}`;
|
|
94
|
+
}
|