@reventlessdev/reventless-spec 3.0.0-alpha.143 → 3.0.0-alpha.145

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,26 @@
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.145 (2026-09-24)
7
+
8
+ ### Features
9
+
10
+ * **dcb:** a command reference no decision reads by stays out of the query ([3ae149a](https://github.com/ReventlessDev/reventless-core/commit/3ae149ae91ebab63413be57df5827c5fd4390160))
11
+
12
+
13
+ # 3.0.0-alpha.144 (2026-09-23)
14
+
15
+ * feat(node)!: every command-line tool reads its arguments through parseArgs ([2e41979](https://github.com/ReventlessDev/reventless-core/commit/2e41979b73646c46356e197e17eb4a3da0154c73))
16
+
17
+ ### BREAKING CHANGES
18
+
19
+ * NodeProcess.onSignal takes the new NodeProcess.signal
20
+ variant (SIGINT | SIGTERM | SIGHUP) instead of a polymorphic variant:
21
+ write onSignal(SIGINT, …) for onSignal(#SIGINT, …). Node receives the
22
+ same string.
23
+
24
+
25
+
6
26
  # 3.0.0-alpha.143 (2026-09-23)
7
27
 
8
28
  **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.143",
3
+ "version": "3.0.0-alpha.145",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "sury": "11.0.0",
27
27
  "sury-ppx": "11.0.0",
28
28
  "yaml": "^2.8.3",
29
- "@reventlessdev/rescript-node": "2.0.0-alpha.14"
29
+ "@reventlessdev/rescript-node": "2.0.0-alpha.15"
30
30
  },
31
31
  "devDependencies": {
32
32
  "rescript": "12.3.1",
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/CertifyTrait.res.mjs"
2
+ import { main } from "./src/generator/CertifyTrait.res.mjs"
3
+ await main()
package/run-generator.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/PluginGenerator.res.mjs"
2
+ import { main } from "./src/generator/PluginGenerator.res.mjs"
3
+ await main()
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/GraftTrait.res.mjs"
2
+ import { main } from "./src/generator/GraftTrait.res.mjs"
3
+ await main()
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/PlatformGenerator.res.mjs"
2
+ import { main } from "./src/generator/PlatformGenerator.res.mjs"
3
+ await main()
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./src/generator/TraitManifestCli.res.mjs"
2
+ import { main } from "./src/generator/TraitManifestCli.res.mjs"
3
+ await main()
@@ -0,0 +1,209 @@
1
+ /***
2
+ How every command-line tool in this repository reads `process.argv`: through
3
+ Node's `parseArgs`, with mistakes worded the way these tools word them, and one
4
+ behaviour on `--help`, on a usage mistake and on a bare invocation (see [run]).
5
+ Non-strict with tokens rather than strict mode, because strict mode names the
6
+ offending argument only inside English prose.
7
+ */
8
+
9
+ /** An undeclared `--key` under `~pairs=true`: with a value, or on its own. */
10
+ type value =
11
+ | Text(string)
12
+ | Present
13
+
14
+ type t = {
15
+ values: NodeUtil.values,
16
+ positionals: array<string>,
17
+ pairs: array<(string, value)>,
18
+ }
19
+
20
+ /** A usage mistake. A tool that ran and failed exits 1, so a caller can tell a
21
+ misinvocation from a run whose answer was no. */
22
+ let usageMistakeExit = 2
23
+
24
+ let unknown = (arg: string) => `unknown argument "${arg}"`
25
+
26
+ /** A positional the tool has no place for, worded like any other unknown argument. */
27
+ let extra = (arg: string): result<'a, string> => Error(unknown(arg))
28
+
29
+ /** `-` alone is a value (stdin, by convention); anything else starting with a
30
+ dash is the next flag, which a string option must not swallow. */
31
+ let flagShaped = (value: string) => value->String.startsWith("-") && value != "-"
32
+
33
+ /** Package managers can forward a bare `--` ahead of a script's own arguments.
34
+ Anywhere else it ends the options, as POSIX says. */
35
+ let withoutLeadingTerminator = (argv: array<string>) =>
36
+ argv->Array.get(0) == Some("--") ? argv->Array.slice(~start=1, ~end=Array.length(argv)) : argv
37
+
38
+ /** Parse against the declared options. `-h`/`--help` is declared for every
39
+ tool. The first mistake in argv order is the one reported. Under
40
+ `~pairs=true` an undeclared long option is kept as a `(key, value)` pair
41
+ instead of refused, taking the argument after it as its value when that
42
+ argument is not itself an option. */
43
+ let parse = (
44
+ ~strings: array<string>=[],
45
+ ~bools: array<string>=[],
46
+ ~lists: array<string>=[],
47
+ ~pairs: bool=false,
48
+ argv: array<string>,
49
+ ): result<t, string> => {
50
+ let args = withoutLeadingTerminator(argv)
51
+ let options: dict<NodeUtil.optionConfig> = Dict.make()
52
+ options->Dict.set("help", {type_: Boolean, short: "h"})
53
+ bools->Array.forEach(name => options->Dict.set(name, {type_: Boolean}))
54
+ strings->Array.forEach(name => options->Dict.set(name, {type_: String}))
55
+ lists->Array.forEach(name => options->Dict.set(name, {type_: String, multiple: true}))
56
+
57
+ let parsed = NodeUtil.parseArgs({
58
+ args,
59
+ options,
60
+ strict: false,
61
+ allowPositionals: true,
62
+ tokens: true,
63
+ })
64
+ let tokens = parsed.tokens->Option.getOr([])
65
+ let positionals = []
66
+ let found = []
67
+
68
+ // Non-strict `parseArgs` types an undeclared option as a boolean and emits
69
+ // the argument after it as the next positional token, so a pair's value is
70
+ // read back from token order.
71
+ let rec walk = i =>
72
+ switch tokens->Array.get(i) {
73
+ | None => Ok()
74
+ | Some(token) =>
75
+ switch token.kind {
76
+ | Terminator => walk(i + 1)
77
+ | Positional =>
78
+ positionals->Array.push(token.value->Option.getOr(""))
79
+ walk(i + 1)
80
+ | Flag =>
81
+ let name = token.name->Option.getOr("")
82
+ let rawName = token.rawName->Option.getOr(name)
83
+ let inline = token.inlineValue->Option.getOr(false)
84
+ switch (options->Dict.get(name), token.value) {
85
+ | (Some({type_: String}), None) => Error(`${rawName} needs a value`)
86
+ | (Some({type_: String}), Some(v)) if !inline && flagShaped(v) =>
87
+ Error(`${rawName} needs a value`)
88
+ | (Some({type_: Boolean}), Some(_)) => Error(`${rawName} takes no value`)
89
+ | (Some(_), _) => walk(i + 1)
90
+ | (None, _) if !pairs || !(rawName->String.startsWith("--")) => Error(unknown(rawName))
91
+ | (None, Some(v)) =>
92
+ found->Array.push((name, Text(v)))
93
+ walk(i + 1)
94
+ | (None, None) =>
95
+ switch tokens->Array.get(i + 1) {
96
+ | Some({kind: Positional, index, value: ?Some(v)}) if index == token.index + 1 =>
97
+ found->Array.push((name, Text(v)))
98
+ walk(i + 2)
99
+ | _ =>
100
+ found->Array.push((name, Present))
101
+ walk(i + 1)
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ walk(0)->Result.map(() => {values: parsed.values, positionals, pairs: found})
108
+ }
109
+
110
+ let string = (t: t, name: string): option<string> => t.values->NodeUtil.string(name)
111
+ let bool = (t: t, name: string): bool => t.values->NodeUtil.bool(name)->Option.getOr(false)
112
+ let strings = (t: t, name: string): array<string> =>
113
+ t.values->NodeUtil.strings(name)->Option.getOr([])
114
+ let positionals = (t: t): array<string> => t.positionals
115
+ let pairs = (t: t): array<(string, value)> => t.pairs
116
+ let help = (t: t): bool => t->bool("help")
117
+
118
+ /** For a tool that takes at most `max` positionals: the first beyond them is
119
+ refused as [extra]. */
120
+ let atMost = (t: t, max: int): result<t, string> =>
121
+ switch t.positionals->Array.get(max) {
122
+ | None => Ok(t)
123
+ | Some(arg) => extra(arg)
124
+ }
125
+
126
+ let noPositionals = (t: t): result<t, string> => t->atMost(0)
127
+
128
+ /** Whether `-h` or `--help` appears before any `--` that ends the options.
129
+ Read off argv rather than a parse, so it answers even when the other
130
+ arguments are wrong. */
131
+ let asksForHelp = (argv: array<string>): bool => {
132
+ let argv = withoutLeadingTerminator(argv)
133
+ let options = switch argv->Array.indexOf("--") {
134
+ | -1 => argv
135
+ | end => argv->Array.slice(~start=0, ~end)
136
+ }
137
+ options->Array.some(arg => arg == "-h" || arg == "--help")
138
+ }
139
+
140
+ /** A tool's command line: its name, its usage (first line `Usage: <bin> …`),
141
+ and how it turns argv into its arguments. */
142
+ type cli<'args> = {
143
+ bin: string,
144
+ usage: string,
145
+ parse: array<string> => result<'args, string>,
146
+ }
147
+
148
+ /** Where [run] writes and how it ends the process, so a test can watch it. */
149
+ type io = {
150
+ out: string => unit,
151
+ err: string => unit,
152
+ exit: int => unit,
153
+ }
154
+
155
+ let processIo = {
156
+ out: text => Console.log(text),
157
+ err: text => Console.error(text),
158
+ exit: NodeProcess.exit,
159
+ }
160
+
161
+ /** Help anywhere prints the usage to stdout and exits 0, whatever else is
162
+ wrong. A usage mistake prints `<bin>: <message>`, a blank line and the usage
163
+ to stderr and exits 2. Otherwise `main` gets the arguments, and its own
164
+ failures keep their exit 1. */
165
+ let run = async (
166
+ cli: cli<'args>,
167
+ ~argv: option<array<string>>=?,
168
+ ~io: io=processIo,
169
+ main: 'args => promise<unit>,
170
+ ): unit => {
171
+ let argv =
172
+ argv->Option.getOr(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length))
173
+ let usage = cli.usage->String.trim
174
+ if asksForHelp(argv) {
175
+ io.out(usage)
176
+ io.exit(0)
177
+ } else {
178
+ switch cli.parse(argv) {
179
+ | Error(message) =>
180
+ io.err(`${cli.bin}: ${message}\n\n${usage}`)
181
+ io.exit(usageMistakeExit)
182
+ | Ok(args) => await main(args)
183
+ }
184
+ }
185
+ }
186
+
187
+ /** What [run] did with one argv, for a tool's own tests. */
188
+ type observed = {
189
+ stdout: array<string>,
190
+ stderr: array<string>,
191
+ exitCode: option<int>,
192
+ reachedMain: bool,
193
+ }
194
+
195
+ /** Run a tool's command line without a process: nothing is printed, nothing
196
+ exits, and `main` is not called — only whether it would have been. */
197
+ let observe = async (cli: cli<'args>, argv: array<string>): observed => {
198
+ let stdout = []
199
+ let stderr = []
200
+ let exitCode = ref(None)
201
+ let reachedMain = ref(false)
202
+ let io = {
203
+ out: text => stdout->Array.push(text),
204
+ err: text => stderr->Array.push(text),
205
+ exit: code => exitCode := Some(code),
206
+ }
207
+ await run(cli, ~argv, ~io, async _ => reachedMain := true)
208
+ {stdout, stderr, exitCode: exitCode.contents, reachedMain: reachedMain.contents}
209
+ }
@@ -0,0 +1,325 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodeutil from "node:util";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7
+
8
+ function unknown(arg) {
9
+ return `unknown argument "` + arg + `"`;
10
+ }
11
+
12
+ function extra(arg) {
13
+ return {
14
+ TAG: "Error",
15
+ _0: unknown(arg)
16
+ };
17
+ }
18
+
19
+ function flagShaped(value) {
20
+ if (value.startsWith("-")) {
21
+ return value !== "-";
22
+ } else {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function withoutLeadingTerminator(argv) {
28
+ if (Primitive_object.equal(argv[0], "--")) {
29
+ return argv.slice(1, argv.length);
30
+ } else {
31
+ return argv;
32
+ }
33
+ }
34
+
35
+ function parse(stringsOpt, boolsOpt, listsOpt, pairsOpt, argv) {
36
+ let strings = stringsOpt !== undefined ? stringsOpt : [];
37
+ let bools = boolsOpt !== undefined ? boolsOpt : [];
38
+ let lists = listsOpt !== undefined ? listsOpt : [];
39
+ let pairs = pairsOpt !== undefined ? pairsOpt : false;
40
+ let args = withoutLeadingTerminator(argv);
41
+ let options = {};
42
+ options["help"] = {
43
+ type: "boolean",
44
+ short: "h"
45
+ };
46
+ bools.forEach(name => {
47
+ options[name] = {
48
+ type: "boolean"
49
+ };
50
+ });
51
+ strings.forEach(name => {
52
+ options[name] = {
53
+ type: "string"
54
+ };
55
+ });
56
+ lists.forEach(name => {
57
+ options[name] = {
58
+ type: "string",
59
+ multiple: true
60
+ };
61
+ });
62
+ let parsed = Nodeutil.parseArgs({
63
+ args: args,
64
+ options: options,
65
+ strict: false,
66
+ allowPositionals: true,
67
+ tokens: true
68
+ });
69
+ let tokens = Stdlib_Option.getOr(parsed.tokens, []);
70
+ let positionals = [];
71
+ let found = [];
72
+ let walk = _i => {
73
+ while (true) {
74
+ let i = _i;
75
+ let token = tokens[i];
76
+ if (token === undefined) {
77
+ return {
78
+ TAG: "Ok",
79
+ _0: undefined
80
+ };
81
+ }
82
+ let match = token.kind;
83
+ switch (match) {
84
+ case "option" :
85
+ let name = Stdlib_Option.getOr(token.name, "");
86
+ let rawName = Stdlib_Option.getOr(token.rawName, name);
87
+ let inline = Stdlib_Option.getOr(token.inlineValue, false);
88
+ let match$1 = options[name];
89
+ let match$2 = token.value;
90
+ if (match$1 !== undefined) {
91
+ if (match$1.type === "string") {
92
+ if (match$2 === undefined) {
93
+ return {
94
+ TAG: "Error",
95
+ _0: rawName + ` needs a value`
96
+ };
97
+ }
98
+ if (!inline && flagShaped(match$2)) {
99
+ return {
100
+ TAG: "Error",
101
+ _0: rawName + ` needs a value`
102
+ };
103
+ }
104
+ _i = i + 1 | 0;
105
+ continue;
106
+ }
107
+ if (match$2 !== undefined) {
108
+ return {
109
+ TAG: "Error",
110
+ _0: rawName + ` takes no value`
111
+ };
112
+ }
113
+ _i = i + 1 | 0;
114
+ continue;
115
+ }
116
+ if (!pairs || !rawName.startsWith("--")) {
117
+ return {
118
+ TAG: "Error",
119
+ _0: unknown(rawName)
120
+ };
121
+ }
122
+ if (match$2 !== undefined) {
123
+ found.push([
124
+ name,
125
+ {
126
+ TAG: "Text",
127
+ _0: match$2
128
+ }
129
+ ]);
130
+ _i = i + 1 | 0;
131
+ continue;
132
+ }
133
+ let match$3 = tokens[i + 1 | 0];
134
+ if (match$3 !== undefined) {
135
+ switch (match$3.kind) {
136
+ case "positional" :
137
+ let v = match$3.value;
138
+ if (v !== undefined && match$3.index === (token.index + 1 | 0)) {
139
+ found.push([
140
+ name,
141
+ {
142
+ TAG: "Text",
143
+ _0: v
144
+ }
145
+ ]);
146
+ _i = i + 2 | 0;
147
+ continue;
148
+ }
149
+ break;
150
+ case "option" :
151
+ case "option-terminator" :
152
+ break;
153
+ }
154
+ }
155
+ found.push([
156
+ name,
157
+ "Present"
158
+ ]);
159
+ _i = i + 1 | 0;
160
+ continue;
161
+ break;
162
+ case "positional" :
163
+ positionals.push(Stdlib_Option.getOr(token.value, ""));
164
+ _i = i + 1 | 0;
165
+ continue;
166
+ case "option-terminator" :
167
+ _i = i + 1 | 0;
168
+ continue;
169
+ }
170
+ };
171
+ };
172
+ return Stdlib_Result.map(walk(0), () => ({
173
+ values: parsed.values,
174
+ positionals: positionals,
175
+ pairs: found
176
+ }));
177
+ }
178
+
179
+ function string(t, name) {
180
+ return t.values[name];
181
+ }
182
+
183
+ function bool(t, name) {
184
+ return Stdlib_Option.getOr(t.values[name], false);
185
+ }
186
+
187
+ function strings(t, name) {
188
+ return Stdlib_Option.getOr(t.values[name], []);
189
+ }
190
+
191
+ function positionals(t) {
192
+ return t.positionals;
193
+ }
194
+
195
+ function pairs(t) {
196
+ return t.pairs;
197
+ }
198
+
199
+ function help(t) {
200
+ return bool(t, "help");
201
+ }
202
+
203
+ function atMost(t, max) {
204
+ let arg = t.positionals[max];
205
+ if (arg !== undefined) {
206
+ return {
207
+ TAG: "Error",
208
+ _0: unknown(arg)
209
+ };
210
+ } else {
211
+ return {
212
+ TAG: "Ok",
213
+ _0: t
214
+ };
215
+ }
216
+ }
217
+
218
+ function noPositionals(t) {
219
+ return atMost(t, 0);
220
+ }
221
+
222
+ function asksForHelp(argv) {
223
+ let argv$1 = withoutLeadingTerminator(argv);
224
+ let end = argv$1.indexOf("--");
225
+ let options = end !== -1 ? argv$1.slice(0, end) : argv$1;
226
+ return options.some(arg => {
227
+ if (arg === "-h") {
228
+ return true;
229
+ } else {
230
+ return arg === "--help";
231
+ }
232
+ });
233
+ }
234
+
235
+ function processIo_out(text) {
236
+ console.log(text);
237
+ }
238
+
239
+ function processIo_err(text) {
240
+ console.error(text);
241
+ }
242
+
243
+ function processIo_exit(prim) {
244
+ process.exit(prim);
245
+ }
246
+
247
+ let processIo = {
248
+ out: processIo_out,
249
+ err: processIo_err,
250
+ exit: processIo_exit
251
+ };
252
+
253
+ async function run(cli, argv, ioOpt, main) {
254
+ let io = ioOpt !== undefined ? ioOpt : processIo;
255
+ let argv$1 = Stdlib_Option.getOr(argv, process.argv.slice(2, process.argv.length));
256
+ let usage = cli.usage.trim();
257
+ if (asksForHelp(argv$1)) {
258
+ io.out(usage);
259
+ return io.exit(0);
260
+ }
261
+ let message = cli.parse(argv$1);
262
+ if (message.TAG === "Ok") {
263
+ return await main(message._0);
264
+ }
265
+ io.err(cli.bin + `: ` + message._0 + `\n\n` + usage);
266
+ return io.exit(2);
267
+ }
268
+
269
+ async function observe(cli, argv) {
270
+ let stdout = [];
271
+ let stderr = [];
272
+ let exitCode = {
273
+ contents: undefined
274
+ };
275
+ let reachedMain = {
276
+ contents: false
277
+ };
278
+ let io_out = text => {
279
+ stdout.push(text);
280
+ };
281
+ let io_err = text => {
282
+ stderr.push(text);
283
+ };
284
+ let io_exit = code => {
285
+ exitCode.contents = code;
286
+ };
287
+ let io = {
288
+ out: io_out,
289
+ err: io_err,
290
+ exit: io_exit
291
+ };
292
+ await run(cli, argv, io, async param => {
293
+ reachedMain.contents = true;
294
+ });
295
+ return {
296
+ stdout: stdout,
297
+ stderr: stderr,
298
+ exitCode: exitCode.contents,
299
+ reachedMain: reachedMain.contents
300
+ };
301
+ }
302
+
303
+ let usageMistakeExit = 2;
304
+
305
+ export {
306
+ usageMistakeExit,
307
+ unknown,
308
+ extra,
309
+ flagShaped,
310
+ withoutLeadingTerminator,
311
+ parse,
312
+ string,
313
+ bool,
314
+ strings,
315
+ positionals,
316
+ pairs,
317
+ help,
318
+ atMost,
319
+ noPositionals,
320
+ asksForHelp,
321
+ processIo,
322
+ run,
323
+ observe,
324
+ }
325
+ /* node:util Not a pure module */
@@ -10,7 +10,7 @@ a tag-metadata flag. That is precisely what we are replacing — so both the run
10
10
  VS Code tooling (building shapes from parsed `.res` source) can feed the same
11
11
  `infer`. See `docs/plans/done/dcb-tag-scope-inference.md` § "Phase 1 design".
12
12
 
13
- The three rules (over the representation):
13
+ The four rules (over the representation):
14
14
 
15
15
  1. **Owner / partition.** A slice's partition key is the key its *own* emitted
16
16
  events are identified by — computed as `producedKeys(S)` minus the keys `S`
@@ -33,6 +33,12 @@ The three rules (over the representation):
33
33
  producing slice's own partition key. Foreign reference keys (e.g. `categoryId`
34
34
  on `ProductAdded`) are payload ⇒ not indexed ⇒ the sibling-leak GSI write never
35
35
  happens.
36
+
37
+ 4. **Command scope.** A scalar key on a *command* is a query tag iff it is the
38
+ slice's partition or a key the slice reads off a foreign event (rule 2's
39
+ candidates). `ShipOrder({orderId, carrierId})` names the carrier as information:
40
+ AND-ing it into the clause would hide every order event that does not carry it,
41
+ so it is payload. See `commandPayloadKeys`.
36
42
  */
37
43
  /** A field the slice graph treats as an entity identity.
38
44
 
@@ -55,6 +61,9 @@ type idField = {
55
61
  byTag?: bool,
56
62
  /** The tag key, when the field's type says it (an identity) rather than its name. */
57
63
  key?: string,
64
+ /** The author asked for the tag (`@dcbTag`, `@partitionTag`, `@crossPartition`,
65
+ `@compositePartitionTag`), so a command keeps it in its query (rule 4). */
66
+ declared?: bool,
58
67
  }
59
68
 
60
69
  /** One variant arm: its constructor name and the `*Id` fields it carries. */
@@ -397,6 +406,35 @@ let crossPartitionForSlice = (s: sliceShape): array<string> => {
397
406
  foreign->Array.filter(k => Some(k) != partition && scalar->Array.includes(k))
398
407
  }
399
408
 
409
+ /**
410
+ Rule 4: the scalar command keys that stay out of the slice's decision query.
411
+
412
+ A key stays in when it is the partition, when the slice reads it off a foreign
413
+ event, when the boundary reads it across partitions, when the author declared it
414
+ on the command or on one of the slice's own arms (an M:N capacity read's
415
+ `@crossPartition` sits on the event), or when the command also carries it as a
416
+ list. Anything else only names another thing, and the fence is the partition.
417
+ */
418
+ let commandPayloadKeys = (s: sliceShape, ~partition: string, ~crossPartition: array<string>): array<
419
+ string,
420
+ > => {
421
+ let foreign = foreignConsumedKeys(s)
422
+ let declared = (f: idField) => f.declared->Option.getOr(false)
423
+ let pinned =
424
+ s.command
425
+ ->Array.filter(f => f.isList || declared(f))
426
+ ->Array.concat(
427
+ Array.concat(s.consumed, s.produced)->Array.flatMap(e => e.idFields->Array.filter(declared)),
428
+ )
429
+ ->Array.map(tagKeyOf)
430
+ commandScalarKeys(s)->Array.filter(k =>
431
+ k != partition &&
432
+ !(foreign->Array.includes(k)) &&
433
+ !(crossPartition->Array.includes(k)) &&
434
+ !(pinned->Array.includes(k))
435
+ )
436
+ }
437
+
400
438
  let infer = (slices: array<sliceShape>): derived => {
401
439
  let {partitionBySlice, ambiguities} = resolvePartitions(slices)
402
440