@vantageos/backend-doctor 0.1.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 +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +94 -0
- package/dist/contract.d.ts +18 -0
- package/dist/contract.js +29 -0
- package/dist/coverage-cli.d.ts +2 -0
- package/dist/coverage-cli.js +17 -0
- package/dist/coverage.d.ts +32 -0
- package/dist/coverage.js +77 -0
- package/dist/detectors/oracle-axis1.d.ts +86 -0
- package/dist/detectors/oracle-axis1.js +221 -0
- package/dist/detectors/oracle-axis2.d.ts +22 -0
- package/dist/detectors/oracle-axis2.js +149 -0
- package/dist/detectors/predicates.d.ts +19 -0
- package/dist/detectors/predicates.js +334 -0
- package/dist/detectors/shared.d.ts +56 -0
- package/dist/detectors/shared.js +103 -0
- package/dist/detectors/source/_scan.d.ts +43 -0
- package/dist/detectors/source/_scan.js +82 -0
- package/dist/detectors/source/backfill.d.ts +27 -0
- package/dist/detectors/source/backfill.js +211 -0
- package/dist/detectors/source/contract.d.ts +33 -0
- package/dist/detectors/source/contract.js +385 -0
- package/dist/detectors/source/identity.d.ts +47 -0
- package/dist/detectors/source/identity.js +410 -0
- package/dist/detectors/source/jobs.d.ts +10 -0
- package/dist/detectors/source/jobs.js +209 -0
- package/dist/detectors/source/lifecycle.d.ts +6 -0
- package/dist/detectors/source/lifecycle.js +147 -0
- package/dist/detectors/source/readbudget.d.ts +6 -0
- package/dist/detectors/source/readbudget.js +133 -0
- package/dist/detectors/source/refusal.d.ts +6 -0
- package/dist/detectors/source/refusal.js +125 -0
- package/dist/detectors/source/security.d.ts +37 -0
- package/dist/detectors/source/security.js +392 -0
- package/dist/detectors/source/writesafety.d.ts +14 -0
- package/dist/detectors/source/writesafety.js +345 -0
- package/dist/detectors/standard/_doc.d.ts +66 -0
- package/dist/detectors/standard/_doc.js +152 -0
- package/dist/detectors/standard/r41-frontier.d.ts +58 -0
- package/dist/detectors/standard/r41-frontier.js +176 -0
- package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
- package/dist/detectors/standard/r46-returns-validator.js +474 -0
- package/dist/detectors/standard/standard-doc.d.ts +12 -0
- package/dist/detectors/standard/standard-doc.js +271 -0
- package/dist/detectors/structural.d.ts +14 -0
- package/dist/detectors/structural.js +83 -0
- package/dist/engine-types.d.ts +10 -0
- package/dist/engine-types.js +1 -0
- package/dist/not-checkable.d.ts +22 -0
- package/dist/not-checkable.js +37 -0
- package/dist/oracle.d.ts +33 -0
- package/dist/oracle.js +133 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +140 -0
- package/dist/report.d.ts +35 -0
- package/dist/report.js +138 -0
- package/dist/severity.d.ts +30 -0
- package/dist/severity.js +121 -0
- package/dist/tally.d.ts +57 -0
- package/dist/tally.js +152 -0
- package/dist/trigger.d.ts +47 -0
- package/dist/trigger.js +143 -0
- package/fixtures/backend-standard-51ec7af1.md +648 -0
- package/fixtures/backend-standard-a9d7877b.md +629 -0
- package/fixtures/mcp-app-standard-a74a042b.md +279 -0
- package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
- package/fixtures/mcp-server-standard-a74a042b.md +434 -0
- package/fixtures/mcp-standard-a74a042b.md +354 -0
- package/fixtures/vp-1200-green/.pin +1 -0
- package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
- package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
- package/fixtures/vp-1200-red/.pin +1 -0
- package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
- package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
- package/fixtures/vp-by-tool-day158.csv +190 -0
- package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
- package/fixtures/vp-convex/.pin +1 -0
- package/fixtures/vp-convex/bun.lock +783 -0
- package/fixtures/vp-convex/convex/auth.config.ts +8 -0
- package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
- package/fixtures/vp-convex/convex/convex.config.ts +14 -0
- package/fixtures/vp-convex/convex/credentials.ts +591 -0
- package/fixtures/vp-convex/convex/crons.ts +72 -0
- package/fixtures/vp-convex/convex/dashboard.ts +199 -0
- package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
- package/fixtures/vp-convex/convex/issues.ts +544 -0
- package/fixtures/vp-convex/convex/kb.ts +305 -0
- package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
- package/fixtures/vp-convex/convex/kbShared.ts +56 -0
- package/fixtures/vp-convex/convex/licenses.ts +443 -0
- package/fixtures/vp-convex/convex/memories.ts +406 -0
- package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
- package/fixtures/vp-convex/convex/messages.ts +971 -0
- package/fixtures/vp-convex/convex/migrations.ts +239 -0
- package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
- package/fixtures/vp-convex/convex/oauth.ts +1608 -0
- package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
- package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
- package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
- package/fixtures/vp-convex/convex/schema.ts +1229 -0
- package/fixtures/vp-convex/convex/tasks.ts +2782 -0
- package/fixtures/vp-r31-green/.pin +1 -0
- package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
- package/fixtures/vp-r31-red/.pin +1 -0
- package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
- package/fixtures/vr-by-tool-day159.csv +89 -0
- package/package.json +39 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// R-46 — a read path's returns validator is DERIVED from its table's schema,
|
|
2
|
+
// never a hand-written restatement. §16 line 445 (backend standard @ a9d7877b):
|
|
3
|
+
// "the returns validator's field set, derived from `schema.ts`, equals the
|
|
4
|
+
// schema's writable field set, or a shortfall carries a written projection
|
|
5
|
+
// reason (red: a schema field writable by some path, absent from the returns
|
|
6
|
+
// validator, with no projection reason; or a validator authored as a literal
|
|
7
|
+
// list rather than derived)."
|
|
8
|
+
//
|
|
9
|
+
// THE CLASS THE FLEET PAID FOR (2026-08-19 get_task P0, origin e9a6a7e): a
|
|
10
|
+
// Convex `returns:` validator that enumerates a stored document (declares
|
|
11
|
+
// `_id: v.id("<table>")`) but OMITS a field the table's schema.ts declares. Any
|
|
12
|
+
// row carrying that field throws ReturnsValidationError at runtime — the row is
|
|
13
|
+
// unreadable through that path while still writable and still readable through
|
|
14
|
+
// others. Nothing in the platform compares a returns validator to the schema it
|
|
15
|
+
// claims to describe; THIS detector is that comparison.
|
|
16
|
+
//
|
|
17
|
+
// This is the LIFT of vantage-peers' returnsValidatorSchemaCoverage.test.ts out
|
|
18
|
+
// of one repository into the instrument, so R-46 is measured on EVERY backend
|
|
19
|
+
// the doctor is pointed at, not rediscovered per repo at production cost. The
|
|
20
|
+
// four traps this lift refuses (each one the fleet already paid for):
|
|
21
|
+
//
|
|
22
|
+
// 1. The field set is DERIVED from the TARGET backend's schema.ts at analysis
|
|
23
|
+
// time — never a list this detector carries. A carried list is the
|
|
24
|
+
// hand-written restatement R-46 itself forbids, one layer up; it rots the
|
|
25
|
+
// first time a field is added. deriveSchemaFields re-reads the target's
|
|
26
|
+
// schema every run.
|
|
27
|
+
// 2. Every document-returning validator in scope is ANALYSED (COVERS) or
|
|
28
|
+
// SKIPPED-with-a-written-reason (a `returns-projection:` marker IN THE
|
|
29
|
+
// TARGET SOURCE) — no third state. What is neither analysed nor
|
|
30
|
+
// exempted-in-writing FAILS. The exemption is read from the target, not an
|
|
31
|
+
// allowlist the detector keeps about itself (that allowlist is the very
|
|
32
|
+
// thing that does not travel to a second backend).
|
|
33
|
+
// 3. A file that cannot be parsed is a FINDING (could-not-judge naming it),
|
|
34
|
+
// never a silent pass — "did not look" and "found nothing" must not both
|
|
35
|
+
// return green.
|
|
36
|
+
// 4. The engine's existing four-state exit contract is honoured
|
|
37
|
+
// (non-conforming / could-not-judge / present / absent) — no fifth state,
|
|
38
|
+
// no second mechanism: the detector reads the tree the engine hands it.
|
|
39
|
+
import * as ts from "typescript";
|
|
40
|
+
/** The standard this detector is pinned against (the re-pinned instrument pin). */
|
|
41
|
+
export const R46_STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp) §16 R-46";
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// tree → text. The engine split each file on "\n"; join reproduces it exactly.
|
|
44
|
+
// We parse the TARGET the engine loaded — never re-read the filesystem (that is
|
|
45
|
+
// the second-mechanism trap). Reconstructing from tree.files keeps R-46 on the
|
|
46
|
+
// engine contract, and lets the mutation probe inject a synthetic tree.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export function fileText(f) {
|
|
49
|
+
return f.lines.map((l) => l.raw).join("\n");
|
|
50
|
+
}
|
|
51
|
+
function parseText(name, text) {
|
|
52
|
+
return ts.createSourceFile(name, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
53
|
+
}
|
|
54
|
+
/** basename of a `rel` path, lower-cased. */
|
|
55
|
+
function base(rel) {
|
|
56
|
+
return (rel.split("/").pop() ?? rel).toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
/** a file we should judge — never a test or generated file. (The engine already
|
|
59
|
+
* drops *.test.ts; this also drops _generated and non-.ts.) */
|
|
60
|
+
function isJudgeable(f) {
|
|
61
|
+
const b = base(f.rel);
|
|
62
|
+
return (b.endsWith(".ts") &&
|
|
63
|
+
!b.endsWith(".test.ts") &&
|
|
64
|
+
!f.rel.includes("__tests__") &&
|
|
65
|
+
!f.rel.includes("_generated/"));
|
|
66
|
+
}
|
|
67
|
+
/** the schema-definition file of the target backend, if present. */
|
|
68
|
+
function isSchemaFile(f) {
|
|
69
|
+
return /(^|\/)schema\.ts$/.test(f.rel) && isJudgeable(f);
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// 1. Derive { tableName -> Set<fieldName> } from the target's schema.ts by
|
|
73
|
+
// parsing defineSchema({ ...defineTable({...}) }) with the TS compiler API.
|
|
74
|
+
// Re-read every run — add a field to schema.ts and this picks it up with zero
|
|
75
|
+
// edits here.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
function objectLiteralKeys(obj) {
|
|
78
|
+
const keys = [];
|
|
79
|
+
for (const prop of obj.properties) {
|
|
80
|
+
if (ts.isPropertyAssignment(prop) ||
|
|
81
|
+
ts.isShorthandPropertyAssignment(prop)) {
|
|
82
|
+
const name = prop.name;
|
|
83
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name))
|
|
84
|
+
keys.push(name.text);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return keys;
|
|
88
|
+
}
|
|
89
|
+
export function deriveSchemaFields(schemaText) {
|
|
90
|
+
const sf = parseText("schema.ts", schemaText);
|
|
91
|
+
const result = new Map();
|
|
92
|
+
function visit(node) {
|
|
93
|
+
if (ts.isCallExpression(node) &&
|
|
94
|
+
ts.isIdentifier(node.expression) &&
|
|
95
|
+
node.expression.text === "defineSchema") {
|
|
96
|
+
const arg = node.arguments[0];
|
|
97
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
98
|
+
for (const prop of arg.properties) {
|
|
99
|
+
if (!ts.isPropertyAssignment(prop))
|
|
100
|
+
continue;
|
|
101
|
+
const nameNode = prop.name;
|
|
102
|
+
const tableName = ts.isIdentifier(nameNode)
|
|
103
|
+
? nameNode.text
|
|
104
|
+
: ts.isStringLiteral(nameNode)
|
|
105
|
+
? nameNode.text
|
|
106
|
+
: undefined;
|
|
107
|
+
if (!tableName)
|
|
108
|
+
continue;
|
|
109
|
+
// value is a `defineTable({...}).index(...).index(...)` chain — walk
|
|
110
|
+
// down to the defineTable(...) call.
|
|
111
|
+
let expr = prop.initializer;
|
|
112
|
+
let defineTableCall;
|
|
113
|
+
while (true) {
|
|
114
|
+
if (ts.isCallExpression(expr) &&
|
|
115
|
+
ts.isIdentifier(expr.expression) &&
|
|
116
|
+
expr.expression.text === "defineTable") {
|
|
117
|
+
defineTableCall = expr;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (ts.isCallExpression(expr) &&
|
|
121
|
+
ts.isPropertyAccessExpression(expr.expression)) {
|
|
122
|
+
expr = expr.expression.expression;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (defineTableCall) {
|
|
128
|
+
const shape = defineTableCall.arguments[0];
|
|
129
|
+
if (shape && ts.isObjectLiteralExpression(shape)) {
|
|
130
|
+
result.set(tableName, new Set(objectLiteralKeys(shape)));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
ts.forEachChild(node, visit);
|
|
137
|
+
}
|
|
138
|
+
visit(sf);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
const QUERY_LIKE = new Set([
|
|
142
|
+
"query",
|
|
143
|
+
"mutation",
|
|
144
|
+
"internalQuery",
|
|
145
|
+
"internalMutation",
|
|
146
|
+
]);
|
|
147
|
+
/** Resolve `v.id("table")` -> "table" if node matches that shape. */
|
|
148
|
+
function idTableName(node) {
|
|
149
|
+
if (ts.isCallExpression(node) &&
|
|
150
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
151
|
+
node.expression.name.text === "id") {
|
|
152
|
+
const arg = node.arguments[0];
|
|
153
|
+
if (arg && ts.isStringLiteral(arg))
|
|
154
|
+
return arg.text;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
export function extractDocMatches(name, text) {
|
|
159
|
+
const sf = parseText(name, text);
|
|
160
|
+
const matches = [];
|
|
161
|
+
const lineOf = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
162
|
+
const constMap = new Map();
|
|
163
|
+
function collectConsts(node) {
|
|
164
|
+
if (ts.isVariableStatement(node)) {
|
|
165
|
+
for (const decl of node.declarationList.declarations) {
|
|
166
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
167
|
+
constMap.set(decl.name.text, decl.initializer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
ts.forEachChild(node, collectConsts);
|
|
172
|
+
}
|
|
173
|
+
collectConsts(sf);
|
|
174
|
+
function resolve(expr, depth = 0) {
|
|
175
|
+
if (depth > 10)
|
|
176
|
+
return expr;
|
|
177
|
+
if (ts.isIdentifier(expr) && constMap.has(expr.text)) {
|
|
178
|
+
return resolve(constMap.get(expr.text), depth + 1);
|
|
179
|
+
}
|
|
180
|
+
return expr;
|
|
181
|
+
}
|
|
182
|
+
function walkForDocObjects(expr, out, exportName, alias, span, depth = 0) {
|
|
183
|
+
if (depth > 20)
|
|
184
|
+
return;
|
|
185
|
+
const nextAlias = ts.isIdentifier(expr) ? expr.text : alias;
|
|
186
|
+
const resolved = resolve(expr);
|
|
187
|
+
if (ts.isCallExpression(resolved) &&
|
|
188
|
+
ts.isPropertyAccessExpression(resolved.expression)) {
|
|
189
|
+
const method = resolved.expression.name.text;
|
|
190
|
+
if (method === "object") {
|
|
191
|
+
const objArg = resolved.arguments[0];
|
|
192
|
+
if (objArg && ts.isObjectLiteralExpression(objArg)) {
|
|
193
|
+
let table;
|
|
194
|
+
for (const prop of objArg.properties) {
|
|
195
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
196
|
+
ts.isIdentifier(prop.name) &&
|
|
197
|
+
prop.name.text === "_id") {
|
|
198
|
+
table = idTableName(resolve(prop.initializer));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (table) {
|
|
202
|
+
out.push({
|
|
203
|
+
exportName,
|
|
204
|
+
alias: nextAlias,
|
|
205
|
+
table,
|
|
206
|
+
declaredKeys: new Set(objectLiteralKeys(objArg)),
|
|
207
|
+
line: lineOf(objArg.getStart(sf)),
|
|
208
|
+
exportStartLine: span.start,
|
|
209
|
+
exportEndLine: span.end,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
for (const prop of objArg.properties) {
|
|
213
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
214
|
+
walkForDocObjects(prop.initializer, out, exportName, undefined, span, depth + 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (method === "union" || method === "array" || method === "optional") {
|
|
221
|
+
for (const arg of resolved.arguments) {
|
|
222
|
+
walkForDocObjects(arg, out, exportName, nextAlias, span, depth + 1);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function visit(node) {
|
|
229
|
+
if (ts.isVariableStatement(node) &&
|
|
230
|
+
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
231
|
+
const span = {
|
|
232
|
+
start: lineOf(node.getStart(sf)),
|
|
233
|
+
end: lineOf(node.getEnd()),
|
|
234
|
+
};
|
|
235
|
+
for (const decl of node.declarationList.declarations) {
|
|
236
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer)
|
|
237
|
+
continue;
|
|
238
|
+
const exportName = decl.name.text;
|
|
239
|
+
const init = decl.initializer;
|
|
240
|
+
if (ts.isCallExpression(init) &&
|
|
241
|
+
ts.isIdentifier(init.expression) &&
|
|
242
|
+
QUERY_LIKE.has(init.expression.text)) {
|
|
243
|
+
const configArg = init.arguments[0];
|
|
244
|
+
if (configArg && ts.isObjectLiteralExpression(configArg)) {
|
|
245
|
+
for (const prop of configArg.properties) {
|
|
246
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
247
|
+
ts.isIdentifier(prop.name) &&
|
|
248
|
+
prop.name.text === "returns") {
|
|
249
|
+
walkForDocObjects(prop.initializer, matches, exportName, undefined, span);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
ts.forEachChild(node, visit);
|
|
257
|
+
}
|
|
258
|
+
visit(sf);
|
|
259
|
+
return matches;
|
|
260
|
+
}
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// 3. The projection exemption — read from the TARGET SOURCE, never an allowlist
|
|
263
|
+
// the detector carries. A shortfall is SKIPPED-with-a-written-reason iff a
|
|
264
|
+
// `returns-projection: <reason>` marker appears within the enclosing export's
|
|
265
|
+
// line span (a comment the author wrote, verified against the handler). The
|
|
266
|
+
// reason must be substantive (a bare marker is not a written reason).
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
const PROJECTION_MARKER = /returns-projection\s*:\s*(\S.*\S|\S)/i;
|
|
269
|
+
const MIN_REASON_LEN = 12;
|
|
270
|
+
export function projectionReason(rawLines, startLine, endLine) {
|
|
271
|
+
for (let n = startLine; n <= endLine && n <= rawLines.length; n++) {
|
|
272
|
+
const m = PROJECTION_MARKER.exec(rawLines[n - 1] ?? "");
|
|
273
|
+
if (m && m[1].trim().length >= MIN_REASON_LEN)
|
|
274
|
+
return m[1].trim();
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
/** Classify every document-returning validator in the target tree against the
|
|
279
|
+
* schema the target itself declares. */
|
|
280
|
+
export function scanR46(files) {
|
|
281
|
+
const inventory = [];
|
|
282
|
+
const findings = [];
|
|
283
|
+
// Derive the schema field sets from the target's schema.ts (may be several,
|
|
284
|
+
// merged). A tree with no schema.ts yields no tables → schemaFound false.
|
|
285
|
+
const schemaFields = new Map();
|
|
286
|
+
let schemaFound = false;
|
|
287
|
+
for (const f of files) {
|
|
288
|
+
if (!isSchemaFile(f))
|
|
289
|
+
continue;
|
|
290
|
+
try {
|
|
291
|
+
const derived = deriveSchemaFields(fileText(f));
|
|
292
|
+
for (const [table, fset] of derived)
|
|
293
|
+
schemaFields.set(table, fset);
|
|
294
|
+
if (derived.size > 0)
|
|
295
|
+
schemaFound = true;
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
findings.push({
|
|
299
|
+
rel: f.rel,
|
|
300
|
+
line: 1,
|
|
301
|
+
note: `schema.ts could not be parsed (${e.message}) — the field set cannot be derived; NOT a silent pass`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const f of files) {
|
|
306
|
+
if (!isJudgeable(f) || isSchemaFile(f))
|
|
307
|
+
continue;
|
|
308
|
+
const moduleName = base(f.rel).replace(/\.ts$/, "");
|
|
309
|
+
let matches;
|
|
310
|
+
try {
|
|
311
|
+
matches = extractDocMatches(f.rel, fileText(f));
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
findings.push({
|
|
315
|
+
rel: f.rel,
|
|
316
|
+
line: 1,
|
|
317
|
+
note: `module could not be parsed (${e.message}) — a validator here may go unchecked; NOT a silent pass`,
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const rawLines = f.lines.map((l) => l.raw);
|
|
322
|
+
for (const m of matches) {
|
|
323
|
+
const label = m.alias
|
|
324
|
+
? `${moduleName}.${m.exportName}#${m.alias}`
|
|
325
|
+
: `${moduleName}.${m.exportName}`;
|
|
326
|
+
const declared = schemaFields.get(m.table);
|
|
327
|
+
if (!declared) {
|
|
328
|
+
// A validator declares v.id("<table>") for a table the schema does not
|
|
329
|
+
// define — cannot judge coverage; a finding, never a silent pass.
|
|
330
|
+
findings.push({
|
|
331
|
+
rel: f.rel,
|
|
332
|
+
line: m.line,
|
|
333
|
+
note: `${label} returns a document of "${m.table}", a table not found in the target schema — coverage cannot be derived`,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const missing = [...declared].filter((k) => !m.declaredKeys.has(k));
|
|
338
|
+
if (missing.length === 0) {
|
|
339
|
+
inventory.push({
|
|
340
|
+
rel: f.rel,
|
|
341
|
+
line: m.line,
|
|
342
|
+
label,
|
|
343
|
+
table: m.table,
|
|
344
|
+
status: "COVERS",
|
|
345
|
+
missing: [],
|
|
346
|
+
});
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// include the contiguous leading-comment block above the export — the
|
|
350
|
+
// natural place a `returns-projection:` reason is written for the read path.
|
|
351
|
+
let leadStart = m.exportStartLine;
|
|
352
|
+
while (leadStart > 1) {
|
|
353
|
+
const prev = (rawLines[leadStart - 2] ?? "").trim();
|
|
354
|
+
if (prev.startsWith("//") ||
|
|
355
|
+
prev.startsWith("*") ||
|
|
356
|
+
prev.startsWith("/*")) {
|
|
357
|
+
leadStart -= 1;
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
const reason = projectionReason(rawLines, leadStart, m.exportEndLine);
|
|
363
|
+
if (reason) {
|
|
364
|
+
inventory.push({
|
|
365
|
+
rel: f.rel,
|
|
366
|
+
line: m.line,
|
|
367
|
+
label,
|
|
368
|
+
table: m.table,
|
|
369
|
+
status: "PROJECTION",
|
|
370
|
+
missing,
|
|
371
|
+
reason,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
inventory.push({
|
|
376
|
+
rel: f.rel,
|
|
377
|
+
line: m.line,
|
|
378
|
+
label,
|
|
379
|
+
table: m.table,
|
|
380
|
+
status: "MISSING",
|
|
381
|
+
missing,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { inventory, findings, schemaFound };
|
|
387
|
+
}
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// decide — the four-state contract, with the COVERAGE INVENTORY rendered.
|
|
390
|
+
// non-conforming a MISSING validator (a schema field absent, no written
|
|
391
|
+
// projection reason) — the live R-46 defect class.
|
|
392
|
+
// could-not-judge a parse/schema finding, OR no schema found, OR zero
|
|
393
|
+
// validators (an instrument that matched nothing has NOT
|
|
394
|
+
// certified a clean bill).
|
|
395
|
+
// present every validator ANALYSED (COVERS) or SKIPPED (PROJECTION).
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
const R46_ID = "R-46 returns validator derived from the schema, never restated";
|
|
398
|
+
const R46_CHECK = "every document-returning `returns:` validator declares the schema's field set for its table, or a shortfall carries a written `returns-projection:` reason";
|
|
399
|
+
function renderInventory(cov) {
|
|
400
|
+
const rows = cov.inventory.map((e) => {
|
|
401
|
+
if (e.status === "COVERS")
|
|
402
|
+
return ` ANALYSED COVERS ${e.label} -> ${e.table} @ ${e.rel}:${e.line}`;
|
|
403
|
+
if (e.status === "PROJECTION")
|
|
404
|
+
return ` SKIPPED PROJECTION ${e.label} -> ${e.table} @ ${e.rel}:${e.line} (omits ${e.missing.join(", ")}) reason: ${e.reason}`;
|
|
405
|
+
return ` ANALYSED MISSING ${e.label} -> ${e.table} @ ${e.rel}:${e.line} missing: ${e.missing.join(", ")}`;
|
|
406
|
+
});
|
|
407
|
+
const finds = cov.findings.map((x) => ` FINDING ${x.note} @ ${x.rel}:${x.line}`);
|
|
408
|
+
return [...rows, ...finds].join("\n");
|
|
409
|
+
}
|
|
410
|
+
export function decideR46(cov) {
|
|
411
|
+
const missing = cov.inventory.filter((e) => e.status === "MISSING");
|
|
412
|
+
const covers = cov.inventory.filter((e) => e.status === "COVERS");
|
|
413
|
+
const projections = cov.inventory.filter((e) => e.status === "PROJECTION");
|
|
414
|
+
const breakdown = `[analysed=${covers.length} projection=${projections.length} missing=${missing.length} findings=${cov.findings.length}]`;
|
|
415
|
+
const inventory = renderInventory(cov);
|
|
416
|
+
if (!cov.schemaFound) {
|
|
417
|
+
const reason = `R-46: no parseable schema.ts with a defineTable in the target tree — the field set cannot be DERIVED (${R46_CHECK}). NOT a clean bill: "did not look" is not "found nothing". ${breakdown}${cov.findings.length ? `\n${inventory}` : ""}`;
|
|
418
|
+
return {
|
|
419
|
+
id: R46_ID,
|
|
420
|
+
anchor: "target:no-schema",
|
|
421
|
+
state: "could-not-judge",
|
|
422
|
+
verdict: reason,
|
|
423
|
+
reason,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (missing.length > 0) {
|
|
427
|
+
const first = missing[0];
|
|
428
|
+
return {
|
|
429
|
+
id: R46_ID,
|
|
430
|
+
anchor: `${first.rel}:${first.line}`,
|
|
431
|
+
state: "non-conforming",
|
|
432
|
+
verdict: `R-46: ${missing.length} returns validator(s) enumerate a stored document but OMIT a schema-declared field with NO written projection reason (${R46_CHECK}). ${breakdown}\n=== coverage inventory ===\n${inventory}\n=== end inventory ===`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (cov.findings.length > 0) {
|
|
436
|
+
const first = cov.findings[0];
|
|
437
|
+
const reason = `R-46: static half clean (${covers.length} COVERS, ${projections.length} written projections, 0 MISSING) but ${cov.findings.length} file/validator could not be judged — a finding is never a silent pass. ${breakdown}\n=== coverage inventory ===\n${inventory}\n=== end inventory ===`;
|
|
438
|
+
return {
|
|
439
|
+
id: R46_ID,
|
|
440
|
+
anchor: `${first.rel}:${first.line}`,
|
|
441
|
+
state: "could-not-judge",
|
|
442
|
+
verdict: reason,
|
|
443
|
+
reason,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (cov.inventory.length === 0) {
|
|
447
|
+
const reason = `R-46: a schema was found but ZERO document-returning validators were matched in the target tree — the check would pass vacuously. NOT a clean bill. ${breakdown}`;
|
|
448
|
+
return {
|
|
449
|
+
id: R46_ID,
|
|
450
|
+
anchor: "target:no-validator",
|
|
451
|
+
state: "could-not-judge",
|
|
452
|
+
verdict: reason,
|
|
453
|
+
reason,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const first = covers[0] ?? projections[0];
|
|
457
|
+
return {
|
|
458
|
+
id: R46_ID,
|
|
459
|
+
anchor: `${first.rel}:${first.line}`,
|
|
460
|
+
state: "present",
|
|
461
|
+
verdict: `R-46: all ${cov.inventory.length} document-returning validator(s) ANALYSED — ${covers.length} COVER the schema, ${projections.length} are SKIPPED as written projections; 0 MISSING (${R46_CHECK}). ${breakdown}\n=== coverage inventory ===\n${inventory}\n=== end inventory ===`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/** The R-46 detector — reads the TARGET tree the engine hands it (never the
|
|
465
|
+
* vendored standard, never the filesystem directly), derives the field set from
|
|
466
|
+
* that target's schema, and classifies every document-returning validator. */
|
|
467
|
+
export const R46 = {
|
|
468
|
+
id: R46_ID,
|
|
469
|
+
run: (tree) => decideR46(scanR46(tree.files)),
|
|
470
|
+
};
|
|
471
|
+
/** In a form the orchestrator spreads into REGISTRY. */
|
|
472
|
+
export const STANDARD_SOURCE_DETECTORS = [
|
|
473
|
+
{ detector: R46, ruleId: "R-46", standardPin: R46_STANDARD_PIN },
|
|
474
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Detector } from "../../engine-types.js";
|
|
2
|
+
import { type DocLine, type DocScan, type StandardDocEntry } from "./_doc.js";
|
|
3
|
+
export declare function scanR42(lines: DocLine[]): DocScan;
|
|
4
|
+
export declare function scanR15(lines: DocLine[]): DocScan;
|
|
5
|
+
export declare function scanR32(lines: DocLine[]): DocScan;
|
|
6
|
+
export declare function scanR39(lines: DocLine[]): DocScan;
|
|
7
|
+
export declare const R42: Detector;
|
|
8
|
+
export declare const R15: Detector;
|
|
9
|
+
export declare const R32: Detector;
|
|
10
|
+
export declare const R39: Detector;
|
|
11
|
+
/** The four, in a form the orchestrator spreads into REGISTRY. */
|
|
12
|
+
export declare const STANDARD_DOC_DETECTORS: StandardDocEntry[];
|