@lovrozagar/oat 0.10.0 → 0.12.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 (278) hide show
  1. package/README.md +25 -2006
  2. package/dist/cli.js +283 -59
  3. package/dist/config/define-config.d.ts +8 -0
  4. package/dist/config/define-config.d.ts.map +1 -1
  5. package/dist/config/define-config.js.map +1 -1
  6. package/dist/config/validate.d.ts +12 -0
  7. package/dist/config/validate.d.ts.map +1 -0
  8. package/dist/config/validate.js +86 -0
  9. package/dist/config/validate.js.map +1 -0
  10. package/dist/conformance/effects.d.ts +9 -0
  11. package/dist/conformance/effects.d.ts.map +1 -0
  12. package/dist/conformance/effects.js +276 -0
  13. package/dist/conformance/effects.js.map +1 -0
  14. package/dist/conformance/expected-failures.d.ts +18 -0
  15. package/dist/conformance/expected-failures.d.ts.map +1 -0
  16. package/dist/conformance/expected-failures.js +48 -0
  17. package/dist/conformance/expected-failures.js.map +1 -0
  18. package/dist/conformance/feature-gate.d.ts +7 -0
  19. package/dist/conformance/feature-gate.d.ts.map +1 -0
  20. package/dist/conformance/feature-gate.js +428 -0
  21. package/dist/conformance/feature-gate.js.map +1 -0
  22. package/dist/conformance/fuzz.d.ts +108 -0
  23. package/dist/conformance/fuzz.d.ts.map +1 -0
  24. package/dist/conformance/fuzz.js +246 -0
  25. package/dist/conformance/fuzz.js.map +1 -0
  26. package/dist/conformance/leaks.d.ts +10 -0
  27. package/dist/conformance/leaks.d.ts.map +1 -0
  28. package/dist/conformance/leaks.js +23 -0
  29. package/dist/conformance/leaks.js.map +1 -0
  30. package/dist/conformance/pool.d.ts +49 -0
  31. package/dist/conformance/pool.d.ts.map +1 -0
  32. package/dist/conformance/pool.js +75 -0
  33. package/dist/conformance/pool.js.map +1 -0
  34. package/dist/conformance/rate-limit.d.ts +7 -0
  35. package/dist/conformance/rate-limit.d.ts.map +1 -0
  36. package/dist/conformance/rate-limit.js +372 -0
  37. package/dist/conformance/rate-limit.js.map +1 -0
  38. package/dist/conformance/scope.d.ts +15 -0
  39. package/dist/conformance/scope.d.ts.map +1 -0
  40. package/dist/conformance/scope.js +78 -0
  41. package/dist/conformance/scope.js.map +1 -0
  42. package/dist/conformance/seed-contract.d.ts +11 -0
  43. package/dist/conformance/seed-contract.d.ts.map +1 -0
  44. package/dist/conformance/seed-contract.js +680 -0
  45. package/dist/conformance/seed-contract.js.map +1 -0
  46. package/dist/conformance/shapes.d.ts +35 -0
  47. package/dist/conformance/shapes.d.ts.map +1 -0
  48. package/dist/conformance/shapes.js +162 -0
  49. package/dist/conformance/shapes.js.map +1 -0
  50. package/dist/conformance/specs.d.ts +21 -0
  51. package/dist/conformance/specs.d.ts.map +1 -0
  52. package/dist/conformance/specs.js +306 -0
  53. package/dist/conformance/specs.js.map +1 -0
  54. package/dist/conformance/suite.d.ts +209 -0
  55. package/dist/conformance/suite.d.ts.map +1 -0
  56. package/dist/conformance/suite.js +1538 -0
  57. package/dist/conformance/suite.js.map +1 -0
  58. package/dist/conformance/unique.d.ts +7 -0
  59. package/dist/conformance/unique.d.ts.map +1 -0
  60. package/dist/conformance/unique.js +586 -0
  61. package/dist/conformance/unique.js.map +1 -0
  62. package/dist/conformance/worker.d.ts +3 -0
  63. package/dist/conformance/worker.d.ts.map +1 -0
  64. package/dist/conformance/worker.js +27 -0
  65. package/dist/conformance/worker.js.map +1 -0
  66. package/dist/index.d.ts +8 -3
  67. package/dist/index.js +5 -2
  68. package/dist/reference/defects.d.ts +71 -1
  69. package/dist/reference/defects.d.ts.map +1 -1
  70. package/dist/reference/defects.js +74 -8
  71. package/dist/reference/defects.js.map +1 -1
  72. package/dist/reference/dialect.d.ts +6 -4
  73. package/dist/reference/dialect.d.ts.map +1 -1
  74. package/dist/reference/dialect.js +1 -0
  75. package/dist/reference/dialect.js.map +1 -1
  76. package/dist/reference/http.d.ts +17 -25
  77. package/dist/reference/http.d.ts.map +1 -1
  78. package/dist/reference/http.js +574 -224
  79. package/dist/reference/http.js.map +1 -1
  80. package/dist/reference/listing.d.ts +51 -0
  81. package/dist/reference/listing.d.ts.map +1 -0
  82. package/dist/reference/listing.js +256 -0
  83. package/dist/reference/listing.js.map +1 -0
  84. package/dist/reference/model.d.ts +35 -1
  85. package/dist/reference/model.d.ts.map +1 -1
  86. package/dist/reference/model.js +42 -0
  87. package/dist/reference/model.js.map +1 -1
  88. package/dist/reference/query.d.ts +132 -43
  89. package/dist/reference/query.d.ts.map +1 -1
  90. package/dist/reference/query.js +397 -315
  91. package/dist/reference/query.js.map +1 -1
  92. package/dist/reference/shapes.d.ts +60 -0
  93. package/dist/reference/shapes.d.ts.map +1 -0
  94. package/dist/reference/shapes.js +142 -0
  95. package/dist/reference/shapes.js.map +1 -0
  96. package/dist/reference/spec.d.ts +9 -2
  97. package/dist/reference/spec.d.ts.map +1 -1
  98. package/dist/reference/spec.js +291 -71
  99. package/dist/reference/spec.js.map +1 -1
  100. package/dist/reference/store-api.d.ts +14 -43
  101. package/dist/reference/store-api.d.ts.map +1 -1
  102. package/dist/reference/store-api.js +8 -58
  103. package/dist/reference/store-api.js.map +1 -1
  104. package/dist/reference/stores/memory.d.ts +12 -15
  105. package/dist/reference/stores/memory.d.ts.map +1 -1
  106. package/dist/reference/stores/memory.js +22 -70
  107. package/dist/reference/stores/memory.js.map +1 -1
  108. package/dist/reference/stores/postgres.d.ts +11 -23
  109. package/dist/reference/stores/postgres.d.ts.map +1 -1
  110. package/dist/reference/stores/postgres.js +53 -334
  111. package/dist/reference/stores/postgres.js.map +1 -1
  112. package/dist/reference/stores/sql-compile.d.ts +36 -0
  113. package/dist/reference/stores/sql-compile.d.ts.map +1 -0
  114. package/dist/reference/stores/sql-compile.js +95 -0
  115. package/dist/reference/stores/sql-compile.js.map +1 -0
  116. package/dist/reference/stores/sqlite.d.ts +12 -27
  117. package/dist/reference/stores/sqlite.d.ts.map +1 -1
  118. package/dist/reference/stores/sqlite.js +54 -355
  119. package/dist/reference/stores/sqlite.js.map +1 -1
  120. package/dist/report/console.d.ts.map +1 -1
  121. package/dist/report/console.js +23 -9
  122. package/dist/report/console.js.map +1 -1
  123. package/dist/report/matrix.d.ts +2 -0
  124. package/dist/report/matrix.d.ts.map +1 -1
  125. package/dist/report/matrix.js +63 -10
  126. package/dist/report/matrix.js.map +1 -1
  127. package/dist/report/render.d.ts +10 -1
  128. package/dist/report/render.d.ts.map +1 -1
  129. package/dist/report/render.js +75 -13
  130. package/dist/report/render.js.map +1 -1
  131. package/dist/runtime/async.d.ts +16 -3
  132. package/dist/runtime/async.d.ts.map +1 -1
  133. package/dist/runtime/async.js +24 -19
  134. package/dist/runtime/async.js.map +1 -1
  135. package/dist/runtime/auth.d.ts +8 -9
  136. package/dist/runtime/auth.d.ts.map +1 -1
  137. package/dist/runtime/auth.js +60 -62
  138. package/dist/runtime/auth.js.map +1 -1
  139. package/dist/runtime/body.d.ts +15 -3
  140. package/dist/runtime/body.d.ts.map +1 -1
  141. package/dist/runtime/body.js +61 -12
  142. package/dist/runtime/body.js.map +1 -1
  143. package/dist/runtime/check-costs.d.ts +15 -0
  144. package/dist/runtime/check-costs.d.ts.map +1 -0
  145. package/dist/runtime/check-costs.js +114 -0
  146. package/dist/runtime/check-costs.js.map +1 -0
  147. package/dist/runtime/check-ids.d.ts +15 -0
  148. package/dist/runtime/check-ids.d.ts.map +1 -0
  149. package/dist/runtime/check-ids.js +140 -0
  150. package/dist/runtime/check-ids.js.map +1 -0
  151. package/dist/runtime/checks.d.ts +88 -20
  152. package/dist/runtime/checks.d.ts.map +1 -1
  153. package/dist/runtime/checks.js +3288 -1947
  154. package/dist/runtime/checks.js.map +1 -1
  155. package/dist/runtime/client.d.ts +112 -0
  156. package/dist/runtime/client.d.ts.map +1 -1
  157. package/dist/runtime/client.js +271 -39
  158. package/dist/runtime/client.js.map +1 -1
  159. package/dist/runtime/cookies.d.ts +16 -2
  160. package/dist/runtime/cookies.d.ts.map +1 -1
  161. package/dist/runtime/cookies.js +83 -22
  162. package/dist/runtime/cookies.js.map +1 -1
  163. package/dist/runtime/exchanges.d.ts +4 -5
  164. package/dist/runtime/exchanges.d.ts.map +1 -1
  165. package/dist/runtime/exchanges.js +33 -55
  166. package/dist/runtime/exchanges.js.map +1 -1
  167. package/dist/runtime/exit.d.ts +40 -0
  168. package/dist/runtime/exit.d.ts.map +1 -0
  169. package/dist/runtime/exit.js +48 -0
  170. package/dist/runtime/exit.js.map +1 -0
  171. package/dist/runtime/feature-gate.d.ts +0 -9
  172. package/dist/runtime/feature-gate.d.ts.map +1 -1
  173. package/dist/runtime/feature-gate.js +0 -21
  174. package/dist/runtime/feature-gate.js.map +1 -1
  175. package/dist/runtime/finding.d.ts +49 -8
  176. package/dist/runtime/finding.d.ts.map +1 -1
  177. package/dist/runtime/finding.js +51 -8
  178. package/dist/runtime/finding.js.map +1 -1
  179. package/dist/runtime/fixture.d.ts +35 -28
  180. package/dist/runtime/fixture.d.ts.map +1 -1
  181. package/dist/runtime/fixture.js +92 -731
  182. package/dist/runtime/fixture.js.map +1 -1
  183. package/dist/runtime/generate.d.ts +63 -0
  184. package/dist/runtime/generate.d.ts.map +1 -0
  185. package/dist/runtime/generate.js +635 -0
  186. package/dist/runtime/generate.js.map +1 -0
  187. package/dist/runtime/headers.d.ts +6 -0
  188. package/dist/runtime/headers.d.ts.map +1 -0
  189. package/dist/runtime/headers.js +16 -0
  190. package/dist/runtime/headers.js.map +1 -0
  191. package/dist/runtime/path.d.ts +7 -0
  192. package/dist/runtime/path.d.ts.map +1 -0
  193. package/dist/runtime/path.js +44 -0
  194. package/dist/runtime/path.js.map +1 -0
  195. package/dist/runtime/payloads.d.ts +2 -0
  196. package/dist/runtime/payloads.d.ts.map +1 -1
  197. package/dist/runtime/payloads.js +2 -1
  198. package/dist/runtime/payloads.js.map +1 -1
  199. package/dist/runtime/rate-limit.d.ts +6 -4
  200. package/dist/runtime/rate-limit.d.ts.map +1 -1
  201. package/dist/runtime/rate-limit.js +22 -33
  202. package/dist/runtime/rate-limit.js.map +1 -1
  203. package/dist/runtime/redact.d.ts +24 -0
  204. package/dist/runtime/redact.d.ts.map +1 -0
  205. package/dist/runtime/redact.js +98 -0
  206. package/dist/runtime/redact.js.map +1 -0
  207. package/dist/runtime/run.d.ts +34 -2
  208. package/dist/runtime/run.d.ts.map +1 -1
  209. package/dist/runtime/run.js +374 -138
  210. package/dist/runtime/run.js.map +1 -1
  211. package/dist/runtime/runs.d.ts +5 -0
  212. package/dist/runtime/runs.d.ts.map +1 -1
  213. package/dist/runtime/runs.js +7 -1
  214. package/dist/runtime/runs.js.map +1 -1
  215. package/dist/runtime/teardown.d.ts +17 -4
  216. package/dist/runtime/teardown.d.ts.map +1 -1
  217. package/dist/runtime/teardown.js +49 -16
  218. package/dist/runtime/teardown.js.map +1 -1
  219. package/dist/runtime/transcript.d.ts +34 -2
  220. package/dist/runtime/transcript.d.ts.map +1 -1
  221. package/dist/runtime/transcript.js +119 -24
  222. package/dist/runtime/transcript.js.map +1 -1
  223. package/dist/runtime/unique.d.ts +2 -11
  224. package/dist/runtime/unique.d.ts.map +1 -1
  225. package/dist/runtime/unique.js +27 -53
  226. package/dist/runtime/unique.js.map +1 -1
  227. package/dist/runtime/validate.d.ts +13 -2
  228. package/dist/runtime/validate.d.ts.map +1 -1
  229. package/dist/runtime/validate.js +51 -31
  230. package/dist/runtime/validate.js.map +1 -1
  231. package/dist/runtime/wait.d.ts +2 -1
  232. package/dist/runtime/wait.d.ts.map +1 -1
  233. package/dist/runtime/wait.js +2 -36
  234. package/dist/runtime/wait.js.map +1 -1
  235. package/dist/runtime/world.d.ts +24 -1
  236. package/dist/runtime/world.d.ts.map +1 -1
  237. package/dist/runtime/world.js +101 -26
  238. package/dist/runtime/world.js.map +1 -1
  239. package/dist/spec/collection.d.ts +8 -2
  240. package/dist/spec/collection.d.ts.map +1 -1
  241. package/dist/spec/collection.js +47 -7
  242. package/dist/spec/collection.js.map +1 -1
  243. package/dist/spec/extensions.d.ts +11 -1
  244. package/dist/spec/extensions.d.ts.map +1 -1
  245. package/dist/spec/extensions.js +10 -2
  246. package/dist/spec/extensions.js.map +1 -1
  247. package/dist/spec/graph.d.ts +46 -0
  248. package/dist/spec/graph.d.ts.map +1 -1
  249. package/dist/spec/graph.js +175 -11
  250. package/dist/spec/graph.js.map +1 -1
  251. package/dist/spec/load.d.ts +25 -5
  252. package/dist/spec/load.d.ts.map +1 -1
  253. package/dist/spec/load.js +160 -59
  254. package/dist/spec/load.js.map +1 -1
  255. package/dist/spec/query-capabilities.d.ts +4 -0
  256. package/dist/spec/query-capabilities.d.ts.map +1 -1
  257. package/dist/spec/query-capabilities.js +6 -0
  258. package/dist/spec/query-capabilities.js.map +1 -1
  259. package/dist/spec/schema.d.ts +20 -0
  260. package/dist/spec/schema.d.ts.map +1 -0
  261. package/dist/spec/schema.js +118 -0
  262. package/dist/spec/schema.js.map +1 -0
  263. package/dist/spec/tag-schema.d.ts +12 -0
  264. package/dist/spec/tag-schema.d.ts.map +1 -0
  265. package/dist/spec/tag-schema.js +144 -0
  266. package/dist/spec/tag-schema.js.map +1 -0
  267. package/dist/spec/types.js +17 -1
  268. package/dist/spec/types.js.map +1 -1
  269. package/docs/api.md +59 -0
  270. package/docs/assumptions.md +99 -0
  271. package/docs/checks.md +183 -0
  272. package/docs/commands.md +214 -0
  273. package/docs/configuration.md +650 -0
  274. package/docs/model.md +184 -0
  275. package/docs/reference-defects.md +94 -0
  276. package/docs/reports.md +211 -0
  277. package/docs/tags.md +403 -0
  278. package/package.json +3 -1
