@vantageos/backend-doctor 0.1.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 (108) hide show
  1. package/README.md +88 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +94 -0
  4. package/dist/contract.d.ts +18 -0
  5. package/dist/contract.js +29 -0
  6. package/dist/coverage-cli.d.ts +2 -0
  7. package/dist/coverage-cli.js +17 -0
  8. package/dist/coverage.d.ts +32 -0
  9. package/dist/coverage.js +77 -0
  10. package/dist/detectors/oracle-axis1.d.ts +86 -0
  11. package/dist/detectors/oracle-axis1.js +221 -0
  12. package/dist/detectors/oracle-axis2.d.ts +22 -0
  13. package/dist/detectors/oracle-axis2.js +149 -0
  14. package/dist/detectors/predicates.d.ts +19 -0
  15. package/dist/detectors/predicates.js +334 -0
  16. package/dist/detectors/shared.d.ts +56 -0
  17. package/dist/detectors/shared.js +103 -0
  18. package/dist/detectors/source/_scan.d.ts +43 -0
  19. package/dist/detectors/source/_scan.js +82 -0
  20. package/dist/detectors/source/backfill.d.ts +27 -0
  21. package/dist/detectors/source/backfill.js +211 -0
  22. package/dist/detectors/source/contract.d.ts +33 -0
  23. package/dist/detectors/source/contract.js +385 -0
  24. package/dist/detectors/source/identity.d.ts +47 -0
  25. package/dist/detectors/source/identity.js +410 -0
  26. package/dist/detectors/source/jobs.d.ts +10 -0
  27. package/dist/detectors/source/jobs.js +209 -0
  28. package/dist/detectors/source/lifecycle.d.ts +6 -0
  29. package/dist/detectors/source/lifecycle.js +147 -0
  30. package/dist/detectors/source/readbudget.d.ts +6 -0
  31. package/dist/detectors/source/readbudget.js +133 -0
  32. package/dist/detectors/source/refusal.d.ts +6 -0
  33. package/dist/detectors/source/refusal.js +125 -0
  34. package/dist/detectors/source/security.d.ts +37 -0
  35. package/dist/detectors/source/security.js +392 -0
  36. package/dist/detectors/source/writesafety.d.ts +14 -0
  37. package/dist/detectors/source/writesafety.js +345 -0
  38. package/dist/detectors/standard/_doc.d.ts +66 -0
  39. package/dist/detectors/standard/_doc.js +152 -0
  40. package/dist/detectors/standard/r41-frontier.d.ts +58 -0
  41. package/dist/detectors/standard/r41-frontier.js +176 -0
  42. package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
  43. package/dist/detectors/standard/r46-returns-validator.js +474 -0
  44. package/dist/detectors/standard/standard-doc.d.ts +12 -0
  45. package/dist/detectors/standard/standard-doc.js +271 -0
  46. package/dist/detectors/structural.d.ts +14 -0
  47. package/dist/detectors/structural.js +83 -0
  48. package/dist/engine-types.d.ts +10 -0
  49. package/dist/engine-types.js +1 -0
  50. package/dist/not-checkable.d.ts +22 -0
  51. package/dist/not-checkable.js +37 -0
  52. package/dist/oracle.d.ts +33 -0
  53. package/dist/oracle.js +133 -0
  54. package/dist/registry.d.ts +11 -0
  55. package/dist/registry.js +140 -0
  56. package/dist/report.d.ts +35 -0
  57. package/dist/report.js +138 -0
  58. package/dist/severity.d.ts +30 -0
  59. package/dist/severity.js +121 -0
  60. package/dist/tally.d.ts +57 -0
  61. package/dist/tally.js +152 -0
  62. package/dist/trigger.d.ts +47 -0
  63. package/dist/trigger.js +143 -0
  64. package/fixtures/backend-standard-51ec7af1.md +648 -0
  65. package/fixtures/backend-standard-a9d7877b.md +629 -0
  66. package/fixtures/mcp-app-standard-a74a042b.md +279 -0
  67. package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
  68. package/fixtures/mcp-server-standard-a74a042b.md +434 -0
  69. package/fixtures/mcp-standard-a74a042b.md +354 -0
  70. package/fixtures/vp-1200-green/.pin +1 -0
  71. package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
  72. package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
  73. package/fixtures/vp-1200-red/.pin +1 -0
  74. package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
  75. package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
  76. package/fixtures/vp-by-tool-day158.csv +190 -0
  77. package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
  78. package/fixtures/vp-convex/.pin +1 -0
  79. package/fixtures/vp-convex/bun.lock +783 -0
  80. package/fixtures/vp-convex/convex/auth.config.ts +8 -0
  81. package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
  82. package/fixtures/vp-convex/convex/convex.config.ts +14 -0
  83. package/fixtures/vp-convex/convex/credentials.ts +591 -0
  84. package/fixtures/vp-convex/convex/crons.ts +72 -0
  85. package/fixtures/vp-convex/convex/dashboard.ts +199 -0
  86. package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
  87. package/fixtures/vp-convex/convex/issues.ts +544 -0
  88. package/fixtures/vp-convex/convex/kb.ts +305 -0
  89. package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
  90. package/fixtures/vp-convex/convex/kbShared.ts +56 -0
  91. package/fixtures/vp-convex/convex/licenses.ts +443 -0
  92. package/fixtures/vp-convex/convex/memories.ts +406 -0
  93. package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
  94. package/fixtures/vp-convex/convex/messages.ts +971 -0
  95. package/fixtures/vp-convex/convex/migrations.ts +239 -0
  96. package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
  97. package/fixtures/vp-convex/convex/oauth.ts +1608 -0
  98. package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
  99. package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
  100. package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
  101. package/fixtures/vp-convex/convex/schema.ts +1229 -0
  102. package/fixtures/vp-convex/convex/tasks.ts +2782 -0
  103. package/fixtures/vp-r31-green/.pin +1 -0
  104. package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
  105. package/fixtures/vp-r31-red/.pin +1 -0
  106. package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
  107. package/fixtures/vr-by-tool-day159.csv +89 -0
  108. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # backend-doctor
