@reventlessdev/reventless-core 3.0.0-alpha.215 → 3.0.0-alpha.217

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 (46) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +6 -6
  3. package/src/ResourceAttribution.res +60 -4
  4. package/src/ResourceAttribution.res.mjs +26 -0
  5. package/src/adapter/QueryInterception/QueryInterception.res +74 -0
  6. package/src/adapter/QueryInterception/QueryInterception.res.mjs +26 -0
  7. package/src/adapter/ResourceAttribution_Deploytime.res +43 -0
  8. package/src/adapter/ResourceAttribution_Deploytime.res.mjs +20 -0
  9. package/src/admin/Platform_Admin_Structure.res +4 -0
  10. package/src/admin/Platform_Admin_Structure.res.mjs +5 -0
  11. package/src/admin/Platform_ComponentDefinitionsApi.res +16 -1
  12. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +24 -1
  13. package/src/components/Aggregate/Aggregate_Callback.res +14 -2
  14. package/src/components/Aggregate/Aggregate_Callback.res.mjs +3 -3
  15. package/src/components/Builder_Helpers.res +21 -3
  16. package/src/components/Builder_Helpers.res.mjs +4 -3
  17. package/src/components/CommandTopic/CommandTopic.res.mjs +12 -0
  18. package/src/components/CommandTopic/CommandTopic_Helpers.res +88 -2
  19. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +48 -2
  20. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +4 -1
  21. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res.mjs +2 -1
  22. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +13 -1
  23. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  24. package/src/plugin/component/Plugin_Builder.res +6 -1
  25. package/src/plugin/component/Plugin_Builder.res.mjs +4 -2
  26. package/src/plugin/component/Plugin_Helpers.res +7 -2
  27. package/src/plugin/component/Plugin_Helpers.res.mjs +4 -3
  28. package/src/plugin/component/Plugin_Structure.res +66 -51
  29. package/src/plugin/component/Plugin_Structure.res.mjs +61 -46
  30. package/tests/ResourceAttributionTest.res +115 -0
  31. package/tests/ResourceAttributionTest.res.mjs +106 -0
  32. package/tests/adapter/QueryInterceptionTest.res +55 -0
  33. package/tests/adapter/QueryInterceptionTest.res.mjs +49 -0
  34. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +21 -0
  35. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -0
  36. package/tests/commandtopic/CommandOutcomeHookTest.res +258 -0
  37. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +372 -0
  38. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +9 -1
  39. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res.mjs +3 -3
  40. package/tests/message/MessageTest.res +1 -0
  41. package/tests/message/MessageTest.res.mjs +1 -0
  42. package/tests/plugin/PluginStructureTest.res +27 -0
  43. package/tests/plugin/PluginStructureTest.res.mjs +27 -0
  44. package/tests/plugin/StateChangeSlice/PsShipOrder.res +3 -1
  45. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +7 -1
  46. package/tests/plugin/pluginDefinitionRequiredScalars.txt +8 -0
