@reventlessdev/reventless-spec 3.0.0-alpha.135 → 3.0.0-alpha.137
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 +28 -0
- package/package.json +2 -2
- package/src/components/DcbScopeInference.res +224 -73
- package/src/components/DcbScopeInference.res.mjs +187 -49
- package/src/components/DcbTag.res +142 -186
- package/src/components/DcbTag.res.mjs +137 -128
- package/src/components/DcbValidation.res +50 -0
- package/src/components/DcbValidation.res.mjs +51 -0
- package/src/generator/Codegen.res +2 -1
- package/src/generator/Codegen.res.mjs +1 -1
- package/src/lifecycle/CheckLifecycleModel.res +114 -4
- package/src/lifecycle/CheckLifecycleModel.res.mjs +148 -4
- package/src/semantic/Semantic.res +44 -0
- package/src/semantic/Semantic.res.mjs +100 -12
- package/src/types/AccountsManifest.res +6 -0
- package/src/types/AccountsManifest.res.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
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.137 (2026-09-20)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **spec:** resolve the drift guard's paths from the module, not the cwd ([7f4d8de](https://github.com/ReventlessDev/reventless-core/commit/7f4d8de896f37ce4a855ecc6a980788f01d0e1c7))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **spec:** enumerate the transparent-string semantics, and check the list against the sources ([995c31c](https://github.com/ReventlessDev/reventless-core/commit/995c31c5eb1a9bdb2ad88d39b2b9a8a5348be0da))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 3.0.0-alpha.136 (2026-09-16)
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **aws:** make the demo accounts work on a pool that signs in by email ([97b036f](https://github.com/ReventlessDev/reventless-core/commit/97b036f86979ba55dbbf93d460fe8076c35abf1a))
|
|
21
|
+
* feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **spec:** let a slice's chapter break a partition-key tie ([2b9ae77](https://github.com/ReventlessDev/reventless-core/commit/2b9ae77e98eb42177d2bdd4977bc62bb7c7911aa))
|
|
25
|
+
* **spec:** report which command yields which event or error ([ac0bdc2](https://github.com/ReventlessDev/reventless-core/commit/ac0bdc23f76a5b71b5bf5c99415ff0937690a279))
|
|
26
|
+
|
|
27
|
+
### BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* DcbTag.derivePartitionTag is removed; a boundary whose
|
|
30
|
+
partition cannot be inferred no longer deploys.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
6
34
|
# 3.0.0-alpha.135 (2026-09-11)
|
|
7
35
|
|
|
8
36
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.137",
|
|
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-rc.2",
|
|
27
27
|
"sury-ppx": "11.0.0-rc.2",
|
|
28
28
|
"yaml": "^2.8.3",
|
|
29
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
29
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.12"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"rescript": "12.3.0",
|
|
@@ -17,7 +17,12 @@ The three rules (over the representation):
|
|
|
17
17
|
reads from a *foreign* producer (a consumed arm whose event type is produced by
|
|
18
18
|
a different slice). For `AddProduct` (`ProductAdded({productId, categoryId})`,
|
|
19
19
|
consuming `CategoryAdded({categoryId})`) the foreign-read `categoryId` is
|
|
20
|
-
removed, leaving `productId`.
|
|
20
|
+
removed, leaving `productId`. Only when that leaves nothing is a key given
|
|
21
|
+
back: one whose every foreign arm comes from a slice partitioned by that same
|
|
22
|
+
key, since reading your own entity's events is identity, not a reference.
|
|
23
|
+
That step depends on other slices' partitions, so it runs to a fixpoint.
|
|
24
|
+
When several keys are left, the slice's chapter breaks the tie if exactly one
|
|
25
|
+
of them is carried by every event its chapter writes.
|
|
21
26
|
|
|
22
27
|
2. **Cross-partition.** A key is cross-partition iff some slice reads it on a
|
|
23
28
|
*foreign* consumed event while partitioned by something else *and* the key is
|
|
@@ -50,6 +55,8 @@ type sliceShape = {
|
|
|
50
55
|
consumed: array<eventShape>,
|
|
51
56
|
produced: array<eventShape>,
|
|
52
57
|
partitionHint: option<string>,
|
|
58
|
+
/** The chapter the slice sits in (`src/<Chapter>/<Kind>/…`), when it has one. */
|
|
59
|
+
chapter?: string,
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
/** Derived scope for one tag key. */
|
|
@@ -129,39 +136,227 @@ let foreignConsumedKeys = (s: sliceShape): array<string> => {
|
|
|
129
136
|
)
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
/** Rule 1's outcome for a boundary, before the scope rules build on it. */
|
|
140
|
+
type partitionResolution = {
|
|
141
|
+
/** sliceName -> its partition key (absent when unresolved). */
|
|
142
|
+
partitionBySlice: dict<string>,
|
|
143
|
+
/** sliceName -> the keys rule 1 left standing; one entry means resolved. */
|
|
144
|
+
candidatesBySlice: dict<array<string>>,
|
|
145
|
+
/** sliceName -> (produced key, the foreign arms that kept it from the slice). */
|
|
146
|
+
blockersBySlice: dict<array<(string, array<string>)>>,
|
|
147
|
+
/** (sliceName, reason) for slices whose partition couldn't be inferred. */
|
|
148
|
+
ambiguities: array<(string, string)>,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let sameAssignment = (a: dict<string>, b: dict<string>): bool => {
|
|
152
|
+
let entries = d => d->Dict.toArray->Array.toSorted(((x, _), (y, _)) => String.compare(x, y))
|
|
153
|
+
entries(a) == entries(b)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
Rule 1 over a whole boundary.
|
|
158
|
+
|
|
159
|
+
1. **Seeds.** A slice whose events carry a single key is partitioned by it, whatever
|
|
160
|
+
it reads; an explicit `@partitionTag` naming a produced key is a seed too.
|
|
161
|
+
2. **Subtraction.** Otherwise, `producedKeys − foreignConsumedKeys`. One key left
|
|
162
|
+
is the partition; several go to the chapter (4), then to `@partitionTag`.
|
|
163
|
+
3. **Give-back.** Only when the subtraction leaves nothing: a key comes back when
|
|
164
|
+
every foreign arm carrying it is produced by a slice partitioned by that key.
|
|
165
|
+
An unseen producer makes the arm a reference. An unresolved producer counts as
|
|
166
|
+
possibly partitioned by any key it writes, which breaks cycles such as two
|
|
167
|
+
slices each reading the other's event; the answer counts only once a further
|
|
168
|
+
pass changes nothing, so that optimism never decides it alone.
|
|
169
|
+
4. **Chapter.** When several keys are left, keep the ones every id-carrying event
|
|
170
|
+
written in the slice's chapter carries. `PlaceOrder` is left with `orderId` and
|
|
171
|
+
`customerId`, whose reads never show the customer to be a reference; every
|
|
172
|
+
event under `Order/` carries `orderId`, and only some carry `customerId`. Only a
|
|
173
|
+
single survivor decides — a join such as `RecordProductDemand`, whose chapter's
|
|
174
|
+
events all carry both ids, stays ambiguous. It is a tie-breaker, not a rule:
|
|
175
|
+
it never overrides a seed, the subtraction or the give-back, and moving a slice
|
|
176
|
+
to another chapter can change only a partition this step decided.
|
|
177
|
+
|
|
178
|
+
Applying the give-back everywhere would be wrong: `AddProduct` reads
|
|
179
|
+
`CategoryAdded({categoryId})` from a slice partitioned by `categoryId`, which
|
|
180
|
+
would hand `categoryId` back and leave it two candidates.
|
|
181
|
+
*/
|
|
182
|
+
let resolvePartitions = (slices: array<sliceShape>): partitionResolution => {
|
|
183
|
+
let producersByEventType: dict<array<sliceShape>> = Dict.make()
|
|
184
|
+
slices->Array.forEach(s =>
|
|
185
|
+
s.produced->Array.forEach(e => {
|
|
186
|
+
let prev = producersByEventType->Dict.get(e.eventType)->Option.getOr([])
|
|
187
|
+
if !(prev->Array.some(p => p.sliceName == s.sliceName)) {
|
|
188
|
+
producersByEventType->Dict.set(e.eventType, prev->Array.concat([s]))
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
// chapter -> the keys every id-carrying event written in that chapter carries.
|
|
193
|
+
let chapterKeys = Dict.make()
|
|
194
|
+
slices->Array.forEach(s =>
|
|
195
|
+
s.chapter->Option.forEach(chapter => {
|
|
196
|
+
let events = s.produced->Array.filter(e => e.idFields->Array.length > 0)
|
|
197
|
+
events->Array.forEach(
|
|
198
|
+
e => {
|
|
199
|
+
let keys = e->keysOfEvent
|
|
200
|
+
let shared = switch chapterKeys->Dict.get(chapter) {
|
|
201
|
+
| Some(prev) => prev->Array.filter(k => keys->Array.includes(k))
|
|
202
|
+
| None => dedupSorted(keys)
|
|
203
|
+
}
|
|
204
|
+
chapterKeys->Dict.set(chapter, shared)
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
let chapterKeysOf = (s: sliceShape) => s.chapter->Option.flatMap(c => chapterKeys->Dict.get(c))
|
|
210
|
+
let byChapter = (s: sliceShape, candidates) =>
|
|
211
|
+
switch s->chapterKeysOf {
|
|
212
|
+
| Some(keys) =>
|
|
213
|
+
switch candidates->Array.filter(k => keys->Array.includes(k)) {
|
|
214
|
+
| [k] => [k]
|
|
215
|
+
| _ => candidates
|
|
216
|
+
}
|
|
217
|
+
| None => candidates
|
|
218
|
+
}
|
|
219
|
+
let foreignArms = (s: sliceShape) => {
|
|
220
|
+
let own = Set.make()
|
|
221
|
+
s.produced->Array.forEach(e => own->Set.add(e.eventType))
|
|
222
|
+
s.consumed->Array.filter(e => !(own->Set.has(e.eventType)))
|
|
223
|
+
}
|
|
224
|
+
// An arm is identity evidence for `k` iff every producer of its event type is
|
|
225
|
+
// (or, while unresolved, could be) partitioned by `k`.
|
|
226
|
+
let armIsIdentity = (e: eventShape, k, known: dict<string>) =>
|
|
227
|
+
switch producersByEventType->Dict.get(e.eventType) {
|
|
228
|
+
| None | Some([]) => false
|
|
229
|
+
| Some(producers) =>
|
|
230
|
+
producers->Array.every(p =>
|
|
231
|
+
switch known->Dict.get(p.sliceName) {
|
|
232
|
+
| Some(pk) => pk == k
|
|
233
|
+
| None => producedKeys(p)->Array.includes(k)
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
let blockersOf = (s: sliceShape, known) =>
|
|
238
|
+
producedKeys(s)->Array.filterMap(k => {
|
|
239
|
+
let arms =
|
|
240
|
+
s
|
|
241
|
+
->foreignArms
|
|
242
|
+
->Array.filter(e => e->keysOfEvent->Array.includes(k) && !armIsIdentity(e, k, known))
|
|
243
|
+
->Array.map(e => e.eventType)
|
|
244
|
+
arms->Array.length > 0 ? Some((k, arms)) : None
|
|
245
|
+
})
|
|
246
|
+
let seedOf = (s: sliceShape) => {
|
|
247
|
+
let produced = producedKeys(s)
|
|
248
|
+
switch (s.partitionHint, produced) {
|
|
249
|
+
| (Some(h), _) if produced->Array.includes(h) => Some(h)
|
|
250
|
+
| (_, [single]) => Some(single)
|
|
251
|
+
| _ => None
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
let subtracted = (s: sliceShape) => {
|
|
255
|
+
let foreign = foreignConsumedKeys(s)
|
|
256
|
+
producedKeys(s)->Array.filter(k => !(foreign->Array.includes(k)))
|
|
257
|
+
}
|
|
258
|
+
let candidatesOf = (s: sliceShape, known) =>
|
|
259
|
+
switch seedOf(s) {
|
|
260
|
+
| Some(k) => [k]
|
|
261
|
+
| None =>
|
|
262
|
+
switch subtracted(s) {
|
|
263
|
+
| [] =>
|
|
264
|
+
let blocked = s->blockersOf(known)->Array.map(((k, _)) => k)
|
|
265
|
+
s->byChapter(producedKeys(s)->Array.filter(k => !(blocked->Array.includes(k))))
|
|
266
|
+
| remaining => s->byChapter(remaining)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let pass = known => {
|
|
271
|
+
let next = Dict.make()
|
|
272
|
+
slices->Array.forEach(s =>
|
|
273
|
+
switch candidatesOf(s, known) {
|
|
274
|
+
| [k] => next->Dict.set(s.sliceName, k)
|
|
275
|
+
| _ => ()
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
next
|
|
279
|
+
}
|
|
280
|
+
// Each pass can resolve at most one more link of a chain, so a boundary that
|
|
281
|
+
// settles does so within one pass per slice; the extra pass confirms it.
|
|
282
|
+
let rec settle = (known, remaining) => {
|
|
283
|
+
let next = pass(known)
|
|
284
|
+
if sameAssignment(next, known) {
|
|
285
|
+
(next, true)
|
|
286
|
+
} else if remaining == 0 {
|
|
287
|
+
(next, false)
|
|
288
|
+
} else {
|
|
289
|
+
settle(next, remaining - 1)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let (settled, converged) = settle(Dict.make(), slices->Array.length + 1)
|
|
293
|
+
let previous = pass(settled)
|
|
294
|
+
|
|
295
|
+
let partitionBySlice = Dict.make()
|
|
296
|
+
let candidatesBySlice = Dict.make()
|
|
297
|
+
let blockersBySlice = Dict.make()
|
|
298
|
+
let ambiguities = []
|
|
299
|
+
slices->Array.forEach(s => {
|
|
300
|
+
let candidates = candidatesOf(s, settled)
|
|
301
|
+
candidatesBySlice->Dict.set(s.sliceName, candidates)
|
|
302
|
+
switch candidates {
|
|
303
|
+
| _ if !converged && settled->Dict.get(s.sliceName) != previous->Dict.get(s.sliceName) =>
|
|
304
|
+
ambiguities->Array.push((
|
|
305
|
+
s.sliceName,
|
|
306
|
+
`partition inference did not settle — the slices it reads from keep changing each other's partition. Add an explicit @partitionTag`,
|
|
307
|
+
))
|
|
308
|
+
| [k] => partitionBySlice->Dict.set(s.sliceName, k)
|
|
309
|
+
| [] =>
|
|
310
|
+
// Name the arms that took the key. Almost always a lifecycle arm
|
|
311
|
+
// declaring the id the slice is already partitioned by, where the fix is
|
|
312
|
+
// to drop the field rather than to annotate around it.
|
|
313
|
+
let blockers = s->blockersOf(settled)
|
|
314
|
+
blockersBySlice->Dict.set(s.sliceName, blockers)
|
|
315
|
+
let blame =
|
|
316
|
+
blockers
|
|
317
|
+
->Array.map(((key, arms)) => `${arms->Array.join("/")} declares ${key}`)
|
|
318
|
+
->Array.join("; ")
|
|
319
|
+
ambiguities->Array.push((
|
|
320
|
+
s.sliceName,
|
|
321
|
+
`no own partition key — every produced *Id is read from a foreign producer (${blame}). If that field is this slice's own partition, remove it from the consumed arm; if the slice really is a pure join, add an explicit @partitionTag`,
|
|
322
|
+
))
|
|
323
|
+
| many =>
|
|
324
|
+
let chapterNote = switch (s.chapter, s->chapterKeysOf) {
|
|
325
|
+
| (Some(chapter), Some(keys)) =>
|
|
326
|
+
` The ${chapter} chapter does not decide: the ids every event in it carries are [${keys->Array.join(
|
|
327
|
+
", ",
|
|
328
|
+
)}].`
|
|
329
|
+
| _ => ""
|
|
330
|
+
}
|
|
331
|
+
ambiguities->Array.push((
|
|
332
|
+
s.sliceName,
|
|
333
|
+
`multiple candidate partition keys (${many->Array.join(
|
|
334
|
+
", ",
|
|
335
|
+
)}) — add an explicit @partitionTag.${chapterNote}`,
|
|
336
|
+
))
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
{partitionBySlice, candidatesBySlice, blockersBySlice, ambiguities}
|
|
340
|
+
}
|
|
341
|
+
|
|
132
342
|
/**
|
|
133
343
|
Which consumed arms cost a slice its partition — the actionable half of the
|
|
134
|
-
"no own partition key" ambiguity.
|
|
344
|
+
"no own partition key" ambiguity, seen from this slice alone.
|
|
135
345
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
whose every event carries `productId`), which reads as "this id comes from a
|
|
141
|
-
foreign producer" when it is in fact this slice's own partition.
|
|
346
|
+
A slice whose produced keys all ride consumed arms from other producers, and
|
|
347
|
+
whose arms cannot be given back, is left with nothing. In practice that is almost
|
|
348
|
+
always one mistake: a *lifecycle* arm naming the id the slice is already
|
|
349
|
+
partitioned by, which reads as "this id comes from a foreign producer".
|
|
142
350
|
|
|
143
351
|
The ambiguity message can only say a partition was not found. This says which arm
|
|
144
|
-
to delete, which is the whole difference between a diagnostic and a puzzle
|
|
145
|
-
is here, beside the rule that produces the ambiguity, rather than duplicated by
|
|
146
|
-
each surface that reports one.
|
|
352
|
+
to delete, which is the whole difference between a diagnostic and a puzzle.
|
|
147
353
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
354
|
+
Alone, no producer is in sight, so every foreign arm is a reference. Returns one
|
|
355
|
+
entry per produced key a foreign arm claims, naming those arms. Empty when the
|
|
356
|
+
slice has a partition or when too many keys are left (answered by `@partitionTag`).
|
|
151
357
|
*/
|
|
152
|
-
let partitionBlockers = (s: sliceShape): array<(string, array<string>)> =>
|
|
153
|
-
|
|
154
|
-
let ownProduced = Set.make()
|
|
155
|
-
s.produced->Array.forEach(e => ownProduced->Set.add(e.eventType))
|
|
156
|
-
producedKeys(s)
|
|
157
|
-
->Array.filter(k => foreign->Array.includes(k))
|
|
158
|
-
->Array.map(k => (
|
|
159
|
-
k,
|
|
160
|
-
s.consumed
|
|
161
|
-
->Array.filter(e => !(ownProduced->Set.has(e.eventType)) && e->keysOfEvent->Array.includes(k))
|
|
162
|
-
->Array.map(e => e.eventType),
|
|
163
|
-
))
|
|
164
|
-
}
|
|
358
|
+
let partitionBlockers = (s: sliceShape): array<(string, array<string>)> =>
|
|
359
|
+
resolvePartitions([s]).blockersBySlice->Dict.get(s.sliceName)->Option.getOr([])
|
|
165
360
|
|
|
166
361
|
/**
|
|
167
362
|
Per-slice cross-partition keys for the test harness, which has no global owner
|
|
@@ -173,58 +368,14 @@ annotation so capacity/escape-hatch reads remain covered.
|
|
|
173
368
|
let crossPartitionForSlice = (s: sliceShape): array<string> => {
|
|
174
369
|
let foreign = foreignConsumedKeys(s)
|
|
175
370
|
let scalar = commandScalarKeys(s)
|
|
176
|
-
let partition =
|
|
177
|
-
| Some(h) if producedKeys(s)->Array.includes(h) => Some(h)
|
|
178
|
-
| _ =>
|
|
179
|
-
switch producedKeys(s)->Array.filter(k => !(foreign->Array.includes(k))) {
|
|
180
|
-
| [single] => Some(single)
|
|
181
|
-
| _ => None
|
|
182
|
-
}
|
|
183
|
-
}
|
|
371
|
+
let partition = resolvePartitions([s]).partitionBySlice->Dict.get(s.sliceName)
|
|
184
372
|
// A foreign read is cross-partition only when the command carries the key as a
|
|
185
373
|
// scalar (must be fanned); an array-only foreign key auto-fans partition-scoped.
|
|
186
374
|
foreign->Array.filter(k => Some(k) != partition && scalar->Array.includes(k))
|
|
187
375
|
}
|
|
188
376
|
|
|
189
377
|
let infer = (slices: array<sliceShape>): derived => {
|
|
190
|
-
|
|
191
|
-
// foreign-consumed key rides a consumed arm the slice does not itself produce.
|
|
192
|
-
// An explicit @partitionTag hint overrides the derivation (escape hatch for
|
|
193
|
-
// slices whose own events legitimately carry two owned keys).
|
|
194
|
-
let partitionBySlice = Dict.make()
|
|
195
|
-
let ambiguities = []
|
|
196
|
-
slices->Array.forEach(s => {
|
|
197
|
-
let produced = producedKeys(s)
|
|
198
|
-
switch s.partitionHint {
|
|
199
|
-
| Some(h) if produced->Array.includes(h) => partitionBySlice->Dict.set(s.sliceName, h)
|
|
200
|
-
| _ =>
|
|
201
|
-
let foreign = foreignConsumedKeys(s)
|
|
202
|
-
switch produced->Array.filter(k => !(foreign->Array.includes(k))) {
|
|
203
|
-
| [single] => partitionBySlice->Dict.set(s.sliceName, single)
|
|
204
|
-
| [] =>
|
|
205
|
-
// Name the arms that took the key. Almost always a lifecycle arm
|
|
206
|
-
// declaring the id the slice is already partitioned by, where the fix is
|
|
207
|
-
// to drop the field rather than to annotate around it.
|
|
208
|
-
let blame =
|
|
209
|
-
partitionBlockers(s)
|
|
210
|
-
->Array.map(((key, arms)) => `${arms->Array.join("/")} declares ${key}`)
|
|
211
|
-
->Array.join("; ")
|
|
212
|
-
let _ =
|
|
213
|
-
ambiguities->Array.push((
|
|
214
|
-
s.sliceName,
|
|
215
|
-
`no own partition key — every produced *Id is read from a foreign producer (${blame}). If that field is this slice's own partition, remove it from the consumed arm; if the slice really is a pure join, add an explicit @partitionTag`,
|
|
216
|
-
))
|
|
217
|
-
| many =>
|
|
218
|
-
let _ =
|
|
219
|
-
ambiguities->Array.push((
|
|
220
|
-
s.sliceName,
|
|
221
|
-
`multiple candidate partition keys (${many->Array.join(
|
|
222
|
-
", ",
|
|
223
|
-
)}) — add an explicit @partitionTag`,
|
|
224
|
-
))
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
})
|
|
378
|
+
let {partitionBySlice, ambiguities} = resolvePartitions(slices)
|
|
228
379
|
|
|
229
380
|
// owner map + the set of keys that are *some* entity's partition.
|
|
230
381
|
let ownerByKey = Dict.make()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
3
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
5
6
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
@@ -50,79 +51,214 @@ function foreignConsumedKeys(s) {
|
|
|
50
51
|
}));
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
s.produced.forEach(e => {
|
|
57
|
-
ownProduced.add(e.eventType);
|
|
58
|
-
});
|
|
59
|
-
return producedKeys(s).filter(k => foreign.includes(k)).map(k => [
|
|
60
|
-
k,
|
|
61
|
-
s.consumed.filter(e => {
|
|
62
|
-
if (ownProduced.has(e.eventType)) {
|
|
63
|
-
return false;
|
|
64
|
-
} else {
|
|
65
|
-
return e.idFields.map(tagKeyOf).includes(k);
|
|
66
|
-
}
|
|
67
|
-
}).map(e => e.eventType)
|
|
68
|
-
]);
|
|
54
|
+
function sameAssignment(a, b) {
|
|
55
|
+
let entries = d => Object.entries(d).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
|
|
56
|
+
return Primitive_object.equal(entries(a), entries(b));
|
|
69
57
|
}
|
|
70
58
|
|
|
71
|
-
function
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
59
|
+
function resolvePartitions(slices) {
|
|
60
|
+
let producersByEventType = {};
|
|
61
|
+
slices.forEach(s => {
|
|
62
|
+
s.produced.forEach(e => {
|
|
63
|
+
let prev = Stdlib_Option.getOr(producersByEventType[e.eventType], []);
|
|
64
|
+
if (!prev.some(p => p.sliceName === s.sliceName)) {
|
|
65
|
+
producersByEventType[e.eventType] = prev.concat([s]);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
let chapterKeys = {};
|
|
71
|
+
slices.forEach(s => Stdlib_Option.forEach(s.chapter, chapter => {
|
|
72
|
+
let events = s.produced.filter(e => e.idFields.length !== 0);
|
|
73
|
+
events.forEach(e => {
|
|
74
|
+
let keys = e.idFields.map(tagKeyOf);
|
|
75
|
+
let prev = chapterKeys[chapter];
|
|
76
|
+
let shared = prev !== undefined ? prev.filter(k => keys.includes(k)) : dedupSorted(keys);
|
|
77
|
+
chapterKeys[chapter] = shared;
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
80
|
+
let chapterKeysOf = s => Stdlib_Option.flatMap(s.chapter, c => chapterKeys[c]);
|
|
81
|
+
let byChapter = (s, candidates) => {
|
|
82
|
+
let keys = chapterKeysOf(s);
|
|
83
|
+
if (keys === undefined) {
|
|
84
|
+
return candidates;
|
|
85
|
+
}
|
|
86
|
+
let match = candidates.filter(k => keys.includes(k));
|
|
87
|
+
if (match.length !== 1) {
|
|
88
|
+
return candidates;
|
|
89
|
+
} else {
|
|
90
|
+
return [match[0]];
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
let foreignArms = s => {
|
|
94
|
+
let own = new Set();
|
|
95
|
+
s.produced.forEach(e => {
|
|
96
|
+
own.add(e.eventType);
|
|
97
|
+
});
|
|
98
|
+
return s.consumed.filter(e => !own.has(e.eventType));
|
|
99
|
+
};
|
|
100
|
+
let armIsIdentity = (e, k, known) => {
|
|
101
|
+
let producers = producersByEventType[e.eventType];
|
|
102
|
+
if (producers !== undefined && producers.length !== 0) {
|
|
103
|
+
return producers.every(p => {
|
|
104
|
+
let pk = known[p.sliceName];
|
|
105
|
+
if (pk !== undefined) {
|
|
106
|
+
return pk === k;
|
|
107
|
+
} else {
|
|
108
|
+
return producedKeys(p).includes(k);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
89
111
|
} else {
|
|
90
112
|
return false;
|
|
91
113
|
}
|
|
114
|
+
};
|
|
115
|
+
let blockersOf = (s, known) => Stdlib_Array.filterMap(producedKeys(s), k => {
|
|
116
|
+
let arms = foreignArms(s).filter(e => {
|
|
117
|
+
if (e.idFields.map(tagKeyOf).includes(k)) {
|
|
118
|
+
return !armIsIdentity(e, k, known);
|
|
119
|
+
} else {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}).map(e => e.eventType);
|
|
123
|
+
if (arms.length !== 0) {
|
|
124
|
+
return [
|
|
125
|
+
k,
|
|
126
|
+
arms
|
|
127
|
+
];
|
|
128
|
+
}
|
|
92
129
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
let seedOf = s => {
|
|
131
|
+
let produced = producedKeys(s);
|
|
132
|
+
let match = s.partitionHint;
|
|
133
|
+
if (match !== undefined && produced.includes(match)) {
|
|
134
|
+
return match;
|
|
135
|
+
}
|
|
136
|
+
if (produced.length !== 1) {
|
|
137
|
+
return;
|
|
138
|
+
} else {
|
|
139
|
+
return produced[0];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
let subtracted = s => {
|
|
143
|
+
let foreign = foreignConsumedKeys(s);
|
|
144
|
+
return producedKeys(s).filter(k => !foreign.includes(k));
|
|
145
|
+
};
|
|
146
|
+
let candidatesOf = (s, known) => {
|
|
147
|
+
let k = seedOf(s);
|
|
148
|
+
if (k !== undefined) {
|
|
149
|
+
return [k];
|
|
150
|
+
}
|
|
151
|
+
let remaining = subtracted(s);
|
|
152
|
+
if (remaining.length !== 0) {
|
|
153
|
+
return byChapter(s, remaining);
|
|
154
|
+
}
|
|
155
|
+
let blocked = blockersOf(s, known).map(param => param[0]);
|
|
156
|
+
return byChapter(s, producedKeys(s).filter(k => !blocked.includes(k)));
|
|
157
|
+
};
|
|
158
|
+
let pass = known => {
|
|
159
|
+
let next = {};
|
|
160
|
+
slices.forEach(s => {
|
|
161
|
+
let match = candidatesOf(s, known);
|
|
162
|
+
if (match.length !== 1) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let k = match[0];
|
|
166
|
+
next[s.sliceName] = k;
|
|
167
|
+
});
|
|
168
|
+
return next;
|
|
169
|
+
};
|
|
170
|
+
let settle = (_known, _remaining) => {
|
|
171
|
+
while (true) {
|
|
172
|
+
let remaining = _remaining;
|
|
173
|
+
let known = _known;
|
|
174
|
+
let next = pass(known);
|
|
175
|
+
if (sameAssignment(next, known)) {
|
|
176
|
+
return [
|
|
177
|
+
next,
|
|
178
|
+
true
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
if (remaining === 0) {
|
|
182
|
+
return [
|
|
183
|
+
next,
|
|
184
|
+
false
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
_remaining = remaining - 1 | 0;
|
|
188
|
+
_known = next;
|
|
189
|
+
continue;
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
let match = settle({}, slices.length + 1 | 0);
|
|
193
|
+
let converged = match[1];
|
|
194
|
+
let settled = match[0];
|
|
195
|
+
let previous = pass(settled);
|
|
96
196
|
let partitionBySlice = {};
|
|
197
|
+
let candidatesBySlice = {};
|
|
198
|
+
let blockersBySlice = {};
|
|
97
199
|
let ambiguities = [];
|
|
98
200
|
slices.forEach(s => {
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
201
|
+
let candidates = candidatesOf(s, settled);
|
|
202
|
+
candidatesBySlice[s.sliceName] = candidates;
|
|
203
|
+
if (!converged && Primitive_object.notequal(settled[s.sliceName], previous[s.sliceName])) {
|
|
204
|
+
ambiguities.push([
|
|
205
|
+
s.sliceName,
|
|
206
|
+
`partition inference did not settle — the slices it reads from keep changing each other's partition. Add an explicit @partitionTag`
|
|
207
|
+
]);
|
|
103
208
|
return;
|
|
104
209
|
}
|
|
105
|
-
let
|
|
106
|
-
let many = produced.filter(k => !foreign.includes(k));
|
|
107
|
-
let len = many.length;
|
|
210
|
+
let len = candidates.length;
|
|
108
211
|
if (len !== 1) {
|
|
109
212
|
if (len !== 0) {
|
|
213
|
+
let match = s.chapter;
|
|
214
|
+
let match$1 = chapterKeysOf(s);
|
|
215
|
+
let chapterNote = match !== undefined && match$1 !== undefined ? ` The ` + match + ` chapter does not decide: the ids every event in it carries are [` + match$1.join(", ") + `].` : "";
|
|
110
216
|
ambiguities.push([
|
|
111
217
|
s.sliceName,
|
|
112
|
-
`multiple candidate partition keys (` +
|
|
218
|
+
`multiple candidate partition keys (` + candidates.join(", ") + `) — add an explicit @partitionTag.` + chapterNote
|
|
113
219
|
]);
|
|
114
220
|
return;
|
|
115
221
|
}
|
|
116
|
-
let
|
|
222
|
+
let blockers = blockersOf(s, settled);
|
|
223
|
+
blockersBySlice[s.sliceName] = blockers;
|
|
224
|
+
let blame = blockers.map(param => param[1].join("/") + ` declares ` + param[0]).join("; ");
|
|
117
225
|
ambiguities.push([
|
|
118
226
|
s.sliceName,
|
|
119
227
|
`no own partition key — every produced *Id is read from a foreign producer (` + blame + `). If that field is this slice's own partition, remove it from the consumed arm; if the slice really is a pure join, add an explicit @partitionTag`
|
|
120
228
|
]);
|
|
121
229
|
return;
|
|
122
230
|
}
|
|
123
|
-
let
|
|
124
|
-
partitionBySlice[s.sliceName] =
|
|
231
|
+
let k = candidates[0];
|
|
232
|
+
partitionBySlice[s.sliceName] = k;
|
|
125
233
|
});
|
|
234
|
+
return {
|
|
235
|
+
partitionBySlice: partitionBySlice,
|
|
236
|
+
candidatesBySlice: candidatesBySlice,
|
|
237
|
+
blockersBySlice: blockersBySlice,
|
|
238
|
+
ambiguities: ambiguities
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function partitionBlockers(s) {
|
|
243
|
+
return Stdlib_Option.getOr(resolvePartitions([s]).blockersBySlice[s.sliceName], []);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function crossPartitionForSlice(s) {
|
|
247
|
+
let foreign = foreignConsumedKeys(s);
|
|
248
|
+
let scalar = commandScalarKeys(s);
|
|
249
|
+
let partition = resolvePartitions([s]).partitionBySlice[s.sliceName];
|
|
250
|
+
return foreign.filter(k => {
|
|
251
|
+
if (Primitive_object.notequal(k, partition)) {
|
|
252
|
+
return scalar.includes(k);
|
|
253
|
+
} else {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function infer(slices) {
|
|
260
|
+
let match = resolvePartitions(slices);
|
|
261
|
+
let partitionBySlice = match.partitionBySlice;
|
|
126
262
|
let ownerByKey = {};
|
|
127
263
|
let ownedPartitionKeys = new Set();
|
|
128
264
|
slices.forEach(s => {
|
|
@@ -178,7 +314,7 @@ function infer(slices) {
|
|
|
178
314
|
ownerByKey: ownerByKey,
|
|
179
315
|
crossPartitionTagKeys: crossPartitionTagKeys,
|
|
180
316
|
tagKeysByEventType: tagKeysByEventType,
|
|
181
|
-
ambiguities: ambiguities
|
|
317
|
+
ambiguities: match.ambiguities
|
|
182
318
|
};
|
|
183
319
|
}
|
|
184
320
|
|
|
@@ -190,6 +326,8 @@ export {
|
|
|
190
326
|
consumedKeys,
|
|
191
327
|
commandScalarKeys,
|
|
192
328
|
foreignConsumedKeys,
|
|
329
|
+
sameAssignment,
|
|
330
|
+
resolvePartitions,
|
|
193
331
|
partitionBlockers,
|
|
194
332
|
crossPartitionForSlice,
|
|
195
333
|
infer,
|