@vantageos/backend-doctor 0.2.0 → 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 +48 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +19 -1
- package/dist/build-stamp.json +2 -2
- package/dist/caller-resolution.js +11 -8
- package/dist/cli.js +164 -15
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.js +5 -4
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +32 -16
- package/dist/detectors/predicates.js +12 -3
- package/dist/detectors/shared.js +3 -2
- 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 +62 -17
- 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 +22 -0
- package/dist/detectors/source/guard-helper-flow.js +49 -6
- 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 +8 -1
- package/dist/detectors/source/identity.js +364 -35
- package/dist/detectors/source/jobs.js +13 -2
- 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.js +291 -0
- 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.js +455 -26
- 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.js +3 -2
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +75 -43
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +11 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +27 -3
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +14 -6
- 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/rule-classes-2825122.json +228 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasJustificationTokenInText } from "../oracle-axis2.js";
|
|
1
2
|
import { base, code, emptyScan, isJudgeable, SOURCE_STANDARD_PIN, srcDetector, } from "./_scan.js";
|
|
2
3
|
// A schema definition file — where defineTable declarations live.
|
|
3
4
|
const SCHEMA_FILE = /schema\.ts$/;
|
|
@@ -104,7 +105,9 @@ export function scanR44(files) {
|
|
|
104
105
|
let directInsert = false;
|
|
105
106
|
let loopsSource = false;
|
|
106
107
|
let referenced = false;
|
|
108
|
+
let migText = "";
|
|
107
109
|
for (const f of migFiles) {
|
|
110
|
+
migText += `${f.lines.map((l) => l.raw).join("\n")}\n`;
|
|
108
111
|
for (const l of f.lines) {
|
|
109
112
|
if (!code(l))
|
|
110
113
|
continue;
|
|
@@ -126,18 +129,39 @@ export function scanR44(files) {
|
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
131
|
else if (directInsert) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// judgement retired (task k179jnhx, §23): flagged (no visible query/
|
|
133
|
+
// paginate of the source table); decides on the migration file's written
|
|
134
|
+
// JUSTIFIÉ:/JUSTIFIED: token, never a person's opinion of coverage.
|
|
135
|
+
if (hasJustificationTokenInText(migText)) {
|
|
136
|
+
scan.conform.push({
|
|
137
|
+
rel: d.rel,
|
|
138
|
+
n: d.n,
|
|
139
|
+
note: `derived structure ${d.table} has a migration inserting it; no query/paginate of ${d.source} is visible, but the migration file carries a written JUSTIFIÉ:/JUSTIFIED: token — accepted, not silently passed`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
scan.violate.push({
|
|
144
|
+
rel: d.rel,
|
|
145
|
+
n: d.n,
|
|
146
|
+
note: `derived structure ${d.table} has a migration inserting it, but no query/paginate of the source table ${d.source} is visible, and no written JUSTIFIÉ:/JUSTIFIED: token in the migration file — coverage of existing source rows owes a fix or a recorded justification`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
134
149
|
}
|
|
135
150
|
else if (referenced) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
if (hasJustificationTokenInText(migText)) {
|
|
152
|
+
scan.conform.push({
|
|
153
|
+
rel: d.rel,
|
|
154
|
+
n: d.n,
|
|
155
|
+
note: `derived structure ${d.table} has a backfill migration NAMED for it, writing the structure via a helper; the migration file carries a written JUSTIFIÉ:/JUSTIFIED: token — accepted, not silently passed`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
scan.violate.push({
|
|
160
|
+
rel: d.rel,
|
|
161
|
+
n: d.n,
|
|
162
|
+
note: `derived structure ${d.table} has a backfill migration NAMED for it, but it writes the structure via a helper (no direct ctx.db.insert("${d.table}") in the migration), and no written JUSTIFIÉ:/JUSTIFIED: token in the migration file — idempotence/coverage owes a fix or a recorded justification`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
141
165
|
}
|
|
142
166
|
else {
|
|
143
167
|
scan.violate.push({
|
|
@@ -154,10 +178,12 @@ export const R44 = srcDetector("R-44", "R-44 a derived structure carries a same-
|
|
|
154
178
|
// R-45 — a derived structure carries a test whose fixture bypasses the write
|
|
155
179
|
// path and brackets the migration.
|
|
156
180
|
// ===========================================================================
|
|
181
|
+
const R45_TOKEN_WINDOW = 6;
|
|
157
182
|
export function scanR45(files) {
|
|
158
183
|
const scan = emptyScan();
|
|
159
184
|
const derived = derivedStructures(files);
|
|
160
185
|
const testFiles = files.filter(isTest);
|
|
186
|
+
const byRel = new Map(files.map((f) => [f.rel, f]));
|
|
161
187
|
for (const d of derived) {
|
|
162
188
|
const insertSourceRe = INSERT_OF(d.source);
|
|
163
189
|
const stem = d.table.toLowerCase();
|
|
@@ -200,13 +226,32 @@ export function scanR45(files) {
|
|
|
200
226
|
});
|
|
201
227
|
}
|
|
202
228
|
else {
|
|
203
|
-
// No test reaches the subject at all —
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
// No test reaches the subject at all — flagged. judgement retired
|
|
230
|
+
// (task k179jnhx, §23): the schema declaration's comment window is this
|
|
231
|
+
// site's remediation cell. No JUSTIFIÉ:/JUSTIFIED: token → non-conforming;
|
|
232
|
+
// token present → present, decided by the token's presence, never a
|
|
233
|
+
// person's opinion of whether the untested state is sound.
|
|
234
|
+
const declFile = byRel.get(d.rel);
|
|
235
|
+
const window = declFile
|
|
236
|
+
? declFile.lines
|
|
237
|
+
.slice(Math.max(0, d.n - 1 - R45_TOKEN_WINDOW), d.n - 1 + R45_TOKEN_WINDOW)
|
|
238
|
+
.map((l) => l.raw)
|
|
239
|
+
.join("\n")
|
|
240
|
+
: "";
|
|
241
|
+
if (hasJustificationTokenInText(window)) {
|
|
242
|
+
scan.conform.push({
|
|
243
|
+
rel: d.rel,
|
|
244
|
+
n: d.n,
|
|
245
|
+
note: `derived structure ${d.table}: no test constructs its source row ${d.source} without the write path, but the schema declaration's comment window carries a written JUSTIFIÉ:/JUSTIFIED: token — accepted, not silently passed`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
scan.violate.push({
|
|
250
|
+
rel: d.rel,
|
|
251
|
+
n: d.n,
|
|
252
|
+
note: `derived structure ${d.table}: no test constructs its source row ${d.source} without the write path (and no create-only fixture to mark red), and no written JUSTIFIÉ:/JUSTIFIED: token near its schema declaration — fixture-independence owes a test or a recorded justification`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
210
255
|
}
|
|
211
256
|
}
|
|
212
257
|
return scan;
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// zero-applicable-site abstain that resolves to could-not-judge WITH a reason.
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
+
import { siteCount, withCount } from "../../violation-count.js";
|
|
22
|
+
import { hasJustificationTokenInText } from "../oracle-axis2.js";
|
|
21
23
|
/** The standard all four are pinned against (short hash a9d7877b). */
|
|
22
24
|
export const SOURCE_CONTRACT_STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) §15";
|
|
23
25
|
function emptyScan() {
|
|
@@ -73,12 +75,12 @@ function decide(ruleId, id, check, scan, emptyReason) {
|
|
|
73
75
|
const breakdown = `[conform=${nC} non-conforming=${nV} could-not-judge=${nJ}]`;
|
|
74
76
|
const fmt = (s) => s.slice(0, 3).map((x) => `${x.rel}:${x.n} (${x.note})`).join("; ");
|
|
75
77
|
if (nV > 0) {
|
|
76
|
-
return {
|
|
78
|
+
return withCount({
|
|
77
79
|
id,
|
|
78
80
|
anchor: `${scan.violate[0].rel}:${scan.violate[0].n}`,
|
|
79
81
|
state: "non-conforming",
|
|
80
82
|
verdict: `${ruleId}: ${nV} site(s) VIOLATE (${check}). ${breakdown} Violations: ${fmt(scan.violate)}.${nC ? ` Conform site(s): ${fmt(scan.conform)}.` : ""}`,
|
|
81
|
-
};
|
|
83
|
+
}, siteCount(scan.violate));
|
|
82
84
|
}
|
|
83
85
|
if (nJ > 0) {
|
|
84
86
|
const reason = `${ruleId}: ${nJ} site(s) could not be judged — ${fmt(scan.cnj)}. This is NOT a clean bill. ${breakdown}${nC ? ` Conform site(s): ${fmt(scan.conform)}.` : ""}`;
|
|
@@ -320,15 +322,19 @@ export function scanR25(files) {
|
|
|
320
322
|
// authority-tier / boundary-key declaration: rag has only an import comment; the
|
|
321
323
|
// agentEngine block documents durability/versioning ("long-task survival",
|
|
322
324
|
// "Pinned exactly at 0.1.0-alpha.2"), not an isolation contract. Per §8 an
|
|
323
|
-
// undeclared seam is non-conforming as an app-owned table with a blank field
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
325
|
+
// undeclared seam is non-conforming as an app-owned table with a blank field.
|
|
326
|
+
// judgement retired (task k179jnhx, §23): whether the library's tables are in
|
|
327
|
+
// fact used fleet-internal/master only (which would justify the silence) is no
|
|
328
|
+
// longer a human disposition — it decides on the PRESENCE of a written
|
|
329
|
+
// JUSTIFIÉ:/JUSTIFIED: token in the seam's comment window (that window IS this
|
|
330
|
+
// site's remediation cell, the same mechanism R-7/R-8 use on an oracle row). No
|
|
331
|
+
// token → non-conforming; token present → present. could-not-judge is reserved
|
|
332
|
+
// for a seam the instrument could not read, never reached here (a parsed
|
|
333
|
+
// app.use() call always resolves to one of the two).
|
|
329
334
|
// PATTERN: each `app.use(<component>)` line in a *.config.ts seam is conform iff its
|
|
330
335
|
// surrounding window (comments included — the seam contract is documented prose)
|
|
331
|
-
// declares an authority tier + boundary key
|
|
336
|
+
// declares an authority tier + boundary key, OR carries a written justification
|
|
337
|
+
// token; else non-conforming.
|
|
332
338
|
// ---------------------------------------------------------------------------
|
|
333
339
|
const MOUNT_CALL = /app\.use\s*\(\s*(\w+)/;
|
|
334
340
|
const SEAM_CONTRACT = /authority tier|boundary key|isolation contract|org-scoped|fleet-internal|master-only|namespace-bound|tenant boundary/i;
|
|
@@ -350,14 +356,16 @@ export function scanR26(files) {
|
|
|
350
356
|
continue;
|
|
351
357
|
const comp = m[1];
|
|
352
358
|
// seam contract is documented as prose — scan ALL lines in the window.
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
.some((x) => SEAM_CONTRACT.test(x.raw));
|
|
359
|
+
const window = f.lines.slice(Math.max(0, i - R26_WINDOW), i + R26_WINDOW);
|
|
360
|
+
const declared = window.some((x) => SEAM_CONTRACT.test(x.raw));
|
|
356
361
|
if (declared) {
|
|
357
362
|
scan.conform.push({ rel: f.rel, n: l.n, note: `component "${comp}" mount declares an authority tier + boundary key at the seam` });
|
|
358
363
|
}
|
|
364
|
+
else if (hasJustificationTokenInText(window.map((x) => x.raw).join("\n"))) {
|
|
365
|
+
scan.conform.push({ rel: f.rel, n: l.n, note: `component "${comp}" mounted with no stated authority tier / boundary key, but the seam's comment window carries a written JUSTIFIÉ:/JUSTIFIED: token — accepted, not silently passed` });
|
|
366
|
+
}
|
|
359
367
|
else {
|
|
360
|
-
scan.
|
|
368
|
+
scan.violate.push({ rel: f.rel, n: l.n, note: `component "${comp}" mounted with no stated authority tier / boundary key at the seam, and no written JUSTIFIÉ:/JUSTIFIED: token in the seam's comment window — usable fleet-internal/master only; §8 owes a fix or a recorded justification` });
|
|
361
369
|
}
|
|
362
370
|
}
|
|
363
371
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { Detector } from "../../engine-types.js";
|
|
3
|
+
import { type FnDef, type Parsed, type SchemaTable, type SrcFile } from "./_ast.js";
|
|
4
|
+
import { type Scan, type SourceDetectorEntry } from "./_scan.js";
|
|
5
|
+
export declare const R52_ID = "R-52 a write reaching a row with no tenant value through a caller-choosable value checks the target's tenant first";
|
|
6
|
+
export declare function fleetOnlyExpr(e0: ts.Expression, isFleetHelper: (name: string) => boolean, isFleetLocal?: (name: string) => boolean): boolean;
|
|
7
|
+
export interface R52Facts {
|
|
8
|
+
parsed: Parsed[];
|
|
9
|
+
schema: Map<string, SchemaTable> | undefined;
|
|
10
|
+
schemaRel: string | undefined;
|
|
11
|
+
callerChosen: Map<string, Set<string>>;
|
|
12
|
+
fns: Map<string, FnDef[]>;
|
|
13
|
+
readsTenant: Set<string>;
|
|
14
|
+
refusing: Set<string>;
|
|
15
|
+
/** helpers every truthy return of which is a fleet-scope decision. */
|
|
16
|
+
fleetOnly: Set<string>;
|
|
17
|
+
fleetRefusal: Set<string>;
|
|
18
|
+
singleTable: Map<string, string>;
|
|
19
|
+
/** helpers that hand their table argument to `db.normalizeId`. */
|
|
20
|
+
normalizers: Set<string>;
|
|
21
|
+
/** per table carrying a `namespace` and no required tenant column: whether
|
|
22
|
+
* that namespace is checked against the caller's allowed prefixes. */
|
|
23
|
+
namespace: Map<string, NamespaceDecision>;
|
|
24
|
+
}
|
|
25
|
+
/** Standard 2825122 R-52 counts a `namespace` as a tenant column only when it is
|
|
26
|
+
* "checked against the caller's allowed prefixes". Decided per table from the
|
|
27
|
+
* public doors (query/mutation/action) that read or write it:
|
|
28
|
+
* checked every door runs a prefix check (or refuses every non-master);
|
|
29
|
+
* unchecked no door does — the namespace is a label, the table is untenanted;
|
|
30
|
+
* undecided some do and some do not, or no public door reaches the table —
|
|
31
|
+
* every site targeting it is could-not-judge, never green. */
|
|
32
|
+
export interface NamespaceDecision {
|
|
33
|
+
verdict: "checked" | "unchecked" | "undecided";
|
|
34
|
+
why: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function r52Facts(files: SrcFile[]): R52Facts;
|
|
37
|
+
export interface R52Detail {
|
|
38
|
+
scan: Scan;
|
|
39
|
+
/** tables carrying no tenant value (none, or optional), as read from schema. */
|
|
40
|
+
untenanted: string[];
|
|
41
|
+
schemaRel: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
export declare function scanR52(files: SrcFile[]): Scan;
|
|
44
|
+
export declare function scanR52Detailed(files: SrcFile[]): R52Detail;
|
|
45
|
+
export declare const R52: Detector;
|
|
46
|
+
export declare const SOURCE_CROSS_TENANT_WRITE_DETECTORS: SourceDetectorEntry[];
|