@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.114",
3
+ "version": "3.0.0-alpha.116",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -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 (see docs/plans/translation-external-boxes.md).
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 (see docs/plans/translation-external-boxes.md).
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
- Status values under which this command is meaningful. `None` means the command
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 status field is not in the set — see `queryableDef.statusField`
176
- for how the row's status is located. `Some([])` is the defensive "never show" form.
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 status value this command's handler writes — the command's *to*
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
- `@targetState("Shipped")` command-variant annotation. `None` (absent
183
- annotation) is the back-compat default: AutoUI's board resolver then falls
184
- back to its name-stem heuristic. `Some("Shipped")` lets the resolver move a
185
- row by a declared transition instead of a guess. js_nullable for JSON safety,
186
- same as `allowedStates`.
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 `statusField`.
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 status, used
270
- by AutoUI together with `commandDef.allowedStates` to filter the per-row command
271
- menu. Resolution order (codegen): (1) field annotated `@status`; (2) a field
272
- literally named `"status"`; (3) `None`. Spec authors that hand-roll a
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
- statusField: @s.matches(stringOptionSchema) option<string>,
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 `statusField`.
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 `statusField`.
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
- statusField: s.m(stringOptionSchema),
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 (`@status`, `@groupBy`), or a
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 `@status` on the state record (PPX-emitted). `Some(name)`
68
- when one such annotation exists; the PPX errors on duplicate `@status`
69
- annotations within the same record. Codegen consumes this to populate
70
- `queryableDef.statusField` (with a fallback to a field literally named
71
- `"status"` when this annotation is absent).
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
- status: option<string>,
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.
@@ -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.
@@ -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
@@ -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 */