@reventlessdev/reventless-spec 3.0.0-alpha.80 → 3.0.0-alpha.81

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,14 @@
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.81 (2026-07-26)
7
+
8
+ ### Features
9
+
10
+ * **auto-ui:** declare command target state via [@target](https://github.com/target)State ([5fc0374](https://github.com/ReventlessDev/reventless-core/commit/5fc03741a8816c57085b86a4ad7d595e3b690193)), closes [#5](https://github.com/ReventlessDev/reventless-core/issues/5)
11
+ * **auto-ui:** declare field semantics + dashboard metrics ([@semantic](https://github.com/semantic), [@metric](https://github.com/metric)) ([d74ff77](https://github.com/ReventlessDev/reventless-core/commit/d74ff7721e18e8638a82931a370a549b304dac94)), closes [#4](https://github.com/ReventlessDev/reventless-core/issues/4)
12
+
13
+
6
14
  # 3.0.0-alpha.80 (2026-07-22)
7
15
 
8
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.80",
3
+ "version": "3.0.0-alpha.81",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -177,6 +177,16 @@ type commandDef = {
177
177
  */
178
178
  allowedStates: @s.matches(stringArrayOptionSchema) option<array<string>>,
179
179
  /**
180
+ The single status value this command's handler writes — the command's *to*
181
+ 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`.
187
+ */
188
+ targetState: @s.matches(stringOptionSchema) option<string>,
189
+ /**
180
190
  Whether this command variant is exposed in the generated API (a non-`@noApi`
181
191
  variant of a non-`@noApi` command). Dev tooling badges API-exposed commands in
182
192
  the event graph. js_nullable (T | null) so it stays JSON-safe inside the
@@ -102,6 +102,7 @@ let commandDefSchema = S.schema(s => ({
102
102
  mutationField: s.m(S.string),
103
103
  references: s.m(S.array(fieldReferenceSchema)),
104
104
  allowedStates: s.m(stringArrayOptionSchema),
105
+ targetState: s.m(stringOptionSchema),
105
106
  apiExposed: s.m(boolOptionSchema)
106
107
  }));
107
108
 
@@ -26,6 +26,13 @@ free on the in-memory adapter but is `O(n)` Scan + FilterExpression on
26
26
  DynamoDB-backed adapters — the annotation is the explicit signal that the
27
27
  read model is small enough or the cost is acceptable.
28
28
  */
29
+ /**
30
+ Declared aggregation for a `@metric`-annotated numeric field. `aggregate` is one
31
+ of `"count"`, `"sum"`, `"avg"` (the UI's dashboard vocabulary); `label` is the
32
+ KPI label, or `""` to let the UI derive one from the field name.
33
+ */
34
+ type metricSpec = {aggregate: string, label: string}
35
+
29
36
  type stateAnnotationSpec = {
30
37
  ids: array<string>,
31
38
  compositeIds: array<string>,
@@ -40,6 +47,22 @@ type stateAnnotationSpec = {
40
47
  scan: array<string>,
41
48
  scanSort: array<string>,
42
49
  /**
50
+ Fields annotated `@semantic("<id>")` — `(fieldName, semanticId)` pairs. The
51
+ semantic id is the UI's `AutoSemantics` vocabulary (e.g. `"currency"`,
52
+ `"geo-lat"`); the PPX can't validate it (the UI owns that vocabulary).
53
+ `SuryToJsonSchema` emits `x-reventless-semantic` on the named field, which
54
+ AutoUI reads at `#Annotation` provenance — above its heuristic, below a
55
+ `ui-hints.json` `fields:` override.
56
+ */
57
+ semantic: array<(string, string)>,
58
+ /**
59
+ Fields annotated `@metric(...)` — `(fieldName, metricSpec)` pairs.
60
+ `SuryToJsonSchema` emits `x-reventless-metric: {aggregate, label}` on the named
61
+ field, which AutoUI folds into the dashboard metric list it already builds from
62
+ hints — so a plugin gets a Dashboard page from its schema alone.
63
+ */
64
+ metric: array<(string, metricSpec)>,
65
+ /**
43
66
  Field annotated `@status` on the state record (PPX-emitted). `Some(name)`
44
67
  when one such annotation exists; the PPX errors on duplicate `@status`
45
68
  annotations within the same record. Codegen consumes this to populate
@@ -24,7 +24,7 @@ let name = "CategoriesView"
24
24
  | CategoryRenamed({categoryId: string, name: string})
25
25
  | CategoryArchived({categoryId: string})
26
26
 
27
- let project = event => switch event {
27
+ let project = ({event}) => switch event {
28
28
  | CategoryAdded({categoryId, name}) =>
29
29
  [Set(categoryId, {categoryId, name, archived: false})]
30
30
  | CategoryRenamed({categoryId, name}) =>
@@ -81,6 +81,29 @@ module type Spec = {
81
81
  let visibility: Visibility.t
82
82
  }
83
83
 
84
+ /**
85
+ The envelope a projection receives for each consumed event.
86
+
87
+ Carries the decoded event alongside the metadata the storage layer already
88
+ holds, so a projection can persist producer time or the acting user without the
89
+ command author having to duplicate that framework state into the event payload.
90
+
91
+ `meta.time` and `recordedAt` are **different clocks** — pick deliberately:
92
+
93
+ - `meta.time` — *producer* time, stamped when the command handler created the
94
+ event. This is the domain-meaningful timestamp (when the order was placed,
95
+ when it shipped); it is identical across every path.
96
+ - `recordedAt` — *storage* time, when the event was appended to the DCB log. On
97
+ the AWS (DynamoDB-stream) path this is the authoritative stored column; on the
98
+ local (topic) path it is stamped at publish, so it may sit a few milliseconds
99
+ after the stored value. Use it for storage-lag diagnostics, not domain dates.
100
+ */
101
+ type consumed<'e> = {
102
+ event: 'e,
103
+ meta: Message.meta,
104
+ recordedAt: string,
105
+ }
106
+
84
107
  /**
85
108
  The Projection — the pure projection function from consumed events to actions.
86
109
  */
@@ -89,9 +112,10 @@ module type Projection = {
89
112
 
90
113
  /**
91
114
  Projects one consumed event into read model actions.
92
- Receives only events declared in `Spec.consumedEvent` — no wildcard needed.
115
+ Receives the event wrapped in a `consumed` envelope (event + `meta` +
116
+ `recordedAt`); only events declared in `Spec.consumedEvent` — no wildcard needed.
93
117
  */
94
- let project: Spec.consumedEvent => array<Projection.action<string, Spec.state>>
118
+ let project: consumed<Spec.consumedEvent> => array<Projection.action<string, Spec.state>>
95
119
 
96
120
  /** File URL of this Projection module (`import.meta.url`). */
97
121
  let moduleUrl: string
@@ -0,0 +1,33 @@
1
+ /**
2
+ Marks a `string` state field as an ISO-8601 date-time.
3
+
4
+ Sury has no string-typed datetime format (`S.datetime` transforms to `Js.Date.t`,
5
+ changing the field's runtime type), so this mirrors the `DcbTag.string`
6
+ precedent: a `S.t<string>` carrying sury metadata that downstream schema walkers
7
+ detect. `SchemaType`/`SuryToJsonSchema` surface it as `format: "date-time"` on
8
+ the field's JSON Schema, which the AutoUI date heuristics key off (CalendarView,
9
+ TimelineView, date-axis charts).
10
+
11
+ Use on a producer/storage timestamp a projection writes into its state — most
12
+ commonly a `meta.time`-derived field such as `placedAt` / `shippedAt`:
13
+
14
+ @example
15
+ ```rescript
16
+ @schema
17
+ type state = {
18
+ orderId: string,
19
+ placedAt: @s.matches(Reventless.DateTime.string) string,
20
+ }
21
+ ```
22
+ */
23
+
24
+ /** Internal sury metadata ID used to mark a date-time string field. */
25
+ let dateTimeId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(~namespace="reventless", ~name="dateTime")
26
+
27
+ /** A sury string schema annotated as an ISO-8601 date-time field.
28
+ Use with `@s.matches(Reventless.DateTime.string)`. */
29
+ let string: S.t<string> = S.string->S.Metadata.set(~id=dateTimeId, true)
30
+
31
+ /** Whether a field schema carries the date-time marker. */
32
+ let isDateTime = (fieldSchema: S.t<unknown>) =>
33
+ S.Metadata.get(fieldSchema, ~id=dateTimeId)->Option.isSome
@@ -0,0 +1,19 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+
6
+ let dateTimeId = S.Metadata.Id.make("reventless", "dateTime");
7
+
8
+ let string = S.Metadata.set(S.string, dateTimeId, true);
9
+
10
+ function isDateTime(fieldSchema) {
11
+ return Stdlib_Option.isSome(S.Metadata.get(fieldSchema, dateTimeId));
12
+ }
13
+
14
+ export {
15
+ dateTimeId,
16
+ string,
17
+ isDateTime,
18
+ }
19
+ /* dateTimeId Not a pure module */