package/docs/checks.md ADDED
@@ -0,0 +1,183 @@
1
+ # oat — Checks and verdicts
2
+
3
+ [← back to the overview](../README.md)
4
+
5
+ ## Checks
6
+
7
+ 99 checks. A check that cannot run says so (`did not apply` + `needs`). A check that depends on a broken primitive is `BLOCKED`. A check that ran and stopped is inconclusive, not a pass.
8
+
9
+ Order is fixed (foundations first) so cascade suppression has a cause to point at. Mutating checks run alone; read-only checks may share in-flight requests under `maxInFlight`.
10
+
11
+ `depends` is the `dependsOn` list: if any of those already failed **for this entity**, this check is `BLOCKED` rather than reported as a second defect. Suppression is transitive.
12
+
13
+ | id | asserts | needs | depends |
14
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------------- |
15
+ | `list.read-after-write` | a just-created record appears on the list | create + a seeded record | — |
16
+ | `create.persists-submitted-fields` | every writable field sent on create is echoed | create that echoes the record | `list.read-after-write` |
17
+ | `payload.string-survives` | a documented-valid string is stored exactly; 4xx after an ASCII control is a fail | update or create+delete, item GET, unconstrained string | `list.read-after-write` |
18
+ | `create.status-matches-document` | create status is one the document declared | create | — |
19
+ | `response.status-is-documented` | every non-create exchange returns a status that operation names (`default` ≠ 201) | a modeled non-create operation oat invoked | — |
20
+ | `schema.success-response-matches-document` | create body validates against the success schema | success schema on create | `create.status-matches-document` |
21
+ | `schema.error-response-matches-document` | an error body validates against the documented error schema | error schema on the item route | — |
22
+ | `pagination.limit-bounds-page-size` | page size ≤ the requested limit | page-size _role_ (aliases include `limit`, `per_page`, …) | `list.read-after-write` |
23
+ | `pagination.limit-respects-documented-max` | requesting more than `maxLimit` does not return more | declared `maxLimit` and a larger cohort | `pagination.limit-bounds-page-size` |
24
+ | `pagination.has-more-is-accurate` | `hasMore` / `Link rel=next` matches whether another page exists | page-forward + `hasMore` or `Link rel=next` | `pagination.limit-bounds-page-size` |
25
+ | `pagination.page-walk-covers-set` | walking pages covers the collection with no gaps or dupes | page or offset + ≥3 records | `pagination.limit-bounds-page-size` |
26
+ | `pagination.bounds-handled` | a zero, negative, fractional or non-numeric page size, and a page past the end, are refused or served sanely — never a 5xx or a repeat | page-size or page-number parameter + ≥1 record | `pagination.page-walk-covers-set` |
27
+ | `pagination.cursor-agrees-with-page` | cursor walk and page walk yield the same set | both cursor and page | `pagination.limit-bounds-page-size` |
28
+ | `filter.unknown-field-rejected` | a filter on a field that does not exist is not silently ignored | a filter expression | — |
29
+ | `filter.equality-selects-exactly-one` | `id.eq.<one>` returns that one record | equality on the identity | `list.read-after-write` |
30
+ | `filter.zero-match-returns-none` | a filter that matches nothing returns an empty page, not the whole set | same | `list.read-after-write` |
31
+ | `filter.negation-partitions-the-set` | `eq` ∪ `neq` = whole set, intersection empty | eq and neq | `list.read-after-write`, equality |
32
+ | `filter.and-composes-as-intersection` | `and(A,B)` = A ∩ B | two filterable fields + AND | equality / list |
33
+ | `filter.or-composes-as-union` | `or(A,B)` = A ∪ B | `or()` — **postgrest grammar only** | equality / list |
34
+ | `filter.like-metacharacters-escaped` | `%` `_` `*` in a value are literals, not wildcards | like operator | `list.read-after-write` |
35
+ | `filter.numeric-comparison-is-numeric` | `gt`/`lt` on a number uses numeric order, not TEXT (`1,10,2`) | numeric field + a filter param | `list.read-after-write`, unknown-field |
36
+ | `filter.in-is-union-of-eq` | `in.(a,b)` = `eq.a` ∪ `eq.b` | field allows `in`; ≥2 distinct values | equality |
37
+ | `filter.nin-complements-in` | `in` ∩ `nin` empty; union = set minus nulls | field allows `in` and `nin` | `in` |
38
+ | `filter.gte-is-gt-or-eq` | `gte.x` = `gt.x` ∪ `eq.x` | field allows `gte` and `gt`; ordered type | numeric comparison |
39
+ | `filter.lte-is-lt-or-eq` | `lte.x` = `lt.x` ∪ `eq.x` | field allows `lte` and `lt` | numeric comparison |
40
+ | `filter.ordered-triple-partitions` | `lt` ∪ `eq` ∪ `gt` = set minus nulls; pairwise disjoint | field allows all three | numeric comparison |
41
+ | `filter.ilike-is-case-insensitive` | case-flipped value: `ilike` matches; `like` does not (unresolved if both match) | field allows both | like |
42
+ | `filter.is-null-selects-nulls` | `is.null` = nulls; `is.notnull` = complement | field allows `is`; cohort has a null | foundations |
43
+ | `filter.contains-membership` | `contains.<el>` = records whose array value includes `el` | array field or ops include `contains` | foundations |
44
+ | `filter.nested-and-or-distributes` | `and(A,or(B,C))` = (A∩B) ∪ (A∩C) | postgrest grammar; ≥2 filterable fields | `and` / `or` |
45
+ | `filter.alias-matches-canonical` | each declared alias token selects the same id-set as its target | `aliases` non-empty | equality |
46
+ | `filter.illegal-op-rejected` | one op **not** in that field's `ops` is 4xx | closed `ops` list | unknown-field |
47
+ | `filter.empty-in` | `in.()` is 4xx (`reject`) or zero rows (`match-none`) | `emptyIn` set; field allows `in` | `in` |
48
+ | `filter.in-over-limit-rejected` | `in` list of `maxInValues+1` is 4xx | `maxInValues` set | foundations |
49
+ | `filter.condition-cap-rejected` | `maxFilterConditions+1` `eq` terms is 4xx | `maxFilterConditions` set | foundations |
50
+ | `spec.declared-filterable-ops-accepted` | every declared field × every op in that field's `ops` returns <400 | closed `ops` | declared-filterable |
51
+ | `error.malformed-filter-not-5xx` | garbage filter text is 4xx, never 5xx | a filter expression | — |
52
+ | `query.filter-selects-from-whole-set` | a filter is applied to the collection, not to the current page | filterable + ≥3 records | list / walk |
53
+ | `sort.order-is-applied` | requesting a sort actually rearranges the page | order + a sortable field | `pagination.limit-bounds-page-size` |
54
+ | `sort.reverse-symmetry` | desc is the reverse of asc (nulls included) | order + asc/desc | order-is-applied |
55
+ | `sort.unknown-field-rejected` | `order` on an undeclared field is 4xx, not silent ignore | order param | — |
56
+ | `sort.numeric-order-is-numeric` | `1,10,2` sorts as numbers, not text | numeric sortable field whose lexical order disagrees | order-is-applied |
57
+ | `sort.nulls-first-last` | `nullsfirst` / `nullslast` put nulls at the start / end of asc | declared nulls token; dotted grammar; cohort has a null | order + reverse |
58
+ | `sort.multi-key-tiebreak` | ties on the first key are ordered by the second | ≥2 sortable fields; `maxKeys` absent or ≥2 | order + reverse |
59
+ | `sort.default-order-applied` | omitting `order` matches `defaultOrder` | `defaultOrder` set; walk complete | order + walk |
60
+ | `sort.stable-tiebreak` | the same `order` twice yields the same sequence | `stableTiebreak` set | order-is-applied |
61
+ | `spec.declared-sortable-nulls-accepted` | each declared nulls token returns <400 | some field/global declares `nulls` | order-is-applied |
62
+ | `search.q-narrows-result` | a search term that matches one record does not return the whole set | search param + searchable fields | `list.read-after-write` |
63
+ | `search.tokens-and` | `q=a b` = intersection (AND). Sameness of AND and OR passes | searchable fields; two tokens that split the cohort | `q-narrows` |
64
+ | `search.case-insensitive` | case-flipped token matches the same set (unresolved if the backend is case-sensitive) | searchable field with a letter | `q-narrows` |
65
+ | `search.empty-q` | `q=` is ignore / match-all / reject as declared | `searchEmpty` set | `q-narrows` |
66
+ | `search.mode-accepted` | each declared `searchModes` value is <400 on the mode param | `searchModes` set and a mode role | — |
67
+ | `select.projection-honoured` | `select=id,name` does not return undeclared fields | select param | — |
68
+ | `select.requested-fields-present` | every name in `select=` appears on each returned item | select param; ≥1 selectable field | projection |
69
+ | `select.unknown-field-rejected` | unknown select name is 4xx (`reject`) or dropped (`ignore`) | `select.unknown` set | projection |
70
+ | `select.nested-honoured` | `rel(col)` returns `rel` as an object/array carrying only `col` | `select.nested` and a named relation | projection |
71
+ | `count.consistent-with-returned-page` | envelope total ≥ rows on this page, and is not zero when the page is not | envelope total | `list.read-after-write` |
72
+ | `count.matches-filtered-set` | filtered total equals the size of the filtered walk | total + a filter | list, equality |
73
+ | `query.axes-compose` | filter + sort together: filter still holds on the sorted page | filterable + sortable | filter + sort foundations |
74
+ | `query.filter-and-select-compose` | filter + select together | filterable + select | same |
75
+ | `query.search-and-filter-compose` | search + filter together | filterable + search | same |
76
+ | `query.filter-sort-select-compose` | filter + sort + select | filter + sort + select | same |
77
+ | `query.filter-search-sort-compose` | filter + search + sort | filter + search + sort | same |
78
+ | `query.filter-search-select-compose` | filter + search + select | filter + search + select | same |
79
+ | `query.sort-and-select-compose` | sort + select: order holds; extras dropped; requested fields present | sortable + selectable | sort + select |
80
+ | `query.search-and-select-compose` | search + select: search membership holds; extras dropped | searchable + selectable | search + select |
81
+ | `query.search-and-sort-compose` | search + sort: search membership holds; remaining rows ordered | searchable + sortable | search + sort |
82
+ | `query.filter-search-sort-select-compose` | all four: filter ∩ search; order holds; extras dropped | all four axes declared | the triples |
83
+ | `query.unknown-parameter-consistent` | an undocumented query parameter is either refused or ignored — it never silently changes the set | a listing + ≥1 record | `pagination.page-walk-covers-set` |
84
+ | `spec.declared-filterable-is-filterable` | every `x-query.filterable` field actually accepts a filter | `x-query` naming filterable fields | filter foundations |
85
+ | `spec.declared-sortable-is-sortable` | every `x-query.sortable` field actually accepts a sort | `x-query` naming sortable fields | sort foundations |
86
+ | `spec.declared-selectable-is-selectable` | every `x-query.selectable` field actually accepts a select | `x-query` naming selectable fields | select |
87
+ | `tenant.item-not-readable-cross-tenant` | principal B cannot GET principal A's item | second principal, different `roots`, and a tagged or inferred tenant | — |
88
+ | `tenant.item-not-writable-cross-tenant` | principal B cannot update or delete principal A's item | second principal in another tenant, create, and update or delete | `tenant.item-not-readable-cross-tenant` |
89
+ | `tenant.denial-does-not-reveal-existence` | 404 vs 403 (or equivalent) does not distinguish "exists other tenant" from "missing" | second principal, and a tagged or inferred tenant | `tenant.item-not-readable-cross-tenant` |
90
+ | `tenant.filter-does-not-bypass-scope` | `filter=id.eq.<other tenant>` does not return that row | second principal, a filter, and a tagged or inferred tenant | `query.filter-selects-from-whole-set` |
91
+ | `tenant.parent-not-reachable-from-another-root` | a nested collection does not accept another tenant's parent under the caller's own tenant | second tenant, a collection nested below the tenant | `tenant.item-not-readable-cross-tenant` |
92
+ | `auth.rank-is-monotonic` | a lower rank cannot do what a higher rank is denied | two same-tenant principals at different `rank` | `list.read-after-write` |
93
+ | `auth.rank-is-monotonic-on-writes` | a lower rank cannot create, update or delete where a higher rank is denied | two same-tenant principals at different `rank`, and a create | `auth.rank-is-monotonic` |
94
+ | `auth.invite-grants-then-revokes` | invite → accept grants; revoke takes it back | `x-invite` + peer with `inviteAs` | list, cross-tenant |
95
+ | `create.unique-conflict-rejected` | a second create colliding a documented unique set is 409, not 2xx; the list does not grow | `x-unique` with a probeable create body column and a known row | — |
96
+ | `update.unique-conflict-rejected` | PATCHing a **different** row onto another row's unique-set values is 409, not 2xx | `x-unique`, update, two known rows; skip all-`x-immutable` sets | — |
97
+ | `patch.immutable-field-rejected` | PATCHing an `x-immutable` field is rejected or ignored | `x-immutable` | — |
98
+ | `softdelete.absent-from-default-list` | a soft-deleted row is gone from the default list | `x-soft-delete` | `list.read-after-write` |
99
+ | `invalidation.declared-route-changes` | after a write, the other entity's listed route actually changes | `x-invalidate` naming another entity | list, persist |
100
+ | `effects.declared-effect-occurs` | `x-effects` cardinality delta is observed on the named list (`count` exact, `min` at-least; nested child lists bind the created parent id) | `x-effects` | `list.read-after-write` |
101
+ | `effects.side-effect-arrives` | after the write, the named GET’s JSON path is occupied before `timeoutMs` | `x-wait` | `list.read-after-write` |
102
+ | `async.reaches-terminal-state` | `x-async` reaches `until` (poll, or a terminal SSE frame) before `timeoutMs` | `x-async` | — |
103
+ | `async.receipt-identifies-the-job` | `idFrom` on the receipt (JSON object or SSE event JSON) resolves to a pollable job | `x-async` + `idFrom` | — |
104
+ | `patch.minimality` | PATCH `{ name }` does not clear other writable fields | update + item route | — |
105
+ | `idempotency.replay-does-not-duplicate` | same Idempotency-Key + same body does not create a second row | create + documented Idempotency-Key header | list, persist |
106
+ | `delete.absent-record-returns-404` | DELETE of a missing id is 404, not 200 | delete | — |
107
+ | `concurrency.no-lost-update` | two PATCHes to different fields do not clobber each other | update + two writable strings | persist + patch |
108
+ | `validation.enum-enforced` | a value outside the enum is rejected | enum in the request schema | — |
109
+ | `validation.max-length-enforced` | a string over `maxLength` is rejected | maxLength | — |
110
+ | `validation.required-enforced` | omitting a required field is rejected | required field | — |
111
+ | `validation.content-type-enforced` | a wrong Content-Type is 415 when 415 is documented | documented 415 | — |
112
+ | `spec.declared-rate-limit-is-honoured` | no 429 arrives while oat paces under the rate `x-rate-limit` declares | `x-rate-limit` with a rate | — |
113
+ | `consistency.projections-agree` | list, item, and filtered views of the same field agree | item route + a comparable field | list + persist + filter |
114
+
115
+ On a typical untagged CRUD document (create, list, item, `page`/`limit`, maybe `sort`):
116
+
117
+ - Foundations, PATCH/delete, and schema checks usually run.
118
+ - The query matrix runs when filter/order/select/search roles resolve.
119
+ - Isolation runs only if you configured two principals.
120
+ - Spec-as-adversary and tagged behaviour never run — `doctor` says so.
121
+
122
+ Worked examples of what a finding looks like:
123
+
124
+ ```text
125
+ BACKEND_BUG table created row missing from GET /tables
126
+ list.read-after-write
127
+
128
+ BACKEND_BUG product PATCH { name } cleared description
129
+ patch.minimality
130
+
131
+ SECURITY table GET /tables/{id} readable with the other tenant's key
132
+ tenant.item-not-readable-cross-tenant
133
+
134
+ SPEC_BUG table x-query.filterable lists "ghost"; filter=ghost.eq.x is 400
135
+ spec.declared-filterable-is-filterable
136
+
137
+ BACKEND_BUG table in() is not the union of the equalities it lists
138
+ filter.in-is-union-of-eq
139
+
140
+ BACKEND_BUG table an illegal filter operator is accepted
141
+ filter.illegal-op-rejected
142
+
143
+ COVERAGE_GAP batch no x-async; receipt treated as the result
144
+ async.reaches-terminal-state
145
+ ```
146
+
147
+ ## Verdicts, skips, and exit codes
148
+
149
+ | verdict | meaning | fails `oat run`? |
150
+ | -------------- | -------------------------------------------------------------------- | ---------------- |
151
+ | `BACKEND_BUG` | the handler is wrong | yes |
152
+ | `SPEC_BUG` | the document is wrong (or disagrees with the handler) | yes |
153
+ | `SECURITY` | isolation/authz failure and `x-tenant` (or equivalent) was declared | yes |
154
+ | `AMBIGUITY` | same evidence, but the tenant boundary was only inferred | yes |
155
+ | `COVERAGE_GAP` | the check could not run; the report names the missing tag or surface | no |
156
+ | `BLOCKED` | a check this one depends on already failed | no |
157
+
158
+ Separate from findings:
159
+
160
+ - **did not apply** — entity never had what `needs` lists (printed in the report, not a pass). Example: no `select` parameter → `select.projection-honoured` did not apply.
161
+ - **inconclusive** — the check ran and stopped (empty listing, probe 4xx, no shared filterable field, …). Not a pass. The report prints the reason.
162
+
163
+ Coverage is split **never** (zero entities could run it) vs **partial** (ran on some entities, skipped on others). A clean run still prints both. "Nothing found" and "nothing was looked for" are different.
164
+
165
+ Cascade suppression is transitive: one root cause is one finding, not a page of consequences. A blocked check has **not** been verified; re-run after the cause is fixed.
166
+
167
+ `oat run` exit `1` if any finding has a failing verdict. Gaps and blocked entries do not fail a full run. A targeted run (`--ops`) also exits `1` when any target did not end `held`.
168
+
169
+ Console (stdout) after a run:
170
+
171
+ ```
172
+ 50 checks · 5 entities · 842 requests · 41.2s · p95 90ms · 4 checks did not apply
173
+
174
+ BACKEND DEFECTS (1)
175
+ product PATCH { name } also cleared description
176
+ patch.minimality
177
+
178
+ DID NOT APPLY — no entity had what these need
179
+ async.reaches-terminal-state needs an operation declaring x-async
180
+
181
+ leftover teardown printed next
182
+ report / matrix / graph / progress paths
183
+ ```
@@ -0,0 +1,214 @@
1
+ # oat — Commands
2
+
3
+ [← back to the overview](../README.md)
4
+
5
+ ## Commands
6
+
7
+ ```
8
+ oat run --config <file> test a live backend and write a report
9
+ oat doctor --spec <url|file> what oat can and cannot test, and why
10
+ oat plan --spec <url|file> print the derived model (offline)
11
+ oat serve [--defects A,B] run the demo API
12
+ oat conformance self-test: injected defects vs detection
13
+ oat help
14
+ ```
15
+
16
+ `--spec` for `doctor` / `plan` can be replaced by `--config` (the spec is read from the config). `--json` makes those two commands emit machine-readable output. `--base-url` on `doctor` / `plan` is only used to resolve a relative spec path.
17
+
18
+ `--untagged` is a **serve** flag (and a conformance concern). It is not a `run` flag.
19
+
20
+ ### `oat run`
21
+
22
+ Requires `--config`. CLI flags override the same field in the config when both are set.
23
+
24
+ | flag | default | meaning |
25
+ | ------------------------------------------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | `--config` | required | module or JSON file, default export |
27
+ | `--base-url` | `config.baseUrl` | backend origin |
28
+ | `--ops` | `config.ops` or every operation | comma-separated operationIds to grade; everything else is support. `*` globs, `<originId>:` prefixes. See [Targeted runs](#targeted-runs---ops) |
29
+ | `--only` | `config.only` or all entities | comma-separated entity names as `oat plan` prints them (singularised); each grades every operation it owns. Joins `--ops` as a union |
30
+ | `--seed` | `config.seed` or `1` | fixture generation seed (reproducible) |
31
+ | `--out` | `config.outDir` or `./.oat/runs` | history root; each run writes `<out>/<datetime>/` and updates `latest` |
32
+ | `--max-in-flight` | `config.maxInFlight` or `4` | HTTP requests allowed at once |
33
+ | `--keep-fixtures` | `config.keepFixtures` or false | do not DELETE what the run created |
34
+ | `--quiet` | false | no stderr progress; files under `--out` still update |
35
+ | `--save-exchanges` / `--no-save-exchanges` | on unless `--profile cheap` | persist every HTTP exchange under the run dir (`exchanges.jsonl`, `exchanges/`, `blobs/`). Does **not** change in-memory transcript RAM |
36
+
37
+ **Exit codes:** `0` no defects, `1` at least one root-cause finding (`BACKEND_BUG`, `SPEC_BUG`, `SECURITY`, `AMBIGUITY`) **or**, on a targeted run, a target that `failed`, `2` usage error (missing `--config`, no principals, unknown flag, a flag without its value or with a value of the wrong kind, an `--ops` / `--only` name that matches nothing, a target the profile excludes), `3` oat could not do what it was asked — the network never came back, the run graded no operation at all, a target was never judged, or oat itself failed. `COVERAGE_GAP` and `BLOCKED` do not fail a full run. `latest` moves to a run only once its reports are written.
38
+
39
+ Example:
40
+
41
+ ```bash
42
+ oat run --config oat.config.ts --only store,product --out .oat/runs/prod
43
+ ```
44
+
45
+ `--only store,product` matches the **entity names** from `oat plan`, not path segments. `/v1/stores` is usually the entity `store`. An unknown name exits `2` with the nearest names.
46
+
47
+ A run with no principals exits `2`. Isolation checks then need a second principal; they are skipped, not failed, when only one is present.
48
+
49
+ ### Targeted runs (`--ops`)
50
+
51
+ A full run grades every operation. After changing two endpoints you usually want those two, not the whole API:
52
+
53
+ ```bash
54
+ oat run --config oat.config.ts --ops table.create,row.list
55
+ oat run --config oat.config.ts --ops 'row.*' # glob within an operationId
56
+ oat run --config oat.config.ts --ops cdn:asset.get # an operation on the origin with id "cdn"
57
+ oat plan --config oat.config.ts --ops table.create # dry run: what would be graded, queued, called
58
+ ```
59
+
60
+ **Targets** are graded: every check whose subjects include a target runs the full matrix against it. **Support** operations are called and never graded: principal sign-up, parent creates, the cohort create behind a list, reads used as an oracle, teardown deletes. A list route cannot be tested without rows in it, so its create still runs, but a defect in that create is out of this run's scope.
61
+
62
+ How the scope is decided:
63
+
64
+ - Every check declares its **subjects**, the operations whose contract it judges (`filter.*` judges the list route, `patch.minimality` the update, `effects.declared-effect-occurs` each `x-effects` operation). A check runs when one of its subjects is a target. Others are **out of scope**: not run, not skipped.
65
+ - An entity is seeded only when one of its checks grades a target. Declared edges pull in the other direction too: targeting a read route that another entity's create names in `x-invalidate` queues that entity, so its invalidation check grades the route.
66
+ - Checks that judge several operations (`effects.*`, `async.*`, `response.status-is-documented`, `schema.error-response-matches-document`, `invalidation.declared-route-changes`) invoke and judge only the targeted ones. An untargeted `x-effects` upload is never POSTed.
67
+ - Every finding names the operations it judges (`operations` in `oat-report.json`).
68
+ - An operation counts as graded only when a check exercised it. A check that judges after the fact (`response.status-is-documented` reads the transcript) grades only the operations it saw. A check that reports a coverage gap for an operation did not grade it. `--only <entity>` therefore surfaces every action no check can reach as `untested`.
69
+
70
+ Each target ends with one status:
71
+
72
+ | status | meaning |
73
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `held` | at least one check graded it and none failed |
75
+ | `failed` | a defect finding is attributed to it |
76
+ | `inconclusive` | checks ran and none could decide |
77
+ | `blocked` | every check was suppressed, or a support operation it needed failed (named, with evidence) |
78
+ | `untested` | nothing exercised it. The reason says why (`unmodeled`, `entity-not-testable`, `coverage gap: …`, `no check applied: needs …`, `no check exercised it`) |
79
+
80
+ A targeted run exits `0` only when every target is `held`. A target nothing could grade must not read as a pass.
81
+
82
+ Validation happens before any principal signs up: a name that matches nothing exits `2` with the nearest operationIds; an exact target the active `--profile` excludes exits `2`; a glob or `--only` match the profile excludes is dropped and listed. An origin no target names is not run.
83
+
84
+ A targeted run is for iterating. It grades what you named and what those operations observably affect through declared edges. It does not catch a regression in an untargeted operation caused by shared code, so run the full suite before a release.
85
+
86
+ ### `oat doctor`
87
+
88
+ Offline. Loads the document, builds the model, prints coverage.
89
+
90
+ ```bash
91
+ oat doctor --spec https://api.example.com/openapi.json
92
+ oat doctor --config oat.config.ts --json
93
+ oat doctor --spec ./openapi.yaml --base-url https://api.example.com
94
+ ```
95
+
96
+ Human output:
97
+
98
+ - `trackable` — entities with an identity and a read surface
99
+ - `listable` — those that also have a list (query checks need this)
100
+ - tags that are absent, and the checks each tag would unlock
101
+ - tags that would **sharpen** checks that already run (`x-query`, `x-tenant`)
102
+ - per-operation gaps (`x-entity` could not be inferred, assumed tenant param, …)
103
+ - external `$ref`s that were not fetched
104
+
105
+ `--json` shape:
106
+
107
+ ```json
108
+ {
109
+ "blocking": 1,
110
+ "entities": 12,
111
+ "trackableEntities": 10,
112
+ "testableEntities": 10,
113
+ "listableEntities": 8,
114
+ "roots": ["organization_id"],
115
+ "externalRefs": ["https://example.com/shared.yaml"],
116
+ "gaps": [{ "operationId": "table.list", "tag": "x-query", "detail": "…" }]
117
+ }
118
+ ```
119
+
120
+ Exit `1` if there are **blocking** gaps: entities that are not trackable (no identity / no read), or the document has roots oat cannot create. Advisory gaps (missing `x-query`, no `x-async`) print and still exit `0`.
121
+
122
+ ### `oat plan`
123
+
124
+ Offline. Prints the derived entity graph, operations, and query capability.
125
+
126
+ ```bash
127
+ oat plan --spec ./openapi.yaml
128
+ oat plan --config oat.config.ts --json
129
+ ```
130
+
131
+ Human columns:
132
+
133
+ ```
134
+ entity CLRUD ident read surface
135
+ store CLRU· id 2 route(s) (inferred)
136
+ GET /v1/stores
137
+ GET /v1/stores/{store_id}
138
+ ```
139
+
140
+ `CLRUD` is Create / List / Read / Update / Delete. `·` means that slot is missing. `ident` is the identity property. Read surface is declared (`x-invalidate`) or inferred (sibling collection/item routes).
141
+
142
+ `--json` is `{ entities, operations, roots }` — the full `SpecModel` maps, including conventions, query capability, async, invite, and path params. Use this when you need to know what oat will call something.
143
+
144
+ With `--ops` / `--only` (flag or config), `plan` prints the targeted scope instead: each target with its entity and the checks that would grade it, untestable targets with the reason, the entities that would be queued, and the support operations predicted from the graph. `--json` adds a `scope` object with the same fields. An unknown name exits `2`.
145
+
146
+ ### `oat serve`
147
+
148
+ In-process demo API. Same fixture as conformance.
149
+
150
+ ```bash
151
+ oat serve
152
+ oat serve --defects STALE_LIST,PATCH_REPLACES
153
+ oat serve --backend sqlite --dialect classic
154
+ oat serve --untagged
155
+ ```
156
+
157
+ | flag | default | meaning |
158
+ | ------------ | ----------- | ---------------------------------------------------------------------------------------------------------- |
159
+ | `--backend` | `memory` | `memory` \| `sqlite` \| `postgres` |
160
+ | `--dialect` | `postgrest` | `postgrest` \| `classic` \| `linked` \| `jsonapi` \| `plain` |
161
+ | `--defects` | none | comma-separated names from [Reference defects](reference-defects.md#reference-defects-oat-serve---defects) |
162
+ | `--untagged` | false | serve the same API behind a spec with every `x-*` tag stripped |
163
+
164
+ Printed keys: `key_alpha` (tenant `proj_alpha`), `key_beta` (tenant `proj_beta`). Spec: `{url}/v1/openapi/spec`. Stop with ctrl-c.
165
+
166
+ `labs/local.config.ts` is written for this server.
167
+
168
+ Dialects are **reference-backend shapes**, not something you configure against your API. They exist so conformance proves checks read the document rather than one fixture's spelling:
169
+
170
+ | dialect | filter | sort | select | page model | envelope |
171
+ | ----------- | --------------------------- | ---------- | ---------------- | ------------------- | --------------------------------- |
172
+ | `postgrest` | `filter=status.eq.active` | `name.asc` | `select=id,name` | `page` + `cursor` | entity-named + `count`/`hasMore` |
173
+ | `classic` | `filter=status=eq:active` | `sort=` | `fields=` | `page` + `per_page` | `{ data, total_count, has_more }` |
174
+ | `linked` | postgrest | dotted | `fields=` | `offset` + `limit` | raw array + `Link: rel=next` |
175
+ | `jsonapi` | postgrest | `-name` | `fields[table]=` | `page` + `size` | `{ data, total, has_more }` |
176
+ | `plain` | `?status=active` (equality) | `name:asc` | `fields=` | `page` + `limit` | `{ items, total, has_more }` |
177
+
178
+ `postgres` needs a server on the default `postgres` database (local, default `postgres` driver connection). `sqlite` needs Node's `node:sqlite` (`--experimental-sqlite` on Node 22). Missing backends fail at serve time rather than falling back.
179
+
180
+ ### `oat conformance`
181
+
182
+ Self-test. Not for your API. Injects named defects into the reference backend and asserts oat reports the matching check.
183
+
184
+ ```bash
185
+ # this repo
186
+ npm test
187
+
188
+ # after install, from a checkout with --experimental-sqlite if you want sqlite
189
+ oat conformance
190
+ oat conformance --backend memory --dialect plain
191
+ oat conformance --fuzz 300 --max-defects 12 --seed 7
192
+ oat conformance --precision 60 --backend memory
193
+ oat conformance --parser
194
+ oat conformance --backend d1
195
+ oat conformance --only STALE_LIST,PATCH_REPLACES
196
+ ```
197
+
198
+ | flag | meaning |
199
+ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
200
+ | `--backend` | `memory` \| `sqlite` \| `postgres` \| `d1`. Default: every _local_ backend that is available. `d1` is never default — it is remote |
201
+ | `--dialect` | pin one shape; default runs postgrest on each backend plus classic/linked/jsonapi/plain on memory |
202
+ | `--fuzz [n]` | random _sets_ of defects (default 25 if flag is bare) |
203
+ | `--max-defects` | cap per fuzz combination (default 4) |
204
+ | `--precision [n]` | vary _data_ against a correct backend; any finding is a false positive (default 50 if flag is bare) |
205
+ | `--seed` | replay a fuzz/precision run |
206
+ | `--parser` | only the hostile-document + example-spec + tag-unlock suites |
207
+ | `--only` | restrict injected defects (comma-separated `STALE_LIST,…`) |
208
+ | `--jobs` | legs run at once, each on its own thread (default: CPUs − 1, at most 4) |
209
+
210
+ A default `oat conformance` (no `--fuzz` / `--precision` / `--parser`) also runs a 40-case combination smoke on memory after the one-at-a-time matrix, and `--ops` recall for every defect, reusing the matrix's own runs. The extra dialects run only the defects whose symptom depends on how a listing is asked for and answered.
211
+
212
+ D1 needs `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_D1_DATABASE_ID`, `CLOUDFLARE_API_TOKEN`. Postgres needs a reachable server on the default connection (`database: "postgres"`). Missing backends are skipped with a printed reason, not treated as a pass.
213
+
214
+ `--parser` still always runs first (hostile documents, `labs/annotated-openapi.yaml` model lock, tag-unlock map). Exit `1` if any parser, matrix, fuzz, or precision case fails.