@reventlessdev/reventless-core 3.0.0-alpha.239 → 3.0.0-alpha.240

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +9 -9
  3. package/src/Message.res +11 -11
  4. package/src/Message.res.mjs +15 -14
  5. package/src/Projection.res +2 -2
  6. package/src/Projection.res.mjs +5 -4
  7. package/src/adapter/Monitoring/Monitoring.res +24 -2
  8. package/src/adapter/Monitoring/Monitoring.res.mjs +3 -3
  9. package/src/admin/Platform_AdminApi.res.mjs +3 -3
  10. package/src/admin/Platform_Admin_Structure.res +33 -113
  11. package/src/admin/Platform_Admin_Structure.res.mjs +8 -139
  12. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +12 -12
  13. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs +14 -14
  14. package/src/components/Aggregate/Aggregate_Callback.res +2 -2
  15. package/src/components/Aggregate/Aggregate_Callback.res.mjs +4 -4
  16. package/src/components/Api/ApiAllowedStatesHelpers.res.mjs +3 -3
  17. package/src/components/Api/ApiNoApiHelpers.res.mjs +5 -5
  18. package/src/components/Api/ApiTargetStateHelpers.res.mjs +3 -3
  19. package/src/components/Api/GraphQL_FragmentGenerator.res +18 -12
  20. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +7 -13
  21. package/src/components/Api/GraphQL_SchemaInspector.res +1 -1
  22. package/src/components/Api/GraphQL_SchemaInspector.res.mjs +1 -1
  23. package/src/components/Api/MCP_SchemaGenerator.res +1 -1
  24. package/src/components/Api/MCP_SchemaGenerator.res.mjs +1 -1
  25. package/src/components/Api/SchemaType.res +6 -10
  26. package/src/components/Api/SchemaType.res.mjs +4 -4
  27. package/src/components/Api/StorageRefFields.res +1 -1
  28. package/src/components/Api/StorageRefFields.res.mjs +1 -1
  29. package/src/components/Api/SuryToJsonSchema.res +11 -0
  30. package/src/components/Api/SuryToJsonSchema.res.mjs +2 -1
  31. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -4
  32. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +22 -23
  33. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -2
  34. package/src/components/CommandTopic/CommandTopic_Callback.res.mjs +3 -4
  35. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -1
  36. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +3 -3
  37. package/src/components/Counter/Counter.res.mjs +2 -2
  38. package/src/components/Counter/Counter_Callback.res.mjs +4 -4
  39. package/src/components/Counter/Counter_Operations.res.mjs +4 -4
  40. package/src/components/Dcb/Dcb_Builder.res +1 -1
  41. package/src/components/Dcb/Dcb_Builder.res.mjs +8 -6
  42. package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +4 -3
  43. package/src/components/EventCollector/EventCollector_Builder.res +1 -1
  44. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +1 -2
  45. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +11 -12
  46. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +3 -4
  47. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +24 -25
  48. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +1 -1
  49. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res.mjs +3 -3
  50. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +2 -2
  51. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  52. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -1
  53. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +3 -3
  54. package/src/plugin/api/PluginBaseFragment.res +12 -29
  55. package/src/plugin/api/PluginBaseFragment.res.mjs +3 -22
  56. package/src/plugin/component/Capability_Inference.res +118 -1
  57. package/src/plugin/component/Capability_Inference.res.mjs +128 -1
  58. package/src/plugin/component/Plugin_Builder.res +2 -2
  59. package/src/plugin/component/Plugin_Builder.res.mjs +2 -1
  60. package/src/plugin/component/Plugin_Helpers.res +11 -11
  61. package/src/plugin/component/Plugin_Helpers.res.mjs +11 -10
  62. package/src/plugin/component/Plugin_Structure.res +349 -274
  63. package/src/plugin/component/Plugin_Structure.res.mjs +212 -175
  64. package/src/plugin/component/Plugin_SubscriptionSchema.res +1 -1
  65. package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +1 -1
  66. package/src/plugin/component/SchemaWalker.res +11 -11
  67. package/src/plugin/component/SchemaWalker.res.mjs +8 -9
  68. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +15 -15
  69. package/src/plugin/lifecycle/PluginBehavior.res.mjs +55 -55
  70. package/src/plugin/lifecycle/PluginSpec.res +22 -7
  71. package/src/plugin/lifecycle/PluginSpec.res.mjs +91 -36
  72. package/src/plugin/lifecycle/PluginsReadModelSpec.res +36 -9
  73. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +33 -22
  74. package/src/util/Interstack.res +3 -3
  75. package/src/util/LogFormat.res +1 -2
  76. package/src/util/LogFormat.res.mjs +2 -2
  77. package/tests/IdentityTest.res +6 -7
  78. package/tests/IdentityTest.res.mjs +7 -9
  79. package/tests/adapter/MonitoringTest.res +30 -3
  80. package/tests/adapter/MonitoringTest.res.mjs +31 -12
  81. package/tests/adapter/RuntimeExtensionHookReachTest.res +0 -1
  82. package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +5 -7
  83. package/tests/admin/Platform_Admin_StructureTest.res +254 -0
  84. package/tests/admin/Platform_Admin_StructureTest.res.mjs +219 -0
  85. package/tests/aggregate/AggregateCacheTest.res +0 -1
  86. package/tests/aggregate/AggregateCacheTest.res.mjs +13 -15
  87. package/tests/aggregate/AggregateConflictTest.res +0 -1
  88. package/tests/aggregate/AggregateConflictTest.res.mjs +7 -9
  89. package/tests/aggregate/AggregateFixtures.res +0 -1
  90. package/tests/aggregate/AggregateFixtures.res.mjs +19 -21
  91. package/tests/aggregate/AggregateSnapshotTest.res +1 -2
  92. package/tests/aggregate/AggregateSnapshotTest.res.mjs +15 -16
  93. package/tests/api/ApiNoApiTest.res.mjs +17 -9
  94. package/tests/api/GraphQL_FragmentGeneratorTest.res +62 -1
  95. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +102 -58
  96. package/tests/api/PsInternalFieldsReadModel.res +21 -0
  97. package/tests/api/PsInternalFieldsReadModel.res.mjs +74 -0
  98. package/tests/api/StorageRefFieldsTest.res.mjs +13 -13
  99. package/tests/api/SuryToJsonSchemaTest.res +43 -0
  100. package/tests/api/SuryToJsonSchemaTest.res.mjs +352 -849
  101. package/tests/commandgenerator/CommandGeneratorFixtures.res +0 -1
  102. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +19 -21
  103. package/tests/commandgenerator/OwnerStampingTest.res +0 -1
  104. package/tests/commandgenerator/OwnerStampingTest.res.mjs +9 -11
  105. package/tests/commandtopic/CommandTopicCallbackFixtures.res +0 -1
  106. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +7 -9
  107. package/tests/counter/CounterFixtures.res +0 -1
  108. package/tests/counter/CounterFixtures.res.mjs +1 -4
  109. package/tests/dcb/DcbDecodeTest.res.mjs +24 -24
  110. package/tests/dcb/DcbEventLogOperationsTest.res +2 -2
  111. package/tests/dcb/DcbEventLogOperationsTest.res.mjs +3 -3
  112. package/tests/dcb/DcbEventLogSchemaTest.res.mjs +7 -7
  113. package/tests/dcb/DcbFixtures.res +0 -1
  114. package/tests/dcb/DcbFixtures.res.mjs +87 -89
  115. package/tests/dcb/DcbTagTest.res.mjs +6 -6
  116. package/tests/dcb/DcbValidationTest.res.mjs +52 -52
  117. package/tests/eventlog/EventLogFixtures.res +0 -1
  118. package/tests/eventlog/EventLogFixtures.res.mjs +7 -9
  119. package/tests/eventmapper/EventMapperFixtures.res +0 -1
  120. package/tests/eventmapper/EventMapperFixtures.res.mjs +24 -26
  121. package/tests/eventtopic/EventTopicFixtures.res +0 -1
  122. package/tests/eventtopic/EventTopicFixtures.res.mjs +7 -9
  123. package/tests/extensionpoint/ExtensionPointFixtures.res +0 -1
  124. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +10 -12
  125. package/tests/extensionpoint/ExtensionPointOperationsTest.res +1 -2
  126. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -10
  127. package/tests/logger/LogFormatTest.res +3 -2
  128. package/tests/logger/LogFormatTest.res.mjs +7 -9
  129. package/tests/message/MessageTest.res +2 -2
  130. package/tests/message/MessageTest.res.mjs +8 -7
  131. package/tests/message/MetaEnvelopeTest.res +5 -5
  132. package/tests/message/MetaEnvelopeTest.res.mjs +12 -11
  133. package/tests/plugin/ManifestVisibilityTest.res.mjs +7 -7
  134. package/tests/plugin/PluginDefinitionScalars.res +5 -3
  135. package/tests/plugin/PluginDefinitionScalars.res.mjs +2 -2
  136. package/tests/plugin/PluginLifecycleCorpusTest.res +4 -3
  137. package/tests/plugin/PluginLifecycleCorpusTest.res.mjs +3 -6
  138. package/tests/plugin/PluginSpecExperiment.res +0 -1
  139. package/tests/plugin/PluginSpecExperiment.res.mjs +12 -14
  140. package/tests/plugin/PluginStructureTest.res +188 -1
  141. package/tests/plugin/PluginStructureTest.res.mjs +293 -102
  142. package/tests/plugin/Plugin_EventQuerySchemaTest.res.mjs +3 -3
  143. package/tests/plugin/SchemaWalkerTest.res +0 -1
  144. package/tests/plugin/SchemaWalkerTest.res.mjs +17 -19
  145. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +5 -5
  146. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -8
  147. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +5 -5
  148. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +9 -9
  149. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +5 -5
  150. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -8
  151. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +15 -15
  152. package/tests/plugin/StateChangeSlice/PsTypoStore.res +31 -0
  153. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +62 -0
  154. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -8
  155. package/tests/plugin/StateChangeSlice/PsUploadImages.res +40 -0
  156. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +66 -0
  157. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +13 -12
  158. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res.mjs +8 -8
  159. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +6 -6
  160. package/tests/plugin/StateViewSlice/PsCustomersView.res.mjs +10 -10
  161. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +6 -6
  162. package/tests/plugin/StateViewSlice/PsOrdersView.res.mjs +9 -9
  163. package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +11 -10
  164. package/tests/plugin/pluginDefinitionRequiredScalars.txt +15 -0
  165. package/tests/querydb/QueryDbFixtures.res +0 -1
  166. package/tests/querydb/QueryDbFixtures.res.mjs +5 -7
  167. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +0 -1
  168. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +4 -6
  169. package/tests/util/CommandPublisherTest.res +0 -1
  170. package/tests/util/CommandPublisherTest.res.mjs +4 -6
