@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.
Files changed (63) hide show
  1. package/README.md +48 -4
  2. package/dist/args.d.ts +1 -1
  3. package/dist/args.js +19 -1
  4. package/dist/build-stamp.json +2 -2
  5. package/dist/caller-resolution.js +11 -8
  6. package/dist/cli.js +164 -15
  7. package/dist/contract.d.ts +25 -7
  8. package/dist/contract.js +52 -7
  9. package/dist/coverage.d.ts +1 -1
  10. package/dist/coverage.js +2 -2
  11. package/dist/detectors/oracle-axis1.js +5 -4
  12. package/dist/detectors/oracle-axis2.d.ts +6 -0
  13. package/dist/detectors/oracle-axis2.js +32 -16
  14. package/dist/detectors/predicates.js +12 -3
  15. package/dist/detectors/shared.js +3 -2
  16. package/dist/detectors/source/_ast.d.ts +92 -0
  17. package/dist/detectors/source/_ast.js +353 -0
  18. package/dist/detectors/source/_scan.js +4 -3
  19. package/dist/detectors/source/backfill.js +62 -17
  20. package/dist/detectors/source/contract.js +21 -13
  21. package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
  22. package/dist/detectors/source/cross-tenant-write.js +1408 -0
  23. package/dist/detectors/source/guard-helper-flow.d.ts +22 -0
  24. package/dist/detectors/source/guard-helper-flow.js +49 -6
  25. package/dist/detectors/source/identity-by-id.d.ts +80 -0
  26. package/dist/detectors/source/identity-by-id.js +1783 -0
  27. package/dist/detectors/source/identity.d.ts +8 -1
  28. package/dist/detectors/source/identity.js +364 -35
  29. package/dist/detectors/source/jobs.js +13 -2
  30. package/dist/detectors/source/lifecycle.js +14 -2
  31. package/dist/detectors/source/local-identity.d.ts +26 -0
  32. package/dist/detectors/source/local-identity.js +661 -0
  33. package/dist/detectors/source/package-principal.d.ts +9 -0
  34. package/dist/detectors/source/package-principal.js +177 -0
  35. package/dist/detectors/source/r6-shapes.d.ts +20 -0
  36. package/dist/detectors/source/r6-shapes.js +285 -0
  37. package/dist/detectors/source/readbudget.js +291 -0
  38. package/dist/detectors/source/recipient-by-id.d.ts +40 -0
  39. package/dist/detectors/source/recipient-by-id.js +1108 -0
  40. package/dist/detectors/source/refusal.d.ts +14 -0
  41. package/dist/detectors/source/refusal.js +75 -2
  42. package/dist/detectors/source/security.js +455 -26
  43. package/dist/detectors/source/writesafety.js +27 -14
  44. package/dist/detectors/standard/_doc.d.ts +1 -1
  45. package/dist/detectors/standard/_doc.js +5 -4
  46. package/dist/detectors/standard/r41-frontier.js +3 -2
  47. package/dist/detectors/standard/r46-returns-validator.js +3 -2
  48. package/dist/detectors/structural.d.ts +6 -7
  49. package/dist/detectors/structural.js +75 -43
  50. package/dist/not-checkable.d.ts +1 -1
  51. package/dist/not-checkable.js +11 -0
  52. package/dist/registry.d.ts +1 -1
  53. package/dist/registry.js +27 -3
  54. package/dist/rule-class.d.ts +85 -0
  55. package/dist/rule-class.js +218 -0
  56. package/dist/tally.d.ts +13 -5
  57. package/dist/tally.js +14 -6
  58. package/dist/violation-count.d.ts +48 -0
  59. package/dist/violation-count.js +94 -0
  60. package/fixtures/backend-standard-2825122.md +722 -0
  61. package/fixtures/backend-standard-4dcd3e0.md +722 -0
  62. package/fixtures/rule-classes-2825122.json +228 -0
  63. 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
