@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
@@ -1,4 +1,5 @@
1
- import { isInventoryRow, readOracle, } from "../oracle.js";
1
+ import { isInventoryRow, oracleLabel, readOracle, refuseWithoutOracle, } from "../oracle.js";
2
+ import { itemCount, withCount } from "../violation-count.js";
2
3
  // A written justification token turns a flagged divergence from a DEFECT the
3
4
  // author owes into a recorded, accepted divergence. It is the mechanical pole
4
5
  // that replaced "a human decides whether the divergence is sound": the control
@@ -7,22 +8,37 @@ import { isInventoryRow, readOracle, } from "../oracle.js";
7
8
  // are remediation-owed markers (R-28) — a fix still owed, NOT a justification —
8
9
  // so they do not satisfy this predicate; only an explicit JUSTIFIÉ/JUSTIFIED/
9
10
  // STATELESS/ACCEPTED token does.
11
+ //
12
+ // Anchored on the DOCUMENTED shape: the standard writes the token with a
13
+ // trailing colon (`JUSTIFIÉ:` / `JUSTIFIED:` / `STATELESS:`), at a word
14
+ // boundary. Two failures a bare substring match let through, both closed here:
15
+ // • a LEADING-letter negation — "UNJUSTIFIED" contains "JUSTIFIED", so the
16
+ // leading `(?<![A-Za-zÀ-ÿ-])` (not preceded by a letter OR a hyphen — a
17
+ // hyphen-joined negation like French `non-justifié:` clears a letter-only
18
+ // lookbehind, Eta #12 round-2) rejects it; and R-37's own output prints
19
+ // "N UNJUSTIFIED divergence(s)".
20
+ // • a token with NO colon — "not justified", "justified by design" — cleared
21
+ // the guard though the standard requires the colon; the trailing `\s*:`
22
+ // now rejects it. Only the recorded token form (`TOKEN:`) is a justification.
23
+ // No trailing \b before the colon: an accented final letter (É in JUSTIFIÉ) is
24
+ // not an ASCII word char, so \b would not match after it — the colon is the anchor.
25
+ //
26
+ // ONE regex, shared by every axis (oracle-row remediation cells here, and a
27
+ // source-scan's nearby comment window in the SOURCE_STANDARD_PIN detectors) —
28
+ // task k179jnhx: "Do not write a second token matcher; extract/share the
29
+ // existing one." Two implementations of one predicate is the defect class the
30
+ // fleet keeps filing.
31
+ export const JUSTIFICATION_TOKEN_RE = /(?<![A-Za-zÀ-ÿ-])(JUSTIFI(É|E|ED)|STATELESS|ACCEPT(É|E|ED)|WONTFIX)\s*:/i;
32
+ /** The oracle-row form: reads the row's remediation cell (`rbac_adjustment_needed`). */
10
33
  export function hasJustificationToken(r) {
11
34
  const cell = (r.by.rbac_adjustment_needed ?? "").trim();
12
- // Anchored on the DOCUMENTED shape: the standard writes the token with a
13
- // trailing colon (`JUSTIFIÉ:` / `JUSTIFIED:` / `STATELESS:`), at a word
14
- // boundary. Two failures a bare substring match let through, both closed here:
15
- // • a LEADING-letter negation — "UNJUSTIFIED" contains "JUSTIFIED", so the
16
- // leading `(?<![A-Za-zÀ-ÿ-])` (not preceded by a letter OR a hyphen — a
17
- // hyphen-joined negation like French `non-justifié:` clears a letter-only
18
- // lookbehind, Eta #12 round-2) rejects it; and R-37's own output prints
19
- // "N UNJUSTIFIED divergence(s)".
20
- // • a token with NO colon — "not justified", "justified by design" — cleared
21
- // the guard though the standard requires the colon; the trailing `\s*:`
22
- // now rejects it. Only the recorded token form (`TOKEN:`) is a justification.
23
- // No trailing \b before the colon: an accented final letter (É in JUSTIFIÉ) is
24
- // not an ASCII word char, so \b would not match after it — the colon is the anchor.
25
- return /(?<![A-Za-zÀ-ÿ-])(JUSTIFI(É|E|ED)|STATELESS|ACCEPT(É|E|ED)|WONTFIX)\s*:/i.test(cell);
35
+ return JUSTIFICATION_TOKEN_RE.test(cell);
36
+ }
37
+ /** The raw-text form — for a SOURCE-scan site with no oracle row: a nearby code
38
+ * comment window is that site's "remediation cell". Same regex, same word-
39
+ * boundary/negation guards; never a second matcher. */
40
+ export function hasJustificationTokenInText(text) {
41
+ return JUSTIFICATION_TOKEN_RE.test(text);
26
42
  }
