@reventlessdev/reventless-spec 3.0.0-alpha.114 → 3.0.0-alpha.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/schema/platform-api.graphql +4 -2
- package/src/components/DcbScopeInference.res +1 -1
- package/src/components/InboundTranslationSlice.res +1 -1
- package/src/components/OutboundTranslationSlice.res +1 -1
- package/src/components/Plugin.res +58 -18
- package/src/components/Plugin.res.mjs +3 -1
- package/src/components/Snapshot.res +1 -1
- package/src/components/StateAnnotations.res +75 -7
- package/src/generator/Discovery.res +1 -1
- package/src/types/Behavior.res +1 -1
- package/src/types/Message.res +1 -1
- package/src/types/OwnerScope.res +105 -0
- package/src/types/OwnerScope.res.mjs +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.116 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **ppx:** declare a command's lifecycle edge once, as [@transition](https://github.com/transition) ([dd35130](https://github.com/ReventlessDev/reventless-core/commit/dd3513014f14b71879ee21263c6755d3c3d95096))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.115 (2026-08-16)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **core:** let [@retired](https://github.com/retired) name a lifecycle state, not only a boolean ([6bb346b](https://github.com/ReventlessDev/reventless-core/commit/6bb346b4f6a5f33826fc24537953482a76067177))
|
|
18
|
+
* **core:** mark the state that retires a row, and allow more than one ([cb1461f](https://github.com/ReventlessDev/reventless-core/commit/cb1461f024d3ca3b53fd9c8b010a054e3fcc4555))
|
|
19
|
+
* **core:** publish queryableDef.retiredField from the [@retired](https://github.com/retired) annotation ([b44436a](https://github.com/ReventlessDev/reventless-core/commit/b44436a997b6c4ff0531f0b07d793cc858eef94a))
|
|
20
|
+
* **spec:** [@retired](https://github.com/retired) state-field annotation and its schema emission ([2d8234b](https://github.com/ReventlessDev/reventless-core/commit/2d8234b6b3dd8f479031a67eb5b4b47b5c0c2ff9))
|
|
21
|
+
* **spec:** classify a caller against a view's retirement flag ([5031ce5](https://github.com/ReventlessDev/reventless-core/commit/5031ce573ce56d2407888c7777274e5580c4fb51))
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 3.0.0-alpha.114 (2026-08-15)
|
|
7
25
|
|
|
8
26
|
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
package/package.json
CHANGED
|
@@ -212,15 +212,17 @@ type Platform_ReadSideDef {
|
|
|
212
212
|
idFieldSource: String
|
|
213
213
|
labelField: String!
|
|
214
214
|
labelFieldSource: String
|
|
215
|
+
lifecycleField: String
|
|
215
216
|
linkedWriteSide: [String!]!
|
|
216
217
|
name: String!
|
|
217
218
|
ownerField: String
|
|
218
219
|
queryField: String!
|
|
219
220
|
requiredAccess: [String!]
|
|
221
|
+
retiredField: String
|
|
222
|
+
retiredValues: [String!]
|
|
220
223
|
schema: String!
|
|
221
224
|
searchableFields: [String!]!
|
|
222
225
|
singleQueryField: String
|
|
223
|
-
statusField: String
|
|
224
226
|
visibility: String
|
|
225
227
|
}
|
|
226
228
|
|
|
@@ -290,7 +292,7 @@ type Query {
|
|
|
290
292
|
Platform_ComponentDefinitions: [Platform_ComponentDefinitionEntry!]!
|
|
291
293
|
Platform_Plugin(id: ID!): Platform_Plugin
|
|
292
294
|
Platform_PluginStructures: [Platform_PluginStructureEntry!]!
|
|
293
|
-
Platform_Plugins(after: String, before: String, filter: Platform_PluginFilter, first: Int, last: Int): Platform_PluginConnection!
|
|
295
|
+
Platform_Plugins(after: String, before: String, filter: Platform_PluginFilter, first: Int, includeRetired: Boolean, last: Int): Platform_PluginConnection!
|
|
294
296
|
Platform_PluginsByIds(ids: [String!]!): [Platform_Plugin!]!
|
|
295
297
|
Platform_UIFragments: [Platform_UIFragmentEntry!]!
|
|
296
298
|
}
|
|
@@ -8,7 +8,7 @@ each slice (variant names + `*Id`-shaped fields), never an `S.t` schema and neve
|
|
|
8
8
|
a tag-metadata flag. That is precisely what we are replacing — so both the runtime
|
|
9
9
|
(building shapes from `S.t` schemas, via `DcbTag.sliceShapeFromSchemas`) and the
|
|
10
10
|
VS Code tooling (building shapes from parsed `.res` source) can feed the same
|
|
11
|
-
`infer`. See `docs/plans/dcb-tag-scope-inference.md` § "Phase 1 design".
|
|
11
|
+
`infer`. See `docs/plans/done/dcb-tag-scope-inference.md` § "Phase 1 design".
|
|
12
12
|
|
|
13
13
|
The three rules (over the representation):
|
|
14
14
|
|
|
@@ -53,7 +53,7 @@ module type Spec = {
|
|
|
53
53
|
|
|
54
54
|
/** Optional display name of the foreign system this anti-corruption slice receives
|
|
55
55
|
from (e.g. `"SupplierFeed"`). Drives the **external box** drawn outside the plugin
|
|
56
|
-
in the Event Graph / Context Map
|
|
56
|
+
in the Event Graph / Context Map.
|
|
57
57
|
Auto-injected by `@@reventless.spec` defaulting to `None` — set it to name the box. */
|
|
58
58
|
let externalSystem: option<string>
|
|
59
59
|
|
|
@@ -103,7 +103,7 @@ module type Spec = {
|
|
|
103
103
|
|
|
104
104
|
/** Optional display name of the foreign system this anti-corruption slice publishes
|
|
105
105
|
to (e.g. `"EmailService"`). Drives the **external box** drawn outside the plugin
|
|
106
|
-
in the Event Graph / Context Map
|
|
106
|
+
in the Event Graph / Context Map.
|
|
107
107
|
Auto-injected by `@@reventless.spec` defaulting to `None` — set it to name the box. */
|
|
108
108
|
let externalSystem: option<string>
|
|
109
109
|
}
|
|
@@ -170,20 +170,22 @@ type commandDef = {
|
|
|
170
170
|
mutationField: string,
|
|
171
171
|
references: array<fieldReference>,
|
|
172
172
|
/**
|
|
173
|
-
|
|
173
|
+
Lifecycle states under which this command is meaningful. `None` means the command
|
|
174
174
|
is always available (back-compat default). `Some([…])` lets AutoUI hide the
|
|
175
|
-
command on rows whose
|
|
176
|
-
for how the row's
|
|
175
|
+
command on rows whose lifecycle field is not in the set — see
|
|
176
|
+
`queryableDef.lifecycleField` for how the row's state is located. `Some([])` is
|
|
177
|
+
the defensive "never show" form.
|
|
177
178
|
*/
|
|
178
179
|
allowedStates: @s.matches(stringArrayOptionSchema) option<array<string>>,
|
|
179
180
|
/**
|
|
180
|
-
The single
|
|
181
|
+
The single lifecycle state this command's handler writes — the command's *to*
|
|
181
182
|
state, sibling of `allowedStates`' *from* set. Source: the
|
|
182
|
-
`@
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
target of the `@transition(([Placed]) => Shipped)` command-variant annotation.
|
|
184
|
+
`Some("Shipped")` lets a resolver move a row by a declared transition instead
|
|
185
|
+
of a guess. `None` means no target was declared — and note that this is two
|
|
186
|
+
different statements depending on `allowedStates`: with a from-set present it
|
|
187
|
+
is the command declaring it does not move the row, and with none it is simply
|
|
188
|
+
an unannotated command. js_nullable for JSON safety, same as `allowedStates`.
|
|
187
189
|
*/
|
|
188
190
|
targetState: @s.matches(stringOptionSchema) option<string>,
|
|
189
191
|
/**
|
|
@@ -262,17 +264,17 @@ type queryableDef = {
|
|
|
262
264
|
`None` means not stated — defs persisted before this field existed, and
|
|
263
265
|
hand-rolled defs that decline to say. Distinct from `Some("fallback")`, which
|
|
264
266
|
is this side stating that it looked. js_nullable for the same JSON-safety
|
|
265
|
-
reason as `
|
|
267
|
+
reason as `lifecycleField`.
|
|
266
268
|
*/
|
|
267
269
|
labelFieldSource: @s.matches(stringOptionSchema) option<string>,
|
|
268
270
|
/**
|
|
269
|
-
Name of the state field whose value identifies the row's lifecycle
|
|
270
|
-
|
|
271
|
-
menu. Resolution order (codegen): (1) field annotated `@
|
|
272
|
-
literally named `"
|
|
273
|
-
`queryableDef` set this explicitly.
|
|
271
|
+
Name of the state field whose value identifies the row's lifecycle, used by
|
|
272
|
+
AutoUI together with `commandDef.allowedStates` to filter the per-row command
|
|
273
|
+
menu. Resolution order (codegen): (1) field annotated `@lifecycle`; (2) a field
|
|
274
|
+
literally named `"lifecycle"` whose shape is an enum; (3) `None`. Spec authors
|
|
275
|
+
that hand-roll a `queryableDef` set this explicitly.
|
|
274
276
|
*/
|
|
275
|
-
|
|
277
|
+
lifecycleField: @s.matches(stringOptionSchema) option<string>,
|
|
276
278
|
/**
|
|
277
279
|
Name of the state field that ties a row to the principal owning it (`@owner`),
|
|
278
280
|
when the view declares one. Two consequences for a client: reads of this view
|
|
@@ -285,6 +287,44 @@ type queryableDef = {
|
|
|
285
287
|
*/
|
|
286
288
|
ownerField: @s.matches(stringOptionSchema) option<string>,
|
|
287
289
|
/**
|
|
290
|
+
Name of the boolean state field that withdraws a row from ordinary reads
|
|
291
|
+
(`@retired`), when the view declares one. Reads of this view exclude rows whose
|
|
292
|
+
flag is true for callers outside `OwnerScope.elevatedGroups`, on the list door
|
|
293
|
+
and the single-entity door alike; an exempt caller reaches them by asking for
|
|
294
|
+
them.
|
|
295
|
+
|
|
296
|
+
Derived from the annotation and from nothing else — deliberately no fallback to
|
|
297
|
+
a conventionally-named boolean, unlike `lifecycleField`. A field named `archived`
|
|
298
|
+
that nobody annotated must not start hiding rows the day this ships, and the
|
|
299
|
+
cost of guessing wrong here is data disappearing rather than a menu filtering
|
|
300
|
+
oddly.
|
|
301
|
+
|
|
302
|
+
The label the flag reads as is not here. It travels on the state schema, which
|
|
303
|
+
every consumer of this def already holds, and a second copy is a second thing
|
|
304
|
+
to keep in step.
|
|
305
|
+
*/
|
|
306
|
+
retiredField: @s.matches(stringOptionSchema) option<string>,
|
|
307
|
+
/**
|
|
308
|
+
The states a row is retired *in*, when the view declares the state form of
|
|
309
|
+
`@retired` — `Some(["Archived", "Discontinued"])` beside
|
|
310
|
+
`retiredField: Some("shelfStatus")`. `None` is the boolean form, where the
|
|
311
|
+
excluded value is always `true` and naming it would be a field that can only
|
|
312
|
+
hold one thing.
|
|
313
|
+
|
|
314
|
+
A set: a lifecycle may be withdrawn by more than one state, which exclude
|
|
315
|
+
identically and differ only in the way back. Retired iff the field's value is in
|
|
316
|
+
it, and one member is the ordinary case rather than a special one.
|
|
317
|
+
|
|
318
|
+
Published beside the field rather than left for a consumer to re-derive from the
|
|
319
|
+
state schema: a client holding this def holds the whole predicate, and two places
|
|
320
|
+
deriving one comparison is how they come to disagree about it.
|
|
321
|
+
|
|
322
|
+
The state form is also what lets a command's `@transition` answer applicability
|
|
323
|
+
when retired, with no annotation beyond the one — retirement expressed in the
|
|
324
|
+
vocabulary a command's stance is already written in.
|
|
325
|
+
*/
|
|
326
|
+
retiredValues: @s.matches(stringArrayOptionSchema) option<array<string>>,
|
|
327
|
+
/**
|
|
288
328
|
Component visibility hint (`@@reventless.visibility`). `Some("Internal")` marks a
|
|
289
329
|
ReadModel / StateViewSlice that the deployed AutoUI hides from its menu, drill-down
|
|
290
330
|
pages, web event graph and cross-plugin edges. `None` (absent) means Public. Internal
|
|
@@ -323,7 +363,7 @@ type queryableDef = {
|
|
|
323
363
|
|
|
324
364
|
`None` means not stated — defs persisted before this field existed, and
|
|
325
365
|
hand-rolled defs that decline to say; a consumer falls back to its own
|
|
326
|
-
derivation there. js_nullable for the same JSON-safety reason as `
|
|
366
|
+
derivation there. js_nullable for the same JSON-safety reason as `lifecycleField`.
|
|
327
367
|
*/
|
|
328
368
|
singleQueryField: @s.matches(stringOptionSchema) option<string>,
|
|
329
369
|
/**
|
|
@@ -334,7 +374,7 @@ type queryableDef = {
|
|
|
334
374
|
`None` means unresolved: a state with several `*Id` fields and no name match,
|
|
335
375
|
or with none at all. Such a component gets no key-derived filter or sort until
|
|
336
376
|
its spec declares `@id`. Also `None` on defs persisted before this field
|
|
337
|
-
existed. js_nullable for the same JSON-safety reason as `
|
|
377
|
+
existed. js_nullable for the same JSON-safety reason as `lifecycleField`.
|
|
338
378
|
*/
|
|
339
379
|
idField: @s.matches(stringOptionSchema) option<string>,
|
|
340
380
|
/**
|
|
@@ -118,8 +118,10 @@ let queryableDefSchema = S.schema(s => ({
|
|
|
118
118
|
labelField: s.m(S.string),
|
|
119
119
|
searchableFields: s.m(S.array(S.string)),
|
|
120
120
|
labelFieldSource: s.m(stringOptionSchema),
|
|
121
|
-
|
|
121
|
+
lifecycleField: s.m(stringOptionSchema),
|
|
122
122
|
ownerField: s.m(stringOptionSchema),
|
|
123
|
+
retiredField: s.m(stringOptionSchema),
|
|
124
|
+
retiredValues: s.m(stringArrayOptionSchema),
|
|
123
125
|
visibility: s.m(stringOptionSchema),
|
|
124
126
|
chapter: s.m(stringOptionSchema),
|
|
125
127
|
singleQueryField: s.m(stringOptionSchema),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Per-aggregate persisted-snapshot configuration
|
|
2
|
-
// (docs/plans/aggregate-snapshotting.md).
|
|
2
|
+
// (docs/plans/done/aggregate-snapshotting.md).
|
|
3
3
|
//
|
|
4
4
|
// Snapshotting is opt-in and lives on the Behavior module (where `state` is
|
|
5
5
|
// defined): `Behavior.T.snapshot = None` (the default, auto-injected by
|
|
@@ -5,7 +5,7 @@ Spec describing the structural annotations declared on the fields of an
|
|
|
5
5
|
`@compositeId`, `@subId`, `@compositeSubId`, `@index`), a visibility
|
|
6
6
|
annotation (`@hidden`, `@summary`), a hierarchical-rendering annotation
|
|
7
7
|
(`@drillTarget`, `@collapsed`), a server-query opt-in annotation
|
|
8
|
-
(`@scan`, `@scanSort`), a UI-list annotation (`@
|
|
8
|
+
(`@scan`, `@scanSort`), a UI-list annotation (`@lifecycle`, `@groupBy`), or a
|
|
9
9
|
type-level live-updates annotation (`@live` on the `state` declaration).
|
|
10
10
|
Downstream consumers (UI, MCP, codegen) read the
|
|
11
11
|
spec to surface field roles in JSON Schema as `x-reventless-*` extension
|
|
@@ -34,6 +34,55 @@ KPI label, or `""` to let the UI derive one from the field name.
|
|
|
34
34
|
*/
|
|
35
35
|
type metricSpec = {aggregate: string, label: string}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
The field declared `@retired`, and how a consumer should title it.
|
|
39
|
+
|
|
40
|
+
`field` names what withdraws the row from ordinary use — a deactivated customer,
|
|
41
|
+
an archived category. `label` is what that state is called ("Archived"), or `""`
|
|
42
|
+
to let a consumer derive one from the field name. `showWhenFalse` asks a consumer
|
|
43
|
+
to surface the flag in its negative state too; the default is false, because a
|
|
44
|
+
caller who is not exempt from the narrowing never receives a retired row and would
|
|
45
|
+
otherwise see the same negative marker on every record they can read.
|
|
46
|
+
|
|
47
|
+
`value` is what decides which of the two forms this is:
|
|
48
|
+
|
|
49
|
+
- `None` — the **boolean** form. The row is retired when the field is `true`. The
|
|
50
|
+
right shape for a record whose retirement genuinely is a flag: a `Products` view
|
|
51
|
+
with an `archived` boolean and no lifecycle should not have to invent a
|
|
52
|
+
two-valued enum.
|
|
53
|
+
- `Some(v)` — the **state** form. The row is retired when the field equals `v`,
|
|
54
|
+
and the field is the record's `@lifecycle` field. One field then carries the
|
|
55
|
+
fact once instead of twice, and `@transition(([Deactivated]) => Active)` on the
|
|
56
|
+
way-back command is enough to make a generated menu offer it there and nowhere
|
|
57
|
+
else —
|
|
58
|
+
because retirement is finally expressible in the vocabulary that stance is
|
|
59
|
+
already written in.
|
|
60
|
+
|
|
61
|
+
The narrowing itself is not described here. This spec carries what the field
|
|
62
|
+
*means*; who still sees a retired row is one deployment-wide rule
|
|
63
|
+
(`OwnerScope.elevatedGroups`), resolved where the query is answered.
|
|
64
|
+
*/
|
|
65
|
+
type retiredSpec = {
|
|
66
|
+
field: string,
|
|
67
|
+
label: string,
|
|
68
|
+
showWhenFalse: bool,
|
|
69
|
+
/**
|
|
70
|
+
The states that withdraw the row, or `None` for the boolean form.
|
|
71
|
+
|
|
72
|
+
A **set**, because a lifecycle may end in more than one way — a product is
|
|
73
|
+
withdrawn `Archived` or `Discontinued`, by different routes and with different
|
|
74
|
+
ways back — and one field carries all of them. Retired iff the field's value is
|
|
75
|
+
in the set; a single-element set is the ordinary case rather than a special one.
|
|
76
|
+
|
|
77
|
+
The `option` is the **form discriminator** and is load-bearing. `None` is the
|
|
78
|
+
boolean form, where the row is retired when the field is `true`. Flattened to a
|
|
79
|
+
bare array, `[]` would mean both "boolean form" and "state form naming no
|
|
80
|
+
states", and every consumer's boolean-form handling — the column suppression
|
|
81
|
+
most visibly — would stop firing on a value that looks merely empty.
|
|
82
|
+
*/
|
|
83
|
+
values: option<array<string>>,
|
|
84
|
+
}
|
|
85
|
+
|
|
37
86
|
type stateAnnotationSpec = {
|
|
38
87
|
ids: array<string>,
|
|
39
88
|
compositeIds: array<string>,
|
|
@@ -64,13 +113,16 @@ type stateAnnotationSpec = {
|
|
|
64
113
|
*/
|
|
65
114
|
metric: array<(string, metricSpec)>,
|
|
66
115
|
/**
|
|
67
|
-
Field annotated `@
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
116
|
+
Field annotated `@lifecycle` on the state record (PPX-emitted) — the enum a
|
|
117
|
+
command's `@transition` is written in terms of, a board draws its columns
|
|
118
|
+
from and a state diagram renders. `Some(name)` when one such annotation
|
|
119
|
+
exists; the PPX errors on duplicate `@lifecycle` annotations within the same
|
|
120
|
+
record. Codegen consumes this to populate `queryableDef.lifecycleField`, and
|
|
121
|
+
falls back to a field literally named `"lifecycle"` whose shape is an enum
|
|
122
|
+
when this annotation is absent — so a record whose field can honestly be
|
|
123
|
+
called `lifecycle` declares one without ceremony.
|
|
72
124
|
*/
|
|
73
|
-
|
|
125
|
+
lifecycle: option<string>,
|
|
74
126
|
/**
|
|
75
127
|
Field annotated `@groupBy` on the state record (PPX-emitted). `Some(name)`
|
|
76
128
|
when one such annotation exists; the PPX errors on duplicate `@groupBy`
|
|
@@ -96,6 +148,22 @@ type stateAnnotationSpec = {
|
|
|
96
148
|
control is offered (`true`) or hidden (`false`) for the view.
|
|
97
149
|
*/
|
|
98
150
|
live: option<bool>,
|
|
151
|
+
/**
|
|
152
|
+
Field annotated `@retired` on the state record (PPX-emitted). `Some(spec)` when
|
|
153
|
+
one such annotation exists; the PPX errors on duplicates and on a non-boolean
|
|
154
|
+
field. `SuryToJsonSchema.deriveObjectSchema` emits `x-reventless-retired:
|
|
155
|
+
{label?, showWhenFalse}` on the named field.
|
|
156
|
+
|
|
157
|
+
`option<retiredSpec>` rather than an array: at most one per record. Two
|
|
158
|
+
retirement flags are not a stricter rule but an unanswered one — the read
|
|
159
|
+
predicate would have to guess whether they conjoin or disjoin, and the query
|
|
160
|
+
layer narrows on a single field.
|
|
161
|
+
|
|
162
|
+
Both the boolean and the state form ride this one key. `x-reventless-retired`
|
|
163
|
+
carries `value` only in the state form, on the same omit-rather-than-write-empty
|
|
164
|
+
rule `label` already follows.
|
|
165
|
+
*/
|
|
166
|
+
retired: option<retiredSpec>,
|
|
99
167
|
}
|
|
100
168
|
|
|
101
169
|
/** Sury metadata ID used to attach a `stateAnnotationSpec` to a state schema. */
|
|
@@ -29,7 +29,7 @@ let folderToComponentType = ComponentKind.folderToKind
|
|
|
29
29
|
// `None`. Uses the single-source `ComponentKind.isKindFolder`, so a chapter read
|
|
30
30
|
// here (build time, disk) agrees with the authoring tool's identical heuristic and
|
|
31
31
|
// with a chapter reflected off the deployed plugin structure. See
|
|
32
|
-
// docs/plans/deployed-chapter-grouping.md.
|
|
32
|
+
// docs/plans/done/deployed-chapter-grouping.md.
|
|
33
33
|
let chapterOf = (relPath: string): option<string> => {
|
|
34
34
|
let segments = relPath->String.split("/")
|
|
35
35
|
// Need at least one directory segment before the filename.
|
package/src/types/Behavior.res
CHANGED
|
@@ -72,7 +72,7 @@ module type T = {
|
|
|
72
72
|
*/
|
|
73
73
|
let decide: (state, Spec.command) => result<array<Spec.event>, Spec.error>
|
|
74
74
|
|
|
75
|
-
/** Persisted-snapshot configuration (docs/plans/aggregate-snapshotting.md).
|
|
75
|
+
/** Persisted-snapshot configuration (docs/plans/done/aggregate-snapshotting.md).
|
|
76
76
|
`None` — the default, auto-injected by `@@reventless.behavior` — keeps
|
|
77
77
|
full replay; `Some({interval, stateSchema})` writes a keep-one snapshot
|
|
78
78
|
every `interval` events and seeds cold replays from the latest one.
|
package/src/types/Message.res
CHANGED
|
@@ -162,7 +162,7 @@ type commandJson = {
|
|
|
162
162
|
// (clones via a JSON round-trip; never re-encodes through the schema), is idempotent on
|
|
163
163
|
// valid data, and falls back to the ORIGINAL error when the fill doesn't resolve the
|
|
164
164
|
// failure — so genuine corruption still surfaces.
|
|
165
|
-
// See docs/plans/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
165
|
+
// See docs/plans/done/platform-infrastructure-in-plugin-list.md (durable fix option 2).
|
|
166
166
|
//
|
|
167
167
|
// The scalar arm is the odd one out and is deliberately noisy. Every other fill is
|
|
168
168
|
// *derived* — the schema states what an absent value means, and the fill supplies exactly
|
package/src/types/OwnerScope.res
CHANGED
|
@@ -225,3 +225,108 @@ let scopeOf = (decision: decision): option<(string, string)> =>
|
|
|
225
225
|
| ScopeTo(field, required) => Some((field, required))
|
|
226
226
|
| Unscoped | RefuseOwned => None
|
|
227
227
|
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
What `@retired` does to one read of one view.
|
|
231
|
+
|
|
232
|
+
Deliberately in this module rather than beside the annotation. Owner scoping and
|
|
233
|
+
retirement narrowing are two rules over the same question — which rows does this
|
|
234
|
+
caller get — and they resolve the caller with the same `resolve`, against the
|
|
235
|
+
same deployment-wide `elevatedGroups`. Written apart, they could disagree about
|
|
236
|
+
who an operator is, and the disagreement would be invisible until a caller
|
|
237
|
+
elevated for one rule turned out to be scoped by the other.
|
|
238
|
+
|
|
239
|
+
`ExcludeRetired` carries the whole predicate — the field and, for the state form
|
|
240
|
+
of `@retired`, every state that retires the row. `values: None` is the boolean
|
|
241
|
+
form, where the excluded value is always `true`; naming it there would be a
|
|
242
|
+
parameter that can only hold one thing, and a parameter a call site can pass
|
|
243
|
+
wrongly.
|
|
244
|
+
|
|
245
|
+
Both halves travel together for the reason the pair exists at all: a field
|
|
246
|
+
without its values is half a comparison, and an adapter left to fetch the other
|
|
247
|
+
half from the schema is an adapter that can disagree with the one next to it
|
|
248
|
+
about which rows a caller may see.
|
|
249
|
+
*/
|
|
250
|
+
type retiredScope = {
|
|
251
|
+
field: string,
|
|
252
|
+
values: option<array<string>>,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
type retiredDecision =
|
|
256
|
+
| RetiredVisible
|
|
257
|
+
| ExcludeRetired(retiredScope)
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
Combine a view's declared retirement flag with the caller behind the request.
|
|
261
|
+
|
|
262
|
+
`~asked` is the caller's `includeRetired` request, honoured only where the
|
|
263
|
+
caller was going to see those rows anyway. Reading it before classifying would
|
|
264
|
+
make the argument the decision rather than a request.
|
|
265
|
+
|
|
266
|
+
`Unidentified` excludes rather than refuses, which is where this parts company
|
|
267
|
+
with `decide`. An owner-scoped read of an unidentified caller has no value to
|
|
268
|
+
match, so scoping it would be indistinguishable from an empty view and a refusal
|
|
269
|
+
says more. Retirement has no such value — the predicate is the same for every
|
|
270
|
+
non-exempt caller — so the fail-closed action is simply the narrow read.
|
|
271
|
+
*/
|
|
272
|
+
let decideRetired = (
|
|
273
|
+
identity: Identity.t,
|
|
274
|
+
~retiredField: option<string>,
|
|
275
|
+
~retiredValues: option<array<string>>=?,
|
|
276
|
+
~asked: bool=false,
|
|
277
|
+
~elevated: array<string>=elevatedGroups(),
|
|
278
|
+
): retiredDecision =>
|
|
279
|
+
switch retiredField {
|
|
280
|
+
| None => RetiredVisible
|
|
281
|
+
| Some(field) =>
|
|
282
|
+
let scope = {field, values: retiredValues}
|
|
283
|
+
switch resolve(identity, ~elevated) {
|
|
284
|
+
| System | Elevated(_) => asked ? RetiredVisible : ExcludeRetired(scope)
|
|
285
|
+
| Owned(_) | Unidentified(_) => ExcludeRetired(scope)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
The field a read excludes on, or `None` when it excludes nothing.
|
|
291
|
+
|
|
292
|
+
Note that an exempt caller who did not ask still gets `Some`: retired rows are
|
|
293
|
+
withheld from everyone by default, and elevation buys the ability to ask for
|
|
294
|
+
them rather than a standing exemption. An archive that is always underfoot is
|
|
295
|
+
not an archive.
|
|
296
|
+
*/
|
|
297
|
+
let retiredScopeOf = (decision: retiredDecision): option<retiredScope> =>
|
|
298
|
+
switch decision {
|
|
299
|
+
| ExcludeRetired(scope) => Some(scope)
|
|
300
|
+
| RetiredVisible => None
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
Whether a row's value in the retirement field is a retiring one.
|
|
305
|
+
|
|
306
|
+
The single place the two forms differ, so every reader asks one question rather
|
|
307
|
+
than branching on the form itself. The state form is a membership test: a
|
|
308
|
+
lifecycle may be withdrawn by several states, and they exclude identically —
|
|
309
|
+
what differs between them is the way back, which commands express and this does
|
|
310
|
+
not need to know.
|
|
311
|
+
|
|
312
|
+
`None` — the field absent from the row — answers false in both, which is the
|
|
313
|
+
mirror image of the owner rule and lands the opposite way for the same reason: an
|
|
314
|
+
owner-scoped read excludes a row that states no owner, because such a row belongs
|
|
315
|
+
to nobody in particular, while a retirement read KEEPS a row that states no flag,
|
|
316
|
+
because absent means not retired. That is what a row written before the
|
|
317
|
+
annotation existed is, and excluding those would empty the view the day the
|
|
318
|
+
annotation lands.
|
|
319
|
+
|
|
320
|
+
A state form naming no states excludes nothing, which is the same reading one
|
|
321
|
+
step further: no state has been declared to withdraw a row, so no row is.
|
|
322
|
+
*/
|
|
323
|
+
let isRetiredValue = (scope: retiredScope, cell: option<JSON.t>): bool =>
|
|
324
|
+
switch (scope.values, cell) {
|
|
325
|
+
| (_, None) => false
|
|
326
|
+
| (None, Some(v)) => v->JSON.Decode.bool->Option.getOr(false)
|
|
327
|
+
| (Some(states), Some(v)) =>
|
|
328
|
+
switch v->JSON.Decode.string {
|
|
329
|
+
| None => false
|
|
330
|
+
| Some(s) => states->Array.includes(s)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
3
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
5
6
|
import * as Identity$Reventless from "./Identity.res.mjs";
|
|
@@ -155,6 +156,70 @@ function scopeOf(decision) {
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
function decideRetired(identity, retiredField, retiredValues, askedOpt, elevatedOpt) {
|
|
160
|
+
let asked = askedOpt !== undefined ? askedOpt : false;
|
|
161
|
+
let elevated = elevatedOpt !== undefined ? elevatedOpt : elevatedGroups();
|
|
162
|
+
if (retiredField === undefined) {
|
|
163
|
+
return "RetiredVisible";
|
|
164
|
+
}
|
|
165
|
+
let scope = {
|
|
166
|
+
field: retiredField,
|
|
167
|
+
values: retiredValues
|
|
168
|
+
};
|
|
169
|
+
let match = resolve(identity, elevated);
|
|
170
|
+
if (typeof match !== "object") {
|
|
171
|
+
if (asked) {
|
|
172
|
+
return "RetiredVisible";
|
|
173
|
+
} else {
|
|
174
|
+
return {
|
|
175
|
+
TAG: "ExcludeRetired",
|
|
176
|
+
_0: scope
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
switch (match.TAG) {
|
|
181
|
+
case "Elevated" :
|
|
182
|
+
if (asked) {
|
|
183
|
+
return "RetiredVisible";
|
|
184
|
+
} else {
|
|
185
|
+
return {
|
|
186
|
+
TAG: "ExcludeRetired",
|
|
187
|
+
_0: scope
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
case "Owned" :
|
|
191
|
+
case "Unidentified" :
|
|
192
|
+
return {
|
|
193
|
+
TAG: "ExcludeRetired",
|
|
194
|
+
_0: scope
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function retiredScopeOf(decision) {
|
|
200
|
+
if (typeof decision !== "object") {
|
|
201
|
+
return;
|
|
202
|
+
} else {
|
|
203
|
+
return decision._0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isRetiredValue(scope, cell) {
|
|
208
|
+
let match = scope.values;
|
|
209
|
+
if (cell === undefined) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (match === undefined) {
|
|
213
|
+
return Stdlib_Option.getOr(Stdlib_JSON.Decode.bool(cell), false);
|
|
214
|
+
}
|
|
215
|
+
let s = Stdlib_JSON.Decode.string(cell);
|
|
216
|
+
if (s !== undefined) {
|
|
217
|
+
return match.includes(s);
|
|
218
|
+
} else {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
158
223
|
export {
|
|
159
224
|
isJsString,
|
|
160
225
|
systemProviders,
|
|
@@ -169,5 +234,8 @@ export {
|
|
|
169
234
|
isExempt,
|
|
170
235
|
decide,
|
|
171
236
|
scopeOf,
|
|
237
|
+
decideRetired,
|
|
238
|
+
retiredScopeOf,
|
|
239
|
+
isRetiredValue,
|
|
172
240
|
}
|
|
173
241
|
/* Identity-Reventless Not a pure module */
|