@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.
- package/README.md +66 -5
- package/dist/args.d.ts +16 -0
- package/dist/args.js +72 -0
- package/dist/build-stamp.d.ts +13 -0
- package/dist/build-stamp.js +76 -0
- package/dist/build-stamp.json +4 -0
- package/dist/caller-resolution.d.ts +222 -0
- package/dist/caller-resolution.js +1198 -0
- package/dist/cli.js +249 -17
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage-cli.js +8 -0
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.d.ts +5 -3
- package/dist/detectors/oracle-axis1.js +28 -18
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +37 -22
- package/dist/detectors/predicates.d.ts +3 -0
- package/dist/detectors/predicates.js +93 -19
- package/dist/detectors/shared.js +8 -7
- package/dist/detectors/source/_ast.d.ts +92 -0
- package/dist/detectors/source/_ast.js +353 -0
- package/dist/detectors/source/_scan.js +4 -3
- package/dist/detectors/source/backfill.js +71 -18
- package/dist/detectors/source/contract.js +21 -13
- package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
- package/dist/detectors/source/cross-tenant-write.js +1408 -0
- package/dist/detectors/source/guard-helper-flow.d.ts +134 -0
- package/dist/detectors/source/guard-helper-flow.js +1216 -0
- package/dist/detectors/source/identity-by-id.d.ts +80 -0
- package/dist/detectors/source/identity-by-id.js +1783 -0
- package/dist/detectors/source/identity.d.ts +58 -2
- package/dist/detectors/source/identity.js +1158 -28
- package/dist/detectors/source/isolation.d.ts +11 -1
- package/dist/detectors/source/isolation.js +106 -25
- package/dist/detectors/source/jobs.js +178 -4
- package/dist/detectors/source/lifecycle.js +14 -2
- package/dist/detectors/source/local-identity.d.ts +26 -0
- package/dist/detectors/source/local-identity.js +661 -0
- package/dist/detectors/source/package-principal.d.ts +9 -0
- package/dist/detectors/source/package-principal.js +177 -0
- package/dist/detectors/source/r6-shapes.d.ts +20 -0
- package/dist/detectors/source/r6-shapes.js +285 -0
- package/dist/detectors/source/readbudget.d.ts +1 -0
- package/dist/detectors/source/readbudget.js +358 -4
- package/dist/detectors/source/recipient-by-id.d.ts +40 -0
- package/dist/detectors/source/recipient-by-id.js +1108 -0
- package/dist/detectors/source/refusal.d.ts +14 -0
- package/dist/detectors/source/refusal.js +75 -2
- package/dist/detectors/source/security.d.ts +16 -0
- package/dist/detectors/source/security.js +680 -33
- package/dist/detectors/source/write-isolation.d.ts +8 -1
- package/dist/detectors/source/write-isolation.js +64 -30
- package/dist/detectors/source/writesafety.js +27 -14
- package/dist/detectors/standard/_doc.d.ts +1 -1
- package/dist/detectors/standard/_doc.js +5 -4
- package/dist/detectors/standard/r41-frontier.js +3 -2
- package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
- package/dist/detectors/standard/r46-returns-validator.js +48 -16
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +84 -48
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +11 -0
- package/dist/oracle.d.ts +92 -3
- package/dist/oracle.js +229 -12
- package/dist/registry.d.ts +8 -1
- package/dist/registry.js +74 -8
- package/dist/report.d.ts +11 -1
- package/dist/report.js +15 -2
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/state-rank.d.ts +5 -0
- package/dist/state-rank.js +9 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +15 -13
- package/dist/target.d.ts +32 -0
- package/dist/target.js +173 -0
- package/dist/violation-count.d.ts +48 -0
- package/dist/violation-count.js +94 -0
- package/fixtures/backend-standard-2825122.md +722 -0
- package/fixtures/backend-standard-4dcd3e0.md +722 -0
- package/fixtures/cr1-classes/_generated/api.d.ts +24 -0
- package/fixtures/cr1-classes/surface.ts +206 -0
- package/fixtures/cr1-empty/nothing.ts +1 -0
- package/fixtures/cr1-refusal-on-resolved/_generated/api.d.ts +24 -0
- package/fixtures/cr1-refusal-on-resolved/doors.ts +180 -0
- package/fixtures/cr1-refusal-on-resolved/lib/auth.ts +104 -0
- package/fixtures/r34-vp-auth-30e821b/auth-805-812.ts.txt +8 -0
- package/fixtures/r34-vp-auth-30e821b/auth-878-890.ts.txt +13 -0
- package/fixtures/rule-classes-2825122.json +228 -0
- package/fixtures/vp-writer-tier-1e4ab36.csv +55 -0
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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:
|
|
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 —
|
|
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: "
|
|
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
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 = {
|
package/dist/detectors/shared.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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:
|
|
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>;
|