27
43
  // ---- R-7 read/write coherence divergence (INCOHERENT* table-aggregate flag) -
28
44
  // §18: "`INCOHERENT*` flag (mechanical); divergence justified (human)". The
@@ -152,13 +168,12 @@ export function findDivergences(rows) {
152
168
  return sites;
153
169
  }
154
170
  const R37_ID = "R-37 filled-neighbour divergence";
155
- const R37_ANCHOR = "oracle:vp-by-tool-day158.csv";
156
171
  /** Wrap the filled-neighbour compare into a Detector. `rowsProvider` is
157
172
  * injectable for tests; production reads the memoized real oracle. */
158
173
  export function makeR37(rowsProvider = readOracle) {
159
174
  return {
160
175
  id: R37_ID,
161
- run: () => {
176
+ run: () => refuseWithoutOracle(R37_ID, () => {
162
177
  const all = findDivergences(rowsProvider());
163
178
  // MECHANICAL disposition: an unjustified divergence is the defect; a
164
179
  // divergence carrying a written per-tool justification token is accepted.
@@ -173,7 +188,7 @@ export function makeR37(rowsProvider = readOracle) {
173
188
  const reason = `R-37: no UNJUSTIFIED filled-neighbour divergence across the oracle's per-table groups (${all.length} divergence(s), all carrying a written per-tool justification token, or none present). The mechanical compare is exhaustive and the disposition is decided by the token's presence, not a human verdict. ${breakdown}`;
174
189
  return {
175
190
  id: R37_ID,
176
- anchor: R37_ANCHOR,
191
+ anchor: oracleLabel(),
177
192
  state: "present",
178
193
  verdict: reason,
179
194
  };
@@ -183,12 +198,12 @@ export function makeR37(rowsProvider = readOracle) {
183
198
  .map((s) => `${s.table}.${s.field}: ${s.toolA} "${s.valA.slice(0, 32)}" vs ${s.toolB} "${s.valB.slice(0, 32)}"`)
184
199
  .join("; ");
185
200
  const reason = `R-37: ${sites.length} UNJUSTIFIED filled-neighbour divergence(s) across ${tables.size} tables — the mechanical compare names WHICH TWO TOOLS diverge on a shared contract field, and none carries a written per-tool justification token (JUSTIFIÉ:/JUSTIFIED: in rbac_adjustment_needed); each owes a fix or a recorded justification. This IS a violation, not an abstention. ${breakdown} Examples: ${examples}.`;
186
- return {
201
+ return withCount({
187
202
  id: R37_ID,
188
- anchor: `oracle:vp-by-tool-day158.csv table=${sites[0].table}`,
203
+ anchor: `${oracleLabel()} table=${sites[0].table}`,
189
204
  state: "non-conforming",
190
205
  verdict: reason,
191
- };
192
- },
206
+ }, itemCount(sites.length));
207
+ }),
193
208
  };
194
209
  }
@@ -1,3 +1,4 @@
1
+ import { type WriterTierTable } from "../oracle.js";
1
2
  import { type RulePredicate } from "./shared.js";
2
3
  /** Collapse a raw `surface` to one of the three canonical exposures, or null. */
3
4
  export declare function collapseSurface(raw: string): string | null;
@@ -11,6 +12,8 @@ export declare const r4: RulePredicate;
11
12
  export declare function collapseTier(raw: string): string | null;
12
13
  export declare const r5: RulePredicate;
13
14
  export declare const r9: RulePredicate;
15
+ export declare const WRITER_TIERS: ReadonlySet<string>;
16
+ export declare function makeR10(sideCar: () => WriterTierTable): RulePredicate;
14
17
  export declare const r10: RulePredicate;
15
18
  export declare const r12: RulePredicate;
16
19
  export declare const r13: RulePredicate;
@@ -1,3 +1,10 @@
1
+ // The eleven per-row first-wave predicates. Each transcribes its §15 "The
2
+ // check" row and its §2 field mapping from the pinned standard, then applies
3
+ // that predicate to the oracle rows. Structural R-14 and R-38 live in
4
+ // structural.ts (they judge the inventory SCHEMA, not a row).
5
+ import { basename } from "node:path";
6
+ import { WRITER_TIER_NAME, readWriterTier } from "../oracle.js";
7
+ import { hasJustificationToken } from "./oracle-axis2.js";
1
8
  import { LIST_VERBS, WRITE_VERBS, isMcpPublic } from "./shared.js";
2
9
  // ---- R-1 exposure declared ------------------------------------------------
3
10
  // §2 line 32: exposure -> `surface` = `public` (MCP surface) / `internal` /
@@ -175,10 +182,18 @@ export const r5 = {
175
182
  // no arg= in the boundary: NOW the identity-evidencing label may confirm.
176
183
  if (rbacEvidencesIdentity(r.by.rbac_qui))
177
184
  return { kind: "conform" };
178
- // maps to a tier but the row evidences NEITHER identity nor arg — abstain, never a silent pass.
185
+ // maps to a tier but the row evidences NEITHER identity nor arg — flagged.
186
+ // judgement is retired (task k179jnhx, §23): the disposition is decided by
187
+ // the written justification token's PRESENCE in the remediation cell, never
188
+ // by a person's opinion of whether the ambiguity is sound. No token →
189
+ // non-conforming; a JUSTIFIÉ:/JUSTIFIED: token → present. could-not-judge
190
+ // is reserved for a row the instrument could not read (never reached here —
191
+ // this row parsed fine, the disposition is undecided-by-token, not unreadable).
192
+ if (hasJustificationToken(r))
193
+ return { kind: "conform" };
179
194
  return {
180
- kind: "cnj",
181
- note: `tier maps to ${tier} but rbac_qui="${r.by.rbac_qui}"/namespace_ou_org="${ns}" do not evidence identity-resolution; R-11 flags the locus`,
195
+ kind: "violate",
196
+ note: `tier maps to ${tier} but rbac_qui="${r.by.rbac_qui}"/namespace_ou_org="${ns}" do not evidence identity-resolution, and no written JUSTIFIÉ:/JUSTIFIED: token is recorded in the remediation cell (rbac_adjustment_needed="${(r.by.rbac_adjustment_needed ?? "").trim() || "<empty>"}"); R-11 flags the locus`,
182
197
  };
183
198
  },
184
199
  };
@@ -200,22 +215,81 @@ export const r9 = {
200
215
  },
201
216
  };
