@cat-factory/kernel 0.272.0 → 0.274.0

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 (43) hide show
  1. package/dist/domain/binary-store-registry.d.ts +98 -0
  2. package/dist/domain/binary-store-registry.d.ts.map +1 -0
  3. package/dist/domain/binary-store-registry.js +79 -0
  4. package/dist/domain/binary-store-registry.js.map +1 -0
  5. package/dist/domain/gate-registry.d.ts +53 -21
  6. package/dist/domain/gate-registry.d.ts.map +1 -1
  7. package/dist/domain/gate-registry.js +24 -5
  8. package/dist/domain/gate-registry.js.map +1 -1
  9. package/dist/domain/notification-audience.d.ts +27 -0
  10. package/dist/domain/notification-audience.d.ts.map +1 -0
  11. package/dist/domain/notification-audience.js +40 -0
  12. package/dist/domain/notification-audience.js.map +1 -0
  13. package/dist/domain/types.d.ts +1 -1
  14. package/dist/domain/types.d.ts.map +1 -1
  15. package/dist/domain/vcs-errors.d.ts +25 -0
  16. package/dist/domain/vcs-errors.d.ts.map +1 -1
  17. package/dist/domain/vcs-errors.js +41 -0
  18. package/dist/domain/vcs-errors.js.map +1 -1
  19. package/dist/domain/workspace-cascade.d.ts +1 -1
  20. package/dist/domain/workspace-cascade.d.ts.map +1 -1
  21. package/dist/domain/workspace-cascade.js +1 -0
  22. package/dist/domain/workspace-cascade.js.map +1 -1
  23. package/dist/index.d.ts +4 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +7 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/ports/binary-artifacts.d.ts +33 -2
  28. package/dist/ports/binary-artifacts.d.ts.map +1 -1
  29. package/dist/ports/binary-artifacts.js +27 -0
  30. package/dist/ports/binary-artifacts.js.map +1 -1
  31. package/dist/ports/index.d.ts +4 -3
  32. package/dist/ports/index.d.ts.map +1 -1
  33. package/dist/ports/index.js +2 -2
  34. package/dist/ports/index.js.map +1 -1
  35. package/dist/ports/notification-channel.d.ts +98 -4
  36. package/dist/ports/notification-channel.d.ts.map +1 -1
  37. package/dist/ports/notification-channel.js +129 -2
  38. package/dist/ports/notification-channel.js.map +1 -1
  39. package/dist/ports/notification-settings-repositories.d.ts +16 -0
  40. package/dist/ports/notification-settings-repositories.d.ts.map +1 -0
  41. package/dist/ports/notification-settings-repositories.js +2 -0
  42. package/dist/ports/notification-settings-repositories.js.map +1 -0
  43. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import type { Notification, NotificationPayload, NotificationType } from '../domain/types.js';
