@vantageos/backend-doctor 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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 `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.
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 `bacc54a`, 56 rules — on ElPi-Corp `main`; it adds R-54..R-57 and the MCP rule S-28, merged as ElPi-Corp #239 = `7d7df89` before the 2026-10-10 history rewrite, same file blob `85d8530e`; 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
 
@@ -21,7 +21,7 @@ The detectors read four kinds of material, each a **comparison target, never an
21
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
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
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
+ | 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-56 |
25
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 |
26
26
 
27
27
  ### The acceptance property — surfacing, not deferring
@@ -102,14 +102,23 @@ Operator order 2026-10-09: *"No application, no MCP server, and no starter defin
102
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
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
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.
105
+ - **Ceiling 0**: CR-2 is mechanical (`fixtures/rule-classes-bacc54a.json`). Any violation is counted in `mechanical_rule_counts` / `mechanical_rule_module_counts` and exits 1.
106
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`.
107
+ **CR-2 is the standard's R-57, reported under its doctor-owned key.** It was authored before the standard numbered it; the standard at `bacc54a` defines the same rule as R-57. The count stays under the key `CR-2` because the vantage-peers deploy-gate baseline (`.claude/config/backend-doctor-baseline.json`) reads `CR-2`; reporting it under both ids would count one defect as two rules, so R-57 itself is listed under NOT_MECHANICALLY_CHECKABLE with that reason. Renaming the key to `R-57` is a follow-up that must move that baseline in the same delivery. Source and rationale: `src/detectors/source/local-identity.ts`.
108
108
 
109
109
  ### R-31 — the read budget of a bulk mutation
110
110
 
111
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
112
 