202
217
  // ---- R-10 writer authority, a distinct resolved tier ----------------------
203
- // §2 writer authority [shared] -> `rbac_qui`. §15: writer tier present, DISTINCT,
204
- // one of five. The oracle has a SINGLE `rbac_qui` column, so no write can carry
205
- // a writer tier distinct from its reader tier — the headline merge (R-14).
206
- export const r10 = {
207
- ruleId: "R-10",
208
- id: "R-10 writer authority, distinct tier",
209
- check: "writer tier present, distinct from reader, one of five",
210
- classify: (r) => {
211
- if (!WRITE_VERBS.has(r.by.crud))
212
- return { kind: "na" };
213
- return {
214
- kind: "violate",
215
- note: "writer authority is not recorded as a field distinct from reader authority — both merged into the single `rbac_qui` column",
216
- };
217
- },
218
- };
218
+ // §2 line 36: writer authority is "a **distinct** tier from the same set, resolved
219
+ // from a verified identity"; §15 line 413: "writer tier present, distinct, one of
220
+ // five"; line 63: "Reader and writer are two fields". DISTINCT is a property of
221
+ // the FIELD, not of the value: the standard requires the writer tier to be
222
+ // RECORDED apart from the reader tier so a `read=public write=admin` divergence
223
+ // can be written — it nowhere forbids a tool whose writer tier happens to equal
224
+ // its reader tier (an org member reading and writing its own org's rows). So an
225
+ // equal VALUE conforms; a MERGED field (no separate record, or one cell naming
226
+ // several tiers) violates.
227
+ //
228
+ // Material: the writer-tier side-car (src/oracle.ts WRITER_TIER_NAME), joined on
229
+ // `outil`. Branches, per write row:
230
+ // side-car absent / unreadable -> cnj, naming the file(s) looked at
231
+ // no side-car row for the tool -> violate (tier absent)
232
+ // two rows with different tiers -> violate (merged)
233
+ // tier empty / `?` -> violate (unknown)
234
+ // tier naming several tiers (a/b, a+b) -> violate (merged)
235
+ // tier outside the five -> violate
236
+ // writer_gate empty or `mcp-scope:<t>` -> cnj: the tier was not derived from
237
+ // a gate on a write door, so "resolved from a verified identity" is not
238
+ // evidenced by the side-car (never a silent pass)
239
+ // otherwise -> conform
240
+ export const WRITER_TIERS = new Set([
241
+ "public",
242
+ "org-member",
243
+ "org-admin",
244
+ "fleet-internal",
245
+ "master",
246
+ ]);
247
+ const UNKNOWN_TIER = new Set(["", "?", "unknown", "n/a", "tbd"]);
248
+ const MERGED_TIER = /[/+,|&]|\s(and|or|et|ou)\s/i;
249
+ export function makeR10(sideCar) {
250
+ return {
251
+ ruleId: "R-10",
252
+ id: "R-10 writer authority, distinct tier",
253
+ check: "writer tier recorded in its own field (side-car joined on `outil`), one of five, gated on a write door; equal-to-reader values conform (the field must be distinct, §2 line 36)",
254
+ classify: (r) => {
255
+ if (!WRITE_VERBS.has(r.by.crud))
256
+ return { kind: "na" };
257
+ const t = sideCar();
258
+ if (t.status === "absent")
259
+ return {
260
+ kind: "cnj",
261
+ note: `writer-tier side-car ${WRITER_TIER_NAME} ABSENT (${t.origin}; searched: ${t.searched.join(", ") || "<none>"}) — this oracle has write rows, so writer authority cannot be judged`,
262
+ };
263
+ if (t.status === "unreadable")
264
+ return { kind: "cnj", note: `writer-tier side-car ${t.path} unreadable — ${t.reason}` };
265
+ const file = basename(t.path);
266
+ const entries = t.byTool.get(r.by.outil) ?? [];
267
+ if (entries.length === 0)
268
+ return { kind: "violate", note: `no writer tier recorded for "${r.by.outil}" in ${file} — writer authority absent` };
269
+ const tiers = [...new Set(entries.map((e) => e.writer_tier))];
270
+ if (tiers.length > 1)
271
+ return {
272
+ kind: "violate",
273
+ note: `${file} records ${tiers.length} writer tiers for "${r.by.outil}" (${tiers.join(", ")}) — merged, not one tier`,
274
+ };
275
+ const tier = tiers[0];
276
+ if (UNKNOWN_TIER.has(tier.toLowerCase()))
277
+ return { kind: "violate", note: `writer_tier="${tier || "<empty>"}" for "${r.by.outil}" — unknown, not a tier` };
278
+ if (MERGED_TIER.test(tier))
279
+ return { kind: "violate", note: `writer_tier="${tier}" names several tiers — merged, not one tier` };
280
+ if (!WRITER_TIERS.has(tier))
281
+ return { kind: "violate", note: `writer_tier="${tier}" is none of the five tiers {${[...WRITER_TIERS].join(", ")}}` };
282
+ const gate = entries[0].writer_gate;
283
+ if (gate === "" || gate.startsWith("mcp-scope:"))
284
+ return {
285
+ kind: "cnj",
286
+ note: `writer_tier="${tier}" for "${r.by.outil}" rests on ${gate === "" ? "no writer_gate" : `the MCP-scope fallback "${gate}"`} — no write-door gate evidences resolution from a verified identity`,
287
+ };
288
+ return { kind: "conform" };
289
+ },
290
+ };
291
+ }
292
+ export const r10 = makeR10(readWriterTier);
219
293
  // ---- R-12 vector search declares namespace + topK -------------------------