1
+ import type { Notification, NotificationDeliveryChannel, NotificationPayload, NotificationType } from '../domain/types.js';
2
2
  /**
3
3
  * The input to raise (or re-raise) a notification. The canonical shape the engine and
4
4
  * the gate/resolver extension seams use to surface a human-actionable notification; the
@@ -14,18 +14,112 @@ export interface RaiseNotificationInput {
14
14
  body: string;
15
15
  payload?: NotificationPayload | null;
16
16
  }
17
+ /**
18
+ * WHICH lifecycle edge a delivery reports. The NotificationService re-delivers a card on
19
+ * every transition it makes, and the transports split hard on what that means:
20
+ *
21
+ * - A STATE transport (the in-app push, the outbound webhook) carries the card's current
22
+ * value, so it wants every edge: a board holding an open card has to see it settle, or
23
+ * it renders an already-dismissed decision as still actionable until the next reload.
24
+ * - An ALERT transport (email, Slack) interrupts a human, so it wants the FIRST edge only.
25
+ * Mailing a board "Decision needed: …" after the decision was made is not a stale render,
26
+ * it is a wrong statement that cannot be taken back.
27
+ *
28
+ * Without this, a channel can only guess from `notification.status`, and the two edges that
29
+ * matter most are indistinguishable there: an escalation and a fresh raise are both `open`.
30
+ * It is a required parameter so a new call site fails to typecheck rather than silently
31
+ * picking whichever reading the channel it happens to reach assumes.
32
+ *
33
+ * The members, in lifecycle order:
34
+ * - `raised`: a human is being asked something they have NOT been asked. A new card, or one
35
+ * whose user-visible content changed.
36
+ * - `refreshed`: the same open card moved without a new ask. The escalation sweep flipped it
37
+ * red; a failed action put it back.
38
+ * - `settled`: the card is no longer actionable. Acted on, resolved, or auto-dismissed.
39
+ *
40
+ * Declared as a LIST rather than a bare union because one consumer has to check it at runtime:
41
+ * the mothership relay reads this off the wire from a node that may be a different build, and a
42
+ * predicate derived from the vocabulary's own members cannot fall out of step with it.
43
+ */
44
+ export declare const NOTIFICATION_DELIVERY_REASONS: readonly ['raised', 'refreshed', 'settled'];
45
+ export type NotificationDeliveryReason = (typeof NOTIFICATION_DELIVERY_REASONS)[number];
46
+ /** Whether an arbitrary value is one of {@link NOTIFICATION_DELIVERY_REASONS}. */
47
+ export declare function isNotificationDeliveryReason(value: unknown): value is NotificationDeliveryReason;
48
+ /**
49
+ * Whether this edge is a NEW ask, and so the one an alert transport delivers on.
50
+ *
51
+ * The single place that judgement lives: three transports would otherwise each restate it,
52
+ * and a fourth would arrive stating it slightly differently. Exhaustive over the union, so
53
+ * adding an edge fails the build here rather than defaulting to "interrupt everyone".
54
+ */
55
+ export declare function isAlertingDelivery(reason: NotificationDeliveryReason): boolean;
17
56
  export interface NotificationChannel {
18
- /** Deliver (or re-deliver, on resolve) a notification to this channel's medium. */
19
- deliver(workspaceId: string, notification: Notification): Promise<void>;
57
+ /** Deliver (or re-deliver) a notification to this channel's medium, on `reason`'s edge. */
58
+ deliver(workspaceId: string, notification: Notification, reason: NotificationDeliveryReason): Promise<void>;
20
59
  }
21
60
  /** Fan a notification out to every configured channel, isolating per-channel failures. */
22
61
  export declare class CompositeNotificationChannel implements NotificationChannel {
23
62
  private readonly channels;
24
63
  constructor(channels: NotificationChannel[]);
25
- deliver(workspaceId: string, notification: Notification): Promise<void>;
64
+ deliver(workspaceId: string, notification: Notification, reason: NotificationDeliveryReason): Promise<void>;
26
65
  }
27
66
  /** The no-op channel: delivers nothing (tests, or a deployment with no channels wired). */
28
67
  export declare class NoopNotificationChannel implements NotificationChannel {
29
68
  deliver(): Promise<void>;
30
69
  }
70
+ /**
71
+ * Whether a workspace routes a notification type to one channel — the notification
72
+ * manager's decision, as the delivery path sees it. The implementation reads the
73
+ * workspace's stored overrides and falls back to the shipped default
74
+ * (`isNotificationRouted` in `@cat-factory/contracts`, the single source of truth the
75
+ * settings UI renders from).
76
+ */
77
+ export interface NotificationRouter {
78
+ isRouted(workspaceId: string, type: NotificationType, channel: NotificationDeliveryChannel): Promise<boolean>;
79
+ }
80
+ /**
81
+ * Gates one channel's deliveries on the workspace's routing for that channel.
82
+ *
83
+ * A DECORATOR rather than a filter inside {@link CompositeNotificationChannel}, because
84
+ * only some transports are routed here: Slack and the outbound webhooks answer "which
85
+ * types" where their destination is declared (a Slack route's channel, a webhook
86
+ * endpoint's own `types` filter), so wrapping them would be a second switch for a
87
+ * question already decided. A facade wraps exactly the channels the manager owns, and
88
+ * what it wrapped is legible at the wiring site.
89
+ *
90
+ * It gates the ALERTING edge ONLY (see {@link NotificationDeliveryReason}). Two things
91
+ * follow from that, and both are the point rather than a concession:
92
+ *
93
+ * - A mute stops the interruption, never a correction. The card is persisted whatever the
94
+ * routing says and reaches every board on its next snapshot, so withholding the SETTLED
95
+ * push would leave those boards rendering a decision that was already made as still
96
+ * waiting for one, curable only by a reload.
97
+ * - The routing read happens on the raise and nowhere else. The escalation sweep re-delivers
98
+ * every overdue card in a workspace in one loop; a gate that consulted the store per card
99
+ * would be a read per card per channel, and one mothership round trip each.
100
+ *
101
+ * What remains is one read per raised card per routed channel. That is deliberate rather
102
+ * than un-batched: the two routed channels land in DIFFERENT channel sets (the in-app push is
103
+ * local, email is external and mothership-delivered), so there is no single point that could
104
+ * ask once for both, and a raise is a human-scale event.
105
+ *
106
+ * A router failure means the decision is UNKNOWN, and the honest reading of that is the
107
+ * SHIPPED DEFAULT rather than silence: a settings read failing must not be the reason a
108
+ * human never hears that their run parked, and it must not turn every muted type into a
109
+ * mailshot either. So a throw falls back to the same default a workspace that never
110
+ * configured anything gets, and is reported through `onError` instead of swallowed.
111
+ */
112
+ export declare class RoutedNotificationChannel implements NotificationChannel {
113
+ private readonly channel;
114
+ private readonly router;
115
+ private readonly inner;
116
+ private readonly onError?;
117
+ constructor(channel: NotificationDeliveryChannel, router: NotificationRouter, inner: NotificationChannel, onError?: ((error: unknown, context: {
118
+ workspaceId: string;
119
+ notificationId: string;
120
+ channel: NotificationDeliveryChannel;
121
+ }) => void) | undefined);
122
+ deliver(workspaceId: string, notification: Notification, reason: NotificationDeliveryReason): Promise<void>;
123
+ private routes;
124
+ }
31
125
  //# sourceMappingURL=notification-channel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-channel.d.ts","sourceRoot":"","sources":["../../src/ports/notification-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE7F;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAA;CACrC;AAcD,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxE;AAED,0FAA0F;AAC1F,qBAAa,4BAA6B,YAAW,mBAAmB;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAArC,YAA6B,QAAQ,EAAE,mBAAmB,EAAE,EAAI;IAE1D,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5E;CACF;AAED,2FAA2F;AAC3F,qBAAa,uBAAwB,YAAW,mBAAmB;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAG;CAClC"}
