@opengeni/core 0.21.10 → 0.24.1

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 (116) hide show
  1. package/dist/access/index.d.ts +1 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +2 -2
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/chunk-6WKPWE5S.js +2380 -0
  6. package/dist/chunk-6WKPWE5S.js.map +1 -0
  7. package/dist/chunk-JJU6I5XD.js +2713 -0
  8. package/dist/chunk-JJU6I5XD.js.map +1 -0
  9. package/dist/chunk-NYPMQ7NO.js +2270 -0
  10. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  11. package/dist/dependencies.d.ts +26 -6
  12. package/dist/domain/capabilities.d.ts +28 -3
  13. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  14. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  15. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  16. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  17. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  18. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  19. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  20. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  21. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  22. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  23. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  24. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  25. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  26. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  27. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  28. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  29. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  30. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  31. package/dist/domain/packs.d.ts +29 -1
  32. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  33. package/dist/domain/scheduled-tasks.d.ts +2 -2
  34. package/dist/domain/session-tool-policy.d.ts +2 -2
  35. package/dist/domain/sessions.d.ts +14 -6
  36. package/dist/domain/skill-imports.d.ts +32 -0
  37. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  38. package/dist/domain/timeline-annotations.d.ts +5 -0
  39. package/dist/domain/video-generation-capabilities.d.ts +9 -0
  40. package/dist/domain/video-generation.d.ts +47 -0
  41. package/dist/editable-artifact-live/application.d.ts +69 -0
  42. package/dist/editable-artifact-live/errors.d.ts +1 -0
  43. package/dist/editable-artifact-live/index.d.ts +8 -0
  44. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  45. package/dist/editable-artifact-live/ports.d.ts +100 -0
  46. package/dist/editable-artifact-live/server.d.ts +71 -0
  47. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  48. package/dist/editable-artifact-live/types.d.ts +213 -0
  49. package/dist/editable-artifact-live/wire.d.ts +34 -0
  50. package/dist/editable-artifact-live.d.ts +2 -0
  51. package/dist/editable-artifact-live.js +58 -0
  52. package/dist/editable-artifact-live.js.map +1 -0
  53. package/dist/editable-artifacts.d.ts +2 -0
  54. package/dist/editable-artifacts.js +179 -0
  55. package/dist/editable-artifacts.js.map +1 -0
  56. package/dist/index.d.ts +35 -26
  57. package/dist/index.js +2792 -609
  58. package/dist/index.js.map +1 -1
  59. package/dist/managed-session.d.ts +1 -1
  60. package/dist/rigs/index.d.ts +1 -1
  61. package/dist/rigs/provider-images.d.ts +31 -0
  62. package/dist/sandbox/routing.d.ts +1 -0
  63. package/dist/session-authorization.d.ts +1 -1
  64. package/dist/workflow-wake-contract.d.ts +7 -0
  65. package/package.json +18 -10
  66. package/src/application/new-session-drafts.ts +2 -0
  67. package/src/application/session-commands.ts +188 -107
  68. package/src/dependencies.ts +30 -2
  69. package/src/domain/capabilities.ts +604 -220
  70. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  71. package/src/domain/durable-learning-slack-publication.ts +202 -0
  72. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  73. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  74. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  75. package/src/domain/editable-artifacts/errors.ts +115 -0
  76. package/src/domain/editable-artifacts/hash.ts +131 -0
  77. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  78. package/src/domain/editable-artifacts/index.ts +14 -0
  79. package/src/domain/editable-artifacts/ports.ts +462 -0
  80. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  81. package/src/domain/editable-artifacts/service.ts +2402 -0
  82. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  83. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  84. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  85. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  86. package/src/domain/editable-artifacts/types.ts +835 -0
  87. package/src/domain/insights.ts +136 -33
  88. package/src/domain/memory-slack-delivery.ts +204 -0
  89. package/src/domain/memory-slack-publication.ts +5 -2
  90. package/src/domain/packs.ts +266 -5
  91. package/src/domain/personal-connection-delegations.ts +97 -6
  92. package/src/domain/scheduled-tasks.ts +155 -29
  93. package/src/domain/session-tool-policy.ts +8 -2
  94. package/src/domain/sessions.ts +274 -131
  95. package/src/domain/skill-imports.ts +350 -0
  96. package/src/domain/slack-publication-secret-safety.ts +42 -0
  97. package/src/domain/timeline-annotations.ts +204 -0
  98. package/src/domain/video-generation-capabilities.ts +69 -0
  99. package/src/domain/video-generation.ts +249 -0
  100. package/src/editable-artifact-live/application.ts +182 -0
  101. package/src/editable-artifact-live/errors.ts +4 -0
  102. package/src/editable-artifact-live/index.ts +8 -0
  103. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  104. package/src/editable-artifact-live/ports.ts +139 -0
  105. package/src/editable-artifact-live/server.ts +1707 -0
  106. package/src/editable-artifact-live/ticket.ts +217 -0
  107. package/src/editable-artifact-live/types.ts +286 -0
  108. package/src/editable-artifact-live/wire.ts +95 -0
  109. package/src/editable-artifact-live.ts +2 -0
  110. package/src/editable-artifacts.ts +2 -0
  111. package/src/index.ts +13 -0
  112. package/src/rigs/index.ts +26 -19
  113. package/src/rigs/provider-images.ts +110 -0
  114. package/src/sandbox/routing.ts +6 -3
  115. package/src/session-authorization.ts +3 -0
  116. package/src/workflow-wake-contract.ts +8 -0