220
294
  // §2 vector boundary -> `search_mecanisme`. §15: real namespace + numeric topK.
221
295
  export const r12 = {
@@ -1,4 +1,5 @@
1
- import { isInventoryRow, readOracle } from "../oracle.js";
1
+ import { isInventoryRow, oracleLabel, readOracle, refuseWithoutOracle, } from "../oracle.js";
2
+ import { itemCount, withCount } from "../violation-count.js";
2
3
  /** Run a predicate over a row set, tallying per-kind. */
3
4
  export function tally(spec, rows) {
4
5
  const t = { conform: 0, violate: 0, cnj: 0, na: 0, violations: [], unjudged: [] };
@@ -52,7 +53,7 @@ export function evaluate(spec, rows) {
52
53
  .join("; ")}. This is NOT a clean bill. ${breakdown}`;
53
54
  return {
54
55
  id: spec.id,
55
- anchor: anchorFor(t.unjudged, "oracle:vp-by-tool-day158.csv"),
56
+ anchor: anchorFor(t.unjudged, oracleLabel()),
56
57
  state: "could-not-judge",
57
58
  verdict: reason,
58
59
  reason,
@@ -63,16 +64,16 @@ export function evaluate(spec, rows) {
63
64
  .slice(0, 3)
64
65
  .map((v) => `row#${v.row.n} "${v.row.by.outil || v.row.by.table}" — ${v.note}`)
65
66
  .join("; ");
66
- return {
67
+ return withCount({
67
68
  id: spec.id,
68
- anchor: anchorFor(t.violations, "oracle:vp-by-tool-day158.csv"),
69
+ anchor: anchorFor(t.violations, oracleLabel()),
69
70
  state: "non-conforming",
70
71
  verdict: `${spec.ruleId}: ${t.violate} of ${applicable} applicable oracle rows VIOLATE (${spec.check}). ${breakdown} Examples: ${examples}.`,
71
- };
72
+ }, itemCount(t.violate));
72
73
  }
73
74
  return {
74
75
  id: spec.id,
75
- anchor: "oracle:vp-by-tool-day158.csv",
76
+ anchor: oracleLabel(),
76
77
  state: "present",
77
78
  verdict: `${spec.ruleId}: ${t.conform} applicable oracle rows conform (${spec.check}); 0 violations. ${breakdown}`,
78
79
  };
@@ -85,7 +86,7 @@ export function evaluate(spec, rows) {
85
86
  export function makeOracleDetector(spec, rowsProvider = readOracle) {
86
87
  return {
87
88
  id: spec.id,
88
- run: () => evaluate(spec, rowsProvider()),
89
+ run: () => refuseWithoutOracle(spec.id, () => evaluate(spec, rowsProvider())),
89
90
  };
90
91
  }
91
92
  // ---- small shared classifiers over oracle vocabulary --------------------
@@ -0,0 +1,92 @@
1
+ import * as ts from "typescript";
2
+ import type { Tree } from "../../engine-types.js";
3
+ export type SrcFile = Tree["files"][number];
4
+ export interface Parsed {
5
+ rel: string;
6
+ sf: ts.SourceFile;
7
+ }
8
+ export declare function sourceOf(f: SrcFile): string;
9
+ export declare function parseFile(f: SrcFile): Parsed;
10
+ export declare function lineOf(n: ts.Node): number;
11
+ /** Strip `await`, parentheses, `!` non-null, `as`/`satisfies` casts. */
12
+ export declare function unwrap(e: ts.Expression): ts.Expression;
13
+ /** A property read: `x.prop`, or `x["prop"]` (a string-literal element access
14
+ * is the same read, spelled differently). A computed key is not a read of a
15
+ * known property and gives undefined. */
16
+ export declare function propRead(e: ts.Node): {
17
+ obj: ts.Expression;
18
+ name: string;
19
+ } | undefined;
20
+ /** The initializer of the NEAREST `const <id> = <init>` visible from `id`, or
21
+ * undefined (a parameter or a `let` in between shadows it, so does a
22
+ * destructure). Exactly one level: the caller never follows an alias of an
23
+ * alias. */
24
+ export declare function constInit(id: ts.Identifier): ts.Expression | undefined;
25
+ /** `e` as a property read, one const alias deep: `x.p`, `x["p"]`, or an
26
+ * identifier declared `const a = x.p` / `const a = x["p"]` (read as `x.p`). */
27
+ export declare function propReadResolved(e: ts.Expression): {
28
+ obj: ts.Expression;
29
+ name: string;
30
+ } | undefined;
31
+ /** Visit every descendant of `n` (pre-order), NOT descending into nested
32
+ * function bodies when `ownOnly` is set. */
33
+ export declare function walk(n: ts.Node, visit: (x: ts.Node) => void, ownOnly?: boolean): void;
34
+ /** `a.b.c` as text, or undefined when the expression is not a dotted chain. */
35
+ export declare function dotted(e: ts.Expression): string | undefined;
36
+ /** The callee's last name: `ctx.db.patch` -> `patch`, `helper` -> `helper`. */
37
+ export declare function calleeName(c: ts.CallExpression): string | undefined;
38
+ export type Builder = "mutation" | "action" | "query" | "internalMutation" | "internalAction" | "internalQuery";
39
+ export interface Registration {
40
+ name: string;
41
+ builder: Builder;
42
+ rel: string;
43
+ line: number;
44
+ /** the handler function (`handler: async (ctx, args) => {…}`). */
45
+ handler: ts.FunctionLikeDeclaration;
46
+ /** arg name -> referenced table, for `v.id("T")` / `v.optional(v.id("T"))`. */
47
+ argIdTable: Map<string, string>;
48
+ }
49
+ export declare function idTableOf(text: string): string | undefined;
50
+ /** The ONE table a validator's text references through `v.id("T")` — through
51
+ * `v.optional`, `v.array`, or a nested `v.object` — or undefined when it names
52
+ * none or several. */
53
+ export declare function singleIdTable(text: string): string | undefined;
54
+ /** Every `export const NAME = <builder>({ … handler … })` of a file. */
55
+ export declare function registrations(p: Parsed): Registration[];
56
+ export interface FnDef {
57
+ name: string;
58
+ rel: string;
59
+ line: number;
60
+ fn: ts.FunctionLikeDeclaration;
61
+ }
62
+ /** Every top-level `function f` / `const f = (…) => …` across the parsed files. */
63
+ export declare function indexFunctions(ps: Parsed[]): Map<string, FnDef[]>;
64
+ /** Names of the plain-identifier functions a body calls. */
65
+ export declare function calledNames(fn: ts.Node): Set<string>;
66
+ /** A per-function boolean property closed over the call graph: `own(fn)` holds,
67
+ * or the function calls (by name) another function for which it holds. */
68
+ export declare function closeOverCalls(idx: Map<string, FnDef[]>, own: (d: FnDef) => boolean): Set<string>;
69
+ /** The tenant columns, as standard 2825122 §4 R-52 lists them:
70
+ * `orgId`/`tenantId`/`workspaceId`/`orgSlug` (vantage-peers stamps its agents
71
+ * under `orgSlug: client_org_mapping.clerkOrgSlug`). A `namespace` is NOT in this
72
+ * set: the standard counts it only when it is "checked against the caller's
73
+ * allowed prefixes", which is a property of the code around the table, not of
74
+ * the schema. `readSchema` records it apart (`SchemaTable.namespace`) and R-52
75
+ * decides it per table (cross-tenant-write.ts `namespaceDecisions`). */
76
+ export declare const TENANT_COLUMNS: ReadonlySet<string>;
77
+ /** The column that is a tenant column only where it is prefix-checked. */
78
+ export declare const NAMESPACE_COLUMN = "namespace";
79
+ export type TenantKind = "required" | "optional" | "none";
80
+ export interface SchemaTable {
81
+ name: string;
82
+ /** from the TENANT_COLUMNS alone. */
83
+ tenant: TenantKind;
84
+ /** whether a `namespace` column exists and is optional — a tenant column
85
+ * only where the code checks it against the caller's allowed prefixes. */
86
+ namespace: TenantKind;
87
+ fields: string[];
88
+ /** field -> the table its `v.id("T")` references. */
89
+ idRef: Map<string, string>;
90
+ }
91
+ /** Read every `name: defineTable({...})` of a schema file. */
92
+ export declare function readSchema(p: Parsed): Map<string, SchemaTable>;