@vantageos/backend-doctor 0.1.13 → 0.2.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 (58) hide show
  1. package/README.md +19 -2
  2. package/dist/args.d.ts +16 -0
  3. package/dist/args.js +54 -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 +1195 -0
  9. package/dist/cli.js +217 -10
  10. package/dist/coverage-cli.js +8 -0
  11. package/dist/crud-coverage.d.ts +81 -0
  12. package/dist/crud-coverage.js +375 -0
  13. package/dist/detectors/oracle-axis1.d.ts +5 -3
  14. package/dist/detectors/oracle-axis1.js +24 -15
  15. package/dist/detectors/oracle-axis2.js +5 -6
  16. package/dist/detectors/predicates.d.ts +3 -0
  17. package/dist/detectors/predicates.js +81 -16
  18. package/dist/detectors/shared.js +5 -5
  19. package/dist/detectors/source/_scan.js +6 -1
  20. package/dist/detectors/source/backfill.js +9 -1
  21. package/dist/detectors/source/guard-helper-flow.d.ts +112 -0
  22. package/dist/detectors/source/guard-helper-flow.js +1173 -0
  23. package/dist/detectors/source/identity.d.ts +51 -2
  24. package/dist/detectors/source/identity.js +815 -14
  25. package/dist/detectors/source/isolation.d.ts +11 -1
  26. package/dist/detectors/source/isolation.js +106 -25
  27. package/dist/detectors/source/jobs.js +165 -2
  28. package/dist/detectors/source/readbudget.d.ts +1 -0
  29. package/dist/detectors/source/readbudget.js +67 -4
  30. package/dist/detectors/source/security.d.ts +16 -0
  31. package/dist/detectors/source/security.js +225 -7
  32. package/dist/detectors/source/write-isolation.d.ts +8 -1
  33. package/dist/detectors/source/write-isolation.js +64 -30
  34. package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
  35. package/dist/detectors/standard/r46-returns-validator.js +45 -14
  36. package/dist/detectors/structural.js +14 -10
  37. package/dist/oracle.d.ts +92 -3
  38. package/dist/oracle.js +229 -12
  39. package/dist/registry.d.ts +7 -0
  40. package/dist/registry.js +48 -6
  41. package/dist/report.d.ts +11 -1
  42. package/dist/report.js +15 -2
  43. package/dist/state-rank.d.ts +5 -0
  44. package/dist/state-rank.js +9 -0
  45. package/dist/tally.d.ts +35 -0
  46. package/dist/tally.js +76 -8
  47. package/dist/target.d.ts +32 -0
  48. package/dist/target.js +173 -0
  49. package/fixtures/cr1-classes/_generated/api.d.ts +24 -0
  50. package/fixtures/cr1-classes/surface.ts +206 -0
  51. package/fixtures/cr1-empty/nothing.ts +1 -0
  52. package/fixtures/cr1-refusal-on-resolved/_generated/api.d.ts +24 -0
  53. package/fixtures/cr1-refusal-on-resolved/doors.ts +180 -0
  54. package/fixtures/cr1-refusal-on-resolved/lib/auth.ts +104 -0
  55. package/fixtures/r34-vp-auth-30e821b/auth-805-812.ts.txt +8 -0
  56. package/fixtures/r34-vp-auth-30e821b/auth-878-890.ts.txt +13 -0
  57. package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
  58. package/package.json +2 -2
package/README.md CHANGED
@@ -18,8 +18,9 @@ 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 |
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 |
23
24
  | source-tree | the real `convex/` source (live path, or `fixtures/vp-convex/` in CI) | R-6, R-11, R-16..R-36 |
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
 
@@ -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,6 +82,18 @@ 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 the 50-of-50 coverage line is unchanged: registering an id the vendored standard does not define would make `coverage.ts` report a phantom rule. Numbering it upstream (proposed **R-52**) is a separate change to the standard document in its own repository. Source and rationale: `src/caller-resolution.ts`.
96
+
80
97
  ## Pins
81
98
 
82
99
  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).
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 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 --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,54 @@
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(["oracle", "vr-oracle", "writer-tier"]);
4
+ export const USAGE = `usage: backend-doctor [<target>] [--oracle <csv>] [--vr-oracle <csv>] [--writer-tier <csv>]
5
+ backend-doctor --version | --help
6
+
7
+ <target> the repository root or its convex/ dir (default: the current dir).
8
+ Both name the same backend: paths are normalised to the repo root
9
+ (nearest dir holding package.json or .git), so the counts are identical.
10
+ --oracle, --vr-oracle, --writer-tier
11
+ explicit oracle CSVs; by default they are read from
12
+ <repo>/convex/.backend-doctor/ or <repo>/.backend-doctor/ — never from
13
+ above the repo root and never from this tool's bundled fixtures.
14
+ --version print the tool version and exit 0.
15
+ --help print this text and exit 0.
16
+
17
+ exit: 0 approve, 1 a violation / revise, 2 the run could not judge (bad usage,
18
+ unreadable target, stale build, unreadable standard).`;
19
+ /** Flag reader. `--version`/`--help` win over everything else; an unknown flag or
20
+ * a value flag with no value is a usage error (exit 2) — never a path. */
21
+ export function parseArgs(argv) {
22
+ if (argv.some((a) => a === "--version" || a === "-V" || a === "-v"))
23
+ return { kind: "version" };
24
+ if (argv.some((a) => a === "--help" || a === "-h"))
25
+ return { kind: "help" };
26
+ const positional = [];
27
+ const flags = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const a = argv[i];
30
+ if (a.startsWith("-") && a !== "-") {
31
+ const body = a.replace(/^--?/, "");
32
+ const eq = body.indexOf("=");
33
+ const name = eq === -1 ? body : body.slice(0, eq);
34
+ if (!a.startsWith("--") || !VALUE_FLAGS.has(name)) {
35
+ return { kind: "usage-error", message: `unknown flag ${a}` };
36
+ }
37
+ if (eq !== -1)
38
+ flags[name] = body.slice(eq + 1);
39
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
40
+ flags[name] = argv[++i];
41
+ else
42
+ return { kind: "usage-error", message: `flag ${a} needs a value` };
43
+ }
44
+ else
45
+ positional.push(a);
46
+ }
47
+ if (positional.length > 1) {
48
+ return {
49
+ kind: "usage-error",
50
+ message: `expected at most one target, got ${positional.length}: ${positional.join(" ")}`,
51
+ };
52
+ }
53
+ return { kind: "run", positional, flags };
54
+ }
@@ -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": "f9c7040c0ed49bdf7a77370eeed51651d4556c1ec8a8f674c2ca8ffd55000592",
3
+ "builtAt": "2026-10-03T20:51:43.107Z"
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;