@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,271 @@
1
+ import { docDetector, emptyDocScan, isDocPlaceholder, STANDARD_DOC_PIN, sectionLines, tableRows, } from "./_doc.js";
2
+ // ---- shared doc helpers ---------------------------------------------------
3
+ /** Every rule id the standard DEFINES — a `**R-NN` rule-definition header. This
4
+ * is the resolution target: a rule cited elsewhere "resolves" iff it is here. */
5
+ function definedRuleIds(lines) {
6
+ const ids = new Set();
7
+ for (const l of lines) {
8
+ const m = /^\*\*(R-\d+)\b/.exec(l.raw);
9
+ if (m)
10
+ ids.add(m[1]);
11
+ }
12
+ return ids;
13
+ }
14
+ /** The first line matching a regex, or undefined. */
15
+ function findLine(lines, re) {
16
+ return lines.find((l) => re.test(l.raw));
17
+ }
18
+ // ===========================================================================
19
+ // R-42 (process; static + delivery gate) — §18 line 431:
20
+ // "each rule's §14 source cell names a source from the KNOWN SET, not merely
21
+ // non-empty (static); no raised subject left un-routed (delivery gate)."
22
+ //
23
+ // KNOWN SET (derived from the §14 table's own spellings): the two audits
24
+ // (`audit`, `audit — Finding …`, `audit re-read`), established practice
25
+ // (`practice …`), the platform (`platform`), the conformance ruling
26
+ // (`self — the conformance ruling …`), and a named production incident
27
+ // (`incident …`) — the provenance the merged standard added at R-44/R-45 (the
28
+ // PR #1200 briefingNoteParticipants incident), on the same footing as an audit
29
+ // finding: a concrete, cited defect the rule was written to stop.
30
+ //
31
+ // STATIC HALF: parse §14; flag a source cell that is empty, a placeholder, or
32
+ // outside the known set. Parse §9b's routing table; flag a raised subject with
33
+ // an empty "Routed to" cell (a subject left un-routed IN the document).
34
+ // GATE HALF: a subject raised in review (D5 delivery gate) that ends neither a
35
+ // numbered rule nor a written §10 exclusion — owed at D5's delivery surface,
36
+ // not readable from the static document → one cnj site.
37
+ // ===========================================================================
38
+ const KNOWN_SOURCE = /^(audit\b|practice\b|platform\b|incident\b|self\s+—\s+the conformance ruling\b)/i;
39
+ export function scanR42(lines) {
40
+ const scan = emptyDocScan();
41
+ const s14 = sectionLines(lines, "14");
42
+ for (const row of tableRows(s14)) {
43
+ if (!/^R-\d+$/.test(row.cells[0] ?? ""))
44
+ continue;
45
+ const ruleId = row.cells[0];
46
+ const source = row.cells[1] ?? "";
47
+ if (isDocPlaceholder(source)) {
48
+ scan.violate.push({
49
+ n: row.n,
50
+ note: `${ruleId} §14 source cell is empty/placeholder ("${source}") — a rule left un-sourced`,
51
+ });
52
+ }
53
+ else if (!KNOWN_SOURCE.test(source)) {
54
+ scan.violate.push({
55
+ n: row.n,
56
+ note: `${ruleId} §14 source "${source}" is not in the known set {the two audits, established practice, the platform, a named incident, the conformance ruling}`,
57
+ });
58
+ }
59
+ else {
60
+ scan.conform.push({
61
+ n: row.n,
62
+ note: `${ruleId} sourced from the known set ("${source}")`,
63
+ });
64
+ }
65
+ }
66
+ // §9b routing table — no raised subject left un-routed, in the document.
67
+ const s9 = sectionLines(lines, "9");
68
+ const i9b = s9.findIndex((l) => /^###\s+9b\b/.test(l.raw));
69
+ const s9b = i9b < 0 ? [] : s9.slice(i9b);
70
+ for (const row of tableRows(s9b)) {
71
+ if (row.cells.length < 4)
72
+ continue;
73
+ if (row.cells[0] === "#")
74
+ continue; // header
75
+ const subject = row.cells[1] ?? "";
76
+ const routed = row.cells[3] ?? "";
77
+ if (isDocPlaceholder(routed)) {
78
+ scan.violate.push({
79
+ n: row.n,
80
+ note: `§9b subject "${subject}" has an empty "Routed to" cell — a raised subject left un-routed`,
81
+ });
82
+ }
83
+ else {
84
+ scan.conform.push({
85
+ n: row.n,
86
+ note: `§9b "${subject}" routed to "${routed}"`,
87
+ });
88
+ }
89
+ }
90
+ // GATE HALF — the never-silence obligation over FUTURE raised subjects.
91
+ const gateAnchor = findLine(lines, /^##\s+14\./) ?? { n: 335 };
92
+ scan.cnj.push({
93
+ n: gateAnchor.n,
94
+ note: "delivery gate (D5): a subject raised in review that ends neither a numbered rule nor a written §10 exclusion blocks acceptance — the never-silence half runs at D5's delivery surface, not readable from the static document",
95
+ });
96
+ return scan;
97
+ }
98
+ // ===========================================================================
99
+ // R-15 (process; delivery gate) — §18 line 404: "a new term/subject reconciled
100
+ // before acceptance." The §3 four shared names (operation, retrieval,
101
+ // pagination, authority) are its STANDING PRODUCT: each must end reconciled as a
102
+ // numbered rule. STATIC HALF: parse the §3 header's four names and confirm each
103
+ // binds to a `**R-NN — <name>` rule definition inside §3. GATE HALF: a NEW audit
104
+ // term / practice subject reconciled before acceptance runs at D5's delivery
105
+ // surface (a new audit being ingested) → one cnj site.
106
+ // ===========================================================================
107
+ export function scanR15(lines) {
108
+ const scan = emptyDocScan();
109
+ const s3 = sectionLines(lines, "3");
110
+ const header = findLine(s3, /^##\s+3\./);
111
+ // The four shared names live after the em-dash in the §3 header.
112
+ const namePart = header ? header.raw.split("—").slice(1).join("—") : "";
113
+ const names = namePart
114
+ .split(",")
115
+ .map((x) => x.trim().toLowerCase())
116
+ .filter((x) => x.length > 0);
117
+ for (const name of names) {
118
+ // authority is split into reader/writer bindings — a match on either half.
119
+ const bind = s3.find((l) => new RegExp(`\\*\\*R-\\d+ — [^*]*\\b${name}\\b`, "i").test(l.raw));
120
+ if (bind) {
121
+ scan.conform.push({
122
+ n: bind.n,
123
+ note: `shared name "${name}" reconciled to a numbered rule (${/\*\*(R-\d+)/.exec(bind.raw)?.[1] ?? "R-?"})`,
124
+ });
125
+ }
126
+ else {
127
+ scan.violate.push({
128
+ n: header?.n ?? 51,
129
+ note: `§3 shared name "${name}" has no reconciling **R-NN — ${name}** rule binding — a standing term left unreconciled`,
130
+ });
131
+ }
132
+ }
133
+ if (names.length === 0) {
134
+ scan.violate.push({
135
+ n: header?.n ?? 51,
136
+ note: "§3 header names no shared vocabulary — the standing product is absent",
137
+ });
138
+ }
139
+ // GATE HALF — a NEW term/subject reconciled before acceptance, at D5.
140
+ const gateAnchor = findLine(lines, /^\*\*R-15 \(process\)/) ?? { n: 278 };
141
+ scan.cnj.push({
142
+ n: gateAnchor.n,
143
+ note: "delivery gate (D5): a new audit term / practice subject must be reconciled to a numbered rule or a written §10 exclusion before the audit is accepted — the 'before acceptance' gate runs at D5's delivery surface",
144
+ });
145
+ return scan;
146
+ }
147
+ // ===========================================================================
148
+ // R-32 (process; delivery gate) — §18 line 421: "a limit-governed rule's change
149
+ // carries a real-deployment proof, not only convex-test." The limit-governed
150
+ // rules are named in the R-32 prose (line 270): R-3, R-4, R-11, R-23, R-30,
151
+ // R-31. STATIC HALF: parse that set from the prose, assert it equals the
152
+ // expected set, and confirm each named rule resolves to a real numbered rule.
153
+ // GATE HALF: the change-carries-real-deployment-proof check runs at D5 — surface
154
+ // the limit-governed rule set as the sites the gate applies to (one cnj each).
155
+ // ===========================================================================
156
+ const R32_EXPECTED = ["R-3", "R-4", "R-11", "R-23", "R-30", "R-31"];
157
+ export function scanR32(lines) {
158
+ const scan = emptyDocScan();
159
+ const prose = findLine(lines, /^\*\*R-32 \(process\)/);
160
+ const defined = definedRuleIds(lines);
161
+ if (!prose) {
162
+ scan.violate.push({
163
+ n: 270,
164
+ note: "R-32 prose not found — the limit-governed rule set cannot be read from the document",
165
+ });
166
+ return scan;
167
+ }
168
+ // the rule ids the prose names, excluding R-32 itself.
169
+ const named = [...prose.raw.matchAll(/R-\d+/g)]
170
+ .map((m) => m[0])
171
+ .filter((id) => id !== "R-32");
172
+ const uniq = [...new Set(named)];
173
+ const set = new Set(uniq);
174
+ const expected = new Set(R32_EXPECTED);
175
+ const drift = uniq.length !== R32_EXPECTED.length ||
176
+ R32_EXPECTED.some((id) => !set.has(id)) ||
177
+ uniq.some((id) => !expected.has(id));
178
+ if (drift) {
179
+ scan.violate.push({
180
+ n: prose.n,
181
+ note: `R-32 limit-governed set drifted — reads {${uniq.join(", ")}}, expected {${R32_EXPECTED.join(", ")}}`,
182
+ });
183
+ }
184
+ for (const id of uniq) {
185
+ if (!defined.has(id)) {
186
+ scan.violate.push({
187
+ n: prose.n,
188
+ note: `R-32 names limit-governed rule ${id}, which resolves to no numbered rule in the document`,
189
+ });
190
+ }
191
+ else {
192
+ const def = findLine(lines, new RegExp(`^\\*\\*${id}\\b`)) ?? {
193
+ n: prose.n,
194
+ };
195
+ scan.conform.push({
196
+ n: def.n,
197
+ note: `limit-governed rule ${id} named by R-32 resolves to a real numbered rule`,
198
+ });
199
+ }
200
+ }
201
+ // GATE HALF — the real-deployment-proof gate, per limit-governed rule, at D5.
202
+ for (const id of R32_EXPECTED) {
203
+ if (!set.has(id))
204
+ continue;
205
+ const def = findLine(lines, new RegExp(`^\\*\\*${id}\\b`)) ?? {
206
+ n: prose.n,
207
+ };
208
+ scan.cnj.push({
209
+ n: def.n,
210
+ note: `delivery gate (D5): a change to ${id} must carry a real-deployment proof (not only convex-test) — the change-carries-proof verdict is owed at D5's delivery surface`,
211
+ });
212
+ }
213
+ return scan;
214
+ }
215
+ // ===========================================================================
216
+ // R-39 (judgement; static + delivery gate) — §18 line 428: "the platform
217
+ // mechanism a rule depends on is named (mechanical); it resolves to a current
218
+ // guarantee (delivery gate)." STATIC/MECHANICAL HALF: the §11 refinements table
219
+ // names, per rule, the Convex mechanism it depends on — flag any §11-refined
220
+ // rule that fails to NAME a mechanism. GATE/HUMAN HALF: whether each named
221
+ // mechanism resolves to a real, current platform guarantee (a renamed API /
222
+ // withdrawn guarantee fails) — surface each (rule, mechanism) pair as a site a
223
+ // human/gate verdict is owed on (one cnj each).
224
+ // ===========================================================================
225
+ export function scanR39(lines) {
226
+ const scan = emptyDocScan();
227
+ const s11 = sectionLines(lines, "11");
228
+ for (const row of tableRows(s11)) {
229
+ const first = row.cells[0] ?? "";
230
+ const m = /^(R-\d+)\b/.exec(first);
231
+ if (!m)
232
+ continue; // not a refinements-table rule row (e.g. the header "Rule")
233
+ const ruleId = m[1];
234
+ const mechanism = row.cells[1] ?? "";
235
+ if (isDocPlaceholder(mechanism)) {
236
+ scan.violate.push({
237
+ n: row.n,
238
+ note: `§11-refined rule ${ruleId} names NO platform mechanism — a rule resting on an unnamed mechanism is unenforceable prose`,
239
+ });
240
+ continue;
241
+ }
242
+ scan.conform.push({
243
+ n: row.n,
244
+ note: `${ruleId} names its platform mechanism (${mechanism.slice(0, 48)}…)`,
245
+ });
246
+ // GATE/HUMAN HALF — does the named mechanism resolve to a CURRENT guarantee?
247
+ scan.cnj.push({
248
+ n: row.n,
249
+ note: `${ruleId} → "${mechanism.slice(0, 40)}…": a human/gate verdict is owed on whether this platform mechanism resolves to a real, current Convex guarantee (a renamed API / withdrawn guarantee fails)`,
250
+ });
251
+ }
252
+ if (scan.conform.length === 0 && scan.violate.length === 0) {
253
+ scan.violate.push({
254
+ n: 247,
255
+ note: "§11 refinements table not found or names no rule/mechanism pair — cannot certify platform-mechanism naming",
256
+ });
257
+ }
258
+ return scan;
259
+ }
260
+ // ---- detectors ------------------------------------------------------------
261
+ export const R42 = docDetector("R-42", "R-42 every rule sourced; no raised subject left silent", "every §14 source cell names a source from the known set (static); no raised subject left un-routed (delivery gate)", scanR42, "R-42: §14 source table not found in the vendored standard — cannot certify the three-source discipline. NOT a clean bill.");
262
+ export const R15 = docDetector("R-15 vocabulary & subject reconciliation", "R-15 vocabulary & subject reconciliation", "the §3 four shared names each reconcile to a numbered rule (static); a new term reconciled before acceptance (delivery gate)", scanR15, "R-15: §3 shared-vocabulary section not found — cannot certify reconciliation. NOT a clean bill.");
263
+ export const R32 = docDetector("R-32 limit-governed rule proven on a real deployment", "R-32 limit-governed rule proven on a real deployment", "the R-32 limit-governed set {R-3,R-4,R-11,R-23,R-30,R-31} is named and each resolves (static); a change carries a real-deployment proof (delivery gate)", scanR32, "R-32: limit-governed rule set not found — cannot certify the real-deployment-proof discipline. NOT a clean bill.");
264
+ export const R39 = docDetector("R-39 a rule's platform mechanism is named and resolves", "R-39 a rule's platform mechanism is named and resolves", "every §11-refined rule names its platform mechanism (mechanical); the named mechanism resolves to a current guarantee (delivery gate)", scanR39, "R-39: §11 refinements table not found — cannot certify platform-mechanism naming. NOT a clean bill.");
265
+ /** The four, in a form the orchestrator spreads into REGISTRY. */
266
+ export const STANDARD_DOC_DETECTORS = [
267
+ { detector: R42, ruleId: "R-42", standardPin: STANDARD_DOC_PIN },
268
+ { detector: R15, ruleId: "R-15", standardPin: STANDARD_DOC_PIN },
269
+ { detector: R32, ruleId: "R-32", standardPin: STANDARD_DOC_PIN },
270
+ { detector: R39, ruleId: "R-39", standardPin: STANDARD_DOC_PIN },
271
+ ];
@@ -0,0 +1,14 @@
1
+ import type { Detector } from "../engine-types.js";
2
+ import { type OracleRow } from "../oracle.js";
3
+ /**
4
+ * R-14 — inventory-schema parity, contract → inventory, one direction.
5
+ * A merged reader/writer authority column is "non-conforming by construction"
6
+ * (§12). The oracle's column 9 `rbac_qui` is that single merged column.
7
+ */
8
+ export declare function makeR14(columns?: readonly string[]): Detector;
9
+ /**
10
+ * R-38 — the completeness gate over the thirteen §2 fields. The oracle cannot
11
+ * satisfy the contract because reader and writer authority are not two separate
12
+ * fields (both map to `rbac_qui`).
13
+ */
14
+ export declare function makeR38(columns?: readonly string[], rowsProvider?: () => OracleRow[]): Detector;
@@ -0,0 +1,83 @@
1
+ // The two STRUCTURAL detectors — R-14 and R-38. They judge the inventory SCHEMA
2
+ // against the §2 contract, not a single row. Run against THIS oracle they MUST
3
+ // return `non-conforming`, because §2 splits authority into two distinct fields
4
+ // (reader R-5, writer R-10) and the oracle carries a SINGLE merged `rbac_qui`
5
+ // column. Agreeing with the oracle here would be certifying the defect the
6
+ // instrument exists to find — so these are NOT tuned to pass.
7
+ import { CONTRACT_FIELDS, READER_AUTHORITY_COLUMN, WRITER_AUTHORITY_COLUMN, authorityIsMerged } from "../contract.js";
8
+ import { ORACLE_COLUMNS, readOracle } from "../oracle.js";
9
+ const CSV_ANCHOR = "oracle:vp-by-tool-day158.csv col#9 rbac_qui";
10
+ /**
11
+ * R-14 — inventory-schema parity, contract → inventory, one direction.
12
+ * A merged reader/writer authority column is "non-conforming by construction"
13
+ * (§12). The oracle's column 9 `rbac_qui` is that single merged column.
14
+ */
15
+ export function makeR14(columns = ORACLE_COLUMNS) {
16
+ return {
17
+ id: "R-14 inventory-schema parity",
18
+ run: () => {
19
+ const authorityColumns = columns.filter((c) => c === "rbac_qui");
20
+ if (authorityIsMerged() || authorityColumns.length < 2) {
21
+ return {
22
+ id: "R-14 inventory-schema parity",
23
+ anchor: CSV_ANCHOR,
24
+ state: "non-conforming",
25
+ verdict: "R-14: the inventory holds a SINGLE merged authority column `rbac_qui` (col#9), " +
26
+ "but §2 splits authority into two distinct contract fields — reader authority (R-5) " +
27
+ "and writer authority (R-10). A merged authority column is non-conforming by construction: " +
28
+ "the reader/writer divergence R-10 and R-7 exist to record cannot be written in it.",
29
+ };
30
+ }
31
+ return {
32
+ id: "R-14 inventory-schema parity",
33
+ anchor: "oracle:vp-by-tool-day158.csv",
34
+ state: "present",
35
+ verdict: "R-14: every §2 contract field present; reader and writer authority are two separate columns.",
36
+ };
37
+ },
38
+ };
39
+ }
40
+ /**
41
+ * R-38 — the completeness gate over the thirteen §2 fields. The oracle cannot
42
+ * satisfy the contract because reader and writer authority are not two separate
43
+ * fields (both map to `rbac_qui`).
44
+ */
45
+ export function makeR38(columns = ORACLE_COLUMNS, rowsProvider = readOracle) {
46
+ return {
47
+ id: "R-38 thirteen-field completeness",
48
+ run: () => {
49
+ const colSet = new Set(columns);
50
+ const missing = [];
51
+ for (const f of CONTRACT_FIELDS) {
52
+ for (const col of f.columns) {
53
+ if (!colSet.has(col))
54
+ missing.push(`${f.field}→${col}`);
55
+ }
56
+ }
57
+ const authorityMerged = READER_AUTHORITY_COLUMN === WRITER_AUTHORITY_COLUMN && colSet.has(READER_AUTHORITY_COLUMN);
58
+ if (authorityMerged || missing.length > 0) {
59
+ const rows = rowsProvider();
60
+ const sample = rows
61
+ .filter((r) => !r.malformed && r.by.rbac_qui && r.by.table !== "TOTAL")
62
+ .slice(0, 3)
63
+ .map((r) => `row#${r.n} "${r.by.outil || r.by.table}" rbac_qui="${r.by.rbac_qui}"`)
64
+ .join("; ");
65
+ return {
66
+ id: "R-38 thirteen-field completeness",
67
+ anchor: CSV_ANCHOR,
68
+ state: "non-conforming",
69
+ verdict: `R-38: the oracle cannot satisfy the thirteen-field §2 contract — reader authority (R-5) and ` +
70
+ `writer authority (R-10) are not two separate fields; both map to the single column ` +
71
+ `\`rbac_qui\` (col#9).${missing.length ? ` Also missing: ${missing.join(", ")}.` : ""} ` +
72
+ `Evidence: ${sample}.`,
73
+ };
74
+ }
75
+ return {
76
+ id: "R-38 thirteen-field completeness",
77
+ anchor: "oracle:vp-by-tool-day158.csv",
78
+ state: "present",
79
+ verdict: "R-38: all thirteen §2 contract fields present per table, each within its vocabulary.",
80
+ };
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,10 @@
1
+ import { runDoctor } from "@vantageos/mcp-doctor";
2
+ /** A detector object the engine's runner accepts in its injectable list. */
3
+ export type Detector = NonNullable<Parameters<typeof runDoctor>[1]>[number];
4
+ /** One row of the engine's verdict table. */
5
+ export type DetectorResult = ReturnType<typeof runDoctor>[number];
6
+ /** The engine's decisive/undecidable state union — derived, not spelled out. */
7
+ export type State = DetectorResult["state"];
8
+ /** The tree shape the engine hands each detector's `run`. Detectors in this
9
+ * wave ignore it (they consult the oracle), so we only need its identity. */
10
+ export type Tree = Parameters<Detector["run"]>[0];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The deferral bucket. R-1..R-47 each carry an executable control (registry.ts),
3
+ * so none is deferred to "a human decides". R-48 (authority-totality, the #130
4
+ * class) was added to the standard at the 51ec7af1 re-pin AFTER its detector; its
5
+ * text is now pinned so coverage enumerates 48, but no detector exists yet — so it
6
+ * is reported here as NOT_MECHANICALLY_CHECKABLE with a written reason rather than
7
+ * silently omitted, which would let coverage pretend 47 is the whole document. The
8
+ * detector is task k17chpr4kzp399j3s7r7edpm4s8cscbz (after D5); when it lands, R-48
9
+ * moves to registry.ts and this entry is removed. The export is consumed by
10
+ * coverage.ts / coverage-cli.ts / cli.ts.
11
+ */
12
+ import type { DetectorResult } from "./engine-types.js";
13
+ export declare const NOT_MECHANICALLY_CHECKABLE: Record<string, string>;
14
+ /**
15
+ * One `could-not-judge` DetectorResult per NOT_MECHANICALLY_CHECKABLE rule, so
16
+ * every rule the standard defines carries EXACTLY ONE result (the tally invariant)
17
+ * without inventing a mechanical verdict a detector has not earned. A pinned rule
18
+ * with no detector is an ABSTENTION — a human/gate verdict is owed — never a
19
+ * silent omission and never a synthesized `present`. Appended to the registry run
20
+ * alongside the executable detectors' results.
21
+ */
22
+ export declare function notCheckableResults(bucket?: Record<string, string>): DetectorResult[];
@@ -0,0 +1,37 @@
1
+ // The deferral bucket — now EMPTY, and kept empty on purpose.
2
+ //
3
+ // Every rule the standard defines carries an executable control (registry.ts), so
4
+ // no rule is deferred to "a human decides". The operator refused a deferral bucket
5
+ // outright: nothing a script or shell command can decide is deferred, and nothing
6
+ // is deferred at all without his explicit agreement. This file is the proof of that
7
+ // refusal — an export that other modules consume but that holds no entries.
8
+ //
9
+ // History (why the bucket existed and why it is gone): the twenty source-tree rules
10
+ // and the nine judgement/process/cross-document rules were each, at one point, filed
11
+ // here under a label — "a machine cannot decide it". That label was false for all of
12
+ // them: a source scan is mechanical; a judgement rule's DETECTION half is mechanical
13
+ // and its human half is surfaced per site, not deferred; a cross-document resolution
14
+ // (R-41) and an Axis-1 between-backend compare (R-40) are mechanical once their second
15
+ // input is vendored. They were built, not relabelled. The bucket was not renamed to a
16
+ // gentler word — it was emptied.
17
+ import { STANDARD_FILE } from "./detectors/standard/_doc.js";
18
+ export const NOT_MECHANICALLY_CHECKABLE = {
19
+ "R-48": "R-48 authority-totality detector pending — task k17chpr4kzp399j3s7r7edpm4s8cscbz; text pinned at 51ec7af1 so coverage cannot pretend 47 is complete. No mechanical control yet; R-48 is enumerated and visible, never silently dropped.",
20
+ };
21
+ /**
22
+ * One `could-not-judge` DetectorResult per NOT_MECHANICALLY_CHECKABLE rule, so
23
+ * every rule the standard defines carries EXACTLY ONE result (the tally invariant)
24
+ * without inventing a mechanical verdict a detector has not earned. A pinned rule
25
+ * with no detector is an ABSTENTION — a human/gate verdict is owed — never a
26
+ * silent omission and never a synthesized `present`. Appended to the registry run
27
+ * alongside the executable detectors' results.
28
+ */
29
+ export function notCheckableResults(bucket = NOT_MECHANICALLY_CHECKABLE) {
30
+ return Object.entries(bucket).map(([ruleId, reason]) => ({
31
+ id: `${ruleId} not-mechanically-checkable`,
32
+ anchor: `${STANDARD_FILE}:0`,
33
+ state: "could-not-judge",
34
+ verdict: `${ruleId}: NOT_MECHANICALLY_CHECKABLE — ${reason}`,
35
+ reason,
36
+ }));
37
+ }
@@ -0,0 +1,33 @@
1
+ /** Absolute path to the pinned oracle fixture. */
2
+ export declare const ORACLE_PATH: string;
3
+ /** The eighteen audit columns, in file order. §2 of the standard maps contract
4
+ * fields onto these column names. */
5
+ export declare const ORACLE_COLUMNS: readonly ["table", "outil", "T2_verdict", "T2_composant_cible", "crud", "operation_detail", "search_mecanisme", "surface", "rbac_qui", "scope_enforcement", "namespace_ou_org", "tokens_estime_contexte", "doublon_alias", "rbac_coherence_table", "rbac_adjustment_needed", "table_purpose", "table_conserver_supprimer", "statut_suppression"];
6
+ export type OracleColumn = (typeof ORACLE_COLUMNS)[number];
7
+ export interface OracleRow {
8
+ /** 1-based data-row index (header excluded). */
9
+ n: number;
10
+ /** the raw parsed cells, exactly as many as the parser found. */
11
+ cells: string[];
12
+ /** column-name → trimmed value; only populated when the row is well-formed. */
13
+ by: Record<OracleColumn, string>;
14
+ /** set when the row could NOT be parsed into the 18-column shape — a detector
15
+ * reads this as could-not-judge, never as a pass. */
16
+ malformed?: string;
17
+ }
18
+ /**
19
+ * RFC-4180-ish CSV parser: handles quoted fields with embedded commas and
20
+ * doubled quotes. The oracle CSV has quoted cells with commas (e.g. the
21
+ * `table_conserver_supprimer` prose), so a naive split on "," is wrong.
22
+ */
23
+ export declare function parseCsv(text: string): string[][];
24
+ /** Build typed rows from raw parsed cells (header already removed). */
25
+ export declare function toRows(dataRows: string[][]): OracleRow[];
26
+ /** Memoized read of the real pinned oracle fixture. */
27
+ export declare function readOracle(): OracleRow[];
28
+ /**
29
+ * A real inventory row is one tool/table entry. The audit's trailing `TOTAL`
30
+ * aggregate row (`table === "TOTAL"`, empty `outil`) is NOT an inventory entry;
31
+ * a per-row detector treats it as not-applicable, never judging it as a tool.
32
+ */
33
+ export declare function isInventoryRow(row: OracleRow): boolean;
package/dist/oracle.js ADDED
@@ -0,0 +1,133 @@
1
+ // The oracle reader. The oracle is the day158 per-tool audit CSV — a COMPARISON
2
+ // TARGET, never an authority. Where the oracle and the pinned standard disagree,
3
+ // the standard wins and the divergence is a FINDING about the oracle.
4
+ //
5
+ // The tree the engine's loadTree builds is .ts-only and does NOT contain this
6
+ // CSV, so every first-wave detector ignores `tree` and consults this memoized
7
+ // reader instead.
8
+ import { readFileSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import { dirname, join } from "node:path";
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ /** Absolute path to the pinned oracle fixture. */
13
+ export const ORACLE_PATH = join(here, "..", "fixtures", "vp-by-tool-day158.csv");
14
+ /** The eighteen audit columns, in file order. §2 of the standard maps contract
15
+ * fields onto these column names. */
16
+ export const ORACLE_COLUMNS = [
17
+ "table",
18
+ "outil",
19
+ "T2_verdict",
20
+ "T2_composant_cible",
21
+ "crud",
22
+ "operation_detail",
23
+ "search_mecanisme",
24
+ "surface",
25
+ "rbac_qui",
26
+ "scope_enforcement",
27
+ "namespace_ou_org",
28
+ "tokens_estime_contexte",
29
+ "doublon_alias",
30
+ "rbac_coherence_table",
31
+ "rbac_adjustment_needed",
32
+ "table_purpose",
33
+ "table_conserver_supprimer",
34
+ "statut_suppression",
35
+ ];
36
+ /**
37
+ * RFC-4180-ish CSV parser: handles quoted fields with embedded commas and
38
+ * doubled quotes. The oracle CSV has quoted cells with commas (e.g. the
39
+ * `table_conserver_supprimer` prose), so a naive split on "," is wrong.
40
+ */
41
+ export function parseCsv(text) {
42
+ const rows = [];
43
+ let field = "";
44
+ let record = [];
45
+ let inQuotes = false;
46
+ for (let i = 0; i < text.length; i++) {
47
+ const c = text[i];
48
+ if (inQuotes) {
49
+ if (c === '"') {
50
+ if (text[i + 1] === '"') {
51
+ field += '"';
52
+ i++;
53
+ }
54
+ else {
55
+ inQuotes = false;
56
+ }
57
+ }
58
+ else {
59
+ field += c;
60
+ }
61
+ continue;
62
+ }
63
+ if (c === '"') {
64
+ inQuotes = true;
65
+ }
66
+ else if (c === ",") {
67
+ record.push(field);
68
+ field = "";
69
+ }
70
+ else if (c === "\n") {
71
+ record.push(field);
72
+ rows.push(record);
73
+ record = [];
74
+ field = "";
75
+ }
76
+ else if (c === "\r") {
77
+ // swallow — the newline handler emits the record
78
+ }
79
+ else {
80
+ field += c;
81
+ }
82
+ }
83
+ // trailing field / record (file with no final newline)
84
+ if (field.length > 0 || record.length > 0) {
85
+ record.push(field);
86
+ rows.push(record);
87
+ }
88
+ return rows;
89
+ }
90
+ /** Build typed rows from raw parsed cells (header already removed). */
91
+ export function toRows(dataRows) {
92
+ return dataRows.map((cells, idx) => {
93
+ const n = idx + 1;
94
+ if (cells.length !== ORACLE_COLUMNS.length) {
95
+ return {
96
+ n,
97
+ cells,
98
+ by: {},
99
+ malformed: `row ${n} has ${cells.length} cells, expected ${ORACLE_COLUMNS.length}`,
100
+ };
101
+ }
102
+ const by = {};
103
+ ORACLE_COLUMNS.forEach((col, i) => {
104
+ by[col] = (cells[i] ?? "").trim();
105
+ });
106
+ return { n, cells, by };
107
+ });
108
+ }
109
+ let cache = null;
110
+ /** Memoized read of the real pinned oracle fixture. */
111
+ export function readOracle() {
112
+ if (cache)
113
+ return cache;
114
+ const text = readFileSync(ORACLE_PATH, "utf8");
115
+ const raw = parseCsv(text).filter((r) => !(r.length === 1 && r[0] === ""));
116
+ const [, ...data] = raw; // drop header
117
+ cache = toRows(data);
118
+ return cache;
119
+ }
120
+ /**
121
+ * A real inventory row is one tool/table entry. The audit's trailing `TOTAL`
122
+ * aggregate row (`table === "TOTAL"`, empty `outil`) is NOT an inventory entry;
123
+ * a per-row detector treats it as not-applicable, never judging it as a tool.
124
+ */
125
+ export function isInventoryRow(row) {
126
+ if (row.malformed)
127
+ return false;
128
+ if (row.by.table === "TOTAL")
129
+ return false;
130
+ if (row.by.outil === "" && row.by.crud === "" && row.by.surface === "")
131
+ return false;
132
+ return true;
133
+ }
@@ -0,0 +1,11 @@
1
+ import type { Detector } from "./engine-types.js";
2
+ /** The standard both pins the whole wave against. */
3
+ export declare const STANDARD_PIN = "analysis/vantagepeers/backend-standard/backend-standard.md @ a9d7877b4947ed8ae61d234433debb2182a2ba44 (elpiarthera/ElPi-Corp)";
4
+ export interface RegistryEntry {
5
+ detector: Detector;
6
+ ruleId: string;
7
+ standardPin: string;
8
+ }
9
+ export declare const REGISTRY: RegistryEntry[];
10
+ /** The rule ids this wave mechanically CHECKS — derived from the registry. */
11
+ export declare function checkedRuleIds(): string[];