@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.
Files changed (108) hide show
  1. package/README.md +88 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +94 -0
  4. package/dist/contract.d.ts +18 -0
  5. package/dist/contract.js +29 -0
  6. package/dist/coverage-cli.d.ts +2 -0
  7. package/dist/coverage-cli.js +17 -0
  8. package/dist/coverage.d.ts +32 -0
  9. package/dist/coverage.js +77 -0
  10. package/dist/detectors/oracle-axis1.d.ts +86 -0
  11. package/dist/detectors/oracle-axis1.js +221 -0
  12. package/dist/detectors/oracle-axis2.d.ts +22 -0
  13. package/dist/detectors/oracle-axis2.js +149 -0
  14. package/dist/detectors/predicates.d.ts +19 -0
  15. package/dist/detectors/predicates.js +334 -0
  16. package/dist/detectors/shared.d.ts +56 -0
  17. package/dist/detectors/shared.js +103 -0
  18. package/dist/detectors/source/_scan.d.ts +43 -0
  19. package/dist/detectors/source/_scan.js +82 -0
  20. package/dist/detectors/source/backfill.d.ts +27 -0
  21. package/dist/detectors/source/backfill.js +211 -0
  22. package/dist/detectors/source/contract.d.ts +33 -0
  23. package/dist/detectors/source/contract.js +385 -0
  24. package/dist/detectors/source/identity.d.ts +47 -0
  25. package/dist/detectors/source/identity.js +410 -0
  26. package/dist/detectors/source/jobs.d.ts +10 -0
  27. package/dist/detectors/source/jobs.js +209 -0
  28. package/dist/detectors/source/lifecycle.d.ts +6 -0
  29. package/dist/detectors/source/lifecycle.js +147 -0
  30. package/dist/detectors/source/readbudget.d.ts +6 -0
  31. package/dist/detectors/source/readbudget.js +133 -0
  32. package/dist/detectors/source/refusal.d.ts +6 -0
  33. package/dist/detectors/source/refusal.js +125 -0
  34. package/dist/detectors/source/security.d.ts +37 -0
  35. package/dist/detectors/source/security.js +392 -0
  36. package/dist/detectors/source/writesafety.d.ts +14 -0
  37. package/dist/detectors/source/writesafety.js +345 -0
  38. package/dist/detectors/standard/_doc.d.ts +66 -0
  39. package/dist/detectors/standard/_doc.js +152 -0
  40. package/dist/detectors/standard/r41-frontier.d.ts +58 -0
  41. package/dist/detectors/standard/r41-frontier.js +176 -0
  42. package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
  43. package/dist/detectors/standard/r46-returns-validator.js +474 -0
  44. package/dist/detectors/standard/standard-doc.d.ts +12 -0
  45. package/dist/detectors/standard/standard-doc.js +271 -0
  46. package/dist/detectors/structural.d.ts +14 -0
  47. package/dist/detectors/structural.js +83 -0
  48. package/dist/engine-types.d.ts +10 -0
  49. package/dist/engine-types.js +1 -0
  50. package/dist/not-checkable.d.ts +22 -0
  51. package/dist/not-checkable.js +37 -0
  52. package/dist/oracle.d.ts +33 -0
  53. package/dist/oracle.js +133 -0
  54. package/dist/registry.d.ts +11 -0
  55. package/dist/registry.js +140 -0
  56. package/dist/report.d.ts +35 -0
  57. package/dist/report.js +138 -0
  58. package/dist/severity.d.ts +30 -0
  59. package/dist/severity.js +121 -0
  60. package/dist/tally.d.ts +57 -0
  61. package/dist/tally.js +152 -0
  62. package/dist/trigger.d.ts +47 -0
  63. package/dist/trigger.js +143 -0
  64. package/fixtures/backend-standard-51ec7af1.md +648 -0
  65. package/fixtures/backend-standard-a9d7877b.md +629 -0
  66. package/fixtures/mcp-app-standard-a74a042b.md +279 -0
  67. package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
  68. package/fixtures/mcp-server-standard-a74a042b.md +434 -0
  69. package/fixtures/mcp-standard-a74a042b.md +354 -0
  70. package/fixtures/vp-1200-green/.pin +1 -0
  71. package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
  72. package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
  73. package/fixtures/vp-1200-red/.pin +1 -0
  74. package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
  75. package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
  76. package/fixtures/vp-by-tool-day158.csv +190 -0
  77. package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
  78. package/fixtures/vp-convex/.pin +1 -0
  79. package/fixtures/vp-convex/bun.lock +783 -0
  80. package/fixtures/vp-convex/convex/auth.config.ts +8 -0
  81. package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
  82. package/fixtures/vp-convex/convex/convex.config.ts +14 -0
  83. package/fixtures/vp-convex/convex/credentials.ts +591 -0
  84. package/fixtures/vp-convex/convex/crons.ts +72 -0
  85. package/fixtures/vp-convex/convex/dashboard.ts +199 -0
  86. package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
  87. package/fixtures/vp-convex/convex/issues.ts +544 -0
  88. package/fixtures/vp-convex/convex/kb.ts +305 -0
  89. package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
  90. package/fixtures/vp-convex/convex/kbShared.ts +56 -0
  91. package/fixtures/vp-convex/convex/licenses.ts +443 -0
  92. package/fixtures/vp-convex/convex/memories.ts +406 -0
  93. package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
  94. package/fixtures/vp-convex/convex/messages.ts +971 -0
  95. package/fixtures/vp-convex/convex/migrations.ts +239 -0
  96. package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
  97. package/fixtures/vp-convex/convex/oauth.ts +1608 -0
  98. package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
  99. package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
  100. package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
  101. package/fixtures/vp-convex/convex/schema.ts +1229 -0
  102. package/fixtures/vp-convex/convex/tasks.ts +2782 -0
  103. package/fixtures/vp-r31-green/.pin +1 -0
  104. package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
  105. package/fixtures/vp-r31-red/.pin +1 -0
  106. package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
  107. package/fixtures/vr-by-tool-day159.csv +89 -0
  108. 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[];