@reventlessdev/reventless-spec 3.0.0-alpha.128 → 3.0.0-alpha.130

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.
@@ -141,22 +141,23 @@ type commandJson = {
141
141
 
142
142
  // ── Schema-migration-on-read ──────────────────────────────────────────────────
143
143
  // Nested `@schema` types (notably `pluginDefinition`/`pluginStructure`) gain fields
144
- // over time — `kind`, `chapter`, `events`, `extensionPoints`, `apiExposed`, … Because
145
- // those types are JSON-encoded inside union-variant payloads, every optional field must
146
- // use the `js_nullable` (`T | null`) encoding: it is the only JSON-safe optional form,
147
- // since `S.option`/`nullableAsOption` carry `undefined`, which fails sury's
148
- // `jsonableValidation` inside a union variant. That encoding is *present-required on
149
- // decode*, so ONE message persisted before a field was added SuryError-bricks decode. For
150
- // an aggregate that rehydrates from its own event log (the Plugin lifecycle aggregate),
151
- // that single event then freezes EVERY later heartbeat/redetect/connect on that instance
152
- // — a silent lifecycle freeze with no error surfaced near the operator.
144
+ // over time — `kind`, `chapter`, `events`, `extensionPoints`, `apiExposed`, … A field
145
+ // that is not optional is *present-required on decode*, so ONE message persisted before
146
+ // it was added SuryError-bricks decode. For an aggregate that rehydrates from its own
147
+ // event log (the Plugin lifecycle aggregate), that single event then freezes EVERY later
148
+ // heartbeat/redetect/connect on that instance — a silent lifecycle freeze with no error
149
+ // surfaced near the operator.
153
150
  //
154
151
  // We heal on read. Strict decode stays the fast path (unchanged for every current
155
152
  // message); only when it throws do we schema-guide the raw JSON and retry once. The fill
156
153
  // walks the target sury schema and inserts, for any absent field, the value that field's
157
- // schema expects: `null` for a `T | null` union (→ `None`), `[]` for a missing array, the
154
+ // schema expects: `null` for a `T | null` union (→ `None`), nothing at all for an
155
+ // `option` (a union admitting `undefined` — also `None`), `[]` for a missing array, the
158
156
  // first variant of a mandatory enum (`kind` → `Domain`), a filled `{}` for a missing
159
- // nested object, and a zero value for a missing scalar. It descends only into values
157
+ // nested object, and a zero value for a missing scalar. The `undefined` arm mirrors the
158
+ // `null` one and must precede the enum/object guesses below it: an absent `option<enum>`
159
+ // would otherwise heal to that enum's first variant and an absent `option<record>` to a
160
+ // filled `{}`, turning `None` into a `Some` of an invented value. It descends only into values
160
161
  // actually present, matches tagged-union members by their `TAG` const, is purely additive
161
162
  // (clones via a JSON round-trip; never re-encodes through the schema), is idempotent on
162
163
  // valid data, and falls back to the ORIGINAL error when the fill doesn't resolve the
@@ -229,6 +230,7 @@ let fillMissingDefaults: (S.t<'a>, JSON.t, array<string>) => JSON.t = %raw(`func
229
230
  var has=schema.has||{};
230
231
  if(value===undefined){
231
232
  if(has.null) return null;
233
+ if(has.undefined) return undefined;
232
234
  var c=firstConst(schema.anyOf); if(c!==undefined) return c;
233
235
  var obj=(schema.anyOf||[]).find(function(s){return s.type==="object";}); if(obj) return fill(obj,{},path);
234
236
  return undefined;
@@ -273,7 +275,7 @@ let parseJsonTolerant = (json, schema) =>
273
275
  ->Int.toString} missing scalar field(s): ${scalarFills->Array.join(
274
276
  ", ",
275
277
  )}. A required scalar was added to a persisted type after this message was ` ++
276
- `written; the value above is fabricated, not recovered. Prefer a js_nullable (T | null) field.`,
278
+ `written; the value above is fabricated, not recovered. Prefer an optional field.`,
277
279
  )
278
280
  }
279
281
  value
@@ -87,6 +87,7 @@ let fillMissingDefaults = (function(schema, json, scalarFills){
87
87
  var has=schema.has||{};
88
88
  if(value===undefined){
89
89
  if(has.null) return null;
90
+ if(has.undefined) return undefined;
90
91
  var c=firstConst(schema.anyOf); if(c!==undefined) return c;
91
92
  var obj=(schema.anyOf||[]).find(function(s){return s.type==="object";}); if(obj) return fill(obj,{},path);
92
93
  return undefined;
@@ -126,7 +127,7 @@ function parseJsonTolerant(json, schema) {
126
127
  throw firstErr;
127
128
  }
128
129
  if (scalarFills.length !== 0) {
129
- console.warn(`[reventless] decoded a stored message by inventing ` + scalarFills.length.toString() + ` missing scalar field(s): ` + scalarFills.join(", ") + `. A required scalar was added to a persisted type after this message was written; the value above is fabricated, not recovered. Prefer a js_nullable (T | null) field.`);
130
+ console.warn(`[reventless] decoded a stored message by inventing ` + scalarFills.length.toString() + ` missing scalar field(s): ` + scalarFills.join(", ") + `. A required scalar was added to a persisted type after this message was written; the value above is fabricated, not recovered. Prefer an optional field.`);
130
131
  }
131
132
  return value;
132
133
  }
@@ -36,10 +36,23 @@
36
36
  // plugin structure is assembled, and what leaves is the pair of names the
37
37
  // structure already carried.
38
38
  type t<'state> =
39
- /** No edge declared: legal in every state, moves the row nowhere. The honest
40
- answer for a report a slice publishes, which must not be refused because
41
- the row moved on while the report was in flight. */
39
+ /** Legal in every state, and moves the row nowhere. The honest answer for a
40
+ report a slice publishes, which must not be refused because the row moved
41
+ on while the report was in flight. A claim, not an omission — see
42
+ `Undeclared`, which is the omission. */
42
43
  | Unrestricted
44
+ /** The spec wrote no switch, and the ppx injected this. Never write it: say
45
+ `Unrestricted` if you mean the command is legal everywhere.
46
+
47
+ It reads exactly like `Unrestricted` — no from-set, no target — everywhere
48
+ but one place, and that place is why it exists. The harvested lifecycle
49
+ model may answer for silence; it may not narrow a claim. A corpus only
50
+ covers the states somebody wrote a scenario for, so letting it answer for
51
+ `Unrestricted` would shrink "legal in every state" down to an accident of
52
+ coverage, and the command would quietly stop being offered on the rows
53
+ nobody tested. With the two spelled apart, that shrinkage cannot happen and
54
+ a scenario that genuinely refutes the claim is a contradiction instead. */
55
+ | Undeclared
43
56
  /** Brings the row into existence, so there is no state it could come from.
44
57
  Distinct from `Unrestricted`, which draws no edge at all. */
45
58
  | Creates('state)
@@ -54,6 +67,7 @@ type t<'state> =
54
67
  tells apart. */
55
68
  let allowedStates = (transition: t<'state>): option<array<'state>> =>
56
69
  switch transition {
70
+ | Undeclared
57
71
  | Unrestricted
58
72
  | Creates(_) => None
59
73
  | Guards(states)
@@ -63,8 +77,21 @@ let allowedStates = (transition: t<'state>): option<array<'state>> =>
63
77
  /** The state the command's handler writes, or `None` for one that moves nothing. */
64
78
  let targetState = (transition: t<'state>): option<'state> =>
65
79
  switch transition {
80
+ | Undeclared
66
81
  | Unrestricted
67
82
  | Guards(_) => None
68
83
  | Creates(state)
69
84
  | Moves(_, state) => Some(state)
70
85
  }
86
+
87
+ /** Whether the command is claimed legal in every state, as opposed to nothing
88
+ being claimed at all. The two erase to the same pair of `None`s above, so this
89
+ is the only thing that can tell a reader of the erased form which it holds. */
90
+ let isUnrestricted = (transition: t<'state>): bool =>
91
+ switch transition {
92
+ | Unrestricted => true
93
+ | Undeclared
94
+ | Creates(_)
95
+ | Guards(_)
96
+ | Moves(_, _) => false
97
+ }
@@ -7,11 +7,11 @@ function allowedStates(transition) {
7
7
  return;
8
8
  }
9
9
  switch (transition.TAG) {
10
- case "Creates" :
11
- return;
12
10
  case "Guards" :
13
11
  case "Moves" :
14
12
  return transition._0;
13
+ default:
14
+ return;
15
15
  }
16
16
  }
17
17
 
@@ -22,15 +22,24 @@ function targetState(transition) {
22
22
  switch (transition.TAG) {
23
23
  case "Creates" :
24
24
  return Primitive_option.some(transition._0);
25
- case "Guards" :
26
- return;
27
25
  case "Moves" :
28
26
  return Primitive_option.some(transition._1);
27
+ default:
28
+ return;
29
+ }
30
+ }
31
+
32
+ function isUnrestricted(transition) {
33
+ if (typeof transition !== "object") {
34
+ return transition === "Unrestricted";
35
+ } else {
36
+ return false;
29
37
  }
30
38
  }
31
39
 
32
40
  export {
33
41
  allowedStates,
34
42
  targetState,
43
+ isUnrestricted,
35
44
  }
36
45
  /* No side effect */