@@ -18,6 +18,71 @@ is the JSON-stringified error payload (or empty string for payload-less variants
18
18
  */
19
19
  type rejectedResult = {errorCode: string, errorDetail: string}
20
20
 
21
+ /**
22
+ Why a command was refused.
23
+
24
+ `DomainRejection` is `Behavior.decide` returning `Error` — the model declining the command,
25
+ which is the model working. `InfrastructureFailure` is the decision never landing: an event-log
26
+ append that failed outright or exhausted its conflict retries. Both arrive on `reportRejected`,
27
+ so without this a consumer would read a broken event log as a business rejection.
28
+ */
29
+ type refusalCause = DomainRejection | InfrastructureFailure
30
+
31
+ /**
32
+ What became of one command. Constructors are prefixed to stay clear of `commandOutcome`'s
33
+ `Accepted`/`Rejected` below — that type is the producer-facing result shape (and the GraphQL
34
+ `CommandResult` union); this one is the observation, and carries the refusal cause it doesn't.
35
+ */
36
+ type observedOutcome =
37
+ | OutcomeAccepted({entityId: option<string>, eventCount: int})
38
+ | OutcomeRejected({errorCode: string, errorDetail: string, cause: refusalCause})
39
+
40
+ /** `reference` is the command's `meta.msgId` on every current transport. */
41
+ type commandOutcomeReport = {
42
+ component: string,
43
+ reference: string,
44
+ outcome: observedOutcome,
45
+ }
46
+
47
+ type commandOutcomeHook = commandOutcomeReport => unit
48
+
49
+ /**
50
+ Module-level outcome hook, in the shape the `RuntimeExtension` cold-start seam already reaches
51
+ — so an extension registers it from `onColdStart` alongside the interception and publish hooks,
52
+ and no new registration path is needed. None = no-op (default).
53
+ */
54
+ let commandOutcomeHook: ref<option<commandOutcomeHook>> = ref(None)
55
+
56
+ let registerCommandOutcome = (hook: commandOutcomeHook) => {
57
+ commandOutcomeHook.contents = Some(hook)
58
+ }
59
+
60
+ let clearCommandOutcome = () => {
61
+ commandOutcomeHook.contents = None
62
+ }
63
+
64
+ // Fired from reportAccepted/reportRejected below, which both write-side kinds call
65
+ // unconditionally — so this sees fire-and-forget dispatches too, not just the ones a
66
+ // producer awaited on the side-channels.
67
+ //
68
+ // A throwing hook is logged and swallowed: the outcome it observes has already happened,
69
+ // and letting an observer fail a completed command would make an extension an outage
70
+ // (the same failure policy the cold-start seam settled on).
71
+ let fireCommandOutcome = (~component: string, ~reference: string, outcome: observedOutcome) =>
72
+ commandOutcomeHook.contents->Option.forEach(hook =>
73
+ switch hook({component, reference, outcome}) {
74
+ | () => ()
75
+ | exception err =>
76
+ EffectLogger.logError(
77
+ ~comp=`CommandTopic(${component})`,
78
+ `command-outcome hook threw (ignored): ${err
79
+ ->JsExn.fromException
80
+ ->Option.flatMap(JsExn.message)
81
+ ->Option.getOr("unknown")}`,
82
+ )->Effect.runSync
83
+ }
84
+ )
85
+
21
86
  // Side-channel for publishJsonsAndWait result propagation.
22
87
  // Set by runInlineAndCollect before invoking the handler; cleared after.
23
88
  // Callback implementations call reportAccepted during inline dispatch.
@@ -28,11 +93,32 @@ let acceptedResultChannel: ref<option<(string, acceptedResult) => unit>> = ref(N
28
93
  // `Rejected` outcomes that carry the real error code and detail.
29
94
  let rejectedResultChannel: ref<option<(string, rejectedResult) => unit>> = ref(None)
30
95
 
31
- let reportAccepted = (reference: string, result: acceptedResult) =>
96
+ // `~component` is the owning component's `Spec.name`: an outcome isn't interpretable without
97
+ // knowing which component produced it, and the side-channel (keyed per inline dispatch) never
98
+ // had to say. `~cause` is required rather than defaulted so a new rejection site has to state
99
+ // which kind it is instead of silently reporting infrastructure as a domain decision.
100
+ let reportAccepted = (~component: string, reference: string, result: acceptedResult) => {
32
101
  acceptedResultChannel.contents->Option.forEach(cb => cb(reference, result))
102
+ fireCommandOutcome(
103
+ ~component,
104
+ ~reference,
105
+ OutcomeAccepted({entityId: result.entityId, eventCount: result.eventCount}),
106
+ )
107
+ }
33
108
 
34
- let reportRejected = (reference: string, result: rejectedResult) =>
109
+ let reportRejected = (
110
+ ~component: string,
111
+ ~cause: refusalCause,
112
+ reference: string,
113
+ result: rejectedResult,
114
+ ) => {
35
115
  rejectedResultChannel.contents->Option.forEach(cb => cb(reference, result))
116
+ fireCommandOutcome(
117
+ ~component,
118
+ ~reference,
119
+ OutcomeRejected({errorCode: result.errorCode, errorDetail: result.errorDetail, cause}),
120
+ )
121
+ }
36
122
 
37
123
  // Placed here (rather than CommandTopic.res) so runInlineAndCollect can use commandOutcome
38
124
  // without importing Adapter.res → @pulumi/pulumi. Re-exported by CommandTopic via include.
@@ -1,11 +1,42 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Effect from "effect/Effect";
6
7
  import * as Stream from "effect/Stream";
7
8
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
8
9
  import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
10
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
+ import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
12
+
13
+ let commandOutcomeHook = {
14
+ contents: undefined
15
+ };
16
+
17
+ function registerCommandOutcome(hook) {
18
+ commandOutcomeHook.contents = hook;
19
+ }
20
+
21
+ function clearCommandOutcome() {
22
+ commandOutcomeHook.contents = undefined;
23
+ }
24
+
25
+ function fireCommandOutcome(component, reference, outcome) {
26
+ Stdlib_Option.forEach(commandOutcomeHook.contents, hook => {
27
+ try {
28
+ hook({
29
+ component: component,
30
+ reference: reference,
31
+ outcome: outcome
32
+ });
33
+ return;
34
+ } catch (raw_err) {
35
+ let err = Primitive_exceptions.internalToException(raw_err);
36
+ return Effect.runSync(EffectLogger$ReventlessCore.logError(`CommandTopic(` + component + `)`, undefined, `command-outcome hook threw (ignored): ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(err), Stdlib_JsExn.message), "unknown")));
37
+ }
38
+ });
39
+ }
9
40
 
10
41
  let acceptedResultChannel = {
11
42
  contents: undefined
@@ -15,12 +46,23 @@ let rejectedResultChannel = {
15
46
  contents: undefined
16
47
  };
17
48
 
18
- function reportAccepted(reference, result) {
49
+ function reportAccepted(component, reference, result) {
19
50
  Stdlib_Option.forEach(acceptedResultChannel.contents, cb => cb(reference, result));
51
+ fireCommandOutcome(component, reference, {
52
+ TAG: "OutcomeAccepted",
53
+ entityId: result.entityId,
54
+ eventCount: result.eventCount
55
+ });
20
56
  }
21
57
 
22
- function reportRejected(reference, result) {
58
+ function reportRejected(component, cause, reference, result) {
23
59
  Stdlib_Option.forEach(rejectedResultChannel.contents, cb => cb(reference, result));
60
+ fireCommandOutcome(component, reference, {
61
+ TAG: "OutcomeRejected",
62
+ errorCode: result.errorCode,
63
+ errorDetail: result.errorDetail,
64
+ cause: cause
65
+ });
24
66
  }
25
67
 
26
68
  function commandOutcomeToJson(outcome) {
@@ -179,6 +221,10 @@ function getHandlers(typeName) {
179
221
  }
180
222
 
181
223
  export {
224
+ commandOutcomeHook,
225
+ registerCommandOutcome,
226
+ clearCommandOutcome,
227
+ fireCommandOutcome,
182
228
  acceptedResultChannel,
183
229
  rejectedResultChannel,
184
230
  reportAccepted,
@@ -63,7 +63,10 @@ module Make = (
63
63
 
64
64
  let (commandTopic, eventTopic, outgoingJsonEventsHandler) =
65
65
  commandTopicResources
66
- ->Pulumi.Output.flatMap(commandTopicResources => {
66
+ // The extension point's runtime and its event topic are both built in
67
+ // here, once the command topic's resources resolve — i.e. after the
68
+ // plugin's construct has returned. Reinstate its attribution.
69
+ ->ResourceAttribution_Deploytime.flatMapAttributed(commandTopicResources => {
67
70
  module ExtensionPointCallback = ExtensionPoint_Callback.Make(
68
71
  {
69
72
  let publishToAggregates = publishToAggregates
@@ -15,6 +15,7 @@ import * as EventTopic_Builder$ReventlessCore from "../EventTopic/EventTopic_Bui
15
15
  import * as CommandTopic_Builder$ReventlessCore from "../CommandTopic/CommandTopic_Builder.res.mjs";
16
16
  import * as ExtensionPoint_Callback$ReventlessCore from "./ExtensionPoint_Callback.res.mjs";
17
17
  import * as ExtensionPoint_Operations$ReventlessCore from "./ExtensionPoint_Operations.res.mjs";
18
+ import * as ResourceAttribution_Deploytime$ReventlessCore from "../../adapter/ResourceAttribution_Deploytime.res.mjs";
18
19
 
19
20
  function Make(Spec) {
20
21
  return Mappings => (RuntimeEnvironment => (CommandTopicChannel => (EventTopicAdapter => (ExtensionPointRuntimeBuilder => (Defaults => {
@@ -42,7 +43,7 @@ function Make(Spec) {
42
43
  }, opts);
43
44
  let aggregateNames = Stdlib_Array.filterMap(Mappings.mappings, Mapping => Stdlib_Option.map(Mapping.mapOutgoingEvent, param => Mapping.delegateName));
44
45
  let commandTopicResources = Adapter$ReventlessCore.resourcesToResolvedOutput(commandTopic.channel.resources);
45
- let match = Output$Pulumi.unzip3(Output$Pulumi.flatMap(commandTopicResources, commandTopicResources => {
46
+ let match = Output$Pulumi.unzip3(ResourceAttribution_Deploytime$ReventlessCore.flatMapAttributed(commandTopicResources, commandTopicResources => {
46
47
  let ExtensionPointCallback = ExtensionPoint_Callback$ReventlessCore.Make({
47
48
  publishToAggregates: publishToAggregates,
48
49
  commandTopicResources: commandTopicResources,
@@ -322,6 +322,7 @@ module Make = (
322
322
  // No append, but the read snapshot is valid — cache it for the next command.
323
323
  cachePut((state, headPosition))
324
324
  CommandTopic_Helpers.reportAccepted(
325
+ ~component=Spec.name,
325
326
  cmdJson.meta.msgId,
326
327
  switch entityId {
327
328
  | Some(eid) => {entityId: eid, eventCount: 0}
@@ -373,6 +374,7 @@ module Make = (
373
374
  // next command's delta read picks our events up from `headPosition`.
374
375
  cachePut((state, headPosition))
375
376
  CommandTopic_Helpers.reportAccepted(
377
+ ~component=Spec.name,
376
378
  cmdJson.meta.msgId,
377
379
  switch entityId {
378
380
  | Some(eid) => {entityId: eid, eventCount: rawEvents->Array.length}
@@ -412,7 +414,12 @@ module Make = (
412
414
  if errorCode == "Conflict" {
413
415
  Metrics.emitCount(~metric="AppendConflict", ~slice=Spec.name)
414
416
  }
417
+ // Both arms here are storage, not the model: a surfaced Conflict is
418
+ // contention the retry loop couldn't absorb, and AppendFailed is the
419
+ // store failing. `decide` had already said Ok.
415
420
  CommandTopic_Helpers.reportRejected(
421
+ ~component=Spec.name,
422
+ ~cause=InfrastructureFailure,
416
423
  cmdJson.meta.msgId,
417
424
  {errorCode, errorDetail: errDetail},
418
425
  )
@@ -433,7 +440,12 @@ module Make = (
433
440
  payloadDict->Dict.toArray->Array.length == 0
434
441
  ? ""
435
442
  : payloadDict->JSON.Encode.object->JSON.stringify
436
- CommandTopic_Helpers.reportRejected(cmdJson.meta.msgId, {errorCode, errorDetail})
443
+ CommandTopic_Helpers.reportRejected(
444
+ ~component=Spec.name,
445
+ ~cause=DomainRejection,
446
+ cmdJson.meta.msgId,
447
+ {errorCode, errorDetail},
448
+ )
437
449
  EffectLogger.logError(
438
450
  ~comp,
439
451
  `decide rejected: ${errorCode} ${errorDetail}`,
@@ -173,7 +173,7 @@ function Make(Spec) {
173
173
  state,
174
174
  headPosition
175
175
  ]);
176
- CommandTopic_Helpers$ReventlessCore.reportAccepted(cmdJson.meta.msgId, entityId !== undefined ? ({
176
+ CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, cmdJson.meta.msgId, entityId !== undefined ? ({
177
177
  entityId: entityId,
178
178
  eventCount: 0
179
179
  }) : ({
@@ -223,7 +223,7 @@ function Make(Spec) {
223
223
  state,
224
224
  headPosition
225
225
  ]);
226
- CommandTopic_Helpers$ReventlessCore.reportAccepted(cmdJson.meta.msgId, entityId !== undefined ? ({
226
+ CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, cmdJson.meta.msgId, entityId !== undefined ? ({
227
227
  entityId: entityId,
228
228
  eventCount: rawEvents.length
229
229
  }) : ({
@@ -257,7 +257,7 @@ function Make(Spec) {
257
257
  if (errorCode === "Conflict") {
258
258
  Metrics$ReventlessCore.emitCount("AppendConflict", Spec.name, undefined);
259
259
  }
260
- CommandTopic_Helpers$ReventlessCore.reportRejected(cmdJson.meta.msgId, {
260
+ CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", cmdJson.meta.msgId, {
261
261
  errorCode: errorCode,
262
262
  errorDetail: errDetail
263
263
  });
@@ -277,7 +277,7 @@ function Make(Spec) {
277
277
  let match = Message$ReventlessCore.splitMessage(errorJson);
278
278
  let payloadDict = match[1];
279
279
  let errorDetail = Object.entries(payloadDict).length === 0 ? "" : JSON.stringify(payloadDict);
280
- CommandTopic_Helpers$ReventlessCore.reportRejected(cmdJson.meta.msgId, {
280
+ CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", cmdJson.meta.msgId, {
281
281
  errorCode: errorCode,
282
282
  errorDetail: errorDetail
283
283
  });
@@ -529,6 +529,7 @@ module Make = (
529
529
  {
530
530
  commandTypes: extractTypes(Scs.Spec.commandSchema),
531
531
  eventTypes: extractEventTypes(Scs.Spec.eventSchema->S.castToUnknown),
532
+ errorTypes: extractTypes(Scs.Spec.errorSchema),
532
533
  consumedEventTypes: extractEventTypes(Scs.Spec.consumedEventSchema->S.castToUnknown),
533
534
  chapter: ?chapterFor(Scs.Spec.name),
534
535
  },
@@ -647,7 +648,11 @@ module Make = (
647
648
  queryEngine,
648
649
  )
649
650
  ->Pulumi.Output.all6
650
- ->Pulumi.Output.apply(((
651
+ // `applyAttributed`, not `apply`: this callback provisions real resources —
652
+ // extension points, tasks, the heartbeat, resolvers — and it runs after
653
+ // this construct has returned, when the ambient attribution context is
654
+ // already empty. It reinstates the plugin's context for the duration.
655
+ ->ResourceAttribution_Deploytime.applyAttributed(((
651
656
  (interstackAdminExtensionPoints, localAdminResolvedEP, _),
652
657
  aggregateResources,
653
658
  publishToAggregates,
@@ -36,6 +36,7 @@ import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.r
36
36
  import * as Plugin_EventQuerySchema$ReventlessCore from "./Plugin_EventQuerySchema.res.mjs";
37
37
  import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
38
38
  import * as Plugin_SubscriptionSchema$ReventlessCore from "./Plugin_SubscriptionSchema.res.mjs";
39
+ import * as ResourceAttribution_Deploytime$ReventlessCore from "../../adapter/ResourceAttribution_Deploytime.res.mjs";
39
40
 
40
41
  function Make(Spec) {
41
42
  return ApiSpec => (FragmentProvider => (RuntimeEnvironment => (EventCollectorChannel => (QueryEngineAdapter => (PluginExtensionPointRemoteChannel => (HeartbeatRunner => (PluginRuntimeBuilder => (DcbEventLogStorage => (DcbEventTopicPublisher => (DcbCommandTopicChannel => (DcbCommandTopicChannelAsync => {
@@ -373,6 +374,7 @@ function Make(Spec) {
373
374
  stateSchemas[Scs.Spec.name] = {
374
375
  commandTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.commandSchema),
375
376
  eventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.eventSchema),
377
+ errorTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.errorSchema),
376
378
  consumedEventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.consumedEventSchema),
377
379
  chapter: chapterByName[Scs.Spec.name]
378
380
  };
@@ -427,7 +429,7 @@ function Make(Spec) {
427
429
  let schemaPushed = pushSchema !== undefined ? pushSchema(extra$1, version, apiSchemaFragment) : Pulumi.output();
428
430
  let apiSchemaFragmentPayload = Plugin_Helpers$ReventlessCore.offloadPayload(apiSchemaFragment, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments");
429
431
  let structurePayload = Stdlib_Option.map(pluginStructure, s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures"));
430
- let builderOutputs = Pulumi.all([
432
+ let builderOutputs = ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(Pulumi.all([
431
433
  Pulumi.all([
432
434
  interstackAdminExtensionPoints,
433
435
  localAdminResolvedEP,
@@ -438,7 +440,7 @@ function Make(Spec) {
438
440
  Pulumi.all(Plugin_Helpers$ReventlessCore.publishToReadModels),
439
441
  scheduler,
440
442
  queryEngine
441
- ]).apply(param => {
443
+ ]), param => {
442
444
  let queryEngine = param[5];
443
445
  let scheduler = param[4];
444
446
  let publishToReadModels = param[3];
@@ -383,7 +383,10 @@ let createConnectPluginExtension = (
383
383
  ~opts,
384
384
  ) =>
385
385
  pluginDefinition
386
- ->Pulumi.Output.apply(pluginDefinition => {
386
+ // Provisions the plugin's connect-extension and its event-collector runtime,
387
+ // so it needs the plugin's attribution reinstated — the apply runs after
388
+ // construct has emptied it.
389
+ ->ResourceAttribution_Deploytime.applyAttributed(pluginDefinition => {
387
390
  module ConnectPluginExtension = PluginConnectExtension_Builder.Make({
388
391
  let pluginDefinition = pluginDefinition
389
392
  let uiFragments = uiFragments
@@ -548,7 +551,9 @@ module MakeEventCollectorHelper = (
548
551
  resources,
549
552
  )
550
553
  ->Pulumi.Output.all5
551
- ->Pulumi.Output.apply(((
554
+ // Builds the plugin's own event-collector runtime, so it needs the plugin's
555
+ // attribution reinstated — this callback runs after construct emptied it.
556
+ ->ResourceAttribution_Deploytime.applyAttributed(((
552
557
  pluginDefinition,
553
558
  connectExtData,
554
559
  extensionsHandlers,
@@ -58,6 +58,7 @@ import * as OutboundTranslationSlice$ReventlessInterop from "@reventlessdev/reve
58
58
  import * as PluginExtensionPoint_Plugin$ReventlessCore from "../connect/PluginExtensionPoint_Plugin.res.mjs";
59
59
  import * as PluginConnectExtension_Builder$ReventlessCore from "../connect/PluginConnectExtension_Builder.res.mjs";
60
60
  import * as PluginConnectExtension_Mapping$ReventlessCore from "../connect/PluginConnectExtension_Mapping.res.mjs";
61
+ import * as ResourceAttribution_Deploytime$ReventlessCore from "../../adapter/ResourceAttribution_Deploytime.res.mjs";
61
62
 
62
63
  let log = Logger$ReventlessCore.fromEnv();
63
64
 
@@ -224,7 +225,7 @@ function extractExtensionDefinitions(extensionsOutputs) {
224
225
  }
225
226
 
226
227
  function createConnectPluginExtension(pluginDefinition, uiFragments, publishToPluginExtensionPoint, publishToAggregates, readModelNamesForSourceName, publishToReadModels, queryEngine, opts) {
227
- return Output$Pulumi.unzip(pluginDefinition.apply(pluginDefinition => {
228
+ return Output$Pulumi.unzip(ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(pluginDefinition, pluginDefinition => {
228
229
  let ConnectPluginExtension = PluginConnectExtension_Builder$ReventlessCore.Make({
229
230
  pluginDefinition: pluginDefinition,
230
231
  uiFragments: uiFragments
@@ -292,13 +293,13 @@ function MakeEventCollectorHelper(RuntimeEnvironment) {
292
293
  param[0],
293
294
  param[1]
294
295
  ]) : Pulumi.output(undefined);
295
- return Pulumi.all([
296
+ return ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(Pulumi.all([
296
297
  pluginDefinition,
297
298
  connectExtData,
298
299
  Pulumi.all(extensionsHandlers),
299
300
  Pulumi.all(extensionPointsHandlers),
300
301
  resources
301
- ]).apply(param => {
302
+ ]), param => {
302
303
  let extensionsHandlers = param[2];
303
304
  let connectExtData = param[1];
304
305
  let pluginDefinition = param[0];
@@ -157,6 +157,70 @@ let labelFieldsFromStateSchema = (
157
157
  }
158
158
  }
159
159
 
160
+ // ── Per-variant event / error field extraction (Phase 6.3) ───────────────────
161
+ // Mirrors toCommandDef/extractCommandDefs for emitted events: name (TAG const),
162
+ // payload JSON Schema, and cross-entity references. Module-level rather than
163
+ // inside `make` because the synthetic Platform_Admin structure — hand-written,
164
+ // because its components never pass through `make` — has to derive its defs the
165
+ // same way, and a second copy of this walk would be free to drift from this one.
166
+
167
+ let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
168
+ let mkDef = (~variantName, ~properties) => {
169
+ let references =
170
+ properties
171
+ ->Dict.toArray
172
+ ->Array.filterMap(((fieldName, fieldSchema)) =>
173
+ Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
174
+ {
175
+ Reventless.Plugin.fieldName,
176
+ entity: target.entity,
177
+ plugin: target.plugin,
178
+ }: Reventless.Plugin.fieldReference
179
+ ))
180
+ )
181
+ ({
182
+ Reventless.Plugin.name: variantName,
183
+ // Derived for the same reason as `commandDef.schema`: an event's field
184
+ // markers are the write side's half of the same vocabulary.
185
+ schema: v->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
186
+ references,
187
+ }: Reventless.Plugin.eventDef)
188
+ }
189
+ switch v {
190
+ | Object({properties}) =>
191
+ properties
192
+ ->Dict.get("TAG")
193
+ ->Option.flatMap(tagSchema =>
194
+ switch tagSchema {
195
+ | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
196
+ | _ => None
197
+ }
198
+ )
199
+ // Payload-less event variants (`| Archived`) compile to a bare string literal.
200
+ // DCB-projection lookups can't WHERE-clause on them, so they stay out of
201
+ // producedEventTypes/consumedEventTypes; but the full `events` list carries them
202
+ // so the event graph can still draw the emitted (orphan) event node.
203
+ | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
204
+ | _ => None
205
+ }
206
+ }
207
+
208
+ let extractEventDefs = (eventSchema: S.t<unknown>): array<Reventless.Plugin.eventDef> =>
209
+ switch eventSchema {
210
+ | Union({anyOf}) => anyOf->Array.filterMap(toEventDef)
211
+ | _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
212
+ }
213
+
214
+ // Declared errors walk identically to emitted events — an error variant is a
215
+ // variant, and the payload-less form (`| CategoryNotFound`) is the common case
216
+ // the bare-string branch above already handles. Reusing the walk rather than
217
+ // copying it keeps the two from drifting; only the def type differs, so that a
218
+ // consumer (and the SDL) can tell a refusal from a fact.
219
+ let extractErrorDefs = (errorSchema: S.t<unknown>): array<Reventless.Plugin.errorDef> =>
220
+ extractEventDefs(errorSchema)->Array.map(({name, schema, references}) => (
221
+ {name, schema, references}: Reventless.Plugin.errorDef
222
+ ))
223
+
160
224
  let make = (
161
225
  type api role,
162
226
  ~name: string,
@@ -330,57 +394,6 @@ let make = (
330
394
  ->Option.mapOr([], def => [def])
331
395
  }
332
396
 
333
- // ── Per-variant event field extraction (Phase 6.3) ─────────────────────────
334
- // Mirrors toCommandDef/extractCommandDefs for emitted events: name (TAG const),
335
- // payload JSON Schema, and cross-entity references.
336
-
337
- let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
338
- let mkDef = (~variantName, ~properties) => {
339
- let references =
340
- properties
341
- ->Dict.toArray
342
- ->Array.filterMap(((fieldName, fieldSchema)) =>
343
- Reventless.Reference.getTarget(fieldSchema)->Option.map(target => (
344
- {
345
- Reventless.Plugin.fieldName,
346
- entity: target.entity,
347
- plugin: target.plugin,
348
- }: Reventless.Plugin.fieldReference
349
- ))
350
- )
351
- ({
352
- Reventless.Plugin.name: variantName,
353
- // Derived for the same reason as `commandDef.schema` above: an event's
354
- // field markers are the write side's half of the same vocabulary.
355
- schema: v->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
356
- references,
357
- }: Reventless.Plugin.eventDef)
358
- }
359
- switch v {
360
- | Object({properties}) =>
361
- properties
362
- ->Dict.get("TAG")
363
- ->Option.flatMap(tagSchema =>
364
- switch tagSchema {
365
- | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
366
- | _ => None
367
- }
368
- )
369
- // Payload-less event variants (`| Archived`) compile to a bare string literal.
370
- // DCB-projection lookups can't WHERE-clause on them, so they stay out of
371
- // producedEventTypes/consumedEventTypes; but the full `events` list carries them
372
- // so the event graph can still draw the emitted (orphan) event node.
373
- | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
374
- | _ => None
375
- }
376
- }
377
-
378
- let extractEventDefs = (eventSchema: S.t<unknown>): array<Reventless.Plugin.eventDef> =>
379
- switch eventSchema {
380
- | Union({anyOf}) => anyOf->Array.filterMap(toEventDef)
381
- | _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
382
- }
383
-
384
397
  // ── Per-component event type extraction ────────────────────────────────────
385
398
 
386
399
  let scsProduced =
@@ -704,6 +717,7 @@ let make = (
704
717
  linkedViews: linkedSvsFor(produced),
705
718
  consistencyRead: consistencyReadFor(consumed),
706
719
  events: extractEventDefs(SCS.Spec.eventSchema->S.castToUnknown),
720
+ errors: extractErrorDefs(SCS.Spec.errorSchema->S.castToUnknown),
707
721
  chapter: chapterOf(SCS.Spec.name),
708
722
  }: Reventless.Plugin.writableDef)
709
723
  })
@@ -726,6 +740,7 @@ let make = (
726
740
  linkedViews: Array.concat(linkedSvsFor(produced), linkedReadModelsFor(A.Spec.name)),
727
741
  consistencyRead: None,
728
742
  events: extractEventDefs(A.Spec.eventSchema->S.castToUnknown),
743
+ errors: extractErrorDefs(A.Spec.errorSchema->S.castToUnknown),
729
744
  chapter: chapterOf(A.Spec.name),
730
745
  }: Reventless.Plugin.writableDef)
731
746
  })