- // COND 1 — the disposition is COULD-NOT-JUDGE, never conform. A marker records a
136
- // CLAIM that reaches a human reviewer; it never closes the rule green (same as
137
- // the R-51 marker, #141).
138
- // COND 2 — the marker MUST name which shape it claims, because the two are checked
139
- // differently and the reviewer must know which question to ask.
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
- // could-not-judge; the site stays non-conforming until the claim is well-formed.
146
- // A well-formed marker → could-not-judge (the reviewer verifies the per-site claim).
147
- // No marker, an unknown shape, or an incomplete shape-(2) claim → non-conforming.
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 marker yields COULD-NOT-JUDGE, never conform —
236
- // the claim is deferred to the reviewer, who verifies it per site.
237
- scan.cnj.push({
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 an atomicity-exception marker claiming shape "${shape}" — ${shape === SHAPE_PER_ITEM_LOOP
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"}. R-29 refuses (a reviewer verifies the claim per site) — never a silent green (Eta ruling 2026-08-31)`,
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-4bcd605.md";
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-4bcd605.md` — the material at the pin. The
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-4bcd605.md";
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). The oracle's column 9 `rbac_qui` is that single merged column.
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. The oracle cannot
11
- * satisfy the contract because reader and writer authority are not two separate
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. Run against THIS oracle they MUST
3
- // return `non-conforming`, because §2 splits authority into two distinct fields
4
- // (reader R-5, writer R-10) and the oracle carries a SINGLE merged `rbac_qui`
5
- // column. Agreeing with the oracle here would be certifying the defect the
6
- // instrument exists to find — so these are NOT tuned to pass.
7
- import { CONTRACT_FIELDS, READER_AUTHORITY_COLUMN, WRITER_AUTHORITY_COLUMN, authorityIsMerged } from "../contract.js";
8
- import { ORACLE_COLUMNS, oracleLabel, readOracle, refuseWithoutOracle, requireOracle, } from "../oracle.js";
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). The oracle's column 9 `rbac_qui` is that single merged column.
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: "R-14 inventory-schema parity",
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("R-14 inventory-schema parity", () => {
40
+ run: () => refuseWithoutOracle(id, () => {
21
41
  requireOracle();
22
- const authorityColumns = columns.filter((c) => c === "rbac_qui");
23
- if (authorityIsMerged() || authorityColumns.length < 2) {
24
- return {
25
- id: "R-14 inventory-schema parity",
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: the inventory holds a SINGLE merged authority column `rbac_qui` (col#9), " +
29
- "but §2 splits authority into two distinct contract fields — reader authority (R-5) " +
30
- "and writer authority (R-10). A merged authority column is non-conforming by construction: " +
31
- "the reader/writer divergence R-10 and R-7 exist to record cannot be written in it.",
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: "R-14 inventory-schema parity",
36
- anchor: oracleLabel(),
60
+ id,
61
+ anchor: `${oracleLabel()} + ${split.sideCar}`,
37
62
  state: "present",
38
- verdict: "R-14: every §2 contract field present; reader and writer authority are two separate columns.",
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. The oracle cannot
45
- * satisfy the contract because reader and writer authority are not two separate
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: "R-38 thirteen-field completeness",
51
- run: () => refuseWithoutOracle("R-38 thirteen-field completeness", () => {
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 authorityMerged = READER_AUTHORITY_COLUMN === WRITER_AUTHORITY_COLUMN && colSet.has(READER_AUTHORITY_COLUMN);
62
- if (authorityMerged || missing.length > 0) {
63
- const rows = rowsProvider();
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) => !r.malformed && r.by.rbac_qui && r.by.table !== "TOTAL")
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: "R-38 thirteen-field completeness",
98
+ return withCount({
99
+ id,
71
100
  anchor: csvAnchor(),
72
101
  state: "non-conforming",
73
- verdict: `R-38: the oracle cannot satisfy the thirteen-field §2 contract — reader authority (R-5) and ` +
74
- `writer authority (R-10) are not two separate fields; both map to the single column ` +
75
- `\`rbac_qui\` (col#9).${missing.length ? ` Also missing: ${missing.join(", ")}.` : ""} ` +
76
- `Evidence: ${sample}.`,
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: "R-38 thirteen-field completeness",
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, each within its vocabulary.",
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
  };
@@ -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
@@ -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
  };
@@ -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 = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4bcd605656ceff52d4f1593a9176cd6b988471be (elpiarthera/ElPi-Corp)";
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 = "analysis/vantagepeers/backend-standard/backend-standard.md @ 4bcd605656ceff52d4f1593a9176cd6b988471be (elpiarthera/ElPi-Corp)";
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
- return {
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 returns could-not-judge, naming the sites a human verdict is owed.
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;