@@ -1,31 +1,29 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
3
+ import * as Sury from "sury";
4
4
 
5
- S.enableJson();
6
-
7
- let recordSchema = S.schema(s => ({
8
- a: s.m(S.meta(S.string, {
5
+ let recordSchema = Sury.$schema(s => ({
6
+ a: s.m(Sury.meta(Sury.string, {
9
7
  description: "aaa"
10
8
  })),
11
- b: s.m(S.string)
9
+ b: s.m(Sury.string)
12
10
  }));
13
11
 
14
- let variantSchema = S.union([
15
- S.schema(s => ({
12
+ let variantSchema = Sury.union([
13
+ Sury.$schema(s => ({
16
14
  TAG: "Q",
17
- _0: s.m(S.int)
15
+ _0: s.m(Sury.int)
18
16
  })),
19
- S.literal("X"),
20
- S.literal("Y")
17
+ Sury.literal("X"),
18
+ Sury.literal("Y")
21
19
  ]);
22
20
 
23
- console.log("Record:", JSON.stringify(S.toJSONSchema(recordSchema), undefined, 2));
21
+ console.log("Record:", JSON.stringify(Sury.toJSONSchema(recordSchema), undefined, 2));
24
22
 
25
- console.log("Variant:", JSON.stringify(S.toJSONSchema(variantSchema), undefined, 2));
23
+ console.log("Variant:", JSON.stringify(Sury.toJSONSchema(variantSchema), undefined, 2));
26
24
 
27
25
  export {
28
26
  recordSchema,
29
27
  variantSchema,
30
28
  }
31
- /* Not a pure module */
29
+ /* recordSchema Not a pure module */
@@ -116,6 +116,34 @@ module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
116
116
  let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
117
117
  }
118
118
 
119
+ module PsUploadImagesSlice: ReventlessInfra.StateChangeSlice.T = {
120
+ module Spec = PsUploadImages
121
+ module Behavior = {
122
+ type state = PsUploadImages.state
123
+ let initialState = PsUploadImages.initialState
124
+ let evolve = PsUploadImages.evolve
125
+ let decide = PsUploadImages.decide
126
+ let moduleUrl = PsUploadImages.moduleUrl
127
+ }
128
+ let isAsync = false
129
+ type component = scsComponent
130
+ let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
131
+ }
132
+
133
+ module PsTypoStoreSlice: ReventlessInfra.StateChangeSlice.T = {
134
+ module Spec = PsTypoStore
135
+ module Behavior = {
136
+ type state = PsTypoStore.state
137
+ let initialState = PsTypoStore.initialState
138
+ let evolve = PsTypoStore.evolve
139
+ let decide = PsTypoStore.decide
140
+ let moduleUrl = PsTypoStore.moduleUrl
141
+ }
142
+ let isAsync = false
143
+ type component = scsComponent
144
+ let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
145
+ }
146
+
119
147
  module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
120
148
  module Spec = PsChangePhoto
121
149
  module Behavior = {
@@ -277,7 +305,7 @@ describe("@transition cross-check", () => {
277
305
  )->ignore
278
306
  None
279
307
  } catch {
280
- | Exn.Error(e) => Some(Exn.message(e)->Option.getOr(""))
308
+ | JsExn(e) => Some(JsExn.message(e)->Option.getOr(""))
281
309
  | _ => Some("")
282
310
  }
283
311
 
@@ -1373,6 +1401,165 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1373
1401
  })
1374
1402
  })
1375
1403
 
1404
+ // A field typed uploadable declares its store by being named. The collection
1405
+ // walk is unchanged: it matches the `StoredIn` payload, never the semantic id,
1406
+ // so a new id carrying one is provisioned by machinery that knows nothing
1407
+ // about it.
1408
+ describe("uploadable types derive their store from the field name", () => {
1409
+ let withUploads = Plugin_Structure.make(
1410
+ ~name="TestPlugin",
1411
+ ~stateChangeSlices=[module(PsUploadImagesSlice)],
1412
+ )
1413
+ let stores = withUploads.requiredStores->Option.getOr([])
1414
+
1415
+ testSync("the derived store is the field name, pluralised", () =>
1416
+ expect(stores->Array.includes("TestPlugin.productImages"))->toBe(true)
1417
+ )
1418
+
1419
+ testSync("an optional field still declares its store", () =>
1420
+ expect(stores->Array.includes("TestPlugin.categoryImages"))->toBe(true)
1421
+ )
1422
+
1423
+ // The array marker lives on the element type. Reading the field schema
1424
+ // directly answers `None`, which is the same silence as never declaring —
1425
+ // so this asserts collection, not just that the fixture compiled.
1426
+ testSync("an array field declares through its element", () =>
1427
+ expect(stores->Array.includes("TestPlugin.datasheets"))->toBe(true)
1428
+ )
1429
+
1430
+ testSync("an explicit @storageRef overrides the derived name", () =>
1431
+ expect(stores->Array.includes("branding.logos"))->toBe(true)
1432
+ )
1433
+
1434
+ testSync("nothing else is collected", () => expect(stores->Array.length)->toBe(4))
1435
+
1436
+ // The event field derives the same store as the command field of the same
1437
+ // name, so the two collapse to one declaration site — the behaviour the
1438
+ // annotation form already had, reached without an annotation.
1439
+ testSync("each requirement keeps its declaring sites", () => {
1440
+ let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1441
+ {
1442
+ store: "TestPlugin.categoryImages",
1443
+ component: "UploadImages",
1444
+ field: "categoryImage",
1445
+ annotation: Some("categoryImages"),
1446
+ },
1447
+ {
1448
+ store: "TestPlugin.datasheets",
1449
+ component: "UploadImages",
1450
+ field: "datasheets",
1451
+ annotation: Some("datasheets"),
1452
+ },
1453
+ {
1454
+ store: "TestPlugin.productImages",
1455
+ component: "UploadImages",
1456
+ field: "productImage",
1457
+ annotation: Some("productImages"),
1458
+ },
1459
+ {
1460
+ store: "branding.logos",
1461
+ component: "UploadImages",
1462
+ field: "logo",
1463
+ annotation: Some("branding.logos"),
1464
+ },
1465
+ ]
1466
+ expect(withUploads.requiredStoreDeclarations)->toEqual(Some(expected))
1467
+ })
1468
+
1469
+ let commandJson =
1470
+ ((withUploads.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)).schema
1471
+ ->JSON.parseOrThrow
1472
+
1473
+ let semanticOf = field =>
1474
+ commandJson
1475
+ ->getPropertyOf(field)
1476
+ ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
1477
+ ->Option.flatMap(JSON.Decode.string)
1478
+
1479
+ // The whole point of the family: one declaration states the content type
1480
+ // too, so the UI stops learning "this is an image" from the field's name.
1481
+ testSync("the wire carries the content half, not just the store", () =>
1482
+ expect(semanticOf("productImage"))->toBe(Some("uploadableImage"))
1483
+ )
1484
+
1485
+ // The override replaces the *store*, not the type's semantic: a field
1486
+ // pointing at another plugin's store is still an image.
1487
+ testSync("an overridden field keeps its uploadable semantic", () =>
1488
+ expect(semanticOf("logo"))->toBe(Some("uploadableImage"))
1489
+ )
1490
+
1491
+ testSync("the non-image arm carries its own id, on the element", () =>
1492
+ expect(
1493
+ commandJson
1494
+ ->getPropertyOf("datasheets")
1495
+ ->Option.flatMap(s => getProperty(s, "items"))
1496
+ ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
1497
+ ->Option.flatMap(JSON.Decode.string),
1498
+ )->toBe(Some("uploadableFile"))
1499
+ )
1500
+ })
1501
+
1502
+ // Deriving the store from the field name turns a typo into a second store
1503
+ // rather than into a compile error, so the plugin has to refuse it.
1504
+ describe("near-duplicate stores are refused", () => {
1505
+ let failureText = () =>
1506
+ try {
1507
+ Plugin_Structure.make(
1508
+ ~name="TestPlugin",
1509
+ ~stateChangeSlices=[module(PsTypoStoreSlice)],
1510
+ )->ignore
1511
+ ""
1512
+ } catch {
1513
+ | JsExn(e) => JsExn.message(e)->Option.getOr("(no message)")
1514
+ | _ => "(not a JS error)"
1515
+ }
1516
+
1517
+ testSync("two stores one transposition apart fail the build", () =>
1518
+ expect(failureText() != "")->toBe(true)
1519
+ )
1520
+
1521
+ testSync("the failure names both stores and both fields", () => {
1522
+ let text = failureText()
1523
+ expect([
1524
+ text->String.includes("TestPlugin.productImages"),
1525
+ text->String.includes("TestPlugin.productImgaes"),
1526
+ text->String.includes("TypoStore.productImage"),
1527
+ text->String.includes("@storageRef"),
1528
+ ])->toEqual([true, true, true, true])
1529
+ })
1530
+
1531
+ // Every legitimate pair in the other fixtures has to stay legitimate — a
1532
+ // check that fails on real designs is worse than the typo it catches.
1533
+ testSync("the four stores of the uploadable fixture do not collide", () =>
1534
+ expect(
1535
+ Capability_Inference.collisions(
1536
+ Plugin_Structure.make(
1537
+ ~name="TestPlugin",
1538
+ ~stateChangeSlices=[module(PsUploadImagesSlice)],
1539
+ ).requiredStoreDeclarations->Option.getOr([]),
1540
+ ),
1541
+ )->toEqual([])
1542
+ )
1543
+
1544
+ testSync("one edit, transpositions included; two edits not", () =>
1545
+ expect(
1546
+ [
1547
+ ("productImages", "productImgaes"),
1548
+ ("productImages", "productImage"),
1549
+ ("logos", "logo"),
1550
+ ("documents", "logos"),
1551
+ ("productImages", "productImgeas"),
1552
+ ]->Array.map(((a, b)) => Capability_Inference.withinOneEdit(a, b)),
1553
+ )->toEqual([true, true, true, false, false])
1554
+ )
1555
+
1556
+ // Reached only through an explicit override: two *derived* names cannot
1557
+ // differ by number, because each is pluralised through its singular stem.
1558
+ testSync("a singular/plural pair collides even at two edits apart", () =>
1559
+ expect(Capability_Inference.sameStem("productImage", "productImages"))->toBe(true)
1560
+ )
1561
+ })
1562
+
1376
1563
  // Making a field optional says the value may be absent. It does not say the
1377
1564
  // field stopped being a storage ref or a reference — but both walks read the
1378
1565
  // marker through `Semantic.get`, and an optional field's marker sits on the