113
+ ### R-54, R-55, R-56 — the door audit's causes 1, 4 and 5
114
+
115
+ Standard `bacc54a` §4. Each is mechanical, has no justification-token exit, and counts in `mechanical_violations`. Their material is the door audit of vantage-peers `16f0907` (`docs/audit/2026-10-09-door-audit.md`, branch `argus/door-audit-16f0907`); the real files are vendored byte-exact in `tests/fixtures/vp-16f0907/` (`.pin`).
116
+
117
+ - **R-54** (`src/detectors/source/service-account-claim.ts`): a public door that resolves its caller through the organisation-scope resolver (`withOrgScope`, or any in-tree function that calls it) must declare a transport-verified claim argument (`verifiedActor` / `verifiedOrg` / `verifiedPerson`) AND refuse the call that is the service account and carries no claim. Red (A): no claim argument. Red (B): a claim argument but no such refusal, so its absence leaves the call master. A refusal is a `throw` (or a refusing `return`) whose path condition holds the service-account state and the claim's absence (a local derived from the claim counts; a refusal of the OTHER population does not). Exits, each listed and none silent: (a) service-account-only (`requireServiceAccount`, or `requireResolvedCaller` with `masterOnly` and `mcpBoundOnly`); (b) callers resolve only through a verified session (`requireOrgAdmin`, a bare identity read) and the door never reaches the scope resolver; (c) `fleet-wide: <reason of 8+ characters>`. Could-not-judge: an action whose scope arrives through a run call, a resolver-shaped callee defined outside the tree, a claim argument spread from a validator the scan cannot find, a door registered through a wrapper. A door that resolves no caller at all is outside the rule (CR-1 / R-6) and not counted. Clause 2 of the rule (the claim confines every row) is a dynamic property and is NOT decided here.
118
+ - **R-55** (`src/detectors/source/narrow-then-bound.ts`): in a public door and the helpers it reaches, a read bounded by `.take`/`.paginate` (or re-bounded by `.slice(0, n)`) whose rows are then narrowed by a caller-dependent predicate (a filter reading the door's arguments or the resolved scope, a `filterBy…` helper, an in-tree helper that narrows its parameter, a loop that skips on such a test) is red. Listed as covered, not silent: a fill loop that holds both the bound and the narrowing, and a refusal on a full window (a throw naming the cap, overflow or truncation) between the latest bound and the narrowing. An unbounded `.collect()` is R-11 / R-47's.
119
+ - **R-56** (`src/detectors/source/credential-hygiene.ts`), four clauses, one site tag each: (1) a credential-named column (`code`, `token`, `secret`, `apiKey`, …) stored without a digest, or a digest of a chosen secret (a door argument named like a secret, an environment secret) that reaches a stored row; (2) a table named like a refresh-token store, read by a hash index, with no function that reads it by hash AND marks the token consumed/revoked or deletes it; (3a) a table whose inserted rows carry `tokenHash` and `expiresAt` with no function that revokes or deletes a row of it, (3b) a mint into such a table (no code or refresh-token linkage) that neither revokes the principal's earlier bearers nor caps them; (4) a request-supplied commit/hash/token identifier matched to stored text by `includes`/`startsWith`/`indexOf` with no 40- or 64-digit check, and (4e) a verification response that echoes stored content. The entropy of an externally generated secret and the binding of a minted bearer to the session's claims are NOT decided here.
120
+ - **R-57** is measured by the doctor-owned CR-2 (`src/detectors/source/local-identity.ts`) and reported under the key `CR-2` (see the CR-2 section). R-57 itself is listed under NOT_MECHANICALLY_CHECKABLE with that reason and is never reported present.
121
+
113
122
  ### R-52 and R-53 — the write reaches the right row, and the actor is who it is by ID
114
123
 
115
124
  - **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.
@@ -134,7 +143,7 @@ It writes exactly `{"sha","cli_commit","convex_path","exit_code","checked","tota
134
143
  - `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
144
  - `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
145
 
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.
146
+ **The class is data, not code.** `fixtures/rule-classes-bacc54a.json` gives every numbered rule plus CR-1 and CR-2 exactly one class with a one-phrase basis (42 mechanical = the standard's 40 + 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 `bacc54a` lists all 56 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 to R-57**; R-16..R-20 and the other §15 judgement rules print but are not counted, exactly as the standard classifies them.
138
147
 
139
148
  **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
149
 
@@ -142,7 +151,7 @@ It writes exactly `{"sha","cli_commit","convex_path","exit_code","checked","tota
142
151
 
143
152
  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).
144
153
 
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.
154
+ **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-bacc54a.md` (56 rules — adds R-54, a door the service account can reach acts inside a transport-verified organisation and refuses a claimless call; R-55, a read narrows before it bounds; R-56, credentials as digests, single-use refresh tokens, revocable bearers, full-length verification tokens; and R-57, no local identity primitive; canonical source: ElPi-Corp `vantagepeers/backend-standard/backend-standard.md`). `bacc54a` is on ElPi-Corp `main` (`gh api repos/elpiarthera/ElPi-Corp/compare/bacc54a53d5f6f9f20e5254908e2b46807902faa...main` -> ahead, behind 0); the PR #239 merge `7d7df89` carries the same blob but left `main` in the 2026-10-10 history rewrite (`PINS.txt`). The previous pin `backend-standard-2825122.md` (52 rules) and the older ones stay in the tree as provenance. 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.
146
155
 
147
156
  ## Discipline (how a detector is proven)
148
157
 
@@ -1,4 +1,4 @@
1
1
  {
2
- "sourceDigest": "b6bfdbdcea24a6c2a672ed0ebc0b19dcaa680893b280820c23cbaf960130423d",
3
- "builtAt": "2026-10-10T15:07:02.464Z"
2
+ "sourceDigest": "d45a7d3884b503e47f03d312079a95b864e185f1ea8e0e21d2b8b008f59d020b",
3
+ "builtAt": "2026-10-11T10:37:03.282Z"
4
4
  }
@@ -4,7 +4,7 @@
4
4
  * canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
5
5
  * as a written-justification-token control (§23) — the paired standard PR the four
6
6
  * decisive detectors below track. Still 48 rules; the pin moves forward with them. */
7
- export declare const STANDARD_SHA = "2825122a3ab1e4ddb4b955cfb136b7e8ab04f8f3";
7
+ export declare const STANDARD_SHA = "bacc54a53d5f6f9f20e5254908e2b46807902faa";
8
8
  export declare const VENDORED_STANDARD_PATH: string;
9
9
  /**
10
10
  * Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
package/dist/coverage.js CHANGED
@@ -18,8 +18,8 @@ const here = dirname(fileURLToPath(import.meta.url));
18
18
  * canonical 48-rule standard (51ec7af1) to 2bb402f7, which closes R-7/R-8/R-37/R-40
19
19
  * as a written-justification-token control (§23) — the paired standard PR the four
20
20
  * decisive detectors below track. Still 48 rules; the pin moves forward with them. */
21
- export const STANDARD_SHA = "2825122a3ab1e4ddb4b955cfb136b7e8ab04f8f3";
22
- export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-2825122.md");
21
+ export const STANDARD_SHA = "bacc54a53d5f6f9f20e5254908e2b46807902faa";
22
+ export const VENDORED_STANDARD_PATH = join(here, "..", "fixtures", "backend-standard-bacc54a.md");
23
23
  /**
24
24
  * Derive the full rule list R-1..R-42 from the VENDORED standard fixture —
25
25
  * never typed, never a git show against a local repo. Reads the `**R-NN`
@@ -31,7 +31,7 @@ export const FIXTURE_VR_ORACLE_PATH = join(here, "..", "..", "fixtures", "vr-by-
31
31
  * §2 [shared] contract columns are DERIVED from this file, never hardcoded.
32
32
  * Defined locally (not imported from coverage.ts) to avoid a registry ↔ coverage
33
33
  * import cycle. */
34
- export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-2825122.md");
34
+ export const VENDORED_STANDARD_PATH = join(here, "..", "..", "fixtures", "backend-standard-bacc54a.md");
35
35
  /** Load any backend inventory CSV generically from its OWN header — the shared
36
36
  * fields are DERIVED from these real headers, never a prose illustration. */
37
37
  export function loadInventory(path) {
@@ -20,6 +20,7 @@ import { existsSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { siteCount, withCount } from "../../violation-count.js";
22
22
  import { hasJustificationTokenInText } from "../oracle-axis2.js";
23
+ import { scanR11UnboundedReads } from "./isolation-read.js";
23
24
  /** The standard all four are pinned against (short hash a9d7877b). */
24
25
  export const SOURCE_CONTRACT_STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) §15";
25
26
  function emptyScan() {
@@ -179,6 +180,13 @@ export function scanR11(files) {
179
180
  }
180
181
  }
181
182
  }
183
+ // The unbounded-read arm (isolation-read.ts): a `.collect()` / `for await` read
184
+ // of a tenant table in a door a non-master caller is served, with no index range
185
+ // on the tenant key. R-55 hands this shape to R-11 by name.
186
+ const unboundedArm = scanR11UnboundedReads(files);
187
+ scan.conform.push(...unboundedArm.conform);
188
+ scan.violate.push(...unboundedArm.violate);
189
+ scan.cnj.push(...unboundedArm.cnj);
182
190
  return scan;
183
191
  }
184
192
  // ---------------------------------------------------------------------------
@@ -375,7 +383,7 @@ export function scanR26(files) {
375
383
  function srcDetector(ruleId, id, check, scanFn, emptyReason) {
376
384
  return { id, run: (tree) => decide(ruleId, id, check, scanFn(tree.files), emptyReason) };
377
385
  }
378
- export const R11 = srcDetector("R-11", "R-11 tenant isolation is an in-query index predicate, not a post-query filter", "a .filter on a scope key is paired with an adjacent .withIndex/.withSearchIndex .eq on that key", scanR11, "R-11: zero scope-key .filter sites matched in the scanned convex tree — cannot certify the isolation locus. NOT a clean bill.");
386
+ export const R11 = srcDetector("R-11", "R-11 tenant isolation is an in-query index predicate, not a post-query filter", "a .filter on a scope key is paired with an adjacent .withIndex/.withSearchIndex .eq on that key; an unbounded read of a tenant table in a door a non-master caller is served carries the tenant key in its index range", scanR11, "R-11: zero scope-key .filter sites and zero unbounded tenant-table reads matched in the scanned convex tree — cannot certify the isolation locus. NOT a clean bill.");
379
387
  export const R24 = srcDetector("R-24", "R-24 filter-selected bulk-write defaults to dry run", "a bulk-write mutation defaults dryRun true and gates apply behind an explicit flag + caller guard", scanR24, "R-24: zero bulk-write mutations matched in the scanned convex tree — cannot certify dry-run-by-default. NOT a clean bill.");
380
388
  export const R25 = srcDetector("R-25", "R-25 minted address / cross-boundary bundle org-bound at both ends", "a storage mint or bundle redemption is org-bound by an auth guard; a minted bundle URL expires", scanR25, "R-25: zero storage mint/redemption sites matched (kb*/okfBundle*) — cannot certify org-binding. NOT a clean bill.");
381
389
  export const R26 = srcDetector("R-26", "R-26 library-owned component declares its seam boundary", "each app.use(component) seam declares an authority tier + boundary key", scanR26, "R-26: no component mount seam (*.config.ts app.use) found — cannot certify the component seam contract. NOT a clean bill.");
@@ -0,0 +1,11 @@
1
+ import type { Detector } from "../../engine-types.js";
2
+ import { type Scan, type SourceDetectorEntry, type SrcFile } from "./_scan.js";
3
+ export declare const R56_ID = "R-56 credentials held as digests of generated secrets, refresh tokens single-use, bearers revocable and session-minted bearers bounded, verification tokens matched in full";
4
+ export declare const R56_STANDARD_PIN = "vantagepeers/backend-standard/backend-standard.md @ bacc54a53d5f6f9f20e5254908e2b46807902faa \u00A74 R-56";
5
+ export interface R56Scan extends Scan {
6
+ /** units read. */
7
+ units: number;
8
+ }
9
+ export declare function scanR56(files: SrcFile[]): R56Scan;
10
+ export declare const R56: Detector;
11
+ export declare const SOURCE_CREDENTIAL_HYGIENE_DETECTORS: SourceDetectorEntry[];