1
+ {"version":3,"file":"notification-channel.d.ts","sourceRoot":"","sources":["../../src/ports/notification-channel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,2BAA2B,EAC3B,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,oBAAoB,CAAA;AAE3B;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAA;CACrC;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,6BAA6B,YAAI,QAAQ,EAAE,WAAW,EAAE,SAAS,CAAU,CAAA;AAExF,MAAM,MAAM,0BAA0B,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvF,kFAAkF;AAClF,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAU9E;AAMD,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,OAAO,CACL,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB;AAED,0FAA0F;AAC1F,qBAAa,4BAA6B,YAAW,mBAAmB;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAArC,YAA6B,QAAQ,EAAE,mBAAmB,EAAE,EAAI;IAE1D,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAUf;CACF;AAED,2FAA2F;AAC3F,qBAAa,uBAAwB,YAAW,mBAAmB;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAG;CAClC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CACN,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,OAAO,CAAC,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,qBAAa,yBAA0B,YAAW,mBAAmB;IAEjE,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAJ3B,YACmB,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,CAAC,GAAE,CACzB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QACP,WAAW,EAAE,MAAM,CAAA;QACnB,cAAc,EAAE,MAAM,CAAA;QACtB,OAAO,EAAE,2BAA2B,CAAA;KACrC,KACE,IAAI,aAAA,EACP;IAEE,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAGf;YAEa,MAAM;CAYrB"}
@@ -1,13 +1,78 @@
1
+ import { defaultNotificationRoute } from '@cat-factory/contracts';
2
+ // Port for *delivering* a notification to humans. The NotificationService owns
3
+ // the canonical persistence + lifecycle (raise / list / resolve); a channel is
4
+ // purely "how a human is told". This is the extension seam for future delivery
5
+ // mechanisms: in-app (push the `notification` WorkspaceEvent to the board) is the
6
+ // only channel today, but an EmailNotificationChannel / SlackNotificationChannel
7
+ // implement the same port and are composed in via CompositeNotificationChannel —
8
+ // no change to the call sites that raise notifications.
9
+ //
10
+ // All deliveries are best-effort: a channel failure must never break the state
11
+ // transition that raised the notification (the row is already persisted). Channels
12
+ // swallow their own errors, exactly like the event publisher.
13
+ /**
14
+ * WHICH lifecycle edge a delivery reports. The NotificationService re-delivers a card on
15
+ * every transition it makes, and the transports split hard on what that means:
16
+ *
17
+ * - A STATE transport (the in-app push, the outbound webhook) carries the card's current
18
+ * value, so it wants every edge: a board holding an open card has to see it settle, or
19
+ * it renders an already-dismissed decision as still actionable until the next reload.
20
+ * - An ALERT transport (email, Slack) interrupts a human, so it wants the FIRST edge only.
21
+ * Mailing a board "Decision needed: …" after the decision was made is not a stale render,
22
+ * it is a wrong statement that cannot be taken back.
23
+ *
24
+ * Without this, a channel can only guess from `notification.status`, and the two edges that
25
+ * matter most are indistinguishable there: an escalation and a fresh raise are both `open`.
26
+ * It is a required parameter so a new call site fails to typecheck rather than silently
27
+ * picking whichever reading the channel it happens to reach assumes.
28
+ *
29
+ * The members, in lifecycle order:
30
+ * - `raised`: a human is being asked something they have NOT been asked. A new card, or one
31
+ * whose user-visible content changed.
32
+ * - `refreshed`: the same open card moved without a new ask. The escalation sweep flipped it
33
+ * red; a failed action put it back.
34
+ * - `settled`: the card is no longer actionable. Acted on, resolved, or auto-dismissed.
35
+ *
36
+ * Declared as a LIST rather than a bare union because one consumer has to check it at runtime:
37
+ * the mothership relay reads this off the wire from a node that may be a different build, and a
38
+ * predicate derived from the vocabulary's own members cannot fall out of step with it.
39
+ */
40
+ export const NOTIFICATION_DELIVERY_REASONS = ['raised', 'refreshed', 'settled'];
41
+ /** Whether an arbitrary value is one of {@link NOTIFICATION_DELIVERY_REASONS}. */
42
+ export function isNotificationDeliveryReason(value) {
43
+ return NOTIFICATION_DELIVERY_REASONS.includes(value);
44
+ }
45
+ /**
46
+ * Whether this edge is a NEW ask, and so the one an alert transport delivers on.
47
+ *
48
+ * The single place that judgement lives: three transports would otherwise each restate it,
49
+ * and a fourth would arrive stating it slightly differently. Exhaustive over the union, so
50
+ * adding an edge fails the build here rather than defaulting to "interrupt everyone".
51
+ */
52
+ export function isAlertingDelivery(reason) {
53
+ switch (reason) {
54
+ case 'raised':
55
+ return true;
56
+ case 'refreshed':
57
+ case 'settled':
58
+ return false;
59
+ default:
60
+ return assertNeverDeliveryReason(reason);
61
+ }
62
+ }
63
+ function assertNeverDeliveryReason(reason) {
64
+ throw new Error(`unhandled notification delivery reason: ${String(reason)}`);
65
+ }
1
66
  /** Fan a notification out to every configured channel, isolating per-channel failures. */
2
67
  export class CompositeNotificationChannel {
3
68
  channels;
4
69
  constructor(channels) {
5
70
  this.channels = channels;
6
71
  }
7
- async deliver(workspaceId, notification) {
72
+ async deliver(workspaceId, notification, reason) {
8
73
  await Promise.all(this.channels.map(async (channel) => {
9
74
  try {
10
- await channel.deliver(workspaceId, notification);
75
+ await channel.deliver(workspaceId, notification, reason);
11
76
  }
12
77
  catch {
13
78
  // Best-effort: one channel failing must not block the others or the caller.
@@ -19,4 +84,66 @@ export class CompositeNotificationChannel {
19
84
  export class NoopNotificationChannel {
20
85
  async deliver() { }
21
86
  }
87
+ /**
88
+ * Gates one channel's deliveries on the workspace's routing for that channel.
89
+ *
90
+ * A DECORATOR rather than a filter inside {@link CompositeNotificationChannel}, because
91
+ * only some transports are routed here: Slack and the outbound webhooks answer "which
92
+ * types" where their destination is declared (a Slack route's channel, a webhook
93
+ * endpoint's own `types` filter), so wrapping them would be a second switch for a
94
+ * question already decided. A facade wraps exactly the channels the manager owns, and
95
+ * what it wrapped is legible at the wiring site.
96
+ *
97
+ * It gates the ALERTING edge ONLY (see {@link NotificationDeliveryReason}). Two things
98
+ * follow from that, and both are the point rather than a concession:
99
+ *
100
+ * - A mute stops the interruption, never a correction. The card is persisted whatever the
101
+ * routing says and reaches every board on its next snapshot, so withholding the SETTLED
102
+ * push would leave those boards rendering a decision that was already made as still
103
+ * waiting for one, curable only by a reload.
104
+ * - The routing read happens on the raise and nowhere else. The escalation sweep re-delivers
105
+ * every overdue card in a workspace in one loop; a gate that consulted the store per card
106
+ * would be a read per card per channel, and one mothership round trip each.
107
+ *
108
+ * What remains is one read per raised card per routed channel. That is deliberate rather
109
+ * than un-batched: the two routed channels land in DIFFERENT channel sets (the in-app push is
110
+ * local, email is external and mothership-delivered), so there is no single point that could
111
+ * ask once for both, and a raise is a human-scale event.
112
+ *
113
+ * A router failure means the decision is UNKNOWN, and the honest reading of that is the
114
+ * SHIPPED DEFAULT rather than silence: a settings read failing must not be the reason a
115
+ * human never hears that their run parked, and it must not turn every muted type into a
116
+ * mailshot either. So a throw falls back to the same default a workspace that never
117
+ * configured anything gets, and is reported through `onError` instead of swallowed.
118
+ */
119
+ export class RoutedNotificationChannel {
120
+ channel;
121
+ router;
122
+ inner;
123
+ onError;
124
+ constructor(channel, router, inner, onError) {
125
+ this.channel = channel;
126
+ this.router = router;
127
+ this.inner = inner;
128
+ this.onError = onError;
129
+ }
130
+ async deliver(workspaceId, notification, reason) {
131
+ if (isAlertingDelivery(reason) && !(await this.routes(workspaceId, notification)))
132
+ return;
133
+ await this.inner.deliver(workspaceId, notification, reason);
134
+ }
135
+ async routes(workspaceId, notification) {
136
+ try {
137
+ return await this.router.isRouted(workspaceId, notification.type, this.channel);
138
+ }
139
+ catch (error) {
140
+ this.onError?.(error, {
141
+ workspaceId,
142
+ notificationId: notification.id,
143
+ channel: this.channel,
144
+ });
145
+ return defaultNotificationRoute(notification.type, this.channel);
146
+ }
147
+ }
148
+ }
22
149
  //# sourceMappingURL=notification-channel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-channel.js","sourceRoot":"","sources":["../../src/ports/notification-channel.ts"],"names":[],"mappings":"AAmCA,0FAA0F;AAC1F,MAAM,OAAO,4BAA4B;IACV,QAAQ;IAArC,YAA6B,QAA+B;wBAA/B,QAAQ;IAA0B,CAAC;IAEhE,KAAK,CAAC,OAAO,CAAC,WAAmB,EAAE,YAA0B;QAC3D,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;YAClC,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;YAClD,CAAC;YAAC,MAAM,CAAC;gBACP,4EAA4E;YAC9E,CAAC;QACH,CAAC,CAAC,CACH,CAAA;IACH,CAAC;CACF;AAED,2FAA2F;AAC3F,MAAM,OAAO,uBAAuB;IAClC,KAAK,CAAC,OAAO,KAAmB,CAAC;CAClC"}
1
+ {"version":3,"file":"notification-channel.js","sourceRoot":"","sources":["../../src/ports/notification-channel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { NotificationRoutingMatrix } from '../domain/types.js';
2
+ export interface NotificationSettingsRecord {
3
+ workspaceId: string;
4
+ /** The {@link NotificationRoutingMatrix}, serialized as JSON. */
5
+ matrixJson: string;
6
+ updatedAt: number;
7
+ }
8
+ export interface NotificationSettingsRepository {
9
+ /** A workspace's routing overrides, or null when it has never configured any. */
10
+ getByWorkspace(workspaceId: string): Promise<NotificationSettingsRecord | null>;
11
+ /** Create or replace a workspace's routing overrides. */
12
+ upsert(record: NotificationSettingsRecord): Promise<void>;
13
+ }
14
+ /** Re-exported for repository implementations that map the persisted matrix. */
15
+ export type { NotificationRoutingMatrix };
16
+ //# sourceMappingURL=notification-settings-repositories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-settings-repositories.d.ts","sourceRoot":"","sources":["../../src/ports/notification-settings-repositories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AAYnE,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,iFAAiF;IACjF,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAAA;IAC/E,yDAAyD;IACzD,MAAM,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1D;AAED,gFAAgF;AAChF,YAAY,EAAE,yBAAyB,EAAE,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notification-settings-repositories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-settings-repositories.js","sourceRoot":"","sources":["../../src/ports/notification-settings-repositories.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/kernel",
3
- "version": "0.272.0",
3
+ "version": "0.274.0",
4
4
  "description": "Shared vocabulary, pure logic, and port interfaces for the Agent Architecture Board.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "ai": "^7.0.51",
28
28
  "yaml": "^2.9.0",
29
- "@cat-factory/contracts": "0.274.0"
29
+ "@cat-factory/contracts": "0.276.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@stryker-mutator/core": "9.6.1",