@lovrozagar/oat 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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +401 -0
  3. package/bin/oat.js +2 -0
  4. package/dist/cli.js +386 -0
  5. package/dist/config/define-config.d.ts +165 -0
  6. package/dist/config/define-config.d.ts.map +1 -0
  7. package/dist/config/define-config.js +24 -0
  8. package/dist/config/define-config.js.map +1 -0
  9. package/dist/config/load.d.ts +10 -0
  10. package/dist/config/load.d.ts.map +1 -0
  11. package/dist/config/load.js +61 -0
  12. package/dist/config/load.js.map +1 -0
  13. package/dist/index.d.ts +17 -0
  14. package/dist/index.js +9 -0
  15. package/dist/reference/defects.d.ts +254 -0
  16. package/dist/reference/defects.d.ts.map +1 -0
  17. package/dist/reference/defects.js +265 -0
  18. package/dist/reference/defects.js.map +1 -0
  19. package/dist/reference/dialect.d.ts +104 -0
  20. package/dist/reference/dialect.d.ts.map +1 -0
  21. package/dist/reference/dialect.js +174 -0
  22. package/dist/reference/dialect.js.map +1 -0
  23. package/dist/reference/http.d.ts +79 -0
  24. package/dist/reference/http.d.ts.map +1 -0
  25. package/dist/reference/http.js +889 -0
  26. package/dist/reference/http.js.map +1 -0
  27. package/dist/reference/model.d.ts +56 -0
  28. package/dist/reference/model.d.ts.map +1 -0
  29. package/dist/reference/model.js +156 -0
  30. package/dist/reference/model.js.map +1 -0
  31. package/dist/reference/query.d.ts +45 -0
  32. package/dist/reference/query.d.ts.map +1 -0
  33. package/dist/reference/query.js +353 -0
  34. package/dist/reference/query.js.map +1 -0
  35. package/dist/reference/spec.d.ts +13 -0
  36. package/dist/reference/spec.d.ts.map +1 -0
  37. package/dist/reference/spec.js +547 -0
  38. package/dist/reference/spec.js.map +1 -0
  39. package/dist/reference/store-api.d.ts +59 -0
  40. package/dist/reference/store-api.d.ts.map +1 -0
  41. package/dist/reference/store-api.js +76 -0
  42. package/dist/reference/store-api.js.map +1 -0
  43. package/dist/reference/stores/memory.d.ts +27 -0
  44. package/dist/reference/stores/memory.d.ts.map +1 -0
  45. package/dist/reference/stores/memory.js +97 -0
  46. package/dist/reference/stores/memory.js.map +1 -0
  47. package/dist/reference/stores/postgres.d.ts +61 -0
  48. package/dist/reference/stores/postgres.d.ts.map +1 -0
  49. package/dist/reference/stores/postgres.js +406 -0
  50. package/dist/reference/stores/postgres.js.map +1 -0
  51. package/dist/reference/stores/sqlite-driver.d.ts +54 -0
  52. package/dist/reference/stores/sqlite-driver.d.ts.map +1 -0
  53. package/dist/reference/stores/sqlite-driver.js +99 -0
  54. package/dist/reference/stores/sqlite-driver.js.map +1 -0
  55. package/dist/reference/stores/sqlite.d.ts +68 -0
  56. package/dist/reference/stores/sqlite.d.ts.map +1 -0
  57. package/dist/reference/stores/sqlite.js +464 -0
  58. package/dist/reference/stores/sqlite.js.map +1 -0
  59. package/dist/report/console.d.ts +33 -0
  60. package/dist/report/console.d.ts.map +1 -0
  61. package/dist/report/console.js +237 -0
  62. package/dist/report/console.js.map +1 -0
  63. package/dist/report/matrix.d.ts +131 -0
  64. package/dist/report/matrix.d.ts.map +1 -0
  65. package/dist/report/matrix.js +865 -0
  66. package/dist/report/matrix.js.map +1 -0
  67. package/dist/report/render.d.ts +66 -0
  68. package/dist/report/render.d.ts.map +1 -0
  69. package/dist/report/render.js +423 -0
  70. package/dist/report/render.js.map +1 -0
  71. package/dist/runtime/async.d.ts +34 -0
  72. package/dist/runtime/async.d.ts.map +1 -0
  73. package/dist/runtime/async.js +130 -0
  74. package/dist/runtime/async.js.map +1 -0
  75. package/dist/runtime/auth.d.ts +46 -0
  76. package/dist/runtime/auth.d.ts.map +1 -0
  77. package/dist/runtime/auth.js +236 -0
  78. package/dist/runtime/auth.js.map +1 -0
  79. package/dist/runtime/checks.d.ts +89 -0
  80. package/dist/runtime/checks.d.ts.map +1 -0
  81. package/dist/runtime/checks.js +3400 -0
  82. package/dist/runtime/checks.js.map +1 -0
  83. package/dist/runtime/client.d.ts +65 -0
  84. package/dist/runtime/client.d.ts.map +1 -0
  85. package/dist/runtime/client.js +122 -0
  86. package/dist/runtime/client.js.map +1 -0
  87. package/dist/runtime/finding.d.ts +50 -0
  88. package/dist/runtime/finding.d.ts.map +1 -0
  89. package/dist/runtime/finding.js +50 -0
  90. package/dist/runtime/finding.js.map +1 -0
  91. package/dist/runtime/fixture.d.ts +20 -0
  92. package/dist/runtime/fixture.d.ts.map +1 -0
  93. package/dist/runtime/fixture.js +175 -0
  94. package/dist/runtime/fixture.js.map +1 -0
  95. package/dist/runtime/progress.d.ts +41 -0
  96. package/dist/runtime/progress.d.ts.map +1 -0
  97. package/dist/runtime/progress.js +186 -0
  98. package/dist/runtime/progress.js.map +1 -0
  99. package/dist/runtime/run.d.ts +69 -0
  100. package/dist/runtime/run.d.ts.map +1 -0
  101. package/dist/runtime/run.js +575 -0
  102. package/dist/runtime/run.js.map +1 -0
  103. package/dist/runtime/teardown.d.ts +38 -0
  104. package/dist/runtime/teardown.d.ts.map +1 -0
  105. package/dist/runtime/teardown.js +129 -0
  106. package/dist/runtime/teardown.js.map +1 -0
  107. package/dist/runtime/validate.d.ts +31 -0
  108. package/dist/runtime/validate.d.ts.map +1 -0
  109. package/dist/runtime/validate.js +111 -0
  110. package/dist/runtime/validate.js.map +1 -0
  111. package/dist/runtime/world.d.ts +49 -0
  112. package/dist/runtime/world.d.ts.map +1 -0
  113. package/dist/runtime/world.js +119 -0
  114. package/dist/runtime/world.js.map +1 -0
  115. package/dist/spec/collection.d.ts +31 -0
  116. package/dist/spec/collection.d.ts.map +1 -0
  117. package/dist/spec/collection.js +121 -0
  118. package/dist/spec/collection.js.map +1 -0
  119. package/dist/spec/conventions.d.ts +103 -0
  120. package/dist/spec/conventions.d.ts.map +1 -0
  121. package/dist/spec/conventions.js +275 -0
  122. package/dist/spec/conventions.js.map +1 -0
  123. package/dist/spec/extensions.d.ts +74 -0
  124. package/dist/spec/extensions.d.ts.map +1 -0
  125. package/dist/spec/extensions.js +335 -0
  126. package/dist/spec/extensions.js.map +1 -0
  127. package/dist/spec/graph.d.ts +89 -0
  128. package/dist/spec/graph.d.ts.map +1 -0
  129. package/dist/spec/graph.js +318 -0
  130. package/dist/spec/graph.js.map +1 -0
  131. package/dist/spec/load.d.ts +36 -0
  132. package/dist/spec/load.d.ts.map +1 -0
  133. package/dist/spec/load.js +166 -0
  134. package/dist/spec/load.js.map +1 -0
  135. package/dist/spec/types.d.ts +57 -0
  136. package/dist/spec/types.d.ts.map +1 -0
  137. package/dist/spec/types.js +24 -0
  138. package/dist/spec/types.js.map +1 -0
  139. package/labs/README.md +130 -0
  140. package/labs/annotated-openapi.yaml +311 -0
  141. package/labs/anyrow.config.ts +107 -0
  142. package/labs/demo.mjs +124 -0
  143. package/labs/local.config.ts +41 -0
  144. package/labs/minimal.config.ts +32 -0
  145. package/package.json +80 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/config/load.ts"],"names":[],"mappings":"AAAA;;;;GAIG;;;;;;;;;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAGxC,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAY;IAC5C,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAA;IAEvE,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAc,CAAA;IACjE,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,kCAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAC,CAE7C,CAAA;QACb,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,IAAI,MAAM,CAAA;QACvC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAA;QACvD,CAAC;QACD,OAAO,MAAM,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACtE,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,oCAAoC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CACd,oBAAoB,IAAI,0DAA0D;gBACjF,uEAAuE,CACxE,CAAA;QACF,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,KAAK,OAAO,EAAE,CAAC,CAAA;IAClE,CAAC;AACF,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,WAAW,CAAI,KAAQ,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC5E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;YACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;YAC1B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,wCAAwC,CAAC,CAAA;YAC3F,CAAC;YACD,OAAO,QAAQ,CAAA;QAChB,CAAC,CAAM,CAAA;IACR,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAM,CAAA;IACjF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACjD,MAAM,GAAG,GAA4B,EAAE,CAAA;QACvC,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YAC5E,GAAG,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,GAAQ,CAAA;IAChB,CAAC;IACD,OAAO,KAAK,CAAA;AACb,CAAC"}