2
+
3
+ A conformance instrument. It scores a Convex backend against the **VantagePeers backend standard** — a *pinned, frozen fixture*, not a document authored here — and prints, per rule, the sites that fail it or the sites a human verdict is owed on. It **consumes** the `@vantageos/mcp-doctor` engine (`runDoctor` + `renderTable`) — it never re-implements it and touches no engine file; the contract types are derived from the engine's own signatures.
4
+
5
+ The standard is vendored under `fixtures/backend-standard-<pin>.md`, byte-identical to its upstream git blob; the canonical source lives in ElPi-Corp at `analysis/vantagepeers/backend-standard/backend-standard.md` (pin `51ec7af1`, 48 rules). The doctor never authors the standard — it scores against the frozen copy.
6
+
7
+ ## What it checks — every rule, one bucket, derived not typed
8
+
9
+ Every mechanical rule of the standard carries an executable control. There is **no deferral bucket**: a rule is either CHECKED, or NOT_MECHANICALLY_CHECKABLE named with a written reason (never silently dropped). The rule count is **whatever the pinned fixture derives** — it is not typed in this README, because a typed count is stale the moment the pin moves (a rule with no detector yet must show as not-checkable against the true denominator, not vanish under a smaller one). Read the live coverage from the tool, never from prose:
10
+
11
+ ```bash
12
+ # the coverage proof — prints "<N> CHECKED + <M> NOT_MECHANICALLY_CHECKABLE = <N+M> of <N+M>"
13
+ # and exits 1 on any gap. This line, not this README, is the count.
14
+ npx --yes @vantageos/backend-doctor@<version> --coverage # or, from a checkout: npx tsx src/coverage-cli.ts
15
+ ```
16
+
17
+ The detectors read four kinds of material, each a **comparison target, never an authority**:
18
+
19
+ | Kind | Reads | Rules (examples) |
20
+ |---|---|---|
21
+ | oracle-CSV (Axis-2) | the per-tool inventory `fixtures/vp-by-tool-day158.csv` | R-1..R-13, R-7, R-8, R-37 |
22
+ | oracle-CSV (Axis-1) | VP oracle **vs** the VR inventory `fixtures/vr-by-tool-day159.csv` | R-40 |
23
+ | source-tree | the real `convex/` source (live path, or `fixtures/vp-convex/` in CI) | R-6, R-11, R-16..R-36 |
24
+ | standard-document | the vendored standard + the four MCP-family docs | R-14, R-15, R-32, R-38, R-39, R-41, R-42 |
25
+
26
+ ### The acceptance property — surfacing, not deferring
27
+
28
+ A rule whose control has a **mechanical detection half and a human/gate half** does not report `present` ("held") or `absent` ("N/A") while its subject exists in the material. It runs the mechanical half **in full** and returns `could-not-judge` naming the concrete sites a human or delivery-gate verdict is owed on — a **work list** where there was silence. Green is the human half surfaced per site, never the detection merely running.
29
+
30
+ ## Run it
31
+
32
+ backend-doctor is its **own tool** (own package `@vantageos/backend-doctor`, own `bin`), like `react-doctor` / `mcp-doctor`. The **supported** invocation is the published, scoped package — so the instrument that judged is the instrument named, and every station runs the same pinned version without cloning:
33
+
34
+ ```bash
35
+ npx --yes @vantageos/backend-doctor@<version> <path-to-a-convex-backend>
36
+ ```
37
+
38
+ Do not type a stale version as the only pin — read the current one from the registry, never from this README:
39
+
40
+ ```bash
41
+ npm view @vantageos/backend-doctor version # the current published version
42
+ npx --yes @vantageos/backend-doctor@$(npm view @vantageos/backend-doctor version) <path-to-a-convex-backend>
43
+ ```
44
+
45
+ Until the scoped package is first published (`npm view @vantageos/backend-doctor` returns 404 until then), the `npx @vantageos/…` form above cannot resolve — run from a checkout (below) in the meantime. Once published, the scoped package is the supported path from that version onward.
46
+
47
+ For **development of the doctor itself**, or to run it before the package is published (not the long-term supported path), from a checkout:
48
+
49
+ ```bash
50
+ npm ci && npm run build
51
+ node dist/cli.js <path-to-a-convex-backend> # the bin; the live verdict + named sites + tally
52
+ npx tsx src/cli.ts <path-to-a-convex-backend> # same, straight from source
53
+ npx tsx src/coverage-cli.ts # the coverage proof; exits 1 on any gap
54
+ npm test # the detector suite (RED-first, both poles, mutation bite proofs)
55
+ npx tsc --noEmit
56
+ ```
57
+
58
+ ### The verdict, the tally, and the exit code (D6)
59
+
60
+ The CLI prints a **machine tally** — the four rule states (`present` / `non-conforming` / `could-not-judge` / `absent`) counted **by code** from the detector results and asserted to sum to the derived rule count (the coverage denominator, never a typed number). No human ever counts rows (the eyeballed-distribution defect is closed). The tally is over **rules** (one worst-state per rule); the printed line also names the raw result count so the two are never confused.
61
+
62
+ It then prints a **severity-mapped verdict** in react-doctor's shape. Each rule's severity is **derived** from the standard's §13 objectives table (never hardcoded):
63
+
64
+ | objective tag (§13) | severity |
65
+ |---|---|
66
+ | `multi-organisation multi-tenant` (tenant-boundary) | **critical** |
67
+ | `highly secure` (without multi-tenant) | major-high |
68
+ | `enterprise-grade` / `production-ready` | major |
69
+ | `token-efficient` only | minor |
70
+
71
+ Verdict mapping, computed from the tally (major = major-high + major):
72
+
73
+ - 0 critical + 0 major violations → **approve** (exit 0)
74
+ - 0 critical + 1..5 major violations → **revise recommended**
75
+ - 0 critical + >5 major violations → **revise mandatory**
76
+ - ≥1 critical violation → **revise mandatory, no bypass**
77
+
78
+ A `could-not-judge` rule is an **abstention** — a human verdict is owed, never a silent approve; any abstention blocks `approve`, and an abstention on a **critical** rule is reported first. The exit code preserves the engine's honesty (and cross-checks `@vantageos/mcp-doctor`'s `computeExitCode`): **1** = a hard violation (≥1 critical, or any major violation); **2** = a human verdict owed with no hard violation (an abstention — never a clean 0); **0** = approve. `approve` is never printed on an exit that means abstention.
79
+
80
+ ## Pins
81
+
82
+ Every input is pinned and, where it is a vendored document, proven byte-identical to its upstream git blob (an un-hashable vendored doc is an authority granted to oneself).
83
+
84
+ **The single authority for which standard fixture is active is the loader constant** `STANDARD_FILE` in [`src/detectors/standard/_doc.ts`](./src/detectors/standard/_doc.ts) — currently `backend-standard-51ec7af1.md` (48 rules, canonical source: ElPi-Corp `analysis/vantagepeers/backend-standard/backend-standard.md`). Everything else is provenance, NOT the active pin: `PINS.txt` and any other `fixtures/backend-standard-<sha>.md` still in the tree (e.g. the superseded `backend-standard-a9d7877b.md`, 47 rules — it lacks R-48) are kept only to show what was scored before. If any of them disagrees with `STANDARD_FILE`, `STANDARD_FILE` wins. Read the active pin from `STANDARD_FILE`, never from `PINS.txt` or a number typed elsewhere — following a stale `PINS.txt` would silently score the superseded 47-rule document and never surface R-48. See [`PINS.txt`](./PINS.txt) for the engine, backend snapshot, and MCP-family doc + VR inventory pins.
85
+
86
+ ## Discipline (how a detector is proven)
87
+
88
+ Every detector is written RED-first against the **real** material (never the standard's prose illustration — the false-green law), proven to **bite** by a mutation injected on material it did not choose and grep-asserted as landed on an executing line before any verdict is read, and proven at **both poles** (a conforming site must pass; a detector that only ever fires is as broken as one that never does). Cross-document rules additionally carry a positive control per document so an unlisted identifier prefix cannot manufacture mass false positives.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ // backend-doctor CLI. Runs the registry THROUGH the engine's runDoctor (reusing
3
+ // its loud-fail trustworthiness contract), renders the results with the engine's
4
+ // renderTable, then prints the coverage summary + the two non-checked lists
5
+ // DISTINCTLY. The target is a real directory so the engine's loadTree succeeds;
6
+ // the first-wave detectors ignore the tree and consult the oracle.
7
+ import { execFileSync } from "node:child_process";
8
+ import { readFileSync } from "node:fs";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { runDoctor, renderTable } from "@vantageos/mcp-doctor";
12
+ import { REGISTRY } from "./registry.js";
13
+ import { coverage, coverageSummary } from "./coverage.js";
14
+ import { NOT_MECHANICALLY_CHECKABLE, notCheckableResults } from "./not-checkable.js";
15
+ import { tally, tallyLine, verdict } from "./tally.js";
16
+ import { writeReport } from "./report.js";
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ /** `git -C <target> rev-parse HEAD`, or "not a git repo" when it is not one. */
19
+ function targetCommit(target) {
20
+ try {
21
+ return execFileSync("git", ["-C", target, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
22
+ }
23
+ catch {
24
+ return "not a git repo";
25
+ }
26
+ }
27
+ /** Untracked INCLUDED — the short porcelain form is not a clean-tree check. */
28
+ function treeStatus(target) {
29
+ try {
30
+ return execFileSync("git", ["-C", target, "status", "--porcelain", "--untracked-files=all"], {
31
+ encoding: "utf8",
32
+ }).trim();
33
+ }
34
+ catch {
35
+ return "not a git repo";
36
+ }
37
+ }
38
+ function toolVersion() {
39
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
40
+ return pkg.version;
41
+ }
42
+ function main() {
43
+ const target = resolve(process.argv[2] ?? process.cwd());
44
+ // Every derived rule carries EXACTLY ONE result: the executable detectors plus
45
+ // one could-not-judge abstention per NOT_MECHANICALLY_CHECKABLE rule (R-48's
46
+ // pending detector) — so the tally's rule total equals the 48 derived rules and
47
+ // nothing is silently dropped.
48
+ const results = [
49
+ ...runDoctor(target, REGISTRY.map((e) => e.detector)),
50
+ ...notCheckableResults(),
51
+ ];
52
+ console.log(renderTable("backend-doctor (VantagePeers oracle @ day158)", results));
53
+ console.log("");
54
+ const cov = coverage();
55
+ console.log(coverageSummary(cov));
56
+ console.log("");
57
+ console.log(`NOT MECHANICALLY CHECKABLE (${cov.notCheckable.length}) — a human decides, or it governs a document; NOT a clean bill:`);
58
+ for (const id of cov.notCheckable) {
59
+ console.log(` ${id} — ${NOT_MECHANICALLY_CHECKABLE[id]}`);
60
+ }
61
+ // D6: the machine TALLY + the severity-mapped VERDICT. The distribution is
62
+ // DERIVED here (no human counts rows) and the verdict maps to a TRUTHFUL exit
63
+ // code — >=1 critical or a hard violation exits 1, a human-verdict-owed
64
+ // abstention exits 2, approve exits 0. See src/tally.ts + src/severity.ts.
65
+ const t = tally(results);
66
+ const v = verdict(t, results);
67
+ console.log("");
68
+ console.log(tallyLine(t));
69
+ console.log(v.summary);
70
+ if (v.criticalViolations > 0) {
71
+ console.log(` critical violations: ${v.violations.critical.join(", ")}`);
72
+ }
73
+ if (v.criticalAbstentions > 0) {
74
+ console.log(` critical abstentions (human verdict owed): ${v.abstentions.critical.join(", ")}`);
75
+ }
76
+ // The run WRITES its report to a FILE, ALWAYS — not behind a flag. The clock and
77
+ // the git facts are gathered HERE (a real process) and handed to the pure writer.
78
+ const ctx = {
79
+ target,
80
+ targetCommit: targetCommit(target),
81
+ toolVersion: toolVersion(),
82
+ dateISO: new Date().toISOString().slice(0, 10),
83
+ treeStatus: treeStatus(target),
84
+ };
85
+ const reportDir = join(here, "..", "reports");
86
+ const path = writeReport(ctx, results, t, v, reportDir);
87
+ console.log(`\nreport written: ${path}`);
88
+ if (!cov.ok) {
89
+ console.log("\nNOTE: coverage is INCOMPLETE — a rule is in neither bucket. See the summary above.");
90
+ process.exit(1);
91
+ }
92
+ process.exit(v.exitCode);
93
+ }
94
+ main();
@@ -0,0 +1,18 @@
1
+ import type { OracleColumn } from "./oracle.js";
2
+ export interface ContractField {
3
+ /** the §2 contract field name. */
4
+ field: string;
5
+ /** the audit column(s) §2 maps it onto. */
6
+ columns: OracleColumn[];
7
+ /** the rule the field carries. */
8
+ rule: string;
9
+ }
10
+ /** The thirteen §2 contract fields (R-38 completeness gate operates on this). */
11
+ export declare const CONTRACT_FIELDS: ContractField[];
12
+ /** The two §2 fields that MUST be two distinct columns — reader and writer
13
+ * authority. The oracle merges both onto `rbac_qui`: the structural defect. */
14
+ export declare const READER_AUTHORITY_COLUMN: OracleColumn;
15
+ export declare const WRITER_AUTHORITY_COLUMN: OracleColumn;
16
+ /** True when reader and writer authority resolve to the SAME column — the
17
+ * merged-authority non-conformance R-14 names by construction. */
18
+ export declare function authorityIsMerged(): boolean;
@@ -0,0 +1,29 @@
1
+ /** The thirteen §2 contract fields (R-38 completeness gate operates on this). */
2
+ export const CONTRACT_FIELDS = [
3
+ { field: "exposure", columns: ["surface"], rule: "R-1" },
4
+ { field: "operation verb", columns: ["crud"], rule: "R-2" },
5
+ { field: "reader authority", columns: ["rbac_qui"], rule: "R-5" },
6
+ { field: "writer authority", columns: ["rbac_qui"], rule: "R-10" },
7
+ { field: "retrieval mechanism", columns: ["search_mecanisme"], rule: "R-4" },
8
+ { field: "pagination", columns: ["search_mecanisme"], rule: "R-3" },
9
+ { field: "isolation locus", columns: ["scope_enforcement"], rule: "R-11" },
10
+ { field: "boundary key", columns: ["namespace_ou_org"], rule: "R-6" },
11
+ { field: "read/write coherence", columns: ["rbac_coherence_table"], rule: "R-7" },
12
+ { field: "vector boundary", columns: ["search_mecanisme"], rule: "R-12" },
13
+ { field: "token cost", columns: ["tokens_estime_contexte"], rule: "R-9" },
14
+ { field: "remediation", columns: ["rbac_adjustment_needed"], rule: "R-28" },
15
+ {
16
+ field: "disposition & lifecycle",
17
+ columns: ["table_purpose", "table_conserver_supprimer", "statut_suppression"],
18
+ rule: "R-13/R-27",
19
+ },
20
+ ];
21
+ /** The two §2 fields that MUST be two distinct columns — reader and writer
22
+ * authority. The oracle merges both onto `rbac_qui`: the structural defect. */
23
+ export const READER_AUTHORITY_COLUMN = "rbac_qui";
24
+ export const WRITER_AUTHORITY_COLUMN = "rbac_qui";
25
+ /** True when reader and writer authority resolve to the SAME column — the
26
+ * merged-authority non-conformance R-14 names by construction. */
27
+ export function authorityIsMerged() {
28
+ return READER_AUTHORITY_COLUMN === WRITER_AUTHORITY_COLUMN;
29
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // Prints the coverage proof over the TWO buckets — CHECKED and
3
+ // NOT_MECHANICALLY_CHECKABLE. Exits non-zero only if the union does not equal
4
+ // the derived rule ids (a rule in no bucket, in two, or a stale bucket entry).
5
+ import { coverage, coverageSummary } from "./coverage.js";
6
+ import { NOT_MECHANICALLY_CHECKABLE } from "./not-checkable.js";
7
+ const r = coverage();
8
+ console.log(coverageSummary(r));
9
+ console.log("");
10
+ console.log(`CHECKED (${r.checked.length}): ${r.checked.join(" ")}`);
11
+ console.log("");
12
+ console.log(`NOT MECHANICALLY CHECKABLE (${r.notCheckable.length}) — a human decides, or it governs a document, not backend data:`);
13
+ for (const id of r.notCheckable)
14
+ console.log(` ${id} — ${NOT_MECHANICALLY_CHECKABLE[id]}`);
15
+ console.log("");
16
+ console.log(`derived rule ids (${r.ruleIds.length}): ${r.ruleIds.join(" ")}`);
17
+ process.exit(r.ok ? 0 : 1);
@@ -0,0 +1,32 @@
1
+ /** The standard is VENDORED at the pin as a fixture — DATA at 51ec7af1, a
2
+ * comparison target on the same footing as the oracle CSV. deriveRuleIds no
3
+ * longer shells out to any local repo, so it is CI-portable. Re-pinned from the
4
+ * stale 47-rule freeze (a9d7877b) to the canonical 48-rule standard (adds R-48,
5
+ * authority-totality) so coverage cannot pretend 47 is the whole document. */
6
+ export declare const STANDARD_SHA = "51ec7af1bc84239aefd751d16389c65af6082c9f";
7
+ export declare const VENDORED_STANDARD_PATH: string;
8
+ /**
9
+ * Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
10
+ * never typed, never a git show against a local repo. Reads the `**R-NN`
11
+ * rule-definition headers from the file at the pin.
12
+ */
13
+ export declare function deriveRuleIds(path?: string): string[];
14
+ export interface CoverageReport {
15
+ ok: boolean;
16
+ ruleIds: string[];
17
+ checked: string[];
18
+ notCheckable: string[];
19
+ /** rules in NO bucket — the failure that names an incomplete doctor. */
20
+ uncovered: string[];
21
+ /** rules in MORE THAN ONE bucket — a rule cannot be two things at once. */
22
+ doubleCovered: string[];
23
+ /** bucket entries that are not a derived rule id — a stale bucket. */
24
+ unknown: string[];
25
+ }
26
+ /**
27
+ * Compute coverage over the three buckets. All inputs are injectable so a test
28
+ * can add a phantom uncovered rule and assert the mechanism fails.
29
+ */
30
+ export declare function coverage(ruleIds?: string[], checked?: string[], notCheckable?: string[]): CoverageReport;
31
+ /** A one-line assertion string naming any gap — used by the CLI and tests. */
32
+ export declare function coverageSummary(r: CoverageReport): string;
@@ -0,0 +1,77 @@
1
+ // The coverage acceptance mechanism. Every rule derived from the pinned standard
2
+ // is in EXACTLY ONE of TWO buckets: CHECKED (a registry entry tags it) or
3
+ // NOT_MECHANICALLY_CHECKABLE (a human decides / governs a document). Their union
4
+ // MUST equal the derived rule ids — none in two, none in neither.
5
+ //
6
+ // This is the assertion that stops backend-doctor silently becoming an
7
+ // INCOMPLETE doctor when a rule is later added. There is no third bucket: a rule
8
+ // a machine can decide is BUILT, not curated on a debt list.
9
+ import { readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { NOT_MECHANICALLY_CHECKABLE } from "./not-checkable.js";
13
+ import { checkedRuleIds } from "./registry.js";
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ /** The standard is VENDORED at the pin as a fixture — DATA at 51ec7af1, a
16
+ * comparison target on the same footing as the oracle CSV. deriveRuleIds no
17
+ * longer shells out to any local repo, so it is CI-portable. Re-pinned from the
18
+ * stale 47-rule freeze (a9d7877b) to the canonical 48-rule standard (adds R-48,
19
+ * authority-totality) so coverage cannot pretend 47 is the whole document. */
20
+ export const STANDARD_SHA = "51ec7af1bc84239aefd751d16389c65af6082c9f";
21
+ export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-51ec7af1.md");
22
+ /**
23
+ * Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
24
+ * never typed, never a git show against a local repo. Reads the `**R-NN`
25
+ * rule-definition headers from the file at the pin.
26
+ */
27
+ export function deriveRuleIds(path = VENDORED_STANDARD_PATH) {
28
+ const blob = readFileSync(path, "utf8");
29
+ const ids = new Set();
30
+ for (const line of blob.split("\n")) {
31
+ const m = /^\*\*(R-[0-9]+)/.exec(line);
32
+ if (m)
33
+ ids.add(m[1]);
34
+ }
35
+ return [...ids].sort((a, b) => Number(a.slice(2)) - Number(b.slice(2)));
36
+ }
37
+ function sortIds(ids) {
38
+ return [...new Set(ids)].sort((a, b) => Number(a.slice(2)) - Number(b.slice(2)));
39
+ }
40
+ /**
41
+ * Compute coverage over the three buckets. All inputs are injectable so a test
42
+ * can add a phantom uncovered rule and assert the mechanism fails.
43
+ */
44
+ export function coverage(ruleIds = deriveRuleIds(), checked = checkedRuleIds(), notCheckable = Object.keys(NOT_MECHANICALLY_CHECKABLE)) {
45
+ const buckets = [checked, notCheckable];
46
+ const ruleSet = new Set(ruleIds);
47
+ const countIn = (r) => buckets.filter((b) => b.includes(r)).length;
48
+ const uncovered = ruleIds.filter((r) => countIn(r) === 0);
49
+ const doubleCovered = ruleIds.filter((r) => countIn(r) > 1);
50
+ const unknown = buckets.flat().filter((r) => !ruleSet.has(r));
51
+ return {
52
+ ok: uncovered.length === 0 &&
53
+ doubleCovered.length === 0 &&
54
+ unknown.length === 0,
55
+ ruleIds,
56
+ checked: sortIds(checked),
57
+ notCheckable: sortIds(notCheckable),
58
+ uncovered,
59
+ doubleCovered,
60
+ unknown,
61
+ };
62
+ }
63
+ /** A one-line assertion string naming any gap — used by the CLI and tests. */
64
+ export function coverageSummary(r) {
65
+ const total = r.checked.length + r.notCheckable.length;
66
+ const head = `coverage: ${r.checked.length} CHECKED + ${r.notCheckable.length} NOT_MECHANICALLY_CHECKABLE = ${total} of ${r.ruleIds.length} derived rules`;
67
+ if (r.ok)
68
+ return `${head} — COMPLETE (no gap).`;
69
+ const gaps = [];
70
+ if (r.uncovered.length)
71
+ gaps.push(`UNCOVERED (in no bucket): ${r.uncovered.join(", ")}`);
72
+ if (r.doubleCovered.length)
73
+ gaps.push(`DOUBLE-COVERED (in >1 bucket): ${r.doubleCovered.join(", ")}`);
74
+ if (r.unknown.length)
75
+ gaps.push(`UNKNOWN (not a derived rule): ${r.unknown.join(", ")}`);
76
+ return `${head} — INCOMPLETE. ${gaps.join(" | ")}`;
77
+ }
@@ -0,0 +1,86 @@
1
+ import type { Detector } from "../engine-types.js";
2
+ import { type OracleRow } from "../oracle.js";
3
+ /** Absolute path to the pinned SECOND-backend (VantageRegistry) oracle fixture.
4
+ * Vendored at pin a74a042b, blob b53e2626954a887303d11d944b6132a11dbd34f6. */
5
+ export declare const VR_ORACLE_PATH: string;
6
+ /** The vendored standard (the R-5 law) — the SAME pin coverage.ts parses. The
7
+ * §2 [shared] contract columns are DERIVED from this file, never hardcoded.
8
+ * Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
9
+ * import cycle. */
10
+ export declare const VENDORED_STANDARD_PATH: string;
11
+ /** A backend inventory: its real header (in file order) + typed rows. Loaded the
12
+ * SAME way as the VP oracle — parseCsv, drop header, key cells by column name. */
13
+ export interface Inventory {
14
+ /** the file's REAL column headers, never hardcoded. */
15
+ header: string[];
16
+ /** column-name → trimmed value, one entry per data row. */
17
+ rows: Record<string, string>[];
18
+ }
19
+ /** Load any backend inventory CSV generically from its OWN header — the shared
20
+ * fields are DERIVED from these real headers, never a prose illustration. */
21
+ export declare function loadInventory(path: string): Inventory;
22
+ /** Memoized read of the pinned VR inventory. */
23
+ export declare function readVrInventory(): Inventory;
24
+ /** The VP inventory, adapted from the memoized VP oracle reader — the SAME rows
25
+ * the axis-2 detectors read, exposed with VP's real header. */
26
+ export declare function vpInventory(rowsProvider?: () => OracleRow[]): Inventory;
27
+ /** The SHARED FIELDS: the columns present in BOTH backends' real headers —
28
+ * computed as the intersection, never hardcoded. Used for the STRUCTURAL
29
+ * (schema) compare; the VALUE compare is narrowed to the §2 [shared] contract
30
+ * columns (see `sharedContractColumns`). */
31
+ export declare function sharedFields(vp: string[], vr: string[]): string[];
32
+ /** The §2 CONTRACT columns the standard marks **[shared]** — DERIVED by parsing
33
+ * the vendored standard's §2 table (the R-5 law), never hardcoded. A [shared]
34
+ * row binds a contract field to its `audit column`, wrapped in backticks in the
35
+ * table's second cell. The five [shared] fields (operation verb, reader
36
+ * authority, writer authority, retrieval mechanism, pagination) resolve to the
37
+ * three audit columns `crud`, `rbac_qui`, `search_mecanisme` — reader/writer
38
+ * authority both live in the single merged `rbac_qui` (itself the R-14 defect),
39
+ * and retrieval mechanism + pagination both live in `search_mecanisme`.
40
+ *
41
+ * The R-40 VALUE compare runs ONLY over these closed-vocabulary columns. Free
42
+ * identifiers — `table`, `outil` (row identity) and `T2_verdict`,
43
+ * `T2_composant_cible` (audit bookkeeping) — are NOT vocabularies and are NEVER
44
+ * sited: comparing them counts row-identity noise as divergence and buries the
45
+ * one finding R-40 exists to catch. */
46
+ export declare function sharedContractColumns(standardPath?: string): string[];
47
+ /** The STRUCTURAL (between-schema) divergence: a column one backend carries and
48
+ * the other lacks. `statut_suppression` (VP) vs absent (VR) is the R-14-class
49
+ * finding the compare must SURFACE, not silently drop. */
50
+ export interface SchemaDivergence {
51
+ vpOnlyColumns: string[];
52
+ vrOnlyColumns: string[];
53
+ }
54
+ export declare function schemaDivergence(vp: string[], vr: string[]): SchemaDivergence;
55
+ /** Per shared field, the closed vocabularies of the two backends compared:
56
+ * `shared` values BOTH populate (agreeing — NEVER a site), `vpOnly` / `vrOnly`
57
+ * values populated by exactly one backend (the between-backend divergence). */
58
+ export interface FieldDivergence {
59
+ field: string;
60
+ shared: string[];
61
+ vpOnly: string[];
62
+ vrOnly: string[];
63
+ }
64
+ /** A field is a divergence SITE iff one backend fills it with a vocabulary
65
+ * member the other never uses. */
66
+ export declare function isSite(d: FieldDivergence): boolean;
67
+ /** Compare two inventories on each §2 [shared] contract field present in BOTH
68
+ * real headers. The scope is the CLOSED-VOCABULARY columns the standard marks
69
+ * [shared] (`crud`, `rbac_qui`, `search_mecanisme`) intersected with the two
70
+ * real headers — never all shared columns (that counts `table`/`outil`/`T2_*`
71
+ * free identifiers as vocabularies). Returns the per-field vocabulary comparison
72
+ * for every in-scope field (sites and agreeing fields alike) so a caller can
73
+ * prove BOTH poles: an agreeing value is not a site, a diverging value is. */
74
+ export declare function compareFields(vp: Inventory, vr: Inventory): FieldDivergence[];
75
+ /** The full Axis-1 comparison result: schema-level (column) divergence + the
76
+ * per-field vocabulary divergences. */
77
+ export interface Axis1Comparison {
78
+ schema: SchemaDivergence;
79
+ fields: FieldDivergence[];
80
+ /** the shared-field sites — fields where a value diverges between backends. */
81
+ sites: FieldDivergence[];
82
+ }
83
+ export declare function compareBackends(vpProvider?: () => OracleRow[], vrProvider?: () => Inventory): Axis1Comparison;
84
+ /** Wrap the Axis-1 compare into a Detector. Providers are injectable for tests;
85
+ * production reads the two memoized real oracles. */
86
+ export declare function makeR40(vpProvider?: () => OracleRow[], vrProvider?: () => Inventory): Detector;