@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.
- package/README.md +19 -2
- package/dist/args.d.ts +16 -0
- package/dist/args.js +54 -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 +1195 -0
- package/dist/cli.js +217 -10
- package/dist/coverage-cli.js +8 -0
- package/dist/crud-coverage.d.ts +81 -0
- package/dist/crud-coverage.js +375 -0
- package/dist/detectors/oracle-axis1.d.ts +5 -3
- package/dist/detectors/oracle-axis1.js +24 -15
- package/dist/detectors/oracle-axis2.js +5 -6
- package/dist/detectors/predicates.d.ts +3 -0
- package/dist/detectors/predicates.js +81 -16
- package/dist/detectors/shared.js +5 -5
- package/dist/detectors/source/_scan.js +6 -1
- package/dist/detectors/source/backfill.js +9 -1
- package/dist/detectors/source/guard-helper-flow.d.ts +112 -0
- package/dist/detectors/source/guard-helper-flow.js +1173 -0
- package/dist/detectors/source/identity.d.ts +51 -2
- package/dist/detectors/source/identity.js +815 -14
- package/dist/detectors/source/isolation.d.ts +11 -1
- package/dist/detectors/source/isolation.js +106 -25
- package/dist/detectors/source/jobs.js +165 -2
- package/dist/detectors/source/readbudget.d.ts +1 -0
- package/dist/detectors/source/readbudget.js +67 -4
- package/dist/detectors/source/security.d.ts +16 -0
- package/dist/detectors/source/security.js +225 -7
- package/dist/detectors/source/write-isolation.d.ts +8 -1
- package/dist/detectors/source/write-isolation.js +64 -30
- package/dist/detectors/standard/r46-returns-validator.d.ts +8 -0
- package/dist/detectors/standard/r46-returns-validator.js +45 -14
- package/dist/detectors/structural.js +14 -10
- package/dist/oracle.d.ts +92 -3
- package/dist/oracle.js +229 -12
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +48 -6
- package/dist/report.d.ts +11 -1
- package/dist/report.js +15 -2
- package/dist/state-rank.d.ts +5 -0
- package/dist/state-rank.js +9 -0
- package/dist/tally.d.ts +35 -0
- package/dist/tally.js +76 -8
- package/dist/target.d.ts +32 -0
- package/dist/target.js +173 -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/vp-writer-tier-1e4ab36.csv +55 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -5,16 +5,22 @@
|
|
|
5
5
|
// DISTINCTLY. The target is a real directory so the engine's loadTree succeeds;
|
|
6
6
|
// the first-wave detectors ignore the tree and consult the oracle.
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
9
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { renderTable, runDoctor } from "@vantageos/mcp-doctor";
|
|
12
|
+
import { parseArgs, USAGE } from "./args.js";
|
|
13
|
+
import { checkDistFreshness } from "./build-stamp.js";
|
|
14
|
+
import { CR1_ID, callerResolutionResult, callerResolutionSection, violations as crViolations, scanCallerResolution, } from "./caller-resolution.js";
|
|
12
15
|
import { coverage, coverageSummary } from "./coverage.js";
|
|
16
|
+
import { crudCoverageSection, extractReactRouterPaths, scanCrudCoverage, } from "./crud-coverage.js";
|
|
13
17
|
import { checkStandardReadable } from "./detectors/standard/_doc.js";
|
|
14
|
-
import { NOT_MECHANICALLY_CHECKABLE, notCheckableResults } from "./not-checkable.js";
|
|
18
|
+
import { NOT_MECHANICALLY_CHECKABLE, notCheckableResults, } from "./not-checkable.js";
|
|
19
|
+
import { activateOracle, describeOracle, resolveOracle } from "./oracle.js";
|
|
15
20
|
import { REGISTRY } from "./registry.js";
|
|
16
21
|
import { writeReport } from "./report.js";
|
|
17
|
-
import { tally, tallyLine, verdict } from "./tally.js";
|
|
22
|
+
import { handlerSurfaceLine, handlerSurfaceStats, tally, tallyLine, verdict, } from "./tally.js";
|
|
23
|
+
import { describeScope, loadScopedTree, onScopedTree, resolveTarget, } from "./target.js";
|
|
18
24
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
25
|
/** `git -C <target> rev-parse HEAD`, or "not a git repo" when it is not one. */
|
|
20
26
|
function targetCommit(target) {
|
|
@@ -42,8 +48,154 @@ function toolVersion() {
|
|
|
42
48
|
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
|
|
43
49
|
return pkg.version;
|
|
44
50
|
}
|
|
51
|
+
/** Recursively collect files under `dir` whose basename matches `pred`, as paths
|
|
52
|
+
* relative to `base`. Silently skips node_modules/.git and unreadable dirs. */
|
|
53
|
+
function collectFiles(dir, base, pred, out = []) {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dir);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
for (const name of entries) {
|
|
62
|
+
if (name === "node_modules" || name === ".git")
|
|
63
|
+
continue;
|
|
64
|
+
const full = join(dir, name);
|
|
65
|
+
let isDir;
|
|
66
|
+
try {
|
|
67
|
+
isDir = statSync(full).isDirectory();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (isDir)
|
|
73
|
+
collectFiles(full, base, pred, out);
|
|
74
|
+
else {
|
|
75
|
+
const rel = relative(base, full);
|
|
76
|
+
if (pred(rel))
|
|
77
|
+
out.push(rel);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** Read the convex tree + the schema + probe an app routes dir, then render the
|
|
83
|
+
* CRUD coverage section. The target is the convex dir; the app routes dir is a
|
|
84
|
+
* sibling probe (`../src/app`, `../app`, `../src/routes`). */
|
|
85
|
+
function scanCrudCoverageForCli(target) {
|
|
86
|
+
const convexRels = collectFiles(target, target, (rel) => rel.endsWith(".ts") && !rel.endsWith(".test.ts"));
|
|
87
|
+
const files = [];
|
|
88
|
+
let schemaText;
|
|
89
|
+
for (const rel of convexRels) {
|
|
90
|
+
let text;
|
|
91
|
+
try {
|
|
92
|
+
text = readFileSync(join(target, rel), "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
files.push({ rel, text });
|
|
98
|
+
if (/(^|\/)schema\.ts$/.test(rel))
|
|
99
|
+
schemaText = text;
|
|
100
|
+
}
|
|
101
|
+
return scanCrudCoverage(schemaText, files, probeRoutes(target));
|
|
102
|
+
}
|
|
103
|
+
/** Probe the app-route surface as a sibling of the convex `target` dir. Reports
|
|
104
|
+
* BOTH whether a candidate app/src routes dir EXISTS and the route paths found in
|
|
105
|
+
* it, so the scan can tell "no routes dir" (n/a) from "dir exists, no router
|
|
106
|
+
* convention recognised" (could-not-judge) — Eta REVISE blocker 3. Recognises
|
|
107
|
+
* Next/SvelteKit convention files AND a react-router `<Route path>` /
|
|
108
|
+
* `createBrowserRouter` router in App.tsx/main.tsx/a routes file. */
|
|
109
|
+
function probeRoutes(target) {
|
|
110
|
+
const ROUTE_FILE = /(^|\/)(page\.tsx|route\.tsx?|\+page\.svelte)$/;
|
|
111
|
+
const appRoot = resolve(target, "..");
|
|
112
|
+
let candidateDir;
|
|
113
|
+
const routePaths = [];
|
|
114
|
+
// (a) Next/SvelteKit convention dirs (a sibling of the convex target).
|
|
115
|
+
for (const candidate of ["src/app", "app", "src/routes"]) {
|
|
116
|
+
const routeRoot = join(appRoot, candidate);
|
|
117
|
+
let isDir = false;
|
|
118
|
+
try {
|
|
119
|
+
isDir = statSync(routeRoot).isDirectory();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
isDir = false;
|
|
123
|
+
}
|
|
124
|
+
if (!isDir)
|
|
125
|
+
continue;
|
|
126
|
+
if (candidateDir === undefined)
|
|
127
|
+
candidateDir = candidate;
|
|
128
|
+
routePaths.push(...collectFiles(routeRoot, routeRoot, (rel) => ROUTE_FILE.test(rel)));
|
|
129
|
+
}
|
|
130
|
+
// (b) react-router (Vite SPA): parse the router source for its route paths.
|
|
131
|
+
for (const rel of [
|
|
132
|
+
"src/App.tsx",
|
|
133
|
+
"src/main.tsx",
|
|
134
|
+
"src/routes.tsx",
|
|
135
|
+
"src/router.tsx",
|
|
136
|
+
"App.tsx",
|
|
137
|
+
"main.tsx",
|
|
138
|
+
]) {
|
|
139
|
+
const full = join(appRoot, rel);
|
|
140
|
+
let text;
|
|
141
|
+
try {
|
|
142
|
+
text = readFileSync(full, "utf8");
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// the src dir hosting the router is a candidate app dir, even if its own
|
|
148
|
+
// route-path extraction turns up empty (→ could-not-judge, never n/a).
|
|
149
|
+
if (candidateDir === undefined)
|
|
150
|
+
candidateDir = dirname(rel) || rel;
|
|
151
|
+
routePaths.push(...extractReactRouterPaths(text));
|
|
152
|
+
}
|
|
153
|
+
return { candidateDir, routePaths };
|
|
154
|
+
}
|
|
45
155
|
function main() {
|
|
46
|
-
|
|
156
|
+
// Refuse to judge from a dist/ compiled from other source than this checkout's
|
|
157
|
+
// (src/build-stamp.ts). Runs before anything is read, so no verdict is printed.
|
|
158
|
+
// Flags first: --version/--help answer without reading a target, and an unknown
|
|
159
|
+
// flag is a usage error — never a path handed to the filesystem (an `-v` used to
|
|
160
|
+
// become `<cwd>/-v` and die on an uncaught ENOENT stack).
|
|
161
|
+
const args = parseArgs(process.argv.slice(2));
|
|
162
|
+
if (args.kind === "version") {
|
|
163
|
+
console.log(toolVersion());
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
if (args.kind === "help") {
|
|
167
|
+
console.log(USAGE);
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
if (args.kind === "usage-error") {
|
|
171
|
+
console.error(`backend-doctor: ${args.message}\n\n${USAGE}`);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
const fresh = checkDistFreshness(here);
|
|
175
|
+
if (!fresh.ok) {
|
|
176
|
+
console.error(`backend-doctor: REFUSING TO RUN — ${fresh.reason}`);
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
const target = resolve(args.positional[0] ?? process.cwd());
|
|
180
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
181
|
+
console.error(`backend-doctor: target ${target} is not a readable directory — could not judge.\n\n${USAGE}`);
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
// ONE normalisation (src/target.ts): `convex` and `.` resolve to the same repo
|
|
185
|
+
// root, convex dir and file set, so every rule sees the same rels either way.
|
|
186
|
+
const scope = resolveTarget(target);
|
|
187
|
+
const tree = loadScopedTree(scope);
|
|
188
|
+
console.log(describeScope(scope, tree));
|
|
189
|
+
// The oracle is DERIVED from the target (never from this tool's own install
|
|
190
|
+
// directory) and the run says which file it used and where that came from.
|
|
191
|
+
// Absent => the oracle-backed rules refuse to judge (could-not-judge).
|
|
192
|
+
const oracleSel = resolveOracle(target, {
|
|
193
|
+
oracle: args.flags.oracle,
|
|
194
|
+
vrOracle: args.flags["vr-oracle"],
|
|
195
|
+
writerTier: args.flags["writer-tier"],
|
|
196
|
+
});
|
|
197
|
+
activateOracle(oracleSel);
|
|
198
|
+
console.log(describeOracle(oracleSel));
|
|
47
199
|
// Instrument self-check, BEFORE any judging: a backend-doctor run reads its
|
|
48
200
|
// pinned standard fixture as material. If that fixture is absent/unreadable the
|
|
49
201
|
// tool cannot judge — REFUSE with exit 2 (could-not-judge) naming what it could
|
|
@@ -58,10 +210,43 @@ function main() {
|
|
|
58
210
|
// pending detector) — so the tally's rule total equals the 48 derived rules and
|
|
59
211
|
// nothing is silently dropped.
|
|
60
212
|
const results = [
|
|
61
|
-
...runDoctor(
|
|
213
|
+
...runDoctor(scope.repoRoot, onScopedTree(tree, REGISTRY.map((e) => e.detector))),
|
|
62
214
|
...notCheckableResults(),
|
|
63
215
|
];
|
|
64
|
-
console.log(renderTable("backend-doctor (
|
|
216
|
+
console.log(renderTable("backend-doctor (oracle derived from target)", results));
|
|
217
|
+
// CRUD COVERAGE — an inventory/coverage report DIMENSION (not a numbered rule
|
|
218
|
+
// in the pinned 50-rule standard). Rows are EVERY defineTable table derived
|
|
219
|
+
// from the target schema at run time; the union of rows MUST equal the schema
|
|
220
|
+
// table set or the run is RED (fail-closed).
|
|
221
|
+
console.log("");
|
|
222
|
+
const crud = scanCrudCoverageForCli(scope.convexDir ?? target);
|
|
223
|
+
console.log(crudCoverageSection(crud));
|
|
224
|
+
const crudDefect = crud.status === "unreadable" || !crud.unionEqualsSchema;
|
|
225
|
+
// CALLER RESOLUTION (CR-1) — a DOCTOR-OWNED CRITICAL control, reported in its own
|
|
226
|
+
// section for the same reason CRUD coverage is: the pinned standard numbers
|
|
227
|
+
// R-1..R-51 and does not define it, so putting it in REGISTRY would make
|
|
228
|
+
// coverage.ts report a phantom rule and would forge the standard's pin. It is run
|
|
229
|
+
// through the engine's OWN loader (never a second tree traversal) and its
|
|
230
|
+
// violations drive the exit code — an unjustified NO-IDENTITY or a FAIL-OPEN
|
|
231
|
+
// registration is mechanical, so the gate refuses on it.
|
|
232
|
+
console.log("");
|
|
233
|
+
let crScan;
|
|
234
|
+
runDoctor(scope.repoRoot, onScopedTree(tree, [
|
|
235
|
+
{
|
|
236
|
+
id: CR1_ID,
|
|
237
|
+
run: (t) => {
|
|
238
|
+
crScan = scanCallerResolution(t);
|
|
239
|
+
return callerResolutionResult(crScan);
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
]));
|
|
243
|
+
if (crScan === undefined) {
|
|
244
|
+
console.error("CR-1: the engine did not run the caller-resolution detector — refusing to report a surface that was never scanned.");
|
|
245
|
+
process.exit(2);
|
|
246
|
+
}
|
|
247
|
+
console.log(callerResolutionSection(crScan));
|
|
248
|
+
const crResult = callerResolutionResult(crScan);
|
|
249
|
+
const crDefect = crViolations(crScan).length > 0;
|
|
65
250
|
console.log("");
|
|
66
251
|
const cov = coverage();
|
|
67
252
|
console.log(coverageSummary(cov));
|
|
@@ -82,6 +267,11 @@ function main() {
|
|
|
82
267
|
const v = verdict(t, results);
|
|
83
268
|
console.log("");
|
|
84
269
|
console.log(tallyLine(t));
|
|
270
|
+
// The handler-surface slice (R-50/R-51) reported APART from the doc/oracle/
|
|
271
|
+
// process rules, so its could-not-judge count is visible on its own and can be
|
|
272
|
+
// driven to 0 (Laurent: an instrument that cannot go red protects nothing).
|
|
273
|
+
console.log(handlerSurfaceLine(handlerSurfaceStats(results)));
|
|
274
|
+
console.log(`CALLER-RESOLUTION TALLY (CR-1, doctor-owned, outside the pinned rule set): state=${crResult.state} violations=${crViolations(crScan).length} | ${crResult.verdict.split("\n")[0]}`);
|
|
85
275
|
console.log(v.summary);
|
|
86
276
|
if (v.criticalViolations > 0) {
|
|
87
277
|
console.log(` critical violations: ${v.violations.critical.join(", ")}`);
|
|
@@ -93,18 +283,35 @@ function main() {
|
|
|
93
283
|
// the git facts are gathered HERE (a real process) and handed to the pure writer.
|
|
94
284
|
const ctx = {
|
|
95
285
|
target,
|
|
96
|
-
targetCommit: targetCommit(
|
|
286
|
+
targetCommit: targetCommit(scope.repoRoot),
|
|
97
287
|
toolVersion: toolVersion(),
|
|
98
288
|
dateISO: new Date().toISOString().slice(0, 10),
|
|
99
|
-
treeStatus: treeStatus(
|
|
289
|
+
treeStatus: treeStatus(scope.repoRoot),
|
|
290
|
+
oracle: describeOracle(oracleSel),
|
|
100
291
|
};
|
|
101
292
|
const reportDir = join(here, "..", "reports");
|
|
102
|
-
const path = writeReport(ctx, results, t, v, reportDir
|
|
293
|
+
const path = writeReport(ctx, results, t, v, reportDir, undefined, [
|
|
294
|
+
callerResolutionSection(crScan),
|
|
295
|
+
]);
|
|
103
296
|
console.log(`\nreport written: ${path}`);
|
|
104
297
|
if (!cov.ok) {
|
|
105
298
|
console.log("\nNOTE: coverage is INCOMPLETE — a rule is in neither bucket. See the summary above.");
|
|
106
299
|
process.exit(1);
|
|
107
300
|
}
|
|
301
|
+
// The CRUD coverage self-check is fail-closed: an unreadable schema or a rows
|
|
302
|
+
// union that does not cover the schema tables is a DOCTOR DEFECT → RED, never a
|
|
303
|
+
// silent pass. (ABSENT cells are honest reported gaps, NOT a defect.)
|
|
304
|
+
if (crudDefect) {
|
|
305
|
+
console.log("\nNOTE: CRUD coverage self-check FAILED — the rows do not cover every schema table (or the schema is unreadable). See the CRUD COVERAGE section above.");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
// CR-1 is CRITICAL: a FAIL-OPEN or unjustified NO-IDENTITY public registration is
|
|
309
|
+
// a mechanical violation, so the gate refuses on it even when every numbered rule
|
|
310
|
+
// is satisfied. Reported BEFORE the numbered-rule exit so the reason is legible.
|
|
311
|
+
if (crDefect) {
|
|
312
|
+
console.log(`\nNOTE: CR-1 (caller resolution) FAILED — ${crViolations(crScan).length} public registration(s) serve or write data without resolving their caller. CRITICAL, no bypass. See the CALLER RESOLUTION section above; every member is named there.`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
108
315
|
process.exit(v.exitCode);
|
|
109
316
|
}
|
|
110
317
|
main();
|
package/dist/coverage-cli.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
// Prints the coverage proof over the TWO buckets — CHECKED and
|
|
3
3
|
// NOT_MECHANICALLY_CHECKABLE. Exits non-zero only if the union does not equal
|
|
4
4
|
// the derived rule ids (a rule in no bucket, in two, or a stale bucket entry).
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { checkDistFreshness } from "./build-stamp.js";
|
|
5
8
|
import { coverage, coverageSummary } from "./coverage.js";
|
|
6
9
|
import { NOT_MECHANICALLY_CHECKABLE } from "./not-checkable.js";
|
|
10
|
+
const fresh = checkDistFreshness(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
if (!fresh.ok) {
|
|
12
|
+
console.error(`backend-doctor --coverage: REFUSING TO RUN — ${fresh.reason}`);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
7
15
|
const r = coverage();
|
|
8
16
|
console.log(coverageSummary(r));
|
|
9
17
|
console.log("");
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** A convex source file, reconstructed as text — the unit the scanner reads. */
|
|
2
|
+
export interface SourceFile {
|
|
3
|
+
rel: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
/** Each cell is exactly one of these. `n/a: <reason>` (a MEASURED absence — there
|
|
7
|
+
* is genuinely no candidate routes dir) and `could-not-judge: <reason>` (a
|
|
8
|
+
* candidate dir EXISTS but no router convention was recognised, so presence was
|
|
9
|
+
* NOT measured) are DISTINCT — `n/a` must never stand for an unmeasured cell. */
|
|
10
|
+
export type Cell = "present" | "ABSENT" | `n/a: ${string}` | `could-not-judge: ${string}`;
|
|
11
|
+
/**
|
|
12
|
+
* The app-route probe the CLI gathers from the filesystem and hands to the scan.
|
|
13
|
+
* It carries the two facts the cell disposition turns on: whether a candidate
|
|
14
|
+
* app/src routes dir EXISTS at all, and the normalized route paths recognised in
|
|
15
|
+
* it (Next/SvelteKit convention file rels AND react-router `<Route path>` /
|
|
16
|
+
* `createBrowserRouter` paths). The distinction closes Eta REVISE blocker 3:
|
|
17
|
+
* `n/a` asserts absence, but the pre-fix probe only knew its OWN matcher found
|
|
18
|
+
* nothing — a full react-router dir rendered n/a, a false assertion of absence.
|
|
19
|
+
*/
|
|
20
|
+
export interface RouteProbe {
|
|
21
|
+
/** a candidate app/src routes dir that EXISTS on disk, named — undefined iff
|
|
22
|
+
* there is genuinely no candidate routes dir at all (the only `n/a` case). */
|
|
23
|
+
candidateDir?: string;
|
|
24
|
+
/** normalized route path strings — Next/SvelteKit file rels OR react-router
|
|
25
|
+
* paths. Non-empty means a router WAS recognised and cells are judged. */
|
|
26
|
+
routePaths: string[];
|
|
27
|
+
}
|
|
28
|
+
/** Extract react-router route paths from a router source (App.tsx / main.tsx /
|
|
29
|
+
* a routes file): both the `<Route path="…">` element form and the
|
|
30
|
+
* `createBrowserRouter([{ path: "…" }])` object form. Deduped. */
|
|
31
|
+
export declare function extractReactRouterPaths(text: string): string[];
|
|
32
|
+
export interface TableRow {
|
|
33
|
+
table: string;
|
|
34
|
+
create: Cell;
|
|
35
|
+
get: Cell;
|
|
36
|
+
update: Cell;
|
|
37
|
+
remove: Cell;
|
|
38
|
+
list: Cell;
|
|
39
|
+
detail: Cell;
|
|
40
|
+
edit: Cell;
|
|
41
|
+
}
|
|
42
|
+
export interface AbsentCell {
|
|
43
|
+
table: string;
|
|
44
|
+
column: string;
|
|
45
|
+
}
|
|
46
|
+
export interface CrudCoverage {
|
|
47
|
+
status: "ok" | "unreadable";
|
|
48
|
+
/** set when status === "unreadable" — names what could not be read. */
|
|
49
|
+
reason?: string;
|
|
50
|
+
rows: TableRow[];
|
|
51
|
+
rowCount: number;
|
|
52
|
+
schemaTableCount: number;
|
|
53
|
+
unionEqualsSchema: boolean;
|
|
54
|
+
absentCells: AbsentCell[];
|
|
55
|
+
}
|
|
56
|
+
export declare function deriveTableNames(schemaText: string): string[];
|
|
57
|
+
/** One public query/mutation, with the three attribution inputs: the file it
|
|
58
|
+
* lives in, its export name, its `args:` validator text (for `v.id("<table>")`)
|
|
59
|
+
* and its handler body text (for the `ctx.db.*` op it performs). */
|
|
60
|
+
export interface Handler {
|
|
61
|
+
/** module basename, lower-cased, no `.ts` (e.g. `tasks`). */
|
|
62
|
+
fileBase: string;
|
|
63
|
+
/** the export name (e.g. `get`, `update`, `remove`). */
|
|
64
|
+
exportName: string;
|
|
65
|
+
/** the `args:` object literal source text (may be ""). */
|
|
66
|
+
argsText: string;
|
|
67
|
+
/** the `handler:` function source text (may be ""). */
|
|
68
|
+
body: string;
|
|
69
|
+
}
|
|
70
|
+
/** Every PUBLIC query/mutation exported from one module, with its attribution
|
|
71
|
+
* inputs. Non-exported / internal* builders are excluded. */
|
|
72
|
+
export declare function publicHandlers(name: string, text: string): Handler[];
|
|
73
|
+
export declare function scanCrudCoverage(schemaText: string | undefined, files: SourceFile[], routeFiles: string[] | RouteProbe): CrudCoverage;
|
|
74
|
+
export declare function coverageUnionOk(schemaTables: string[], rows: {
|
|
75
|
+
table: string;
|
|
76
|
+
}[]): {
|
|
77
|
+
ok: boolean;
|
|
78
|
+
missing: string[];
|
|
79
|
+
};
|
|
80
|
+
export declare function crudCoverageSection(cov: CrudCoverage): string;
|
|
81
|
+
export declare function crudCoverageLine(cov: CrudCoverage): string;
|