@@ -0,0 +1,221 @@
1
+ import {
2
+ CompanyProfileLearningSubjectKind,
3
+ CompanyProfileLearningWrite,
4
+ type CompanyProfileMutationResponse,
5
+ } from "@opengeni/contracts";
6
+ import {
7
+ type CompanyProfileLearningWriteResult,
8
+ type Database,
9
+ rollbackCompanyProfileLearning,
10
+ writeCompanyProfileLearning,
11
+ } from "@opengeni/db";
12
+
13
+ export type CompanyProfileDurableLearningResource = {
14
+ surface: "company_profile";
15
+ id: string;
16
+ version: string | null;
17
+ status: string;
18
+ };
19
+
20
+ export type CompanyProfileDurableLearningWriteResult = {
21
+ outcome: "applied" | "proposed";
22
+ resource: CompanyProfileDurableLearningResource;
23
+ effectiveBoundary: "next_accepted_attempt";
24
+ rollback: {
25
+ supported: boolean;
26
+ targetAttemptId: null;
27
+ token: string | null;
28
+ };
29
+ };
30
+
31
+ export type CompanyProfileDurableLearningRollbackResult = {
32
+ resource: CompanyProfileDurableLearningResource;
33
+ effectiveBoundary: "next_accepted_attempt";
34
+ };
35
+
36
+ /**
37
+ * Structural match for canonical durable-learning router's DurableLearningAuthorityAdapter. The router
38
+ * package owns its complete request/decision types; accepting unknown here
39
+ * keeps this adapter assignable without copying or competing with that router.
40
+ */
41
+ export type CompanyProfileDurableLearningAuthorityAdapter = {
42
+ write: (input: unknown) => Promise<CompanyProfileDurableLearningWriteResult>;
43
+ rollback: (input: unknown) => Promise<CompanyProfileDurableLearningRollbackResult>;
44
+ };
45
+
46
+ export type CompanyProfileDurableLearningAdapterOptions = {
47
+ db: Database;
48
+ authority?: {
49
+ write: typeof writeCompanyProfileLearning;
50
+ rollback: typeof rollbackCompanyProfileLearning;
51
+ };
52
+ };
53
+
54
+ function record(value: unknown, name: string): Record<string, unknown> {
55
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
56
+ throw new Error(`${name} must be an object`);
57
+ }
58
+ return value as Record<string, unknown>;
59
+ }
60
+
61
+ function requiredString(value: unknown, name: string): string {
62
+ if (typeof value !== "string" || value.length === 0) {
63
+ throw new Error(`${name} must be a non-empty string`);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ function nullableString(value: unknown, name: string): string | null {
69
+ if (value === null) return null;
70
+ return requiredString(value, name);
71
+ }
72
+
73
+ function actorKind(value: unknown): "human" | "agent" | "service" {
74
+ if (value === "human" || value === "agent" || value === "service") return value;
75
+ throw new Error("durable-learning attempt actor kind is invalid");
76
+ }
77
+
78
+ function parseAttempt(value: unknown) {
79
+ const attempt = record(value, "durable-learning attempt");
80
+ const actor = record(attempt.actor, "durable-learning actor");
81
+ return {
82
+ id: requiredString(attempt.id, "durable-learning attempt id"),
83
+ accountId: requiredString(attempt.accountId, "durable-learning account id"),
84
+ workspaceId: requiredString(attempt.workspaceId, "durable-learning workspace id"),
85
+ actor: {
86
+ kind: actorKind(actor.kind),
87
+ subjectId: requiredString(actor.subjectId, "durable-learning actor subject"),
88
+ },
89
+ };
90
+ }
91
+
92
+ function parseWriteInput(value: unknown) {
93
+ const input = record(value, "company-profile durable-learning write");
94
+ const attempt = parseAttempt(input.attempt);
95
+ const request = record(input.request, "durable-learning write request");
96
+ const decision = record(input.decision, "durable-learning route decision");
97
+ const scope = record(decision.scope, "durable-learning resolved scope");
98
+ const subject = record(request.subject, "durable-learning subject");
99
+ if (
100
+ request.operation !== "write" ||
101
+ request.attemptId !== attempt.id ||
102
+ request.targetSurface !== "company_profile" ||
103
+ decision.disposition !== "route" ||
104
+ decision.destination !== "company_profile" ||
105
+ scope.kind !== "organization" ||
106
+ (decision.authority !== "active" && decision.authority !== "proposal")
107
+ ) {
108
+ throw new Error(
109
+ "The company-profile adapter accepts only matching routed active or proposal organization company_profile writes",
110
+ );
111
+ }
112
+ return CompanyProfileLearningWrite.parse({
113
+ operationId: attempt.id,
114
+ accountId: attempt.accountId,
115
+ workspaceId: attempt.workspaceId,
116
+ actorSubjectId: attempt.actor.subjectId,
117
+ actorKind: attempt.actor.kind,
118
+ authority: decision.authority,
119
+ subject: {
120
+ kind: CompanyProfileLearningSubjectKind.parse(subject.kind),
121
+ content: requiredString(subject.content, "durable-learning subject content"),
122
+ stableKey: nullableString(subject.stableKey, "durable-learning subject stable key"),
123
+ },
124
+ sourceId: `durable-learning-attempt:${attempt.id}`,
125
+ });
126
+ }
127
+
128
+ function parseRollbackInput(value: unknown) {
129
+ const input = record(value, "company-profile durable-learning rollback");
130
+ const attempt = parseAttempt(input.attempt);
131
+ const targetAttempt = parseAttempt(input.targetAttempt);
132
+ const targetReceipt = record(input.targetReceipt, "durable-learning target receipt");
133
+ const targetResource = record(
134
+ targetReceipt.resource,
135
+ "durable-learning target company-profile resource",
136
+ );
137
+ const receiptRollback = record(
138
+ targetReceipt.rollback,
139
+ "durable-learning target rollback receipt",
140
+ );
141
+ const rollbackToken = requiredString(input.rollbackToken, "company-profile rollback token");
142
+ if (
143
+ targetReceipt.attemptId !== targetAttempt.id ||
144
+ targetResource.surface !== "company_profile" ||
145
+ receiptRollback.supported !== true ||
146
+ receiptRollback.token !== rollbackToken
147
+ ) {
148
+ throw new Error("The company-profile adapter cannot roll back another authority surface");
149
+ }
150
+ return {
151
+ attempt,
152
+ rollbackToken,
153
+ reason: requiredString(input.reason, "company-profile rollback reason"),
154
+ };
155
+ }
156
+
157
+ function writeResource(
158
+ result: CompanyProfileLearningWriteResult,
159
+ ): CompanyProfileDurableLearningResource {
160
+ return {
161
+ surface: "company_profile",
162
+ id: result.revision.id,
163
+ version: String(result.revision.revision),
164
+ status: result.outcome === "applied" ? "active" : "proposal",
165
+ };
166
+ }
167
+
168
+ function rollbackResource(
169
+ result: CompanyProfileMutationResponse,
170
+ ): CompanyProfileDurableLearningResource {
171
+ if (!result.event) throw new Error("Company-profile rollback returned no activation event");
172
+ return {
173
+ surface: "company_profile",
174
+ id: result.head?.revisionId ?? result.event.oldRevision?.id ?? result.event.id,
175
+ version: String(result.event.activationVersion),
176
+ status: result.head ? "active" : "inactive",
177
+ };
178
+ }
179
+
180
+ /** Install this under `authorities.company_profile` in the canonical durable-learning router ports. */
181
+ export function createCompanyProfileDurableLearningAdapter(
182
+ options: CompanyProfileDurableLearningAdapterOptions,
183
+ ): CompanyProfileDurableLearningAuthorityAdapter {
184
+ const authority = options.authority ?? {
185
+ write: writeCompanyProfileLearning,
186
+ rollback: rollbackCompanyProfileLearning,
187
+ };
188
+ return {
189
+ async write(rawInput) {
190
+ const input = parseWriteInput(rawInput);
191
+ const result = await authority.write(options.db, input);
192
+ return {
193
+ outcome: result.outcome,
194
+ resource: writeResource(result),
195
+ effectiveBoundary: result.effectiveBoundary,
196
+ rollback: {
197
+ supported: result.rollbackToken !== null,
198
+ targetAttemptId: null,
199
+ token: result.rollbackToken,
200
+ },
201
+ };
202
+ },
203
+
204
+ async rollback(rawInput) {
205
+ const input = parseRollbackInput(rawInput);
206
+ const result = await authority.rollback(options.db, {
207
+ operationId: input.attempt.id,
208
+ accountId: input.attempt.accountId,
209
+ workspaceId: input.attempt.workspaceId,
210
+ actorSubjectId: input.attempt.actor.subjectId,
211
+ actorKind: input.attempt.actor.kind,
212
+ token: input.rollbackToken,
213
+ reason: input.reason,
214
+ });
215
+ return {
216
+ resource: rollbackResource(result),
217
+ effectiveBoundary: "next_accepted_attempt",
218
+ };
219
+ },
220
+ };
221
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ enqueueMemorySlackPublication,
3
+ getCurrentMemorySlackPublicationConfiguration,
4
+ type Database,
5
+ type EnqueueMemorySlackPublicationResult,
6
+ } from "@opengeni/db";
7
+ import { sanitizeSlackPublicationText } from "./slack-publication-secret-safety";
8
+
9
+ /**
10
+ * Narrow post-persistence port for the immutable durable-learning v1 attempt/receipt contract.
11
+ * The router owns validation, authority, routing, and persistence. This adapter
12
+ * consumes only the fields required to create a bounded notification projection.
13
+ */
14
+ export type DurableLearningSlackOutcome = {
15
+ attempt: {
16
+ id: string;
17
+ accountId: string;
18
+ workspaceId: string;
19
+ inputHash: string;
20
+ actor: { kind: "human" | "agent" | "service"; subjectId: string };
21
+ initiatingHumanSubjectId: string | null;
22
+ sessionId: string | null;
23
+ request:
24
+ | {
25
+ operation: "write";
26
+ subject: {
27
+ kind: string;
28
+ summary: string | null;
29
+ };
30
+ evidence: Array<{ kind: string }>;
31
+ }
32
+ | {
33
+ operation: "rollback";
34
+ targetAttemptId: string;
35
+ };
36
+ };
37
+ receipt: {
38
+ attemptId: string;
39
+ inputHash: string;
40
+ outcome:
41
+ | "applied"
42
+ | "proposed"
43
+ | "evidence_recorded"
44
+ | "noop"
45
+ | "clarification_required"
46
+ | "rejected"
47
+ | "rolled_back"
48
+ | "failed";
49
+ decision: {
50
+ code: string;
51
+ destination: string | null;
52
+ scope: { kind: string } | null;
53
+ authority: string | null;
54
+ };
55
+ resource: {
56
+ surface: string;
57
+ id: string;
58
+ version: string | null;
59
+ status: string;
60
+ } | null;
61
+ createdAt: string;
62
+ };
63
+ };
64
+
65
+ export type DurableLearningSlackPublicationResult =
66
+ | {
67
+ kind:
68
+ | "contract_mismatch"
69
+ | "not_workspace_scoped"
70
+ | "connector_evidence_untrusted"
71
+ | "summary_required"
72
+ | "policy_quiet";
73
+ enqueue: null;
74
+ }
75
+ | { kind: "enqueued"; enqueue: EnqueueMemorySlackPublicationResult };
76
+
77
+ export type DurableLearningSlackProjection = {
78
+ summary: string;
79
+ outcome: DurableLearningSlackOutcome["receipt"]["outcome"];
80
+ decisionCode: string;
81
+ destination: string | null;
82
+ authority: string | null;
83
+ resource: DurableLearningSlackOutcome["receipt"]["resource"];
84
+ occurredAt: string;
85
+ };
86
+
87
+ export async function publishDurableLearningOutcomeToSlack(
88
+ db: Database,
89
+ outcome: DurableLearningSlackOutcome,
90
+ ): Promise<DurableLearningSlackPublicationResult> {
91
+ const { attempt, receipt } = outcome;
92
+ if (attempt.id !== receipt.attemptId || attempt.inputHash !== receipt.inputHash) {
93
+ return { kind: "contract_mismatch", enqueue: null };
94
+ }
95
+ if (receipt.decision.scope?.kind !== "workspace") {
96
+ return { kind: "not_workspace_scoped", enqueue: null };
97
+ }
98
+ if (
99
+ attempt.request.operation === "write" &&
100
+ attempt.request.evidence.some((evidence) => evidence.kind === "connector")
101
+ ) {
102
+ // The v1 router contract intentionally does not identify connector providers.
103
+ // Fail closed rather than risk republishing a Slack-derived learning outcome.
104
+ return { kind: "connector_evidence_untrusted", enqueue: null };
105
+ }
106
+
107
+ const projection = durableLearningSlackProjection(outcome);
108
+ if (!projection) return { kind: "summary_required", enqueue: null };
109
+ const importance = durableLearningImportance(outcome);
110
+ const configuration = await getCurrentMemorySlackPublicationConfiguration(
111
+ db,
112
+ attempt.workspaceId,
113
+ );
114
+ const deliveryMode = configuration?.autoImportances.includes(importance)
115
+ ? "auto"
116
+ : configuration?.reviewImportances.includes(importance)
117
+ ? "review"
118
+ : null;
119
+ if (!configuration?.enabled || !deliveryMode) {
120
+ return { kind: "policy_quiet", enqueue: null };
121
+ }
122
+
123
+ const enqueue = await enqueueMemorySlackPublication(db, {
124
+ accountId: attempt.accountId,
125
+ workspaceId: attempt.workspaceId,
126
+ sourceType: "durable_learning",
127
+ sourceId: receipt.attemptId,
128
+ sourceVersion: receipt.inputHash,
129
+ sourceIdempotencyKey: `durable-learning:${receipt.attemptId}`,
130
+ projection,
131
+ importance,
132
+ deliveryMode,
133
+ actor: {
134
+ kind: attempt.actor.kind,
135
+ subjectId: attempt.actor.subjectId,
136
+ initiatingHumanSubjectId: attempt.initiatingHumanSubjectId,
137
+ sessionId: attempt.sessionId,
138
+ attemptId: attempt.id,
139
+ },
140
+ });
141
+ return { kind: "enqueued", enqueue };
142
+ }
143
+
144
+ export function durableLearningImportance(
145
+ outcome: DurableLearningSlackOutcome,
146
+ ): "major" | "normal" | "minor" {
147
+ if (outcome.receipt.outcome === "failed" || outcome.receipt.outcome === "rolled_back") {
148
+ return "major";
149
+ }
150
+ if (outcome.receipt.outcome === "noop" || outcome.receipt.outcome === "clarification_required") {
151
+ return "minor";
152
+ }
153
+ if (
154
+ outcome.attempt.request.operation === "write" &&
155
+ ["decision", "workspace_charter", "mandatory_operating_context", "workspace_goal"].includes(
156
+ outcome.attempt.request.subject.kind,
157
+ )
158
+ ) {
159
+ return "major";
160
+ }
161
+ return "normal";
162
+ }
163
+
164
+ export function durableLearningSlackProjection(
165
+ outcome: DurableLearningSlackOutcome,
166
+ ): DurableLearningSlackProjection | null {
167
+ const summary = durableLearningSummary(outcome);
168
+ if (!summary) return null;
169
+ return {
170
+ summary,
171
+ outcome: outcome.receipt.outcome,
172
+ decisionCode: outcome.receipt.decision.code,
173
+ destination: boundedSlackPublicationText(outcome.receipt.decision.destination, 128),
174
+ authority: outcome.receipt.decision.authority,
175
+ resource: outcome.receipt.resource
176
+ ? {
177
+ surface: outcome.receipt.resource.surface,
178
+ id: outcome.receipt.resource.id,
179
+ version: outcome.receipt.resource.version,
180
+ status: outcome.receipt.resource.status,
181
+ }
182
+ : null,
183
+ occurredAt: outcome.receipt.createdAt,
184
+ };
185
+ }
186
+
187
+ function durableLearningSummary(outcome: DurableLearningSlackOutcome): string | null {
188
+ if (outcome.attempt.request.operation === "rollback") {
189
+ const destination =
190
+ boundedSlackPublicationText(outcome.receipt.decision.destination, 128) ??
191
+ "governed knowledge";
192
+ return `Rolled back a durable learning change for ${destination}.`;
193
+ }
194
+ const summary = outcome.attempt.request.subject.summary?.replace(/\s+/g, " ").trim();
195
+ return summary ? sanitizeSlackPublicationText(summary).slice(0, 512) : null;
196
+ }
197
+
198
+ function boundedSlackPublicationText(value: string | null, maxChars: number): string | null {
199
+ if (!value) return null;
200
+ const sanitized = sanitizeSlackPublicationText(value.replace(/\s+/g, " ").trim());
201
+ return sanitized ? sanitized.slice(0, maxChars) : null;
202
+ }