@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.
Files changed (93) hide show
  1. package/README.md +66 -5
  2. package/dist/args.d.ts +16 -0
  3. package/dist/args.js +72 -0
  4. package/dist/build-stamp.d.ts +13 -0
  5. package/dist/build-stamp.js +76 -0
  6. package/dist/build-stamp.json +4 -0
  7. package/dist/caller-resolution.d.ts +222 -0
  8. package/dist/caller-resolution.js +1198 -0
  9. package/dist/cli.js +249 -17
  10. package/dist/contract.d.ts +25 -7
  11. package/dist/contract.js +52 -7
  12. package/dist/coverage-cli.js +8 -0
  13. package/dist/coverage.d.ts +1 -1
  14. package/dist/coverage.js +2 -2
  15. package/dist/detectors/oracle-axis1.d.ts +5 -3
  16. package/dist/detectors/oracle-axis1.js +28 -18
  17. package/dist/detectors/oracle-axis2.d.ts +6 -0
  18. package/dist/detectors/oracle-axis2.js +37 -22
  19. package/dist/detectors/predicates.d.ts +3 -0
  20. package/dist/detectors/predicates.js +93 -19
  21. package/dist/detectors/shared.js +8 -7
  22. package/dist/detectors/source/_ast.d.ts +92 -0
  23. package/dist/detectors/source/_ast.js +353 -0
  24. package/dist/detectors/source/_scan.js +4 -3
  25. package/dist/detectors/source/backfill.js +71 -18
  26. package/dist/detectors/source/contract.js +21 -13
  27. package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
  28. package/dist/detectors/source/cross-tenant-write.js +1408 -0
  29. package/dist/detectors/source/guard-helper-flow.d.ts +134 -0
  30. package/dist/detectors/source/guard-helper-flow.js +1216 -0
  31. package/dist/detectors/source/identity-by-id.d.ts +80 -0
  32. package/dist/detectors/source/identity-by-id.js +1783 -0
  33. package/dist/detectors/source/identity.d.ts +58 -2
  34. package/dist/detectors/source/identity.js +1158 -28
  35. package/dist/detectors/source/isolation.d.ts +11 -1
  36. package/dist/detectors/source/isolation.js +106 -25
  37. package/dist/detectors/source/jobs.js +178 -4
  38. package/dist/detectors/source/lifecycle.js +14 -2
  39. package/dist/detectors/source/local-identity.d.ts +26 -0
  40. package/dist/detectors/source/local-identity.js +661 -0
  41. package/dist/detectors/source/package-principal.d.ts +9 -0
  42. package/dist/detectors/source/package-principal.js +177 -0
  43. package/dist/detectors/source/r6-shapes.d.ts +20 -0
  44. package/dist/detectors/source/r6-shapes.js +285 -0
  45. package/dist/detectors/source/readbudget.d.ts +1 -0
  46. package/dist/detectors/source/readbudget.js +358 -4
  47. package/dist/detectors/source/recipient-by-id.d.ts +40 -0
  48. package/dist/detectors/source/recipient-by-id.js +1108 -0
  49. package/dist/detectors/source/refusal.d.ts +14 -0
  50. package/dist/detectors/source/refusal.js +75 -2
  51. package/dist/detectors/source/security.d.ts +16 -0
  52. package/dist/detectors/source/security.js +680 -33
  53. package/dist/detectors/source/write-isolation.d.ts +8 -1
  54. package/dist/detectors/source/write-isolation.js +64 -30
  55. package/dist/detectors/source/writesafety.js +27 -14
  56. package/dist/detectors/standard/_doc.d.ts +1 -1
  57. package/dist/detectors/standard/_doc.js +5 -4
  58. package/dist/detectors/standard/r41-frontier.js +3 -2
  59. package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
  60. package/dist/detectors/standard/r46-returns-validator.js +48 -16
  61. package/dist/detectors/structural.d.ts +6 -7
  62. package/dist/detectors/structural.js +84 -48
  63. package/dist/not-checkable.d.ts +1 -1
  64. package/dist/not-checkable.js +11 -0
  65. package/dist/oracle.d.ts +92 -3
  66. package/dist/oracle.js +229 -12
  67. package/dist/registry.d.ts +8 -1
  68. package/dist/registry.js +74 -8
  69. package/dist/report.d.ts +11 -1
  70. package/dist/report.js +15 -2
  71. package/dist/rule-class.d.ts +85 -0
  72. package/dist/rule-class.js +218 -0
  73. package/dist/state-rank.d.ts +5 -0
  74. package/dist/state-rank.js +9 -0
  75. package/dist/tally.d.ts +13 -5
  76. package/dist/tally.js +15 -13
  77. package/dist/target.d.ts +32 -0
  78. package/dist/target.js +173 -0
  79. package/dist/violation-count.d.ts +48 -0
  80. package/dist/violation-count.js +94 -0
  81. package/fixtures/backend-standard-2825122.md +722 -0
  82. package/fixtures/backend-standard-4dcd3e0.md +722 -0
  83. package/fixtures/cr1-classes/_generated/api.d.ts +24 -0
  84. package/fixtures/cr1-classes/surface.ts +206 -0
  85. package/fixtures/cr1-empty/nothing.ts +1 -0
  86. package/fixtures/cr1-refusal-on-resolved/_generated/api.d.ts +24 -0
  87. package/fixtures/cr1-refusal-on-resolved/doors.ts +180 -0
  88. package/fixtures/cr1-refusal-on-resolved/lib/auth.ts +104 -0
  89. package/fixtures/r34-vp-auth-30e821b/auth-805-812.ts.txt +8 -0
  90. package/fixtures/r34-vp-auth-30e821b/auth-878-890.ts.txt +13 -0
  91. package/fixtures/rule-classes-2825122.json +228 -0
  92. package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
  93. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
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.
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 `vantagepeers/backend-standard/backend-standard.md` (pin `2825122`, 52 rules — the merge of ElPi-Corp PR #228 on `origin/main`, on top of #226's merge `9f7d789`; see `PINS.txt`). The doctor never authors the standard — it scores against the frozen copy.
6
6
 
7
7
  ## What it checks — every rule, one bucket, derived not typed
8
8
 
@@ -18,9 +18,10 @@ The detectors read four kinds of material, each a **comparison target, never an
18
18
 
19
19
  | Kind | Reads | Rules (examples) |
20
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 |
21
+ | oracle-CSV (Axis-2) | the per-tool inventory the target's own `.backend-doctor/vp-by-tool.csv` (or `--oracle <file>`); absent => could-not-judge | R-1..R-13, R-7, R-8, R-37 |
22
+ | writer-tier side-car | `vp-writer-tier.csv` beside the VP oracle (or `--writer-tier <file>`), joined on `outil`; absent => R-10 could-not-judge, file named | R-10 |
23
+ | oracle-CSV (Axis-1) | VP oracle **vs** the VR inventory `.backend-doctor/vr-by-tool.csv` (or `--vr-oracle <file>`) | R-40 |
24
+ | source-tree | the real `convex/` source and `mcp-server/src` (live path, or `fixtures/vp-convex/` in CI) | R-6, R-11, R-16..R-36, R-50..R-53 |
24
25
  | 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
 
26
27
  ### The acceptance property — surfacing, not deferring
@@ -33,6 +34,8 @@ backend-doctor is its **own tool** (own package `@vantageos/backend-doctor`, own
33
34
 
34
35
  ```bash
35
36
  npx --yes @vantageos/backend-doctor@<version> <path-to-a-convex-backend>
37
+ # <path> may be the repo root or its convex/ dir: both are normalised to the repo root
38
+ # (nearest package.json/.git) and produce identical results. --version / --help are supported.
36
39
  ```
37
40
 
38
41
  Do not type a stale version as the only pin — read the current one from the registry, never from this README:
@@ -55,6 +58,8 @@ npm test # the detector suite (RED-firs
55
58
  npx tsc --noEmit
56
59
  ```
57
60
 
61
+ `node dist/cli.js` refuses to run (exit 2) when the `dist/` it runs from was compiled from different source than the checkout's `src/`: `npm run build` stamps `dist/build-stamp.json` with a sha256 of `src/**/*.ts`, and the CLI recomputes it before reading anything (`src/build-stamp.ts`). A gate that must not depend on a build step runs from source (`npx tsx src/cli.ts`). An installed package has no `src/` and is identified by its version.
62
+
58
63
  ### The verdict, the tally, and the exit code (D6)
59
64
 
60
65
  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.
@@ -77,11 +82,67 @@ Verdict mapping, computed from the tally (major = major-high + major):
77
82
 
78
83
  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
84
 
85
+ ### CR-1 — caller resolution on the exposed surface (doctor-owned, CRITICAL)
86
+
87
+ Alongside the numbered rules the CLI renders a **CALLER RESOLUTION (CR-1)** section answering one question per registration: **does this public registration resolve its caller before serving or writing data?**
88
+
89
+ - **Population is the exposed surface itself**, derived from the target tree's own `convex/_generated/api.d.ts` — never a hand-written list and never a pattern over argument names. `internal*`/`httpAction` are excluded and the exclusion is **proven per site** from the builder token on that line. The count classified must **equal** the count exposed; a discrepancy is reported as CR-1's own bug.
90
+ - **Four classes, and every member is named**: `RESOLVES` / `FAIL-OPEN` / `NO-IDENTITY` / `NO-DATA`. `FAIL-OPEN` and unjustified `NO-IDENTITY`/`NO-DATA` are mechanical violations and the gate refuses on them (exit 1).
91
+ - **The only exit is a token in the target source**, `allow-no-caller-resolution: <reason>`, read at the registration. There is no exemption list, no baseline and no tolerated count — `FAIL-OPEN` has no token exit at all.
92
+ - **It asks whether the caller is RESOLVED, never whether a refusal is shaped correctly.** That second question is R-50/R-51's; a handler with no guard has nothing to throw, so it cannot fail a rule about throwing.
93
+ - On a tree that exposes nothing it reports **no applicable site**, never a count.
94
+
95
+ **CR-1 is NOT one of the numbered rules** in the pinned standard, so it is not in `src/registry.ts` and does not enter the coverage line: registering an id the vendored standard does not define would make `coverage.ts` report a phantom rule. Its upstream number is **pending**: the earlier proposal "R-52" is withdrawn because the standard at `2825122` gives R-52 to the cross-tenant-write rule and R-53 to identity-by-ID (§16); CR-1 takes the next free number when it is upstreamed, in the standard's own repository. Source and rationale: `src/caller-resolution.ts`.
96
+
97
+ ### CR-2 — no local identity primitive (doctor-owned, mechanical, ceiling 0)
98
+
99
+ Operator order 2026-10-09: *"No application, no MCP server, and no starter defines its own primitive for: resolving a caller to a tenant, asserting membership of an organisation, filtering rows by a caller's scope, or validating a master credential."* Identity lives only in `@vantageos/cloud-identity`. The CLI renders a **LOCAL IDENTITY (CR-2)** section; every site is named by `file:line`.
100
+
101
+ - **One site per function that defines a primitive** (a helper, an MCP tool handler, or a Convex registration whose own handler does it) in `convex/` and `mcp-server/src`. Each site names every kind it holds, with its line: **tenant** (resolves a caller to a tenant or agent), **membership** (a gate on a role / master field, a membership-table read), **scope** (an allow-list field read, a membership test over a caller's tenant / name / scope list), **credential** (`timingSafeEqual`, token hashing, `jose` / `jsonwebtoken` verification), **name** (an agent identified by name: an index keyed on a name field, a comparison through a name normaliser).
102
+ - **R-53 is reused, not re-derived**: its (d) local identity layers, its doors whose principal is resolved by the handler itself, its clause-1 recipient-by-name sites and its (c) master-on-absence branches become CR-2 sites of the matching kind. What R-53 already sanctions (a `getUserIdentity()` value that reaches only a package resolver, the org-ID adapter read) is not a site.
103
+ - **EXEMPT, and reported as exempt**: a file of `@vantageos/cloud-identity` itself, and a site carrying `allow-local-identity: <reason of 8+ characters>` in its leading comment or body. Each exempt site is listed with its reason.
104
+ - **COULD-NOT-JUDGE**: a file with a syntax error is named and judged nowhere (exit 2 when nothing else is red); an empty population is never a clean bill.
105
+ - **Ceiling 0**: CR-2 is mechanical (`fixtures/rule-classes-2825122.json`). Any violation is counted in `mechanical_rule_counts` / `mechanical_rule_module_counts` and exits 1.
106
+
107
+ **CR-2 is NOT one of the numbered rules** for the same reason as CR-1: the pinned standard numbers R-1..R-53, and an `R-54` it does not define would be reported by `coverage.ts` as an unknown rule and left unclassified by `rule-class.ts` (which would null the mechanical count). Source and rationale: `src/detectors/source/local-identity.ts`.
108
+
109
+ ### R-31 — the read budget of a bulk mutation
110
+
111
+ - **R-31** (`src/detectors/source/readbudget.ts`): a `.collect()` inside a mutation is non-conforming unless it reads an index-equality range carrying `// read-bound: <reason>`; a bounded read (`.take(n)`/`.paginate`) conforms when the mutation self-reschedules with `ctx.scheduler.runAfter`, or when it is a **caller-driven drain** — the `.paginate` result flows to one return statement of the handler itself (a nested callback's return does not count) carrying both the continuation cursor and the `isDone` flag (any field names), so the caller re-invokes it page by page, and no `ctx.runMutation`/`ctx.runAction` in the same transaction carries that cursor or names the mutation; every other bounded read is could-not-judge.
112
+
113
+ ### R-52 and R-53 — the write reaches the right row, and the actor is who it is by ID
114
+
115
+ - **R-52** (`src/detectors/source/cross-tenant-write.ts`): a write that reaches a row with no tenant value (no tenant column, or an optional one) through a value the caller chose — an argument, or a stored field a public write set from its arguments, such as `task.project` — must be dominated by a guard reading a server-stamped tenant: the target's own tenant when it has a column, a fleet-only decision when it has none. Tenant columns are the standard's `orgId`/`tenantId`/`workspaceId`/`orgSlug`, or a `namespace` checked against the caller's allowed prefixes (ElPi-Corp #228). The namespace condition is decided per table from its public doors (every query/mutation/action that reads or writes it): every door runs a prefix test (a function taking a namespace parameter and testing it with `.startsWith`, closed over calls — vantage-peers `isNamespaceAllowedForScope` — called on a namespace value: the argument at its namespace parameter derives from `args.namespace`, a row's `.namespace` or a binding of one, never a constant), refuses every non-master, or reads the table only on the true side of an `isMaster` test -> tenant column; no door does -> the table is untenanted; mixed, or no public door -> every R-52 site targeting it is could-not-judge, never green. On vantage-peers `2faa399`, `memories` is mixed (nine doors checked, `oauth.ts:provisionOrganization` reads it through `findSeatNameCollision` unchecked), so its six sites are could-not-judge.
116
+ - **R-53** (`src/detectors/source/identity-by-id.ts`): every public write door resolves its principal through `@vantageos/cloud-identity` (never a `client_org_mapping` lookup in product code, save the one read the ruling of task k1742q0vm05x2f5pqawnz1eah18fp01p admits: keyed by the Clerk org ID — `.withIndex("by_clerk_org_id", (q) => q.eq("clerkOrgId", K))` or `.filter((q) => q.eq(q.field("clerkOrgId"), K))`, then `.unique()`/`.first()` — where `K` is the org ID the package hands the `orgKindOf` adapter (its first parameter, or a helper parameter every call site fills with it), inside an `orgKindOf` or `organisationById` member of a lookups object passed to `resolveActingPrincipal`/`assertTargetBelongsTo` — inline, through a local, or as the return of a named builder that only assembles (no branch, no throw, no expression statement) — deciding nothing but its own return, an `organisationById` answering only `{ id, active }` read from the row or null; a slug or name key, `withOrgScope`/`lookupOrgMapping`, and a read whose result reaches anything else stay red; a `getUserIdentity()` read is admitted only when its value, followed through locals, destructuring and helpers, ends solely as the first argument of `resolveActingPrincipal`/`resolvePersonPrincipal`, since the package verifies nothing about the credential itself), admits by comparing stored IDs (inline, or through the package's own `assertTargetBelongsTo(principal, { orgId: row.orgId[, ownerId: row.ownerId] }, …)` — synchronous at cloud-identity `a6e5780` or awaited with a lookups argument at PR #21 — whose `!ok` branch throws or returns before the write; never a name field against a typed or credential-carried name), and refuses an unresolved principal; any helper or MCP tool handler that compares names to admit, or resolves identity itself, is a local identity layer.
117
+
118
+ Neither rule has a justification-token exit. Both are mechanical and count in `mechanical_violations`.
119
+
120
+ ### The deploy-gate evidence — `--json-evidence` and `mechanical_violations`
121
+
122
+ The vantage-peers deploy hook (`enforce-backend-doctor-before-deploy.py`) refuses a Convex deploy unless `qa/backend-doctor-<sha>.json` pins the deployed commit and carries an integer `mechanical_violations == 0`. It refuses on **mechanical** non-conformance only, never on a judgement/process rule. The CLI writes that file:
123
+
124
+ ```bash
125
+ npx tsx src/cli.ts <repo>/convex --json-evidence <repo>/qa/ # a directory -> backend-doctor-<sha>.json
126
+ ```
127
+
128
+ It writes exactly `{"sha","cli_commit","convex_path","exit_code","checked","total","mechanical_violations","mechanical_rule_counts","mechanical_rule_module_counts"}` (the last two are additive; the first seven are unchanged):
129
+
130
+ - `sha` = `git -C <convex_path>/.. rev-parse HEAD`; no sha, no file (exit 2).
131
+ - `cli_commit` = this checkout's HEAD (`+dirty` when `src/` or `fixtures/` differ from it), or `npm @vantageos/backend-doctor@<version>` for an installed copy.
132
+ - `exit_code` = the code the process actually exits with; `checked`/`total` = the coverage line.
133
+ - `mechanical_violations` = the number of **rules** whose reduced state is `non-conforming` and whose class is **mechanical** (CR-1 and CR-2 each count as one rule). The same count is printed as `MECHANICAL VIOLATIONS: N` right under the VERDICT line, with the per-rule breakdown.
134
+ - `mechanical_rule_counts` = an object mapping each red mechanical rule id to its **non-conforming count**, the number its report line prints: sites for a source rule (`R-53: 113 site(s) VIOLATE` -> `"R-53": 113`), rows for an oracle rule (`R-2: 6 of 106 applicable oracle rows VIOLATE` -> `"R-2": 6`), registrations for CR-1, defining functions for CR-2. Its key count equals `mechanical_violations`; `{}` on a clean run. The count is attached by the reduction that formats the printed line, from the same variable (`src/violation-count.ts`), never parsed back from the text. A rule reduced from two red halves (R-31) sums its red halves; a single structural finding (R-14, R-38) counts 1. Printed as `MECHANICAL RULE COUNTS: R-2 6, …` under the MECHANICAL line. **`null`** when `mechanical_violations` is null, or when a red mechanical rule's producer attached no count (printed as `MECHANICAL RULE COUNTS: COULD-NOT-JUDGE — no count attached for …`): a partial map would read as complete.
135
+ - `mechanical_rule_module_counts` = for each **site-based** red mechanical rule (source scans, R-46, CR-1, CR-2), `{ module: count }` summing to that rule's `mechanical_rule_counts` entry. A `convex/` file's module is its basename without extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`); a file outside `convex/` keeps its path without extension (R-53 also reads `mcp-server/src`: `mcp-server/src/tools`). Row- and finding-based rules (R-2, R-8, R-13, R-28 and every other oracle rule, R-14, R-37, R-38, R-40, R-41, the standard-document rules) are absent. `null` with `mechanical_rule_counts`.
136
+
137
+ **The class is data, not code.** `fixtures/rule-classes-2825122.json` gives every numbered rule plus CR-1 and CR-2 exactly one class with a one-phrase basis (38 mechanical = the standard's 36 + CR-1 + CR-2; 16 judgement = 13 judgement + 3 process). Each R- entry is cross-checked at run time against the pinned standard's own build kind (§15 table, which at `2825122` lists all 52 rules; a Detector spec's `*kind:*` token is the fallback for a rule the table omits), `process` mapping to judgement. Consequence: **R-6, R-11, R-29, R-33, R-34, R-35 now COUNT** (ElPi-Corp #225 settled them mechanical), as do **R-52 and R-53**; R-16..R-20 and the other §15 judgement rules print but are not counted, exactly as the standard classifies them.
138
+
139
+ **Never a silent 0.** Abstentions (`could-not-judge`) are not violations: they are listed apart on the MECHANICAL line and keep the exit code at 2 when no violation exists. The count becomes `null` (the hook reads it as INCOMPLETE and refuses) when it cannot be certified: a rule with no class, a sidecar class that contradicts the standard, a sidecar pinned to another standard, incomplete coverage, a failed CRUD self-check, or a judged `convex/` tree that differs from the commit `sha` names. Each reason is printed as `CANNOT CERTIFY: …`.
140
+
80
141
  ## Pins
81
142
 
82
143
  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
144
 
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-4bcd605.md` (50 rules — R-50/R-51, org isolation on a subscribed read / a pre-org write is a typed empty/unauthorised result, never an uncaught throw; at this pin a gated `customQuery`/`customMutation` wrapper is JUDGED red/green from its readable `customCtx`, not could-not-judge; 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.
145
+ **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-2825122.md` (52 rules — adds R-52, a write reaching a row with no tenant value through a caller-choosable key checks the target's tenant first, and R-53, actor and target identified by ID through `@vantageos/cloud-identity`, a name never selects or authorises; canonical source: ElPi-Corp `vantagepeers/backend-standard/backend-standard.md`). `2825122` is the merge of ElPi-Corp PR #228 on `origin/main` (after #226 at `9f7d789`); it resolves. The previous pins `backend-standard-4dcd3e0.md` (the provisional #226 head) and `backend-standard-4bcd605.md` (50 rules) stay in the tree as provenance. **Declared divergence at this pin:** the R-53 text at `2825122` rules `resolveVerifiedPerson` red under clause 1 because it selects the organisation by `clerkOrgSlug`, a slug, rather than by ID; the R-53 detector reports the slug-keyed lookup itself (vantage-peers `2faa399` `convex/lib/auth.ts:465` `lookupOrgMapping`, clause (d), `query("client_org_mapping").withIndex(<clerk claim>)`) but does not report `resolveVerifiedPerson` (`convex/lib/personPrincipal.ts:73`), which receives that lookup injected, as a site of its own under clause 1 — a gap carried to a follow-up, not a green. 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
146
 
86
147
  ## Discipline (how a detector is proven)
87
148
 
package/dist/args.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export declare const USAGE = "usage: backend-doctor [<target>] [--oracle <csv>] [--vr-oracle <csv>] [--writer-tier <csv>]\n [--json-evidence <file-or-dir>]\n backend-doctor --version | --help\n\n <target> the repository root or its convex/ dir (default: the current dir).\n Both name the same backend: paths are normalised to the repo root\n (nearest dir holding package.json or .git), so the counts are identical.\n --oracle, --vr-oracle, --writer-tier\n explicit oracle CSVs; by default they are read from\n <repo>/convex/.backend-doctor/ or <repo>/.backend-doctor/ \u2014 never from\n above the repo root and never from this tool's bundled fixtures.\n --json-evidence <file-or-dir>\n also write the deploy-gate evidence JSON, exactly\n {\"sha\",\"cli_commit\",\"convex_path\",\"exit_code\",\"checked\",\"total\",\n \"mechanical_violations\",\"mechanical_rule_counts\",\n \"mechanical_rule_module_counts\"}; a directory receives\n backend-doctor-<sha>.json. mechanical_violations is null (never 0)\n when it cannot be certified (an unclassified rule, incomplete\n coverage, a CRUD self-check failure, or a convex/ tree that differs\n from HEAD). mechanical_rule_counts maps each red mechanical rule to\n the count its report line prints; module counts split site-based\n rules by convex module. Both are null when the count is null or a\n red rule carries no count.\n --version print the tool version and exit 0.\n --help print this text and exit 0.\n\nexit: 0 approve, 1 a violation / revise, 2 the run could not judge (bad usage,\nunreadable target, stale build, unreadable standard).";
2
+ export type ParsedArgs = {
3
+ kind: "run";
4
+ positional: string[];
5
+ flags: Record<string, string>;
6
+ } | {
7
+ kind: "version";
8
+ } | {
9
+ kind: "help";
10
+ } | {
11
+ kind: "usage-error";
12
+ message: string;
13
+ };
14
+ /** Flag reader. `--version`/`--help` win over everything else; an unknown flag or
15
+ * a value flag with no value is a usage error (exit 2) — never a path. */
16
+ export declare function parseArgs(argv: string[]): ParsedArgs;
package/dist/args.js ADDED
@@ -0,0 +1,72 @@
1
+ // CLI argument parsing, kept apart from cli.ts (which runs main() on import) so the
2
+ // flag contract is testable without spawning a process.
3
+ const VALUE_FLAGS = new Set([
4
+ "oracle",
5
+ "vr-oracle",
6
+ "writer-tier",
7
+ "json-evidence",
8
+ ]);
9
+ export const USAGE = `usage: backend-doctor [<target>] [--oracle <csv>] [--vr-oracle <csv>] [--writer-tier <csv>]
10
+ [--json-evidence <file-or-dir>]
11
+ backend-doctor --version | --help
12
+
13
+ <target> the repository root or its convex/ dir (default: the current dir).
14
+ Both name the same backend: paths are normalised to the repo root
15
+ (nearest dir holding package.json or .git), so the counts are identical.
16
+ --oracle, --vr-oracle, --writer-tier
17
+ explicit oracle CSVs; by default they are read from
18
+ <repo>/convex/.backend-doctor/ or <repo>/.backend-doctor/ — never from
19
+ above the repo root and never from this tool's bundled fixtures.
20
+ --json-evidence <file-or-dir>
21
+ also write the deploy-gate evidence JSON, exactly
22
+ {"sha","cli_commit","convex_path","exit_code","checked","total",
23
+ "mechanical_violations","mechanical_rule_counts",
24
+ "mechanical_rule_module_counts"}; a directory receives
25
+ backend-doctor-<sha>.json. mechanical_violations is null (never 0)
26
+ when it cannot be certified (an unclassified rule, incomplete
27
+ coverage, a CRUD self-check failure, or a convex/ tree that differs
28
+ from HEAD). mechanical_rule_counts maps each red mechanical rule to
29
+ the count its report line prints; module counts split site-based
30
+ rules by convex module. Both are null when the count is null or a
31
+ red rule carries no count.
32
+ --version print the tool version and exit 0.
33
+ --help print this text and exit 0.
34
+
35
+ exit: 0 approve, 1 a violation / revise, 2 the run could not judge (bad usage,
36
+ unreadable target, stale build, unreadable standard).`;
37
+ /** Flag reader. `--version`/`--help` win over everything else; an unknown flag or
38
+ * a value flag with no value is a usage error (exit 2) — never a path. */
39
+ export function parseArgs(argv) {
40
+ if (argv.some((a) => a === "--version" || a === "-V" || a === "-v"))
41
+ return { kind: "version" };
42
+ if (argv.some((a) => a === "--help" || a === "-h"))
43
+ return { kind: "help" };
44
+ const positional = [];
45
+ const flags = {};
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const a = argv[i];
48
+ if (a.startsWith("-") && a !== "-") {
49
+ const body = a.replace(/^--?/, "");
50
+ const eq = body.indexOf("=");
51
+ const name = eq === -1 ? body : body.slice(0, eq);
52
+ if (!a.startsWith("--") || !VALUE_FLAGS.has(name)) {
53
+ return { kind: "usage-error", message: `unknown flag ${a}` };
54
+ }
55
+ if (eq !== -1)
56
+ flags[name] = body.slice(eq + 1);
57
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
58
+ flags[name] = argv[++i];
59
+ else
60
+ return { kind: "usage-error", message: `flag ${a} needs a value` };
61
+ }
62
+ else
63
+ positional.push(a);
64
+ }
65
+ if (positional.length > 1) {
66
+ return {
67
+ kind: "usage-error",
68
+ message: `expected at most one target, got ${positional.length}: ${positional.join(" ")}`,
69
+ };
70
+ }
71
+ return { kind: "run", positional, flags };
72
+ }
@@ -0,0 +1,13 @@
1
+ export declare const STAMP_FILE = "build-stamp.json";
2
+ /** sha256 over every `.ts` under `srcDir`: sorted relative path + NUL + bytes. */
3
+ export declare function sourceDigest(srcDir: string): string;
4
+ export type Freshness = {
5
+ ok: true;
6
+ mode: "source" | "installed" | "fresh-dist";
7
+ } | {
8
+ ok: false;
9
+ reason: string;
10
+ };
11
+ /** Judge the build the CLI is running from. `here` is the directory of the
12
+ * running module (`dist/` when built, `src/` under tsx). */
13
+ export declare function checkDistFreshness(here: string): Freshness;
@@ -0,0 +1,76 @@
1
+ // The dist-freshness guard. A gate verdict is a claim about a tree made BY a
2
+ // specific instrument; when `dist/` was compiled from an older `src/` than the
3
+ // checkout it sits in, the verdict silently comes from code nobody is reading
4
+ // (measured: an installed dist built 2026-09-30 judging with detectors that
5
+ // source 60780e6 of 2026-10-01 had already changed).
6
+ //
7
+ // CHOICE — refuse, by CONTENT, not warn by mtime:
8
+ // - `npm run build` stamps `dist/build-stamp.json` with a sha256 over every
9
+ // `src/**/*.ts` (path + bytes, sorted). The CLI, when it runs from `dist/` and a
10
+ // sibling `src/` exists (a checkout), recomputes that digest and REFUSES (exit 2)
11
+ // on a mismatch or a missing stamp, naming both digests and the two fixes.
12
+ // - A content digest is immune to what makes mtime useless here: `git checkout`
13
+ // rewrites mtimes, and a rebuild with no source change must not trip it.
14
+ // - A warning was rejected: the doctor's verdict line is read by gates, not by a
15
+ // human watching stderr, so a warning beside a verdict is still a verdict.
16
+ // - "Document that the gate runs from source" was rejected as the ONLY control:
17
+ // it is the rule that was already being followed when the stale dist ran.
18
+ // - An installed package (no sibling `src/`) is not checked: there the published
19
+ // version IS the instrument's identity, and the tarball carries no source.
20
+ import { createHash } from "node:crypto";
21
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
22
+ import { basename, join, relative } from "node:path";
23
+ export const STAMP_FILE = "build-stamp.json";
24
+ /** sha256 over every `.ts` under `srcDir`: sorted relative path + NUL + bytes. */
25
+ export function sourceDigest(srcDir) {
26
+ const files = [];
27
+ const walk = (dir) => {
28
+ for (const name of readdirSync(dir)) {
29
+ const full = join(dir, name);
30
+ if (statSync(full).isDirectory())
31
+ walk(full);
32
+ else if (name.endsWith(".ts"))
33
+ files.push(full);
34
+ }
35
+ };
36
+ walk(srcDir);
37
+ const h = createHash("sha256");
38
+ for (const f of files.map((x) => relative(srcDir, x)).sort()) {
39
+ h.update(f);
40
+ h.update("\0");
41
+ h.update(readFileSync(join(srcDir, f)));
42
+ h.update("\0");
43
+ }
44
+ return h.digest("hex");
45
+ }
46
+ /** Judge the build the CLI is running from. `here` is the directory of the
47
+ * running module (`dist/` when built, `src/` under tsx). */
48
+ export function checkDistFreshness(here) {
49
+ if (basename(here) !== "dist")
50
+ return { ok: true, mode: "source" };
51
+ const src = join(here, "..", "src");
52
+ if (!existsSync(src))
53
+ return { ok: true, mode: "installed" };
54
+ const stampPath = join(here, STAMP_FILE);
55
+ const current = sourceDigest(src);
56
+ if (!existsSync(stampPath)) {
57
+ return {
58
+ ok: false,
59
+ reason: `dist/ carries no ${STAMP_FILE}, so which source it was compiled from cannot be shown (current src sha256 ${current.slice(0, 12)}). Rebuild with \`npm run build\`, or run from source: \`npx tsx src/cli.ts <target>\`.`,
60
+ };
61
+ }
62
+ let stamped;
63
+ try {
64
+ stamped = JSON.parse(readFileSync(stampPath, "utf8")).sourceDigest;
65
+ }
66
+ catch {
67
+ stamped = undefined;
68
+ }
69
+ if (stamped !== current) {
70
+ return {
71
+ ok: false,
72
+ reason: `dist/ is STALE: compiled from src sha256 ${String(stamped).slice(0, 12)}, the checkout's src is ${current.slice(0, 12)}. A verdict from this build would come from code that is no longer the source. Rebuild with \`npm run build\`, or run from source: \`npx tsx src/cli.ts <target>\`.`,
73
+ };
74
+ }
75
+ return { ok: true, mode: "fresh-dist" };
76
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "sourceDigest": "b6bfdbdcea24a6c2a672ed0ebc0b19dcaa680893b280820c23cbaf960130423d",
3
+ "builtAt": "2026-10-10T15:07:02.464Z"
4
+ }
@@ -0,0 +1,222 @@
1
+ import type { Detector, DetectorResult, Tree } from "./engine-types.js";
2
+ export type SrcFile = Tree["files"][number];
3
+ export type SrcLine = SrcFile["lines"][number];
4
+ /** The four classes. Every exposed public registration lands in exactly one. */
5
+ export type CallerClass = "RESOLVES" | "FAIL-OPEN" | "NO-IDENTITY" | "NO-DATA" | "COULD-NOT-JUDGE";
6
+ export interface Registration {
7
+ /** the api module key the registration is exposed under, e.g. `tasks`. */
8
+ module: string;
9
+ /** the exported name, e.g. `listUnlinkedBlocked`. */
10
+ name: string;
11
+ /** the api path a caller addresses, e.g. `tasks:listUnlinkedBlocked`. */
12
+ api: string;
13
+ rel: string;
14
+ /** 1-based line of the `export const NAME = <builder>(` opener. */
15
+ n: number;
16
+ /** the builder token READ AT THIS SITE — the per-site exclusion proof. */
17
+ builder: string;
18
+ }
19
+ export interface Finding extends Registration {
20
+ cls: CallerClass;
21
+ /** why it landed in that class — the discriminator(s), named. */
22
+ why: string;
23
+ /** true when a justification token in the target source cleared the finding. */
24
+ justified?: boolean;
25
+ }
26
+ export interface CallerResolutionScan {
27
+ /** api.d.ts module key -> file rel path, derived from the target's own api.d.ts. */
28
+ exposedModules: Map<string, string>;
29
+ /** how the population was obtained, for the cross-check line. */
30
+ populationSource: string;
31
+ /** every public registration, classified. */
32
+ findings: Finding[];
33
+ /** `internal*` / httpAction sites, each with the builder token observed. */
34
+ excluded: Registration[];
35
+ /** CR-1's OWN defects: population drift, an unreadable api.d.ts, an
36
+ * undelimitable block, an unresolvable builder. Never a silent pass. */
37
+ selfDefects: string[];
38
+ /** the cross-check: exposed public registrations vs. classified findings. */
39
+ exposedCount: number;
40
+ classifiedCount: number;
41
+ }
42
+ export interface ApiSurface {
43
+ modules: Map<string, string>;
44
+ /** set when the surface could NOT be derived — never a silent empty. */
45
+ unreadable?: string;
46
+ }
47
+ /**
48
+ * Derive the exposed module surface from `<root>/_generated/api.d.ts`. The engine's
49
+ * loadTree excludes `_generated/`, so this reads the file from the target root —
50
+ * the same target the CLI was pointed at, never the tool's own install directory
51
+ * (the defect that let five oracle-scored rules present a frozen CSV count as a
52
+ * verdict on a tree they never opened).
53
+ */
54
+ export declare function deriveApiSurface(root: string, read?: (p: string) => string): ApiSurface;
55
+ export interface BuilderMap {
56
+ /** builder name -> the public base it produces. */
57
+ publicBuilders: Map<string, string>;
58
+ /** builder name -> the non-public base it produces. */
59
+ nonPublicBuilders: Map<string, string>;
60
+ /** a `custom*` wrapper whose base resolves to neither — CR-1 refuses on these. */
61
+ unresolvable: {
62
+ rel: string;
63
+ n: number;
64
+ name: string;
65
+ base: string;
66
+ }[];
67
+ }
68
+ /** a file CR-1 judges — never a test, never generated. */
69
+ export declare function isJudgeable(f: SrcFile): boolean;
70
+ /**
71
+ * Discover every registration BUILDER the tree declares, so a `customQuery(query,…)`
72
+ * wrapper is recognised as public and a `customQuery(internalQuery,…)` wrapper as
73
+ * not. A wrapper whose base resolves to neither leaves the exposed surface
74
+ * unknowable → CR-1 refuses (never a silent green).
75
+ */
76
+ export declare function discoverBuilders(files: SrcFile[]): BuilderMap;
77
+ /**
78
+ * Everything CR-1 knows about the tree's own helpers, DERIVED from the tree.
79
+ *
80
+ * KEYS ARE `rel::name`, NEVER a bare name, and a reference resolves ONLY inside the
81
+ * referencing module's SCOPE (what it declares plus what it imports). A single flat
82
+ * namespace is not a looseness here, it is a SILENT GREEN: `get`, `list`, `create`
83
+ * and `accept` are declared in many modules of this tree, so a flat
84
+ * `\bget\s*\(` test reported `search:searchFixPatterns` — which resolves nothing
85
+ * and hardcodes its namespace — as "consults the verified principal (get())".
86
+ * Measured on run 6; this shape is the fix.
87
+ */
88
+ export interface IdentityModel {
89
+ /** `rel::name` of every function whose body consults the verified principal,
90
+ * directly (`ctx.auth.getUserIdentity()`) or transitively. */
91
+ helpers: Set<string>;
92
+ /**
93
+ * The subset of `helpers` that REFUSE on the absent-identity path — a GATE. A
94
+ * gate decides by throwing, so calling it as a bare statement (`await
95
+ * requireOrgAdmin(ctx)`) is the fail-closed idiom and NOT a discarded principal.
96
+ * Only a RESOLVER — a helper that returns a value the handler must then use —
97
+ * can have its result computed and discarded. Conflating the two made the rule
98
+ * accuse 30 correctly master-gated mutations on its second run.
99
+ */
100
+ gates: Set<string>;
101
+ /**
102
+ * Functions that TOUCH DATA, transitively. A registration whose handler reads
103
+ * nothing itself but delegates to a module-local helper that does IS serving
104
+ * data — `tasks:list` (`return await runTasksList(ctx, args, scope)`) and
105
+ * `search:searchFixPatterns` (`await ragSearch(ctx, {…})`) both do exactly that,
106
+ * and a non-transitive data test filed all of them under NO-DATA on run 5,
107
+ * which would have been a silent pass on real read paths.
108
+ */
109
+ dataFns: Set<string>;
110
+ /**
111
+ * Identity helpers whose absent-identity branch GRANTS — a bare `return;` that
112
+ * lets the caller proceed. `okfBundleNode.ts::assertCanValidate` is exactly this:
113
+ * `if (identity === null || identity === undefined) { return; }` with the comment
114
+ * "CLI / deploy-key — server-trusted path". Calling such a helper is not a gate at
115
+ * all on the path that matters, so a handler whose only authority is one of these
116
+ * is FAIL-OPEN, and CR-1 says so in those words rather than under the vaguer
117
+ * "computed and discarded" reason.
118
+ */
119
+ permissiveOnAbsence: Map<string, string>;
120
+ /** the keys that are REGISTRATIONS: reachable ONLY as `internal.…`/`api.…`. */
121
+ regNames: Set<string>;
122
+ /** rel -> the local names that module DECLARES or IMPORTS. */
123
+ scopes: Map<string, Set<string>>;
124
+ /** identity helpers whose absent-identity branch can return a PRIVILEGED shape
125
+ * when an opt-in flag is passed. flag name -> where it was found. */
126
+ privilegeOnAbsenceFlags: Map<string, string>;
127
+ /**
128
+ * VALUE REFUSERS — plain functions that RECEIVE an already-resolved value as a
129
+ * parameter and THROW on a test of it: `requireTenantNamespace(scope, ns, door)`
130
+ * at vantage-peers 58ed649 `convex/lib/auth.ts:1286`, which delegates to
131
+ * `requireResolvedCaller(scope, …)` (`if (scope.anonymous) throw …`). Keyed
132
+ * `rel::name` -> the parameter POSITIONS refused on, because only a resolved
133
+ * value passed AT that position is refused. See `deriveValueRefusers` for what
134
+ * qualifies and what disqualifies.
135
+ */
136
+ refuserParams: Map<string, Set<number>>;
137
+ }
138
+ /** the `name` half of a `rel::name` key. */
139
+ export declare function nameOf(key: string): string;
140
+ /** the `rel` half of a `rel::name` key. */
141
+ export declare function relOf(key: string): string;
142
+ export interface Ref {
143
+ name: string;
144
+ /** true when only a qualified `internal.…`/`api.…` reference may match it. */
145
+ qualified: boolean;
146
+ }
147
+ /**
148
+ * The members of `set` that a handler in `rel` can actually reach: a plain function
149
+ * declared in `rel` or imported by `rel`, or a registration (reachable from anywhere,
150
+ * but only through its qualified `internal.…`/`api.…` name).
151
+ */
152
+ export declare function reachable(model: IdentityModel, set: Set<string>, rel: string): Ref[];
153
+ /** the refs in `refs` that `text` actually references. */
154
+ export declare function referenced(text: string, refs: Ref[]): Ref[];
155
+ export declare function deriveIdentityModel(files: SrcFile[]): IdentityModel;
156
+ /**
157
+ * `name` is REFERENCED in `text` — called (`name(`) or passed as a value
158
+ * (`internal.lib.auth.resolveOrgScopeForAction,`). The second form is how an action
159
+ * reaches a resolver, so a call-only test is blind to the whole action surface.
160
+ */
161
+ export declare function references(text: string, name: string, qualified?: boolean): boolean;
162
+ /** Every `if (...)` in `text`, with its brace-balanced condition source. */
163
+ export declare function ifConditions(text: string): {
164
+ index: number;
165
+ cond: string;
166
+ end: number;
167
+ }[];
168
+ /**
169
+ * ABSENT-IDENTITY GUARD — an `if` whose condition tests `v` ITSELF for absence:
170
+ * `!v`, `v === null`, `v == null`, `v === undefined`, including as one disjunct of a
171
+ * widened test (`if (identity === null || identity === undefined)`, the real
172
+ * `assertCanExportNamespace` shape). It must NOT match a test on a PROPERTY of `v`
173
+ * (`if (!scope.isMaster)` is an authority test on a RESOLVED principal, not a test
174
+ * for its absence) — reading those as absence accused every correctly-scoped
175
+ * dashboard query on the first run of this rule.
176
+ */
177
+ export declare function absentGuard(text: string, v: string): {
178
+ index: number;
179
+ branch: string;
180
+ } | null;
181
+ /**
182
+ * The GUARDED BRANCH itself, from just after an `if (…)`'s closing paren: a braced
183
+ * block to its matching brace, else the single statement up to its `;`. Reading a
184
+ * fixed-width window instead runs past the branch into the rest of the handler, and
185
+ * that is not a width error — it INVERTS the verdict: `if (ns === null) return [];`
186
+ * followed by the normal data path read as "the absent-identity branch serves data",
187
+ * which accused all three `search:*` actions that in fact refuse first (measured).
188
+ */
189
+ export declare function branchAfter(text: string, from: number): string;
190
+ export interface Classified {
191
+ cls: CallerClass;
192
+ why: string;
193
+ justified?: boolean;
194
+ }
195
+ /**
196
+ * Classify ONE registration from its own block text. `pre` is the comment context
197
+ * immediately above the opener (a justification token may sit there or inside).
198
+ *
199
+ * ORDER MATTERS and is the rule's logic:
200
+ * no data touched -> NO-DATA (token required)
201
+ * data touched, principal never consulted-> NO-IDENTITY (token required)
202
+ * principal consulted but not decisive -> FAIL-OPEN (NO token exit)
203
+ * otherwise -> RESOLVES
204
+ */
205
+ export declare function classify(codeText: string, fullText: string, pre: string, model: IdentityModel, rel: string): Classified;
206
+ /** The exposed surface for a tree, with module rels in the TREE's rel space: a
207
+ * tree rooted at the repository (src/target.ts) carries `convex/` on every convex
208
+ * rel, while api.d.ts names modules relative to the convex dir. */
209
+ export declare function surfaceForTree(tree: Tree): ApiSurface;
210
+ export declare function scanCallerResolution(tree: Tree, surface?: ApiSurface): CallerResolutionScan;
211
+ export declare const CR1_ID = "CR-1 a public registration resolves its caller before serving or writing data";
212
+ export declare function byClass(scan: CallerResolutionScan, cls: CallerClass): Finding[];
213
+ /** the findings that are VIOLATIONS: fail-open, plus unjustified no-identity/no-data. */
214
+ export declare function violations(scan: CallerResolutionScan): Finding[];
215
+ export declare function abstentions(scan: CallerResolutionScan): Finding[];
216
+ export declare function callerResolutionResult(scan: CallerResolutionScan): DetectorResult;
217
+ /** The rendered section. EVERY member of EVERY class is named — the truncation that
218
+ * let R-50 classify thirteen of the fourteen and show a reader none of them is the
219
+ * defect this renderer refuses to repeat. */
220
+ export declare function callerResolutionSection(scan: CallerResolutionScan): string;
221
+ /** CR-1 as an engine Detector, for tests and for a future registry slot. */
222
+ export declare const CR1: Detector;