@reventlessdev/trait-notification 1.0.0-alpha.5 → 1.0.0-alpha.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/trait-notification",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.51",
4
4
  "description": "Notification domain trait: rules, host contract, conformance suite and emitter for a per-recipient contact directory, a kind x channel subscription matrix, and the decision to send, suppress or record a message as undeliverable",
5
5
  "license": "Apache-2.0",
6
6
  "files": [
@@ -11,16 +11,16 @@
11
11
  "README.md"
12
12
  ],
13
13
  "dependencies": {
14
- "sury": "11.0.0-rc.2",
15
- "sury-ppx": "11.0.0-rc.2",
16
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.203",
17
- "@reventlessdev/reventless-spec": "3.0.0-alpha.129"
14
+ "sury": "11.0.0",
15
+ "sury-ppx": "11.0.0",
16
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.249",
17
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.170"
18
18
  },
19
19
  "devDependencies": {
20
- "rescript": "12.3.0"
20
+ "rescript": "12.3.1"
21
21
  },
22
22
  "peerDependencies": {
23
- "rescript": "12.3.0"
23
+ "rescript": "^12.3.1"
24
24
  },
25
25
  "publishConfig": {
26
26
  "registry": "https://registry.npmjs.org"
@@ -19,7 +19,6 @@ What an occurrence *is*. It is told a recipient, a kind, and a reference; it is
19
19
  never told there was an order. That is why the two relays are the only files a
20
20
  graft has to be given, and everything else can be handed over whole.
21
21
  */
22
-
23
22
  /**
24
23
  This trait's own account of itself — see `AddressGeocoding.declaration` for why the
25
24
  version is read rather than written.
@@ -64,21 +63,33 @@ module type Binding = {
64
63
  let transactional: category
65
64
  let optional: category
66
65
 
67
- module Spec: ReventlessGwt.Behavior_GWT.BehaviorSpec
68
- module Behavior: ReventlessGwt.Behavior_GWT.Behavior with module Spec = Spec
69
-
70
- /** History that brings the recipient into existence with nothing on file. */
71
- let created: array<Spec.consumedEvent>
72
-
73
- /** The competency's own facts, as the slice consumes them. */
74
- let announcedC: string => Spec.consumedEvent
75
- let subscribedC: (category, Notification_Rules.channel) => Spec.consumedEvent
76
- let unsubscribedC: (category, Notification_Rules.channel) => Spec.consumedEvent
77
- /** A source taken over, as the slice reads it back. Claims are per source and
78
- not per recipient, so a host produces these from wherever it keeps them —
79
- what this contract requires is only that the decision can see them. */
80
- let claimedC: (string, string) => Spec.consumedEvent
81
- let releasedC: string => Spec.consumedEvent
66
+ module Spec: ReventlessGwt.Decision_GWT.DecisionSpec
67
+ module Decision: ReventlessGwt.Decision_GWT.Decision with module Spec = Spec
68
+
69
+ /** The same facts as the slice **consumes** them, for building history.
70
+
71
+ A slice declares `consumedEvent` and `event` separately and they differ —
72
+ the consumed form carries no recipient id, because the partition already
73
+ says whose it is. So a suite needs both spellings of the same fact: one to
74
+ lay down history, one to assert what was emitted.
75
+
76
+ Grouped rather than suffixed. The pairs used to read `announcedC` against
77
+ `announced`, which said "consumed" only to someone who already knew — and
78
+ in a codebase where every binding also carries commands, a bare `C` reads
79
+ just as naturally as one. */
80
+ module Consumed: {
81
+ /** History that brings the recipient into existence with nothing on file. */
82
+ let created: array<Spec.consumedEvent>
83
+ let announced: string => Spec.consumedEvent
84
+ let subscribed: (category, Notification_Rules.channel) => Spec.consumedEvent
85
+ let unsubscribed: (category, Notification_Rules.channel) => Spec.consumedEvent
86
+ /** A source taken over, as the slice reads it back. Claims are per source
87
+ and not per recipient, so a host produces these from wherever it keeps
88
+ them — what this contract requires is only that the decision can see
89
+ them. */
90
+ let claimed: (string, string) => Spec.consumedEvent
91
+ let released: string => Spec.consumedEvent
92
+ }
82
93
 
83
94
  let announce: string => Spec.command
84
95
  let subscribe: (category, Notification_Rules.channel) => Spec.command
@@ -108,12 +119,7 @@ module type Binding = {
108
119
  /** `requested` as it comes out for a `Configured` request. A separate member
109
120
  because *which* rule asked is the host's own shape — the trait's fact
110
121
  carries no origin at all, so it cannot construct this one. */
111
- let requestedConfigured: (
112
- category,
113
- string,
114
- Notification_Rules.channel,
115
- string,
116
- ) => Spec.event
122
+ let requestedConfigured: (category, string, Notification_Rules.channel, string) => Spec.event
117
123
 
118
124
  /** The refusal for managing preferences for somebody nobody has announced. */
119
125
  let recipientUnknown: Spec.error
@@ -6,7 +6,6 @@ What it asserts is the competency, not the host: the directory, the fallback to
6
6
  the host's posture, and — the part worth being strict about — that the three ways
7
7
  to send nothing stay three different facts.
8
8
  */
9
-
10
9
  /** The suite's title, composed once and read twice: the suite registers it,
11
10
  and `certify-trait` computes the same string to find the run's assertions in
12
11
  a test report. Exported rather than inlined so neither side parses the
@@ -14,17 +13,17 @@ to send nothing stay three different facts.
14
13
  let suiteName = (host: string) => `${host} conforms to the notification trait`
15
14
 
16
15
  module Make = (B: Notification.Binding) => {
17
- module G = ReventlessGwt.Behavior_GWT.Make(B.Spec, B.Behavior)
16
+ module G = ReventlessGwt.Decision_GWT.Make(B.Spec, B.Decision)
18
17
  module R = Notification_Rules
19
18
 
20
- let announced = Array.concat(B.created, [B.announcedC(B.addressA)])
19
+ let announced = Array.concat(B.Consumed.created, [B.Consumed.announced(B.addressA)])
21
20
 
22
21
  let register = () =>
23
22
  G.describe(suiteName(B.Spec.name), () => {
24
23
  G.test("an announced contact is recorded", () =>
25
- G.givenEvents(B.created)->G.whenCmd(B.announce(B.addressA))->G.thenEvent(
26
- B.announced(B.addressA),
27
- )
24
+ G.givenEvents(B.Consumed.created)
25
+ ->G.whenCmd(B.announce(B.addressA))
26
+ ->G.thenEvent(B.announced(B.addressA))
28
27
  )
29
28
 
30
29
  // The relay re-announces on every contact event a host publishes, and its
@@ -36,15 +35,15 @@ module Make = (B: Notification.Binding) => {
36
35
  )
37
36
 
38
37
  G.test("a changed address is recorded", () =>
39
- G.givenEvents(announced)->G.whenCmd(B.announce(B.addressB))->G.thenEvent(
40
- B.announced(B.addressB),
41
- )
38
+ G.givenEvents(announced)
39
+ ->G.whenCmd(B.announce(B.addressB))
40
+ ->G.thenEvent(B.announced(B.addressB))
42
41
  )
43
42
 
44
43
  // A person is at the other end of this one, so they are told rather than
45
44
  // having a fact recorded about them.
46
45
  G.test("managing preferences for an unannounced recipient is refused", () =>
47
- G.givenEvents(B.created)
46
+ G.givenEvents(B.Consumed.created)
48
47
  ->G.whenCmd(B.subscribe(B.optional, B.announcedChannel))
49
48
  ->G.thenError(B.recipientUnknown)
50
49
  )
@@ -81,13 +80,11 @@ module Make = (B: Notification.Binding) => {
81
80
  G.test("a transactional request goes out with no explicit subscription", () =>
82
81
  G.givenEvents(announced)
83
82
  ->G.whenCmd(B.request(B.transactional, "ref-1"))
84
- ->G.thenEvent(
85
- B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressA),
86
- )
83
+ ->G.thenEvent(B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressA))
87
84
  )
88
85
 
89
86
  G.test("the address on the request is the one currently on file", () =>
90
- G.givenEvents(Array.concat(announced, [B.announcedC(B.addressB)]))
87
+ G.givenEvents(Array.concat(announced, [B.Consumed.announced(B.addressB)]))
91
88
  ->G.whenCmd(B.request(B.transactional, "ref-1"))
92
89
  ->G.thenEvent(B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB))
93
90
  )
@@ -100,7 +97,7 @@ module Make = (B: Notification.Binding) => {
100
97
 
101
98
  G.test("a recipient who opted out is suppressed", () =>
102
99
  G.givenEvents(
103
- Array.concat(announced, [B.unsubscribedC(B.transactional, B.announcedChannel)]),
100
+ Array.concat(announced, [B.Consumed.unsubscribed(B.transactional, B.announcedChannel)]),
104
101
  )
105
102
  ->G.whenCmd(B.request(B.transactional, "ref-3"))
106
103
  ->G.thenEvent(B.suppressed(B.transactional, "ref-3"))
@@ -108,7 +105,7 @@ module Make = (B: Notification.Binding) => {
108
105
 
109
106
  // The case the directory exists for.
110
107
  G.test("a request for a recipient nobody announced is undeliverable", () =>
111
- G.givenEvents(B.created)
108
+ G.givenEvents(B.Consumed.created)
112
109
  ->G.whenCmd(B.request(B.transactional, "ref-4"))
113
110
  ->G.thenEvent(B.undeliverable(B.transactional, "ref-4"))
114
111
  )
@@ -129,7 +126,9 @@ module Make = (B: Notification.Binding) => {
129
126
  )
130
127
 
131
128
  G.test("a default request for a claimed source is deferred", () =>
132
- G.givenEvents(Array.concat(announced, [B.claimedC("other:Source", "second-producer")]))
129
+ G.givenEvents(
130
+ Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]),
131
+ )
133
132
  ->G.whenCmd(
134
133
  B.requestFrom(B.transactional, "ref-7", ~source="other:Source", ~origin=Default),
135
134
  )
@@ -140,7 +139,9 @@ module Make = (B: Notification.Binding) => {
140
139
  // keeps firing everywhere it was not taken over. Without this, a single
141
140
  // claim would silence the whole competency.
142
141
  G.test("a claim on one source leaves every other source alone", () =>
143
- G.givenEvents(Array.concat(announced, [B.claimedC("other:Source", "second-producer")]))
142
+ G.givenEvents(
143
+ Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]),
144
+ )
144
145
  ->G.whenCmd(
145
146
  B.requestFrom(B.transactional, "ref-8", ~source=B.defaultSource, ~origin=Default),
146
147
  )
@@ -150,7 +151,9 @@ module Make = (B: Notification.Binding) => {
150
151
  // The producer that owns the source is the one that must get through, or
151
152
  // the handover would silence the entry rather than move it.
152
153
  G.test("a configured request for a claimed source goes through", () =>
153
- G.givenEvents(Array.concat(announced, [B.claimedC("other:Source", "second-producer")]))
154
+ G.givenEvents(
155
+ Array.concat(announced, [B.Consumed.claimed("other:Source", "second-producer")]),
156
+ )
154
157
  ->G.whenCmd(
155
158
  B.requestFrom(B.transactional, "ref-9", ~source="other:Source", ~origin=Configured),
156
159
  )
@@ -163,7 +166,10 @@ module Make = (B: Notification.Binding) => {
163
166
  G.givenEvents(
164
167
  Array.concat(
165
168
  announced,
166
- [B.claimedC("other:Source", "second-producer"), B.releasedC("other:Source")],
169
+ [
170
+ B.Consumed.claimed("other:Source", "second-producer"),
171
+ B.Consumed.released("other:Source"),
172
+ ],
167
173
  ),
168
174
  )
169
175
  ->G.whenCmd(
@@ -183,8 +189,8 @@ module Make = (B: Notification.Binding) => {
183
189
  Array.concat(
184
190
  announced,
185
191
  [
186
- B.subscribedC(B.transactional, channel),
187
- B.unsubscribedC(B.transactional, B.announcedChannel),
192
+ B.Consumed.subscribed(B.transactional, channel),
193
+ B.Consumed.unsubscribed(B.transactional, B.announcedChannel),
188
194
  ],
189
195
  ),
190
196
  )
@@ -1,46 +1,46 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Behavior_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/Behavior_GWT.res.mjs";
3
+ import * as Decision_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/Decision_GWT.res.mjs";
4
4
 
5
5
  function suiteName(host) {
6
6
  return host + ` conforms to the notification trait`;
7
7
  }
8
8
 
9
9
  function Make(B) {
10
- let $$let = B.Behavior;
11
- let G = Behavior_GWT$ReventlessGwt.Make(B.Spec)({
10
+ let $$let = B.Decision;
11
+ let G = Decision_GWT$ReventlessGwt.Make(B.Spec)({
12
12
  initialState: $$let.initialState,
13
13
  evolve: $$let.evolve,
14
14
  decide: $$let.decide
15
15
  });
16
- let announced = B.created.concat([B.announcedC(B.addressA)]);
16
+ let announced = B.Consumed.created.concat([B.Consumed.announced(B.addressA)]);
17
17
  let register = () => G.describe(suiteName(B.Spec.name), () => {
18
- G.test("an announced contact is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.announce(B.addressA)), B.announced(B.addressA)));
18
+ G.test("an announced contact is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(B.Consumed.created), B.announce(B.addressA)), B.announced(B.addressA)));
19
19
  G.test("re-announcing the address already on file is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(announced), B.announce(B.addressA))));
20
20
  G.test("a changed address is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.announce(B.addressB)), B.announced(B.addressB)));
21
- G.test("managing preferences for an unannounced recipient is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.created), B.subscribe(B.optional, B.announcedChannel)), B.recipientUnknown));
21
+ G.test("managing preferences for an unannounced recipient is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.Consumed.created), B.subscribe(B.optional, B.announcedChannel)), B.recipientUnknown));
22
22
  G.test("opting in to a kind that is off by default is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.subscribe(B.optional, B.announcedChannel)), B.subscribed(B.optional, B.announcedChannel)));
23
23
  G.test("subscribing to a kind already on by posture is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(announced), B.subscribe(B.transactional, B.announcedChannel))));
24
24
  G.test("opting out of a kind that is on is recorded", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.unsubscribe(B.transactional, B.announcedChannel)), B.unsubscribed(B.transactional, B.announcedChannel)));
25
25
  G.test("opting out of a kind already off is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(announced), B.unsubscribe(B.optional, B.announcedChannel))));
26
26
  G.test("a transactional request goes out with no explicit subscription", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.request(B.transactional, "ref-1")), B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressA)));
27
- G.test("the address on the request is the one currently on file", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.announcedC(B.addressB)])), B.request(B.transactional, "ref-1")), B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB)));
27
+ G.test("the address on the request is the one currently on file", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.announced(B.addressB)])), B.request(B.transactional, "ref-1")), B.requested(B.transactional, "ref-1", B.announcedChannel, B.addressB)));
28
28
  G.test("an optional request is suppressed with no explicit subscription", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.request(B.optional, "ref-2")), B.suppressed(B.optional, "ref-2")));
29
- G.test("a recipient who opted out is suppressed", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.unsubscribedC(B.transactional, B.announcedChannel)])), B.request(B.transactional, "ref-3")), B.suppressed(B.transactional, "ref-3")));
30
- G.test("a request for a recipient nobody announced is undeliverable", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.request(B.transactional, "ref-4")), B.undeliverable(B.transactional, "ref-4")));
29
+ G.test("a recipient who opted out is suppressed", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.unsubscribed(B.transactional, B.announcedChannel)])), B.request(B.transactional, "ref-3")), B.suppressed(B.transactional, "ref-3")));
30
+ G.test("a request for a recipient nobody announced is undeliverable", () => G.thenEvent(G.whenCmd(G.givenEvents(B.Consumed.created), B.request(B.transactional, "ref-4")), B.undeliverable(B.transactional, "ref-4")));
31
31
  G.test("with nothing claimed, a default request is decided as usual", () => G.thenEvent(G.whenCmd(G.givenEvents(announced), B.requestFrom(B.transactional, "ref-6", B.defaultSource, "Default")), B.requested(B.transactional, "ref-6", B.announcedChannel, B.addressA)));
32
- G.test("a default request for a claimed source is deferred", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.claimedC("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-7", "other:Source", "Default")), B.deferred("ref-7", "other:Source")));
33
- G.test("a claim on one source leaves every other source alone", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.claimedC("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-8", B.defaultSource, "Default")), B.requested(B.transactional, "ref-8", B.announcedChannel, B.addressA)));
34
- G.test("a configured request for a claimed source goes through", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.claimedC("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-9", "other:Source", "Configured")), B.requestedConfigured(B.transactional, "ref-9", B.announcedChannel, B.addressA)));
32
+ G.test("a default request for a claimed source is deferred", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.claimed("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-7", "other:Source", "Default")), B.deferred("ref-7", "other:Source")));
33
+ G.test("a claim on one source leaves every other source alone", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.claimed("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-8", B.defaultSource, "Default")), B.requested(B.transactional, "ref-8", B.announcedChannel, B.addressA)));
34
+ G.test("a configured request for a claimed source goes through", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([B.Consumed.claimed("other:Source", "second-producer")])), B.requestFrom(B.transactional, "ref-9", "other:Source", "Configured")), B.requestedConfigured(B.transactional, "ref-9", B.announcedChannel, B.addressA)));
35
35
  G.test("a released source stops deferring", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([
36
- B.claimedC("other:Source", "second-producer"),
37
- B.releasedC("other:Source")
36
+ B.Consumed.claimed("other:Source", "second-producer"),
37
+ B.Consumed.released("other:Source")
38
38
  ])), B.requestFrom(B.transactional, "ref-10", "other:Source", "Default")), B.requested(B.transactional, "ref-10", B.announcedChannel, B.addressA)));
39
39
  let channel = B.unreachableChannel;
40
40
  if (channel !== undefined) {
41
41
  return G.test("a channel with no address on file is undeliverable, not suppressed", () => G.thenEvent(G.whenCmd(G.givenEvents(announced.concat([
42
- B.subscribedC(B.transactional, channel),
43
- B.unsubscribedC(B.transactional, B.announcedChannel)
42
+ B.Consumed.subscribed(B.transactional, channel),
43
+ B.Consumed.unsubscribed(B.transactional, B.announcedChannel)
44
44
  ])), B.request(B.transactional, "ref-5")), B.undeliverable(B.transactional, "ref-5")));
45
45
  }
46
46
  });
@@ -56,4 +56,4 @@ export {
56
56
  suiteName,
57
57
  Make,
58
58
  }
59
- /* Behavior_GWT-ReventlessGwt Not a pure module */
59
+ /* Decision_GWT-ReventlessGwt Not a pure module */
@@ -9,7 +9,6 @@ surface. It is also why the wording is `Reventless.Template` source: readable
9
9
  text rather than ReScript interpolation, rendered against the payload's schema so
10
10
  a semantic formats itself and a `@sensitive` field is withheld.
11
11
  */
12
-
13
12
  /** How a field is compared. */
14
13
  @schema
15
14
  type comparison =
@@ -165,8 +164,7 @@ let recipientOf = (rule: t, ~payload: JSON.t) => stringAt(payload, rule.recipien
165
164
 
166
165
  /** What it is about. Empty is legal — a notification about nothing in particular
167
166
  is a real case, and a fabricated subject is worse than an absent one. */
168
- let subjectOf = (rule: t, ~payload: JSON.t) =>
169
- stringAt(payload, rule.subjectPath)->Option.getOr("")
167
+ let subjectOf = (rule: t, ~payload: JSON.t) => stringAt(payload, rule.subjectPath)->Option.getOr("")
170
168
 
171
169
  /** The wording to use, the asked-for locale if the rule has it and the first
172
170
  otherwise. Which locale to ask for is the caller's — nothing here knows who
@@ -191,10 +189,7 @@ let render = (source: string, ~payload: JSON.t, ~schema: S.t<'a>) =>
191
189
  let compose = (rule: t, ~payload: JSON.t, ~schema: S.t<'a>, ~locale: option<string>=?) =>
192
190
  switch contentFor(rule, ~locale?) {
193
191
  | None => ("", "")
194
- | Some({subject, body}) => (
195
- render(subject, ~payload, ~schema),
196
- render(body, ~payload, ~schema),
197
- )
192
+ | Some({subject, body}) => (render(subject, ~payload, ~schema), render(body, ~payload, ~schema))
198
193
  }
199
194
 
200
195
  /**
@@ -221,11 +216,12 @@ let validate = (rules: array<t>, ~digestRouted: bool=false, ~sample: JSON.t): ar
221
216
  note("no wording at all")
222
217
  }
223
218
  rule.content->Array.forEach(({locale, subject, body}) =>
224
- [("subject", subject), ("body", body)]->Array.forEach(((which, template)) =>
225
- switch Reventless.Template.parse(template) {
226
- | Ok(_) => ()
227
- | Error(why) => note(`${locale} ${which} does not parse — ${why}`)
228
- }
219
+ [("subject", subject), ("body", body)]->Array.forEach(
220
+ ((which, template)) =>
221
+ switch Reventless.Template.parse(template) {
222
+ | Ok(_) => ()
223
+ | Error(why) => note(`${locale} ${which} does not parse — ${why}`)
224
+ },
229
225
  )
230
226
  )
231
227
  if recipientOf(rule, ~payload=sample) == None {
@@ -23,11 +23,11 @@ let literalSchema = Sury.union([
23
23
  })),
24
24
  Sury.$schema(s => ({
25
25
  TAG: "Number",
26
- _0: s.m(Sury.float)
26
+ _0: s.m(Sury.number)
27
27
  })),
28
28
  Sury.$schema(s => ({
29
29
  TAG: "Flag",
30
- _0: s.m(Sury.bool)
30
+ _0: s.m(Sury.boolean)
31
31
  }))
32
32
  ]);
33
33
 
@@ -68,7 +68,7 @@ let deliverySchema = Sury.union([
68
68
  Sury.literal("Immediate"),
69
69
  Sury.$schema(s => ({
70
70
  TAG: "Digest",
71
- windowSeconds: s.m(Sury.int)
71
+ windowSeconds: s.m(Sury.int32)
72
72
  }))
73
73
  ]);
74
74
 
@@ -17,7 +17,6 @@ is the opposite. So the fallback *rule* lives here — an absent choice defers t
17
17
  the posture — and the *table* is passed in as `~posture`. A trait that hard-coded
18
18
  either answer would be wrong for half its hosts.
19
19
  */
20
-
21
20
  /** How a recipient is reached. Mirrors `Reventless.Messaging.channel`, which is
22
21
  the platform capability's vocabulary; this one is the domain's, so it carries
23
22
  a host's schema and travels on the wire. `Notification_Send` maps the two. */
@@ -72,22 +71,22 @@ let claimedBy = (t, source) =>
72
71
 
73
72
  /** What a host asks the competency to do. */
74
73
  type op =
75
- | /** Relayed from whatever the host publishes when it learns where somebody is.
74
+ /** Relayed from whatever the host publishes when it learns where somebody is.
76
75
  Not a client's command: a caller who could announce another person's
77
76
  address would be redirecting their mail. */
78
- Announce({channel: channel, address: string})
77
+ | Announce({channel: channel, address: string})
79
78
  | Subscribe({category: category, channel: channel})
80
79
  | Unsubscribe({category: category, channel: channel})
81
- | /** Something worth telling them about happened. `reference` is the caller's
80
+ /** Something worth telling them about happened. `reference` is the caller's
82
81
  own key for it, echoed back on whichever outcome follows, so the relay
83
82
  that asked can tell its work is finished. Opaque here on purpose.
84
83
 
85
84
  `source` says which stream it came from and `origin` says who is asking;
86
85
  together they are the whole of the handover — see `decide`. */
87
- Request({category: category, reference: string, source: source, origin: origin})
88
- | /** A second producer takes over one source. Relayed like `Announce`: a
86
+ | Request({category: category, reference: string, source: source, origin: origin})
87
+ /** A second producer takes over one source. Relayed like `Announce`: a
89
88
  client that could claim a source would be silencing somebody else's mail. */
90
- Claim({source: source, by: string})
89
+ | Claim({source: source, by: string})
91
90
  | Release({source: source})
92
91
 
93
92
  /** What the competency decided, for the host to name in its own events. */
@@ -95,18 +94,18 @@ type fact =
95
94
  | Announced({channel: channel, address: string})
96
95
  | Subscribed({category: category, channel: channel})
97
96
  | Unsubscribed({category: category, channel: channel})
98
- | /** The addressed message. `address` is the snapshot delivery uses, which is
97
+ /** The addressed message. `address` is the snapshot delivery uses, which is
99
98
  why it is on the fact and not looked up again later. */
100
- Requested({category: category, reference: string, channel: channel, address: string})
101
- | /** They are reachable and said no. */
102
- Suppressed({category: category, reference: string})
103
- | /** They want it and nobody can be reached — no address on file for any
99
+ | Requested({category: category, reference: string, channel: channel, address: string})
100
+ /** They are reachable and said no. */
101
+ | Suppressed({category: category, reference: string})
102
+ /** They want it and nobody can be reached — no address on file for any
104
103
  channel they left enabled. A different fact from `Suppressed` on purpose:
105
104
  one is the system working and the other is the system falling short, and
106
105
  reporting both the same way hides every delivery gap behind a legitimate
107
106
  preference. */
108
- Undeliverable({category: category, reference: string})
109
- | /** Not acted on because another producer owns this source. A third way of
107
+ | Undeliverable({category: category, reference: string})
108
+ /** Not acted on because another producer owns this source. A third way of
110
109
  not sending, and its own fact for the same reason `Suppressed` and
111
110
  `Undeliverable` are two rather than one: "they declined", "nobody was
112
111
  reachable" and "somebody else has this" are three different things, and a
@@ -116,7 +115,7 @@ type fact =
116
115
  TODO row on an outcome; without a fourth one a deferred request retries
117
116
  its whole budget and lands in `onExhausted`, which is a slow silent
118
117
  failure with nothing in the log to explain it. */
119
- Deferred({reference: string, source: source})
118
+ | Deferred({reference: string, source: source})
120
119
  | Claimed({source: source, by: string})
121
120
  | Released({source: source})
122
121
 
@@ -223,15 +222,14 @@ let decide = (t, op, ~posture: (category, channel) => bool): result<
223
222
 
224
223
  | Request({category, reference}) =>
225
224
  let wanted = channels->Array.filter(channel => enabled(t, ~posture, category, channel))
226
- let addressed =
227
- wanted->Array.filterMap(channel =>
228
- addressFor(t, channel)->Option.map(address => Requested({
229
- category,
230
- reference,
231
- channel,
232
- address,
233
- }))
234
- )
225
+ let addressed = wanted->Array.filterMap(channel =>
226
+ addressFor(t, channel)->Option.map(address => Requested({
227
+ category,
228
+ reference,
229
+ channel,
230
+ address,
231
+ }))
232
+ )
235
233
  switch addressed {
236
234
  | [] =>
237
235
  // Wanted-but-unreachable and not-wanted are the two ways to send nothing,
@@ -15,7 +15,6 @@ is the one thing a trait cannot be told in names.
15
15
  says is the host's sentence, so the emitted rule table carries a `TODO(graft)`
16
16
  marker in place of one — it compiles, runs, and is obvious in a diff.
17
17
  */
18
-
19
18
  /**
20
19
  The names a graft needs, and nothing else.
21
20
 
@@ -137,8 +136,8 @@ let namesOf = (c: config): names => {
137
136
  delivered: "NotificationDelivered",
138
137
  failed: "NotificationFailed",
139
138
  unknownError: n ++ "Unknown",
140
- recipientId: (n->String.slice(~start=0, ~end=1))->String.toLowerCase ++
141
- n->String.slice(~start=1, ~end=n->String.length) ++ "Id",
139
+ recipientId: n->String.slice(~start=0, ~end=1)->String.toLowerCase ++
140
+ n->String.slice(~start=1, ~end=n->String.length) ++ "Id",
142
141
  }
143
142
  }
144
143
 
@@ -154,13 +153,14 @@ let addressB = (c: config) => c.addressB->Option.getOr("new@example.com")
154
153
  graft, so a host whose component is registered under another name edits one
155
154
  line rather than discovering the convention. */
156
155
  let subjectTypeOf = (c: config) => {
157
- let stem = c.occurrenceId->String.endsWith("Id")
158
- ? c.occurrenceId->String.slice(~start=0, ~end=c.occurrenceId->String.length - 2)
159
- : c.occurrenceId
156
+ let stem =
157
+ c.occurrenceId->String.endsWith("Id")
158
+ ? c.occurrenceId->String.slice(~start=0, ~end=c.occurrenceId->String.length - 2)
159
+ : c.occurrenceId
160
160
  switch stem {
161
161
  | "" => "Subject"
162
162
  | s =>
163
- (s->String.slice(~start=0, ~end=1))->String.toUpperCase ++
163
+ s->String.slice(~start=0, ~end=1)->String.toUpperCase ++
164
164
  s->String.slice(~start=1, ~end=s->String.length)
165
165
  }
166
166
  }
@@ -349,7 +349,7 @@ let postureArms = (c: config) => {
349
349
  Array.concat(transactional, [` | _ => false`])
350
350
  }
351
351
 
352
- let sliceBehavior = (c: config): string => {
352
+ let sliceDecision = (c: config): string => {
353
353
  let n = namesOf(c)
354
354
  let id = n.recipientId
355
355
  let first = c.categories->Array.get(0)->Option.getOr("Transactional")
@@ -357,7 +357,7 @@ let sliceBehavior = (c: config): string => {
357
357
  Array.flatMap(
358
358
  [
359
359
  [
360
- `@@reventless.behavior`,
360
+ `@@reventless.decision`,
361
361
  ``,
362
362
  `// The directory, the matrix and the dispatch decision are the trait's; this file`,
363
363
  `// is the mapping onto them, plus the one thing the trait has no opinion about —`,
@@ -373,19 +373,9 @@ let sliceBehavior = (c: config): string => {
373
373
  ` switch (category, channel) {`,
374
374
  ],
375
375
  postureArms(c),
376
- [
377
- ` }`,
378
- ``,
379
- `let categoryKey = (category: category) =>`,
380
- ` switch category {`,
381
- ],
376
+ [` }`, ``, `let categoryKey = (category: category) =>`, ` switch category {`],
382
377
  c.categories->Array.map(cat => ` | ${cat} => "${cat}"`),
383
- [
384
- ` }`,
385
- ``,
386
- `let categoryOf = (key: string) =>`,
387
- ` switch key {`,
388
- ],
378
+ [` }`, ``, `let categoryOf = (key: string) =>`, ` switch key {`],
389
379
  c.categories
390
380
  ->Array.filter(cat => cat != first)
391
381
  ->Array.map(cat => ` | "${cat}" => ${cat}`),
@@ -632,10 +622,10 @@ let claimsSpec = (c: config): string => {
632
622
  ])
633
623
  }
634
624
 
635
- let claimsBehavior = (c: config): string => {
625
+ let claimsDecision = (c: config): string => {
636
626
  let n = namesOf(c)
637
627
  lines([
638
- `@@reventless.behavior`,
628
+ `@@reventless.decision`,
639
629
  ``,
640
630
  `// The claim set is the trait's, folded through the same value the preferences`,
641
631
  `// slice folds. Only the claim half is ever populated here.`,
@@ -725,7 +715,6 @@ let sendSpec = (c: config): string => {
725
715
  `// Retries are for a provider that is down, not one that has refused. The port's`,
726
716
  `// own \`retriable\` rule decides which is which, in the translation next door.`,
727
717
  `let maxRetries = 3`,
728
- `let heartbeatInterval = 60`,
729
718
  `let targetName = Some("${n.slice}")`,
730
719
  ``,
731
720
  `// This plugin's own DCB event log.`,
@@ -773,13 +762,20 @@ let sendTranslation = (c: config): string => {
773
762
  ` item.address`,
774
763
  ` ->Reventless.Phone.fromString`,
775
764
  ` ->Result.map(phone => Reventless.Messaging.ToSms(phone))`,
776
- ` | Push => Ok(Reventless.Messaging.ToPush({deviceToken: item.address}))`,
765
+ ` // The directory stores a channel beside one flat address, and no push address is`,
766
+ ` // one string: two services issue a token, Web Push issues an endpoint with its`,
767
+ ` // encryption keys, and which service issued this one is not recorded either. So`,
768
+ ` // the pair cannot be re-fused, and refusing says so once rather than guessing a`,
769
+ ` // service and failing at whichever provider. A push transport arrives with the`,
770
+ ` // stored shape it needs, in the commit that justifies the columns.`,
771
+ ` | Push =>`,
772
+ ` Error("a push address names its issuing service, and the directory stores one flat address")`,
777
773
  ` }`,
778
774
  ``,
779
775
  `let translate = async (_id, item: outboundItem, ~capabilities: Reventless.Capabilities.t) =>`,
780
776
  ` switch recipientFor(item) {`,
781
- ` // An address the directory holds that its own channel's grammar refuses. Not`,
782
- ` // retryable and not the provider's fault — the row that holds it needs fixing.`,
777
+ ` // An address the directory holds that its own channel cannot use. Not retryable`,
778
+ ` // and not the provider's fault — the row that holds it needs fixing.`,
783
779
  ` | Error(why) =>`,
784
780
  ` Ok(`,
785
781
  ` Some((`,
@@ -865,34 +861,36 @@ let conformanceBinding = (c: config): string => {
865
861
  ` let optional: category = ${optional}`,
866
862
  ``,
867
863
  ` module Spec = ${n.slice}`,
868
- ` module Behavior = ${n.slice}_Behavior`,
869
- ``,
870
- ` // A DCB slice's entity comes into existence with its first fact, so there is`,
871
- ` // no creation event to seed: an unannounced ${c.noun->String.toLowerCase} is one with no history.`,
872
- ` let created: array<Spec.consumedEvent> = []`,
864
+ ` module Decision = ${n.slice}_Decision`,
873
865
  ``,
874
866
  ` let ${id} = "${c.noun->String.toLowerCase}-1"`,
875
867
  ``,
876
868
  ` // Annotated: this slice reads back exactly what it writes, so each of these`,
877
869
  ` // names a constructor of both unions and the later declaration would win.`,
878
- ` let announcedC = (${c.contactField}): Spec.consumedEvent =>`,
879
- ` ${n.announced}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
880
- ` let subscribedC = (category, channel): Spec.consumedEvent =>`,
881
- ` ${n.subscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
882
- ` let unsubscribedC = (category, channel): Spec.consumedEvent =>`,
883
- ` ${n.unsubscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
884
- ` // Written by ${n.claims} and read here across partitions — this slice`,
885
- ` // consumes them and produces neither.`,
886
- ` let claimedC = (source, by): Spec.consumedEvent =>`,
887
- ` ${n.claimed}({sourceId: source, by})`,
888
- ` let releasedC = (source): Spec.consumedEvent => ${n.released}({sourceId: source})`,
870
+ ` module Consumed = {`,
871
+ ` // A DCB slice's entity comes into existence with its first fact, so there`,
872
+ ` // is no creation event to seed: an unannounced ${c.noun->String.toLowerCase} is one with no`,
873
+ ` // history.`,
874
+ ` let created: array<Spec.consumedEvent> = []`,
875
+ ` let announced = (${c.contactField}): Spec.consumedEvent =>`,
876
+ ` ${n.announced}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
877
+ ` let subscribed = (category, channel): Spec.consumedEvent =>`,
878
+ ` ${n.subscribed}({${id}: ${id}, category, channel: Decision.channelOf(channel)})`,
879
+ ` let unsubscribed = (category, channel): Spec.consumedEvent =>`,
880
+ ` ${n.unsubscribed}({${id}: ${id}, category, channel: Decision.channelOf(channel)})`,
881
+ ` // Written by ${n.claims} and read here across partitions — this slice`,
882
+ ` // consumes them and produces neither.`,
883
+ ` let claimed = (source, by): Spec.consumedEvent =>`,
884
+ ` ${n.claimed}({sourceId: source, by})`,
885
+ ` let released = (source): Spec.consumedEvent => ${n.released}({sourceId: source})`,
886
+ ` }`,
889
887
  ``,
890
888
  ` let announce = ${c.contactField} =>`,
891
889
  ` Spec.${n.announceCmd}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
892
890
  ` let subscribe = (category, channel) =>`,
893
- ` Spec.${n.subscribeCmd}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
891
+ ` Spec.${n.subscribeCmd}({${id}: ${id}, category, channel: Decision.channelOf(channel)})`,
894
892
  ` let unsubscribe = (category, channel) =>`,
895
- ` Spec.${n.unsubscribeCmd}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
893
+ ` Spec.${n.unsubscribeCmd}({${id}: ${id}, category, channel: Decision.channelOf(channel)})`,
896
894
  ` // The wording and the subject are this host's and the trait carries neither, so`,
897
895
  ` // the suite supplies whatever it likes and asserts nothing about them.`,
898
896
  ` //`,
@@ -922,15 +920,15 @@ let conformanceBinding = (c: config): string => {
922
920
  ` let announced = ${c.contactField} =>`,
923
921
  ` Spec.${n.announced}({${id}: ${id}, ${c.contactField}: ${c.contactField}})`,
924
922
  ` let subscribed = (category, channel) =>`,
925
- ` Spec.${n.subscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
923
+ ` Spec.${n.subscribed}({${id}: ${id}, category, channel: Decision.channelOf(channel)})`,
926
924
  ` let unsubscribed = (category, channel) =>`,
927
- ` Spec.${n.unsubscribed}({${id}: ${id}, category, channel: Behavior.channelOf(channel)})`,
925
+ ` Spec.${n.unsubscribed}({${id}: ${id}, category, channel: Decision.channelOf(channel)})`,
928
926
  ` let requestedWith = (category, reference, channel, address, origin) =>`,
929
927
  ` Spec.${n.requested}({`,
930
928
  ` ${id}: ${id},`,
931
929
  ` category,`,
932
930
  ` reference,`,
933
- ` channel: Behavior.channelOf(channel),`,
931
+ ` channel: Decision.channelOf(channel),`,
934
932
  ` address,`,
935
933
  ` subjectType: "${subjectTypeOf(c)}",`,
936
934
  ` subjectRef: "subject-1",`,
@@ -1003,7 +1001,7 @@ let contactRelayPatch = (c: config): patch => {
1003
1001
  let n = namesOf(c)
1004
1002
  let id = n.recipientId
1005
1003
  {
1006
- into: `${c.chapter}/OutboundTranslationSlice/${n.relay}.res (new, plus its _Translation)`,
1004
+ into: `${c.chapter}/OutboundTranslation/${n.relay}.res (new, plus its _Translation)`,
1007
1005
  at: `a new file — the trait cannot write what this host's events mean`,
1008
1006
  contents: lines(
1009
1007
  Array.flatMap(
@@ -1036,7 +1034,6 @@ let contactRelayPatch = (c: config): patch => {
1036
1034
  ` | ${n.announceCmd}({${id}: string, ${c.contactField}: string})`,
1037
1035
  ``,
1038
1036
  `let maxRetries = 3`,
1039
- `let heartbeatInterval = 60`,
1040
1037
  `let targetName = Some("${n.slice}")`,
1041
1038
  `let sourceNames = ["${c.contactSource}"]`,
1042
1039
  `// No external box: this slice reaches nothing outside the plugin.`,
@@ -1083,7 +1080,7 @@ let intakeRelayPatch = (c: config): patch => {
1083
1080
  let n = namesOf(c)
1084
1081
  let id = n.recipientId
1085
1082
  {
1086
- into: `${c.chapter}/AutomationSlice/${n.intake}.res (new, plus its _Automation)`,
1083
+ into: `${c.chapter}/Automation/${n.intake}.res (new, plus its _Automation)`,
1087
1084
  at: `a new file — the wording is this host's sentence, not the trait's`,
1088
1085
  contents: lines([
1089
1086
  `// Turns one of this host's occurrences into a request to notify somebody.`,
@@ -1114,7 +1111,6 @@ let intakeRelayPatch = (c: config): patch => {
1114
1111
  ` })`,
1115
1112
  ``,
1116
1113
  `let maxRetries = 3`,
1117
- `let heartbeatInterval = 60`,
1118
1114
  `let targetName = "${n.slice}"`,
1119
1115
  ``,
1120
1116
  `// ── ${n.intake}_Automation.res ──`,
@@ -1228,7 +1224,7 @@ let intakeRelayPatch = (c: config): patch => {
1228
1224
  ` ${id},`,
1229
1225
  ` ${n.intake}.${n.requestCmd}({`,
1230
1226
  ` ${id}: ${id},`,
1231
- ` category: ${n.slice}_Behavior.categoryOf(rule.category),`,
1227
+ ` category: ${n.slice}_Decision.categoryOf(rule.category),`,
1232
1228
  ` reference: id,`,
1233
1229
  ` subjectType: rule.subjectType,`,
1234
1230
  ` subjectRef: Rule.subjectOf(rule, ~payload),`,
@@ -1264,16 +1260,16 @@ let emit = (~config: config, ~into: string, ~tests: string): output => {
1264
1260
  let n = namesOf(config)
1265
1261
  {
1266
1262
  files: [
1267
- {path: `${into}/StateChangeSlice/${n.slice}.res`, contents: sliceSpec(config)},
1268
- {path: `${into}/StateChangeSlice/${n.slice}_Behavior.res`, contents: sliceBehavior(config)},
1269
- {path: `${into}/StateChangeSlice/${n.claims}.res`, contents: claimsSpec(config)},
1263
+ {path: `${into}/StateChange/${n.slice}.res`, contents: sliceSpec(config)},
1264
+ {path: `${into}/StateChange/${n.slice}_Decision.res`, contents: sliceDecision(config)},
1265
+ {path: `${into}/StateChange/${n.claims}.res`, contents: claimsSpec(config)},
1270
1266
  {
1271
- path: `${into}/StateChangeSlice/${n.claims}_Behavior.res`,
1272
- contents: claimsBehavior(config),
1267
+ path: `${into}/StateChange/${n.claims}_Decision.res`,
1268
+ contents: claimsDecision(config),
1273
1269
  },
1274
- {path: `${into}/OutboundTranslationSlice/${n.send}.res`, contents: sendSpec(config)},
1270
+ {path: `${into}/OutboundTranslation/${n.send}.res`, contents: sendSpec(config)},
1275
1271
  {
1276
- path: `${into}/OutboundTranslationSlice/${n.send}_Translation.res`,
1272
+ path: `${into}/OutboundTranslation/${n.send}_Translation.res`,
1277
1273
  contents: sendTranslation(config),
1278
1274
  },
1279
1275
  {path: `${tests}/NotificationConformance_GWT.res`, contents: conformanceBinding(config)},
@@ -254,13 +254,13 @@ function postureArms(c) {
254
254
  return transactional.concat([` | _ => false`]);
255
255
  }
256
256
 
257
- function sliceBehavior(c) {
257
+ function sliceDecision(c) {
258
258
  let n = namesOf(c);
259
259
  let id = n.recipientId;
260
260
  let first = Stdlib_Option.getOr(c.categories[0], "Transactional");
261
261
  return [
262
262
  [
263
- `@@reventless.behavior`,
263
+ `@@reventless.decision`,
264
264
  ``,
265
265
  `// The directory, the matrix and the dispatch decision are the trait's; this file`,
266
266
  `// is the mapping onto them, plus the one thing the trait has no opinion about —`,
@@ -521,10 +521,10 @@ function claimsSpec(c) {
521
521
  ].join("\n");
522
522
  }
523
523
 
524
- function claimsBehavior(c) {
524
+ function claimsDecision(c) {
525
525
  let n = namesOf(c);
526
526
  return [
527
- `@@reventless.behavior`,
527
+ `@@reventless.decision`,
528
528
  ``,
529
529
  `// The claim set is the trait's, folded through the same value the preferences`,
530
530
  `// slice folds. Only the claim half is ever populated here.`,
@@ -612,7 +612,6 @@ function sendSpec(c) {
612
612
  `// Retries are for a provider that is down, not one that has refused. The port's`,
613
613
  `// own \`retriable\` rule decides which is which, in the translation next door.`,
614
614
  `let maxRetries = 3`,
615
- `let heartbeatInterval = 60`,
616
615
  `let targetName = Some("` + n.slice + `")`,
617
616
  ``,
618
617
  `// This plugin's own DCB event log.`,
@@ -660,13 +659,20 @@ function sendTranslation(c) {
660
659
  ` item.address`,
661
660
  ` ->Reventless.Phone.fromString`,
662
661
  ` ->Result.map(phone => Reventless.Messaging.ToSms(phone))`,
663
- ` | Push => Ok(Reventless.Messaging.ToPush({deviceToken: item.address}))`,
662
+ ` // The directory stores a channel beside one flat address, and no push address is`,
663
+ ` // one string: two services issue a token, Web Push issues an endpoint with its`,
664
+ ` // encryption keys, and which service issued this one is not recorded either. So`,
665
+ ` // the pair cannot be re-fused, and refusing says so once rather than guessing a`,
666
+ ` // service and failing at whichever provider. A push transport arrives with the`,
667
+ ` // stored shape it needs, in the commit that justifies the columns.`,
668
+ ` | Push =>`,
669
+ ` Error("a push address names its issuing service, and the directory stores one flat address")`,
664
670
  ` }`,
665
671
  ``,
666
672
  `let translate = async (_id, item: outboundItem, ~capabilities: Reventless.Capabilities.t) =>`,
667
673
  ` switch recipientFor(item) {`,
668
- ` // An address the directory holds that its own channel's grammar refuses. Not`,
669
- ` // retryable and not the provider's fault — the row that holds it needs fixing.`,
674
+ ` // An address the directory holds that its own channel cannot use. Not retryable`,
675
+ ` // and not the provider's fault — the row that holds it needs fixing.`,
670
676
  ` | Error(why) =>`,
671
677
  ` Ok(`,
672
678
  ` Some((`,
@@ -747,34 +753,36 @@ function conformanceBinding(c) {
747
753
  ` let optional: category = ` + optional,
748
754
  ``,
749
755
  ` module Spec = ` + n.slice,
750
- ` module Behavior = ` + n.slice + `_Behavior`,
751
- ``,
752
- ` // A DCB slice's entity comes into existence with its first fact, so there is`,
753
- ` // no creation event to seed: an unannounced ` + c.noun.toLowerCase() + ` is one with no history.`,
754
- ` let created: array<Spec.consumedEvent> = []`,
756
+ ` module Decision = ` + n.slice + `_Decision`,
755
757
  ``,
756
758
  ` let ` + id + ` = "` + c.noun.toLowerCase() + `-1"`,
757
759
  ``,
758
760
  ` // Annotated: this slice reads back exactly what it writes, so each of these`,
759
761
  ` // names a constructor of both unions and the later declaration would win.`,
760
- ` let announcedC = (` + c.contactField + `): Spec.consumedEvent =>`,
761
- ` ` + n.announced + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
762
- ` let subscribedC = (category, channel): Spec.consumedEvent =>`,
763
- ` ` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
764
- ` let unsubscribedC = (category, channel): Spec.consumedEvent =>`,
765
- ` ` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
766
- ` // Written by ` + n.claims + ` and read here across partitions — this slice`,
767
- ` // consumes them and produces neither.`,
768
- ` let claimedC = (source, by): Spec.consumedEvent =>`,
769
- ` ` + n.claimed + `({sourceId: source, by})`,
770
- ` let releasedC = (source): Spec.consumedEvent => ` + n.released + `({sourceId: source})`,
762
+ ` module Consumed = {`,
763
+ ` // A DCB slice's entity comes into existence with its first fact, so there`,
764
+ ` // is no creation event to seed: an unannounced ` + c.noun.toLowerCase() + ` is one with no`,
765
+ ` // history.`,
766
+ ` let created: array<Spec.consumedEvent> = []`,
767
+ ` let announced = (` + c.contactField + `): Spec.consumedEvent =>`,
768
+ ` ` + n.announced + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
769
+ ` let subscribed = (category, channel): Spec.consumedEvent =>`,
770
+ ` ` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Decision.channelOf(channel)})`,
771
+ ` let unsubscribed = (category, channel): Spec.consumedEvent =>`,
772
+ ` ` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Decision.channelOf(channel)})`,
773
+ ` // Written by ` + n.claims + ` and read here across partitions — this slice`,
774
+ ` // consumes them and produces neither.`,
775
+ ` let claimed = (source, by): Spec.consumedEvent =>`,
776
+ ` ` + n.claimed + `({sourceId: source, by})`,
777
+ ` let released = (source): Spec.consumedEvent => ` + n.released + `({sourceId: source})`,
778
+ ` }`,
771
779
  ``,
772
780
  ` let announce = ` + c.contactField + ` =>`,
773
781
  ` Spec.` + n.announceCmd + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
774
782
  ` let subscribe = (category, channel) =>`,
775
- ` Spec.` + n.subscribeCmd + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
783
+ ` Spec.` + n.subscribeCmd + `({` + id + `: ` + id + `, category, channel: Decision.channelOf(channel)})`,
776
784
  ` let unsubscribe = (category, channel) =>`,
777
- ` Spec.` + n.unsubscribeCmd + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
785
+ ` Spec.` + n.unsubscribeCmd + `({` + id + `: ` + id + `, category, channel: Decision.channelOf(channel)})`,
778
786
  ` // The wording and the subject are this host's and the trait carries neither, so`,
779
787
  ` // the suite supplies whatever it likes and asserts nothing about them.`,
780
788
  ` //`,
@@ -804,15 +812,15 @@ function conformanceBinding(c) {
804
812
  ` let announced = ` + c.contactField + ` =>`,
805
813
  ` Spec.` + n.announced + `({` + id + `: ` + id + `, ` + c.contactField + `: ` + c.contactField + `})`,
806
814
  ` let subscribed = (category, channel) =>`,
807
- ` Spec.` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
815
+ ` Spec.` + n.subscribed + `({` + id + `: ` + id + `, category, channel: Decision.channelOf(channel)})`,
808
816
  ` let unsubscribed = (category, channel) =>`,
809
- ` Spec.` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Behavior.channelOf(channel)})`,
817
+ ` Spec.` + n.unsubscribed + `({` + id + `: ` + id + `, category, channel: Decision.channelOf(channel)})`,
810
818
  ` let requestedWith = (category, reference, channel, address, origin) =>`,
811
819
  ` Spec.` + n.requested + `({`,
812
820
  ` ` + id + `: ` + id + `,`,
813
821
  ` category,`,
814
822
  ` reference,`,
815
- ` channel: Behavior.channelOf(channel),`,
823
+ ` channel: Decision.channelOf(channel),`,
816
824
  ` address,`,
817
825
  ` subjectType: "` + subjectTypeOf(c) + `",`,
818
826
  ` subjectRef: "subject-1",`,
@@ -877,7 +885,7 @@ function contactRelayPatch(c) {
877
885
  let n = namesOf(c);
878
886
  let id = n.recipientId;
879
887
  return {
880
- into: c.chapter + `/OutboundTranslationSlice/` + n.relay + `.res (new, plus its _Translation)`,
888
+ into: c.chapter + `/OutboundTranslation/` + n.relay + `.res (new, plus its _Translation)`,
881
889
  at: `a new file — the trait cannot write what this host's events mean`,
882
890
  contents: [
883
891
  [
@@ -908,7 +916,6 @@ function contactRelayPatch(c) {
908
916
  ` | ` + n.announceCmd + `({` + id + `: string, ` + c.contactField + `: string})`,
909
917
  ``,
910
918
  `let maxRetries = 3`,
911
- `let heartbeatInterval = 60`,
912
919
  `let targetName = Some("` + n.slice + `")`,
913
920
  `let sourceNames = ["` + c.contactSource + `"]`,
914
921
  `// No external box: this slice reaches nothing outside the plugin.`,
@@ -950,7 +957,7 @@ function intakeRelayPatch(c) {
950
957
  let n = namesOf(c);
951
958
  let id = n.recipientId;
952
959
  return {
953
- into: c.chapter + `/AutomationSlice/` + n.intake + `.res (new, plus its _Automation)`,
960
+ into: c.chapter + `/Automation/` + n.intake + `.res (new, plus its _Automation)`,
954
961
  at: `a new file — the wording is this host's sentence, not the trait's`,
955
962
  contents: [
956
963
  `// Turns one of this host's occurrences into a request to notify somebody.`,
@@ -981,7 +988,6 @@ function intakeRelayPatch(c) {
981
988
  ` })`,
982
989
  ``,
983
990
  `let maxRetries = 3`,
984
- `let heartbeatInterval = 60`,
985
991
  `let targetName = "` + n.slice + `"`,
986
992
  ``,
987
993
  `// ── ` + n.intake + `_Automation.res ──`,
@@ -1095,7 +1101,7 @@ function intakeRelayPatch(c) {
1095
1101
  ` ` + id + `,`,
1096
1102
  ` ` + n.intake + `.` + n.requestCmd + `({`,
1097
1103
  ` ` + id + `: ` + id + `,`,
1098
- ` category: ` + n.slice + `_Behavior.categoryOf(rule.category),`,
1104
+ ` category: ` + n.slice + `_Decision.categoryOf(rule.category),`,
1099
1105
  ` reference: id,`,
1100
1106
  ` subjectType: rule.subjectType,`,
1101
1107
  ` subjectRef: Rule.subjectOf(rule, ~payload),`,
@@ -1126,27 +1132,27 @@ function emit(config, into, tests) {
1126
1132
  return {
1127
1133
  files: [
1128
1134
  {
1129
- path: into + `/StateChangeSlice/` + n.slice + `.res`,
1135
+ path: into + `/StateChange/` + n.slice + `.res`,
1130
1136
  contents: sliceSpec(config)
1131
1137
  },
1132
1138
  {
1133
- path: into + `/StateChangeSlice/` + n.slice + `_Behavior.res`,
1134
- contents: sliceBehavior(config)
1139
+ path: into + `/StateChange/` + n.slice + `_Decision.res`,
1140
+ contents: sliceDecision(config)
1135
1141
  },
1136
1142
  {
1137
- path: into + `/StateChangeSlice/` + n.claims + `.res`,
1143
+ path: into + `/StateChange/` + n.claims + `.res`,
1138
1144
  contents: claimsSpec(config)
1139
1145
  },
1140
1146
  {
1141
- path: into + `/StateChangeSlice/` + n.claims + `_Behavior.res`,
1142
- contents: claimsBehavior(config)
1147
+ path: into + `/StateChange/` + n.claims + `_Decision.res`,
1148
+ contents: claimsDecision(config)
1143
1149
  },
1144
1150
  {
1145
- path: into + `/OutboundTranslationSlice/` + n.send + `.res`,
1151
+ path: into + `/OutboundTranslation/` + n.send + `.res`,
1146
1152
  contents: sendSpec(config)
1147
1153
  },
1148
1154
  {
1149
- path: into + `/OutboundTranslationSlice/` + n.send + `_Translation.res`,
1155
+ path: into + `/OutboundTranslation/` + n.send + `_Translation.res`,
1150
1156
  contents: sendTranslation(config)
1151
1157
  },
1152
1158
  {
@@ -1172,9 +1178,9 @@ export {
1172
1178
  categoryArms,
1173
1179
  sliceSpec,
1174
1180
  postureArms,
1175
- sliceBehavior,
1181
+ sliceDecision,
1176
1182
  claimsSpec,
1177
- claimsBehavior,
1183
+ claimsDecision,
1178
1184
  sendSpec,
1179
1185
  sendTranslation,
1180
1186
  conformanceBinding,