@@ -0,0 +1,17 @@
1
+ export { defineConfig } from "./config/define-config.ts";
2
+ export type { AuthFlow, AuthStep, Hooks, OatConfig, OperationStep, OutOfBandRequest, OutOfBandStep, Principal, RequestStep, } from "./config/define-config.ts";
3
+ export { loadConfig } from "./config/load.ts";
4
+ export { deriveCollectionShape, deriveIdentity } from "./spec/collection.ts";
5
+ export type { CollectionShape } from "./spec/collection.ts";
6
+ export { buildModel } from "./spec/graph.ts";
7
+ export type { EntityModel, OperationModel, SpecModel } from "./spec/graph.ts";
8
+ export { dereference, loadSpec, normalisePath, parseRouteRef } from "./spec/load.ts";
9
+ export type { OpenApiDocument } from "./spec/types.ts";
10
+ export { run } from "./runtime/run.ts";
11
+ export type { RunOptions, RunResult } from "./runtime/run.ts";
12
+ export type { Actor } from "./runtime/checks.ts";
13
+ export type { Finding, Verdict } from "./runtime/finding.ts";
14
+ export { buildMatrixGraph, matrixViewFromReport, renderMatrixGraph, renderMatrixHtml, } from "./report/matrix.ts";
15
+ export type { EntityMatrix, InvalidateLink, MatrixGraph, MatrixGraphEdge, MatrixGraphNode, } from "./report/matrix.ts";
16
+ export { coverageByCheck, renderConsole, renderJson, renderMarkdown, renderRepros } from "./report/render.ts";
17
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { defineConfig } from "./config/define-config.js";
2
+ export { loadConfig } from "./config/load.js";
3
+ export { deriveCollectionShape, deriveIdentity } from "./spec/collection.js";
4
+ export { buildModel } from "./spec/graph.js";
5
+ export { dereference, loadSpec, normalisePath, parseRouteRef } from "./spec/load.js";
6
+ export { run } from "./runtime/run.js";
7
+ export { buildMatrixGraph, matrixViewFromReport, renderMatrixGraph, renderMatrixHtml, } from "./report/matrix.js";
8
+ export { coverageByCheck, renderConsole, renderJson, renderMarkdown, renderRepros } from "./report/render.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Defects the reference backend can be told to exhibit.
3
+ *
4
+ * Each one is a real bug seen in production APIs, reduced to its smallest form. The conformance
5
+ * suite switches them on individually and asserts oat reports exactly that defect and nothing
6
+ * else — which is what turns "oat is smart" into precision and recall numbers.
7
+ *
8
+ * Keep the implementations naive and obvious. If the reference backend grows clever it starts
9
+ * encoding the same assumptions oat does, and the two drift together into agreeing on nonsense.
10
+ */
11
+ export declare const DEFECTS: {
12
+ /** Sort has no total order, so keyset paging silently drops and repeats rows. */
13
+ readonly UNSTABLE_SORT: "sort lacks a tiebreak; page walks drop or duplicate rows";
14
+ /** Writes land in the item store but the list projection serves a stale snapshot. */
15
+ readonly STALE_LIST: "list projection does not reflect writes visible on the item route";
16
+ /** Unrecognised filter fields are dropped instead of rejected. */
17
+ readonly FILTER_IGNORED: "unknown filter field is silently ignored rather than rejected";
18
+ /** Cursor pagination and offset pagination disagree about the underlying set. */
19
+ readonly CURSOR_DRIFT: "cursor pagination yields a different set than offset pagination";
20
+ /** PATCH behaves as PUT, clearing fields the caller never mentioned. */
21
+ readonly PATCH_REPLACES: "PATCH clears fields absent from the request body";
22
+ /** Total count ignores the active filter. */
23
+ readonly COUNT_IGNORES_FILTER: "count reflects the unfiltered set";
24
+ /** The tenant predicate is applied to the list query but not to filter matches. */
25
+ readonly TENANT_LEAK_VIA_FILTER: "filter bypasses the tenant predicate";
26
+ /**
27
+ * Denies cross-tenant reads with 403 rather than 404.
28
+ *
29
+ * The access decision is correct — the record is not served — but the status distinguishes
30
+ * "exists, not yours" from "does not exist", which turns the item route into an oracle for
31
+ * enumerating other tenants' identifiers.
32
+ */
33
+ readonly EXISTENCE_LEAK_VIA_STATUS: "cross-tenant denial uses 403, revealing that the record exists";
34
+ /**
35
+ * Accepts an Idempotency-Key and creates a second record anyway.
36
+ *
37
+ * The failure mode is a retry — a timeout, a proxy replay, a double-click — silently
38
+ * duplicating a charge or an order. The key is the client's only defence against that, and
39
+ * whether it is honoured cannot be established without replaying a request.
40
+ */
41
+ readonly IDEMPOTENCY_IGNORED: "Idempotency-Key is accepted but replay creates a duplicate record";
42
+ /**
43
+ * A write invalidates a route the document says it invalidates — except it does not.
44
+ *
45
+ * The parent keeps serving a derived value computed before the child write: a denormalised
46
+ * counter nobody refreshes, or a cached projection whose key was never busted. The write
47
+ * succeeds, the child's own listing is correct, and only the *other* route the document named
48
+ * is wrong — which is why nothing short of following `x-invalidate` finds it.
49
+ */
50
+ readonly PARENT_PROJECTION_STALE: "a route named by x-invalidate keeps serving pre-write data";
51
+ /**
52
+ * The collection serves a different value for a field than the item route does.
53
+ *
54
+ * A denormalised listing, a search index rebuilt on a lag, a cached projection: each read path
55
+ * is internally consistent and individually plausible, so every per-projection check passes.
56
+ * Only comparing the paths against each other reveals that a client's view of a record depends
57
+ * on which route it happened to use.
58
+ */
59
+ readonly LIST_DETAIL_DISAGREE: "the collection and the item route serve different values for a field";
60
+ /**
61
+ * The filter is applied when it is the only query parameter, and dropped once a sort joins it.
62
+ *
63
+ * Models the real failure: adding an ORDER BY changes which index the planner picks, and the
64
+ * predicate rides on the index that is no longer chosen. Each axis is provably correct on its
65
+ * own, so a suite that only ever tests one at a time reports the API as clean.
66
+ */
67
+ readonly FILTER_DROPPED_WHEN_SORTED: "a filter stops being applied once a sort is also requested";
68
+ /**
69
+ * The filter is applied when it is the only query parameter, and dropped once a sparse
70
+ * fieldset joins it.
71
+ *
72
+ * The realistic shape: a query builder that projects first and then cannot (or does not)
73
+ * apply a predicate to a column it just omitted — or that takes a different code path the
74
+ * moment `fields=` is present and forgets the WHERE. Each axis is correct alone.
75
+ */
76
+ readonly FILTER_DROPPED_WHEN_SELECTED: "a filter stops being applied once a select is also requested";
77
+ /**
78
+ * The filter is applied when it is the only query parameter, and dropped once a free-text
79
+ * search joins it.
80
+ *
81
+ * Adding `q` often switches the backend onto a search-index path that does not honour the
82
+ * structured predicate. Search alone is correct, the filter alone is correct, and together
83
+ * the filter silently vanishes.
84
+ */
85
+ readonly FILTER_DROPPED_WHEN_SEARCHED: "a filter stops being applied once a search is also requested";
86
+ /**
87
+ * The filter survives each pair — filter+sort and filter+select — and vanishes only when
88
+ * both extra axes are present at once.
89
+ *
90
+ * A query planner that has a two-axis path and a three-axis path, and only the latter
91
+ * forgets the WHERE. Every pairwise check passes; the bug is the triple.
92
+ */
93
+ readonly FILTER_DROPPED_WHEN_SORTED_AND_SELECTED: "a filter stops being applied once a sort and a select are requested together";
94
+ /**
95
+ * The filter survives filter+search and filter+sort, and vanishes only when a search and a
96
+ * sort are requested together — typically the search-index path that also tries to honour
97
+ * an ORDER BY and drops the structured predicate to do it.
98
+ */
99
+ readonly FILTER_DROPPED_WHEN_SORTED_AND_SEARCHED: "a filter stops being applied once a sort and a search are requested together";
100
+ /**
101
+ * The filter survives filter+search and filter+select, and vanishes only when a search and
102
+ * a sparse fieldset are requested together.
103
+ */
104
+ readonly FILTER_DROPPED_WHEN_SEARCHED_AND_SELECTED: "a filter stops being applied once a search and a select are requested together";
105
+ /**
106
+ * The page window is computed over the *unfiltered* set, and the filter is applied to whatever
107
+ * that window happened to contain.
108
+ *
109
+ * Page one usually looks right, which is what makes it survive review. The damage shows up
110
+ * further in: pages come back short, records that match the predicate are never returned at
111
+ * all, and the total is computed over a different set than the rows. Any code that resolves a
112
+ * cursor or an offset before applying the predicate has this bug.
113
+ */
114
+ readonly FILTER_AFTER_PAGINATION: "paging is applied before filtering, so pages miss matching records";
115
+ /**
116
+ * The document declares a field filterable that the backend rejects.
117
+ *
118
+ * Not a backend bug — the backend is entitled to refuse a field it never indexed. The document
119
+ * is what is wrong, and it is wrong in the most expensive way: every client generated from it
120
+ * offers a filter that 400s at runtime. oat is the only thing positioned to notice, because it
121
+ * is the only thing that reads the claim and then tries it.
122
+ */
123
+ readonly SPEC_OVERCLAIMS_FILTERABLE: "x-query declares a field filterable that the backend rejects";
124
+ /**
125
+ * The document declares a field sortable that the backend rejects.
126
+ *
127
+ * Same failure as SPEC_OVERCLAIMS_FILTERABLE, one role over: the backend is entitled to refuse
128
+ * a field it never indexed for ordering, but the document is wrong to promise it can. Every
129
+ * client generated from this document offers an `order` value that 400s at runtime.
130
+ */
131
+ readonly SPEC_OVERCLAIMS_SORTABLE: "x-query declares a field sortable that the backend rejects";
132
+ /**
133
+ * `and(a,b)` and `or(a,b)` are compiled with their joiner swapped: a conjunction evaluates as
134
+ * a disjunction and vice versa.
135
+ *
136
+ * A realistic shape for the bug: a query builder that always joins terms with the same
137
+ * operator regardless of which combinator the expression named, because the grouping was
138
+ * parsed but never actually consulted when assembling the join. Every single-predicate filter
139
+ * check still passes — the defect is invisible until two terms are combined.
140
+ */
141
+ readonly FILTER_GROUP_COMBINATOR_SWAPPED: "and() and or() combinators inside a filter expression are swapped";
142
+ /**
143
+ * The document declares a field selectable that the backend rejects.
144
+ *
145
+ * The select analogue of SPEC_OVERCLAIMS_FILTERABLE and SPEC_OVERCLAIMS_SORTABLE: the backend
146
+ * is entitled to stop projecting a column, but the document is wrong to keep promising it can.
147
+ */
148
+ readonly SPEC_OVERCLAIMS_SELECTABLE: "x-query declares a field selectable that the backend rejects";
149
+ /** Tombstoned records remain in the default listing. */
150
+ readonly SOFT_DELETE_LEAK: "soft-deleted records still appear in the default list";
151
+ /** Server-owned fields accept client writes. */
152
+ readonly IMMUTABLE_WRITABLE: "immutable field accepts a write";
153
+ /** Sparse fieldsets are accepted and ignored. */
154
+ readonly SELECT_IGNORED: "select returns every field regardless of what was requested";
155
+ /** LIKE metacharacters in user input are not escaped. */
156
+ readonly LIKE_UNESCAPED: "unescaped LIKE metacharacters make a filter match everything";
157
+ /** Offset arithmetic is wrong by one row per page. */
158
+ readonly OFF_BY_ONE_PAGE: "page offset is computed one row late";
159
+ /** Create returns a status the document does not declare. */
160
+ readonly CREATED_201_AS_200: "create returns 200 where the document declares 201";
161
+ /** Malformed input crashes instead of being rejected. */
162
+ readonly ERROR_500_ON_BAD_FILTER: "malformed filter produces 500 instead of 400";
163
+ /** Deleting an absent record reports success. */
164
+ readonly DELETE_MISSING_OK: "deleting a nonexistent record returns success instead of 404";
165
+ /** Reads of another tenant's record succeed. */
166
+ readonly CROSS_TENANT_READ: "item route serves records belonging to another tenant";
167
+ /** The page-size parameter is accepted and ignored. */
168
+ readonly LIMIT_IGNORED: "limit is accepted but does not bound the page size";
169
+ /** More rows are returned than the documented maximum allows. */
170
+ readonly LIMIT_EXCEEDS_MAX: "page size exceeds the documented maxLimit";
171
+ /** The more-pages flag is always false, so callers stop after page one. */
172
+ readonly HASMORE_ALWAYS_FALSE: "hasMore is false even when further pages exist";
173
+ /** The sort parameter is accepted and ignored. */
174
+ readonly ORDER_IGNORED: "order is accepted but does not change the result order";
175
+ /** The search parameter is accepted and ignored. */
176
+ readonly SEARCH_IGNORED: "q is accepted but does not filter the result";
177
+ /** A field is accepted at create and silently discarded. */
178
+ readonly CREATE_DROPS_FIELD: "create accepts a field and does not persist it";
179
+ /** An enum field accepts values outside the declared set. */
180
+ readonly ENUM_NOT_VALIDATED: "enum field accepts a value outside the declared set";
181
+ /** A string longer than maxLength is stored. */
182
+ readonly MAXLENGTH_NOT_VALIDATED: "string exceeding maxLength is accepted";
183
+ /** A create missing a required field succeeds. */
184
+ readonly REQUIRED_NOT_VALIDATED: "create succeeds with a required field missing";
185
+ /** A non-JSON content type is accepted on a JSON endpoint. */
186
+ readonly CONTENT_TYPE_NOT_ENFORCED: "request body is parsed regardless of content type";
187
+ /** Error bodies do not match the documented error schema. */
188
+ readonly ERROR_SCHEMA_DRIFT: "error response does not match its documented schema";
189
+ /** Success bodies carry fields the schema does not declare. */
190
+ readonly RESPONSE_SCHEMA_DRIFT: "success response carries undeclared fields";
191
+ /** An equality predicate is parsed and then never applied. */
192
+ readonly FILTER_EQ_NOT_APPLIED: "eq predicate is accepted but matches every record";
193
+ /** An empty filtered set is replaced by the unfiltered one. */
194
+ readonly EMPTY_RESULT_RETURNS_ALL: "a filter matching nothing falls back to returning everything";
195
+ /** SQL three-valued logic leaking through: NULL rows vanish from a negated predicate. */
196
+ readonly NEQ_DROPS_NULLS: "neq silently excludes records whose value is null";
197
+ /** The total is computed by a query that disagrees with the one producing the rows. */
198
+ readonly COUNT_ALWAYS_ZERO: "count reports zero while the same body returns records";
199
+ /** Descending order drops the rows an ascending order includes. */
200
+ readonly SORT_DESC_DROPS_NULLS: "descending sort omits records with a null sort key";
201
+ /** An operation declares an effect in the document and does not perform it. */
202
+ readonly EFFECT_NOT_APPLIED: "declared x-effects side effect never happens";
203
+ /** An async job never leaves its running state. */
204
+ readonly ASYNC_NEVER_COMPLETES: "async job stays pending forever and never reaches a terminal state";
205
+ /** The receipt for an async job omits the identifier needed to poll it. */
206
+ readonly ASYNC_RECEIPT_MISSING_ID: "async receipt omits the job id the document declares";
207
+ /**
208
+ * DDL names a physical column differently from every read.
209
+ *
210
+ * SQL-store only, and modelled on a real production bug: SQLite resolves an unknown
211
+ * double-quoted identifier as a *string literal* instead of erroring, so the mismatch
212
+ * surfaces as every row reporting the column's own name as its value — silent corruption
213
+ * rather than a loud failure.
214
+ */
215
+ readonly COLUMN_NAME_MISMATCH: "physical column name differs from the name every read uses";
216
+ /**
217
+ * Numeric columns compared as text, so `amount > 9` misses 10 and ordering reads
218
+ * 1, 10, 100, 2. Extremely common wherever query values arrive untyped from a URL.
219
+ */
220
+ readonly NUMERIC_COMPARED_AS_TEXT: "numeric column is compared and ordered as text";
221
+ /**
222
+ * The listing and the cursor lookup disagree about collation, so keyset pagination resolves
223
+ * a boundary the listing never had — rows are skipped or repeated between pages.
224
+ */
225
+ readonly COLLATION_INCONSISTENT: "cursor resolution uses a different collation than the listing";
226
+ /**
227
+ * Read-modify-write without locking: the handler reads the row, then writes every column
228
+ * back. Two concurrent patches to different fields, and the later write reinstates the
229
+ * earlier field's old value — the classic ORM `save(entity)` lost update.
230
+ */
231
+ readonly CONCURRENT_WRITE_LOST: "concurrent writes to different fields lose one of them";
232
+ /**
233
+ * A lower-ranked role can read a record that a higher-ranked role in the same tenant cannot.
234
+ *
235
+ * The lattice is inverted on the item route only: a viewer is served, a member is denied,
236
+ * the owner still reads. Isolation and the write path stay intact; the failure is purely
237
+ * that privilege is not monotonic.
238
+ */
239
+ readonly ROLE_MONOTONICITY_BROKEN: "a lower-ranked role can read a record a higher-ranked same-tenant role cannot";
240
+ /** Accept completes but the invitee still cannot read the shared record. */
241
+ readonly INVITE_NEVER_GRANTS: "accepting an invite does not grant access to the record";
242
+ /** Revoke returns success and the invitee can still read. */
243
+ readonly REVOKE_IGNORED: "revoking an invite does not remove access";
244
+ };
245
+ export type DefectName = keyof typeof DEFECTS;
246
+ export declare class DefectSet {
247
+ private readonly enabled;
248
+ constructor(names?: readonly string[]);
249
+ static fromEnv(value: string | undefined): DefectSet;
250
+ has(name: DefectName): boolean;
251
+ list(): DefectName[];
252
+ }
253
+ export declare function isDefectName(value: string): value is DefectName;
254
+ //# sourceMappingURL=defects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defects.d.ts","sourceRoot":"","sources":["../../src/reference/defects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,eAAO,MAAM,OAAO;IACnB,iFAAiF;;IAEjF,qFAAqF;;IAErF,kEAAkE;;IAElE,iFAAiF;;IAEjF,wEAAwE;;IAExE,6CAA6C;;IAE7C,mFAAmF;;IAEnF;;;;;;OAMG;;IAEH;;;;;;OAMG;;IAEH;;;;;;;OAOG;;IAEH;;;;;;;OAOG;;IAEH;;;;;;OAMG;;IAEH;;;;;;;OAOG;;IAEH;;;;;;;OAOG;;IAEH;;;;;;OAMG;;IAGH;;;;OAIG;;IAGH;;;OAGG;;IAGH;;;;;;;;OAQG;;IAEH;;;;;;;OAOG;;IAEH;;;;;;OAMG;;IAEH;;;;;;;;OAQG;;IAEH;;;;;OAKG;;IAEH,wDAAwD;;IAExD,gDAAgD;;IAEhD,iDAAiD;;IAEjD,yDAAyD;;IAEzD,sDAAsD;;IAEtD,6DAA6D;;IAE7D,yDAAyD;;IAEzD,iDAAiD;;IAEjD,gDAAgD;;IAEhD,uDAAuD;;IAEvD,iEAAiE;;IAEjE,2EAA2E;;IAE3E,kDAAkD;;IAElD,oDAAoD;;IAEpD,4DAA4D;;IAE5D,6DAA6D;;IAE7D,gDAAgD;;IAEhD,kDAAkD;;IAElD,8DAA8D;;IAE9D,6DAA6D;;IAE7D,+DAA+D;;IAE/D,8DAA8D;;IAE9D,+DAA+D;;IAE/D,yFAAyF;;IAEzF,uFAAuF;;IAEvF,mEAAmE;;IAEnE,+EAA+E;;IAE/E,mDAAmD;;IAEnD,2EAA2E;;IAE3E;;;;;;;OAOG;;IAEH;;;OAGG;;IAEH;;;OAGG;;IAEH;;;;OAIG;;IAEH;;;;;;OAMG;;IAGH,4EAA4E;;IAE5E,6DAA6D;;CAEpD,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA;AAE7C,qBAAa,SAAS;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,KAAK,GAAE,SAAS,MAAM,EAAO;IAIzC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAKpD,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAI9B,IAAI,IAAI,UAAU,EAAE;CAGpB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Defects the reference backend can be told to exhibit.
3
+ *
4
+ * Each one is a real bug seen in production APIs, reduced to its smallest form. The conformance
5
+ * suite switches them on individually and asserts oat reports exactly that defect and nothing
6
+ * else — which is what turns "oat is smart" into precision and recall numbers.
7
+ *
8
+ * Keep the implementations naive and obvious. If the reference backend grows clever it starts
9
+ * encoding the same assumptions oat does, and the two drift together into agreeing on nonsense.
10
+ */
11
+ export const DEFECTS = {
12
+ /** Sort has no total order, so keyset paging silently drops and repeats rows. */
13
+ UNSTABLE_SORT: "sort lacks a tiebreak; page walks drop or duplicate rows",
14
+ /** Writes land in the item store but the list projection serves a stale snapshot. */
15
+ STALE_LIST: "list projection does not reflect writes visible on the item route",
16
+ /** Unrecognised filter fields are dropped instead of rejected. */
17
+ FILTER_IGNORED: "unknown filter field is silently ignored rather than rejected",
18
+ /** Cursor pagination and offset pagination disagree about the underlying set. */
19
+ CURSOR_DRIFT: "cursor pagination yields a different set than offset pagination",
20
+ /** PATCH behaves as PUT, clearing fields the caller never mentioned. */
21
+ PATCH_REPLACES: "PATCH clears fields absent from the request body",
22
+ /** Total count ignores the active filter. */
23
+ COUNT_IGNORES_FILTER: "count reflects the unfiltered set",
24
+ /** The tenant predicate is applied to the list query but not to filter matches. */
25
+ TENANT_LEAK_VIA_FILTER: "filter bypasses the tenant predicate",
26
+ /**
27
+ * Denies cross-tenant reads with 403 rather than 404.
28
+ *
29
+ * The access decision is correct — the record is not served — but the status distinguishes
30
+ * "exists, not yours" from "does not exist", which turns the item route into an oracle for
31
+ * enumerating other tenants' identifiers.
32
+ */
33
+ EXISTENCE_LEAK_VIA_STATUS: "cross-tenant denial uses 403, revealing that the record exists",
34
+ /**
35
+ * Accepts an Idempotency-Key and creates a second record anyway.
36
+ *
37
+ * The failure mode is a retry — a timeout, a proxy replay, a double-click — silently
38
+ * duplicating a charge or an order. The key is the client's only defence against that, and
39
+ * whether it is honoured cannot be established without replaying a request.
40
+ */
41
+ IDEMPOTENCY_IGNORED: "Idempotency-Key is accepted but replay creates a duplicate record",
42
+ /**
43
+ * A write invalidates a route the document says it invalidates — except it does not.
44
+ *
45
+ * The parent keeps serving a derived value computed before the child write: a denormalised
46
+ * counter nobody refreshes, or a cached projection whose key was never busted. The write
47
+ * succeeds, the child's own listing is correct, and only the *other* route the document named
48
+ * is wrong — which is why nothing short of following `x-invalidate` finds it.
49
+ */
50
+ PARENT_PROJECTION_STALE: "a route named by x-invalidate keeps serving pre-write data",
51
+ /**
52
+ * The collection serves a different value for a field than the item route does.
53
+ *
54
+ * A denormalised listing, a search index rebuilt on a lag, a cached projection: each read path
55
+ * is internally consistent and individually plausible, so every per-projection check passes.
56
+ * Only comparing the paths against each other reveals that a client's view of a record depends
57
+ * on which route it happened to use.
58
+ */
59
+ LIST_DETAIL_DISAGREE: "the collection and the item route serve different values for a field",
60
+ /**
61
+ * The filter is applied when it is the only query parameter, and dropped once a sort joins it.
62
+ *
63
+ * Models the real failure: adding an ORDER BY changes which index the planner picks, and the
64
+ * predicate rides on the index that is no longer chosen. Each axis is provably correct on its
65
+ * own, so a suite that only ever tests one at a time reports the API as clean.
66
+ */
67
+ FILTER_DROPPED_WHEN_SORTED: "a filter stops being applied once a sort is also requested",
68
+ /**
69
+ * The filter is applied when it is the only query parameter, and dropped once a sparse
70
+ * fieldset joins it.
71
+ *
72
+ * The realistic shape: a query builder that projects first and then cannot (or does not)
73
+ * apply a predicate to a column it just omitted — or that takes a different code path the
74
+ * moment `fields=` is present and forgets the WHERE. Each axis is correct alone.
75
+ */
76
+ FILTER_DROPPED_WHEN_SELECTED: "a filter stops being applied once a select is also requested",
77
+ /**
78
+ * The filter is applied when it is the only query parameter, and dropped once a free-text
79
+ * search joins it.
80
+ *
81
+ * Adding `q` often switches the backend onto a search-index path that does not honour the
82
+ * structured predicate. Search alone is correct, the filter alone is correct, and together
83
+ * the filter silently vanishes.
84
+ */
85
+ FILTER_DROPPED_WHEN_SEARCHED: "a filter stops being applied once a search is also requested",
86
+ /**
87
+ * The filter survives each pair — filter+sort and filter+select — and vanishes only when
88
+ * both extra axes are present at once.
89
+ *
90
+ * A query planner that has a two-axis path and a three-axis path, and only the latter
91
+ * forgets the WHERE. Every pairwise check passes; the bug is the triple.
92
+ */
93
+ FILTER_DROPPED_WHEN_SORTED_AND_SELECTED: "a filter stops being applied once a sort and a select are requested together",
94
+ /**
95
+ * The filter survives filter+search and filter+sort, and vanishes only when a search and a
96
+ * sort are requested together — typically the search-index path that also tries to honour
97
+ * an ORDER BY and drops the structured predicate to do it.
98
+ */
99
+ FILTER_DROPPED_WHEN_SORTED_AND_SEARCHED: "a filter stops being applied once a sort and a search are requested together",
100
+ /**
101
+ * The filter survives filter+search and filter+select, and vanishes only when a search and
102
+ * a sparse fieldset are requested together.
103
+ */
104
+ FILTER_DROPPED_WHEN_SEARCHED_AND_SELECTED: "a filter stops being applied once a search and a select are requested together",
105
+ /**
106
+ * The page window is computed over the *unfiltered* set, and the filter is applied to whatever
107
+ * that window happened to contain.
108
+ *
109
+ * Page one usually looks right, which is what makes it survive review. The damage shows up
110
+ * further in: pages come back short, records that match the predicate are never returned at
111
+ * all, and the total is computed over a different set than the rows. Any code that resolves a
112
+ * cursor or an offset before applying the predicate has this bug.
113
+ */
114
+ FILTER_AFTER_PAGINATION: "paging is applied before filtering, so pages miss matching records",
115
+ /**
116
+ * The document declares a field filterable that the backend rejects.
117
+ *
118
+ * Not a backend bug — the backend is entitled to refuse a field it never indexed. The document
119
+ * is what is wrong, and it is wrong in the most expensive way: every client generated from it
120
+ * offers a filter that 400s at runtime. oat is the only thing positioned to notice, because it
121
+ * is the only thing that reads the claim and then tries it.
122
+ */
123
+ SPEC_OVERCLAIMS_FILTERABLE: "x-query declares a field filterable that the backend rejects",
124
+ /**
125
+ * The document declares a field sortable that the backend rejects.
126
+ *
127
+ * Same failure as SPEC_OVERCLAIMS_FILTERABLE, one role over: the backend is entitled to refuse
128
+ * a field it never indexed for ordering, but the document is wrong to promise it can. Every
129
+ * client generated from this document offers an `order` value that 400s at runtime.
130
+ */
131
+ SPEC_OVERCLAIMS_SORTABLE: "x-query declares a field sortable that the backend rejects",
132
+ /**
133
+ * `and(a,b)` and `or(a,b)` are compiled with their joiner swapped: a conjunction evaluates as
134
+ * a disjunction and vice versa.
135
+ *
136
+ * A realistic shape for the bug: a query builder that always joins terms with the same
137
+ * operator regardless of which combinator the expression named, because the grouping was
138
+ * parsed but never actually consulted when assembling the join. Every single-predicate filter
139
+ * check still passes — the defect is invisible until two terms are combined.
140
+ */
141
+ FILTER_GROUP_COMBINATOR_SWAPPED: "and() and or() combinators inside a filter expression are swapped",
142
+ /**
143
+ * The document declares a field selectable that the backend rejects.
144
+ *
145
+ * The select analogue of SPEC_OVERCLAIMS_FILTERABLE and SPEC_OVERCLAIMS_SORTABLE: the backend
146
+ * is entitled to stop projecting a column, but the document is wrong to keep promising it can.
147
+ */
148
+ SPEC_OVERCLAIMS_SELECTABLE: "x-query declares a field selectable that the backend rejects",
149
+ /** Tombstoned records remain in the default listing. */
150
+ SOFT_DELETE_LEAK: "soft-deleted records still appear in the default list",
151
+ /** Server-owned fields accept client writes. */
152
+ IMMUTABLE_WRITABLE: "immutable field accepts a write",
153
+ /** Sparse fieldsets are accepted and ignored. */
154
+ SELECT_IGNORED: "select returns every field regardless of what was requested",
155
+ /** LIKE metacharacters in user input are not escaped. */
156
+ LIKE_UNESCAPED: "unescaped LIKE metacharacters make a filter match everything",
157
+ /** Offset arithmetic is wrong by one row per page. */
158
+ OFF_BY_ONE_PAGE: "page offset is computed one row late",
159
+ /** Create returns a status the document does not declare. */
160
+ CREATED_201_AS_200: "create returns 200 where the document declares 201",
161
+ /** Malformed input crashes instead of being rejected. */
162
+ ERROR_500_ON_BAD_FILTER: "malformed filter produces 500 instead of 400",
163
+ /** Deleting an absent record reports success. */
164
+ DELETE_MISSING_OK: "deleting a nonexistent record returns success instead of 404",
165
+ /** Reads of another tenant's record succeed. */
166
+ CROSS_TENANT_READ: "item route serves records belonging to another tenant",
167
+ /** The page-size parameter is accepted and ignored. */
168
+ LIMIT_IGNORED: "limit is accepted but does not bound the page size",
169
+ /** More rows are returned than the documented maximum allows. */
170
+ LIMIT_EXCEEDS_MAX: "page size exceeds the documented maxLimit",
171
+ /** The more-pages flag is always false, so callers stop after page one. */
172
+ HASMORE_ALWAYS_FALSE: "hasMore is false even when further pages exist",
173
+ /** The sort parameter is accepted and ignored. */
174
+ ORDER_IGNORED: "order is accepted but does not change the result order",
175
+ /** The search parameter is accepted and ignored. */
176
+ SEARCH_IGNORED: "q is accepted but does not filter the result",
177
+ /** A field is accepted at create and silently discarded. */
178
+ CREATE_DROPS_FIELD: "create accepts a field and does not persist it",
179
+ /** An enum field accepts values outside the declared set. */
180
+ ENUM_NOT_VALIDATED: "enum field accepts a value outside the declared set",
181
+ /** A string longer than maxLength is stored. */
182
+ MAXLENGTH_NOT_VALIDATED: "string exceeding maxLength is accepted",
183
+ /** A create missing a required field succeeds. */
184
+ REQUIRED_NOT_VALIDATED: "create succeeds with a required field missing",
185
+ /** A non-JSON content type is accepted on a JSON endpoint. */
186
+ CONTENT_TYPE_NOT_ENFORCED: "request body is parsed regardless of content type",
187
+ /** Error bodies do not match the documented error schema. */
188
+ ERROR_SCHEMA_DRIFT: "error response does not match its documented schema",
189
+ /** Success bodies carry fields the schema does not declare. */
190
+ RESPONSE_SCHEMA_DRIFT: "success response carries undeclared fields",
191
+ /** An equality predicate is parsed and then never applied. */
192
+ FILTER_EQ_NOT_APPLIED: "eq predicate is accepted but matches every record",
193
+ /** An empty filtered set is replaced by the unfiltered one. */
194
+ EMPTY_RESULT_RETURNS_ALL: "a filter matching nothing falls back to returning everything",
195
+ /** SQL three-valued logic leaking through: NULL rows vanish from a negated predicate. */
196
+ NEQ_DROPS_NULLS: "neq silently excludes records whose value is null",
197
+ /** The total is computed by a query that disagrees with the one producing the rows. */
198
+ COUNT_ALWAYS_ZERO: "count reports zero while the same body returns records",
199
+ /** Descending order drops the rows an ascending order includes. */
200
+ SORT_DESC_DROPS_NULLS: "descending sort omits records with a null sort key",
201
+ /** An operation declares an effect in the document and does not perform it. */
202
+ EFFECT_NOT_APPLIED: "declared x-effects side effect never happens",
203
+ /** An async job never leaves its running state. */
204
+ ASYNC_NEVER_COMPLETES: "async job stays pending forever and never reaches a terminal state",
205
+ /** The receipt for an async job omits the identifier needed to poll it. */
206
+ ASYNC_RECEIPT_MISSING_ID: "async receipt omits the job id the document declares",
207
+ /**
208
+ * DDL names a physical column differently from every read.
209
+ *
210
+ * SQL-store only, and modelled on a real production bug: SQLite resolves an unknown
211
+ * double-quoted identifier as a *string literal* instead of erroring, so the mismatch
212
+ * surfaces as every row reporting the column's own name as its value — silent corruption
213
+ * rather than a loud failure.
214
+ */
215
+ COLUMN_NAME_MISMATCH: "physical column name differs from the name every read uses",
216
+ /**
217
+ * Numeric columns compared as text, so `amount > 9` misses 10 and ordering reads
218
+ * 1, 10, 100, 2. Extremely common wherever query values arrive untyped from a URL.
219
+ */
220
+ NUMERIC_COMPARED_AS_TEXT: "numeric column is compared and ordered as text",
221
+ /**
222
+ * The listing and the cursor lookup disagree about collation, so keyset pagination resolves
223
+ * a boundary the listing never had — rows are skipped or repeated between pages.
224
+ */
225
+ COLLATION_INCONSISTENT: "cursor resolution uses a different collation than the listing",
226
+ /**
227
+ * Read-modify-write without locking: the handler reads the row, then writes every column
228
+ * back. Two concurrent patches to different fields, and the later write reinstates the
229
+ * earlier field's old value — the classic ORM `save(entity)` lost update.
230
+ */
231
+ CONCURRENT_WRITE_LOST: "concurrent writes to different fields lose one of them",
232
+ /**
233
+ * A lower-ranked role can read a record that a higher-ranked role in the same tenant cannot.
234
+ *
235
+ * The lattice is inverted on the item route only: a viewer is served, a member is denied,
236
+ * the owner still reads. Isolation and the write path stay intact; the failure is purely
237
+ * that privilege is not monotonic.
238
+ */
239
+ ROLE_MONOTONICITY_BROKEN: "a lower-ranked role can read a record a higher-ranked same-tenant role cannot",
240
+ /** Accept completes but the invitee still cannot read the shared record. */
241
+ INVITE_NEVER_GRANTS: "accepting an invite does not grant access to the record",
242
+ /** Revoke returns success and the invitee can still read. */
243
+ REVOKE_IGNORED: "revoking an invite does not remove access",
244
+ };
245
+ export class DefectSet {
246
+ enabled;
247
+ constructor(names = []) {
248
+ this.enabled = new Set(names);
249
+ }
250
+ static fromEnv(value) {
251
+ if (value === undefined || value.trim() === "")
252
+ return new DefectSet();
253
+ return new DefectSet(value.split(",").map((s) => s.trim()).filter(Boolean));
254
+ }
255
+ has(name) {
256
+ return this.enabled.has(name);
257
+ }
258
+ list() {
259
+ return [...this.enabled];
260
+ }
261
+ }
262
+ export function isDefectName(value) {
263
+ return Object.hasOwn(DEFECTS, value);
264
+ }
265
+ //# sourceMappingURL=defects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defects.js","sourceRoot":"","sources":["../../src/reference/defects.ts"],"names":[],"mappings":"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"}