@opengeni/db 0.28.9 → 0.36.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 (225) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,968 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ CompanyProfileContent,
4
+ CompanyProfileLearningWrite,
5
+ CompanyProfileSnapshot,
6
+ ResolvedCompanyProfileSnapshot,
7
+ type CompanyProfileActivationEvent,
8
+ type CompanyProfileActivationType,
9
+ type CompanyProfileContent as CompanyProfileContentType,
10
+ type CompanyProfileDiffResponse,
11
+ type CompanyProfileHead,
12
+ type CompanyProfileLearningSubjectKind,
13
+ type CompanyProfileListResponse,
14
+ type CompanyProfileMutationResponse,
15
+ type CompanyProfilePrincipalKind,
16
+ type CompanyProfileProvenanceSource,
17
+ type CompanyProfileRevision,
18
+ type CompanyProfileRevisionIntent,
19
+ type CompanyProfileSnapshotEntry,
20
+ } from "@opengeni/contracts";
21
+ import { and, desc, eq, lt, sql } from "drizzle-orm";
22
+ import type { Database } from "./database";
23
+ import { rawRows, withRlsContext, withWorkspaceRls } from "./database";
24
+ import { nestedPostgresSqlState } from "./persistence-errors";
25
+ import * as schema from "./schema";
26
+
27
+ type RevisionRow = typeof schema.companyProfileRevisions.$inferSelect;
28
+ type HeadRow = typeof schema.companyProfileHeads.$inferSelect;
29
+ type EventRow = typeof schema.companyProfileActivationEvents.$inferSelect;
30
+
31
+ export type CompanyProfileAttemptClaims = {
32
+ accountId: string;
33
+ workspaceId: string;
34
+ sessionId: string;
35
+ turnId: string;
36
+ attemptId: string;
37
+ executionGeneration: number;
38
+ };
39
+
40
+ export class CompanyProfileConflictError extends Error {
41
+ readonly name = "CompanyProfileConflictError";
42
+ readonly code = "COMPANY_PROFILE_CONFLICT";
43
+
44
+ constructor(readonly currentHead: CompanyProfileHead | null) {
45
+ super("The active company profile changed in another request");
46
+ }
47
+ }
48
+
49
+ export class CompanyProfileOperationReuseError extends Error {
50
+ readonly name = "CompanyProfileOperationReuseError";
51
+ readonly code = "COMPANY_PROFILE_OPERATION_REUSED";
52
+
53
+ constructor() {
54
+ super("The company-profile operation id was already used for another request");
55
+ }
56
+ }
57
+
58
+ export class CompanyProfileNotFoundError extends Error {
59
+ readonly name = "CompanyProfileNotFoundError";
60
+
61
+ constructor(message = "Company-profile revision was not found") {
62
+ super(message);
63
+ }
64
+ }
65
+
66
+ export class CompanyProfileInvalidOperationError extends Error {
67
+ readonly name = "CompanyProfileInvalidOperationError";
68
+ }
69
+
70
+ export class CompanyProfileSnapshotAuthorityError extends Error {
71
+ readonly name = "CompanyProfileSnapshotAuthorityError";
72
+ }
73
+
74
+ function iso(value: Date | string): string {
75
+ return (value instanceof Date ? value : new Date(value)).toISOString();
76
+ }
77
+
78
+ function sha256(value: string): string {
79
+ return createHash("sha256").update(value, "utf8").digest("hex");
80
+ }
81
+
82
+ function canonicalProfile(profile: CompanyProfileContentType): CompanyProfileContentType {
83
+ const parsed = CompanyProfileContent.parse(profile);
84
+ return {
85
+ identity: parsed.identity,
86
+ mission: parsed.mission,
87
+ products: parsed.products.map(({ key, content }) => ({ key, content })),
88
+ customers: parsed.customers.map(({ key, content }) => ({ key, content })),
89
+ goals: parsed.goals.map(({ key, content }) => ({ key, content })),
90
+ constraints: parsed.constraints.map(({ key, content }) => ({ key, content })),
91
+ };
92
+ }
93
+
94
+ function canonicalProfileJson(profile: CompanyProfileContentType): string {
95
+ return JSON.stringify(canonicalProfile(profile));
96
+ }
97
+
98
+ function operationFingerprint(operation: string, fields: unknown): string {
99
+ return sha256(JSON.stringify(["company_profile_operation", 1, operation, fields]));
100
+ }
101
+
102
+ function principalKindForActor(
103
+ actorKind: "human" | "agent" | "service",
104
+ ): CompanyProfilePrincipalKind {
105
+ if (actorKind === "human") return "human_session";
106
+ if (actorKind === "agent") return "agent_attempt";
107
+ return "service";
108
+ }
109
+
110
+ function revisionFromRow(row: RevisionRow): CompanyProfileRevision {
111
+ return {
112
+ id: row.id,
113
+ operationId: row.operationId,
114
+ accountId: row.accountId,
115
+ revision: row.revision,
116
+ intent: row.intent as CompanyProfileRevisionIntent,
117
+ profile: CompanyProfileContent.parse(JSON.parse(row.contentJson)),
118
+ contentHash: row.contentHash,
119
+ provenance: {
120
+ source: row.provenanceSource as CompanyProfileProvenanceSource,
121
+ sourceId: row.provenanceSourceId,
122
+ },
123
+ supersedesRevisionId: row.supersedesRevisionId,
124
+ createdBySubjectId: row.createdBySubjectId,
125
+ createdAt: iso(row.createdAt),
126
+ };
127
+ }
128
+
129
+ function headFromRow(row: HeadRow): CompanyProfileHead {
130
+ return {
131
+ accountId: row.accountId,
132
+ revisionId: row.revisionId,
133
+ revision: row.revision,
134
+ contentHash: row.contentHash,
135
+ activationVersion: row.activationVersion,
136
+ activatedAt: iso(row.activatedAt),
137
+ };
138
+ }
139
+
140
+ function eventFromRow(row: EventRow): CompanyProfileActivationEvent {
141
+ return {
142
+ id: row.id,
143
+ operationId: row.operationId,
144
+ accountId: row.accountId,
145
+ type: row.type as CompanyProfileActivationType,
146
+ activationVersion: row.activationVersion,
147
+ oldRevision:
148
+ row.oldRevisionId === null
149
+ ? null
150
+ : {
151
+ id: row.oldRevisionId,
152
+ revision: row.oldRevision!,
153
+ contentHash: row.oldContentHash!,
154
+ },
155
+ newRevision:
156
+ row.newRevisionId === null
157
+ ? null
158
+ : {
159
+ id: row.newRevisionId,
160
+ revision: row.newRevision!,
161
+ contentHash: row.newContentHash!,
162
+ },
163
+ actorSubjectId: row.actorSubjectId,
164
+ reason: row.reason,
165
+ createdAt: iso(row.createdAt),
166
+ };
167
+ }
168
+
169
+ async function lockAccount(db: Database, accountId: string): Promise<void> {
170
+ const [account] = await db
171
+ .select({ id: schema.managedAccounts.id })
172
+ .from(schema.managedAccounts)
173
+ .where(eq(schema.managedAccounts.id, accountId))
174
+ .for("update")
175
+ .limit(1);
176
+ if (!account) throw new CompanyProfileNotFoundError("Organization account was not found");
177
+ }
178
+
179
+ async function getHeadInTransaction(db: Database, accountId: string): Promise<HeadRow | null> {
180
+ const [head] = await db
181
+ .select()
182
+ .from(schema.companyProfileHeads)
183
+ .where(eq(schema.companyProfileHeads.accountId, accountId))
184
+ .limit(1);
185
+ return head ?? null;
186
+ }
187
+
188
+ async function getRevisionInTransaction(
189
+ db: Database,
190
+ accountId: string,
191
+ revisionId: string,
192
+ ): Promise<RevisionRow | null> {
193
+ const [revision] = await db
194
+ .select()
195
+ .from(schema.companyProfileRevisions)
196
+ .where(
197
+ and(
198
+ eq(schema.companyProfileRevisions.accountId, accountId),
199
+ eq(schema.companyProfileRevisions.id, revisionId),
200
+ ),
201
+ )
202
+ .limit(1);
203
+ return revision ?? null;
204
+ }
205
+
206
+ async function getRevisionByOperation(
207
+ db: Database,
208
+ accountId: string,
209
+ operationId: string,
210
+ ): Promise<RevisionRow | null> {
211
+ const [revision] = await db
212
+ .select()
213
+ .from(schema.companyProfileRevisions)
214
+ .where(
215
+ and(
216
+ eq(schema.companyProfileRevisions.accountId, accountId),
217
+ eq(schema.companyProfileRevisions.operationId, operationId),
218
+ ),
219
+ )
220
+ .limit(1);
221
+ return revision ?? null;
222
+ }
223
+
224
+ async function getEventByOperation(
225
+ db: Database,
226
+ accountId: string,
227
+ operationId: string,
228
+ ): Promise<EventRow | null> {
229
+ const [event] = await db
230
+ .select()
231
+ .from(schema.companyProfileActivationEvents)
232
+ .where(
233
+ and(
234
+ eq(schema.companyProfileActivationEvents.accountId, accountId),
235
+ eq(schema.companyProfileActivationEvents.operationId, operationId),
236
+ ),
237
+ )
238
+ .limit(1);
239
+ return event ?? null;
240
+ }
241
+
242
+ async function createRevisionInTransaction(
243
+ db: Database,
244
+ input: {
245
+ operationId: string;
246
+ requestFingerprint: string;
247
+ accountId: string;
248
+ intent: CompanyProfileRevisionIntent;
249
+ profile: CompanyProfileContentType;
250
+ provenanceSource: CompanyProfileProvenanceSource;
251
+ provenanceSourceId: string | null;
252
+ supersedesRevisionId: string | null;
253
+ createdBySubjectId: string;
254
+ },
255
+ ): Promise<RevisionRow> {
256
+ const contentJson = canonicalProfileJson(input.profile);
257
+ const existing = await getRevisionByOperation(db, input.accountId, input.operationId);
258
+ if (existing) {
259
+ if (existing.requestFingerprint !== input.requestFingerprint) {
260
+ throw new CompanyProfileOperationReuseError();
261
+ }
262
+ return existing;
263
+ }
264
+ if (input.supersedesRevisionId !== null) {
265
+ const prior = await getRevisionInTransaction(db, input.accountId, input.supersedesRevisionId);
266
+ if (!prior) {
267
+ throw new CompanyProfileInvalidOperationError(
268
+ "A superseded company-profile revision must exist in the same organization",
269
+ );
270
+ }
271
+ }
272
+ const [created] = await db
273
+ .insert(schema.companyProfileRevisions)
274
+ .values({
275
+ operationId: input.operationId,
276
+ requestFingerprint: input.requestFingerprint,
277
+ accountId: input.accountId,
278
+ intent: input.intent,
279
+ contentJson,
280
+ contentHash: sha256(contentJson),
281
+ provenanceSource: input.provenanceSource,
282
+ provenanceSourceId: input.provenanceSourceId,
283
+ supersedesRevisionId: input.supersedesRevisionId,
284
+ createdBySubjectId: input.createdBySubjectId,
285
+ })
286
+ .returning();
287
+ if (!created) throw new Error("Company-profile revision was not recorded");
288
+ return created;
289
+ }
290
+
291
+ async function activateRevisionInTransaction(
292
+ db: Database,
293
+ input: {
294
+ operationId: string;
295
+ requestFingerprint: string;
296
+ accountId: string;
297
+ workspaceId: string;
298
+ target: RevisionRow | null;
299
+ expectedCurrentRevisionId: string | null;
300
+ expectedActivationVersion: number;
301
+ actorSubjectId: string;
302
+ principalKind: CompanyProfilePrincipalKind;
303
+ reason: string;
304
+ type: CompanyProfileActivationType;
305
+ },
306
+ ): Promise<{ head: HeadRow | null; event: EventRow }> {
307
+ await db.execute(sql`select set_config('opengeni.subject_id', ${input.actorSubjectId}, true)`);
308
+ await db.execute(sql`select set_config('opengeni.principal_kind', ${input.principalKind}, true)`);
309
+ const [receipt] = await rawRows<{ eventId: string }>(
310
+ db,
311
+ sql`select event_id as "eventId"
312
+ from company_profile_apply_activation(
313
+ ${input.operationId}::uuid,
314
+ ${input.requestFingerprint},
315
+ ${input.accountId}::uuid,
316
+ ${input.workspaceId}::uuid,
317
+ ${input.target?.id ?? null}::uuid,
318
+ ${input.expectedCurrentRevisionId}::uuid,
319
+ ${input.expectedActivationVersion}::bigint,
320
+ ${input.type},
321
+ ${input.actorSubjectId},
322
+ ${input.principalKind},
323
+ ${input.reason}
324
+ )`,
325
+ );
326
+ if (!receipt) throw new Error("Company-profile activation returned no lifecycle receipt");
327
+ const [event] = await db
328
+ .select()
329
+ .from(schema.companyProfileActivationEvents)
330
+ .where(eq(schema.companyProfileActivationEvents.id, receipt.eventId))
331
+ .limit(1);
332
+ if (!event) throw new Error("Company-profile activation event was not recorded");
333
+ const head = event.newRevisionId
334
+ ? ({
335
+ accountId: event.accountId,
336
+ revisionId: event.newRevisionId,
337
+ revision: event.newRevision!,
338
+ contentHash: event.newContentHash!,
339
+ activationVersion: event.activationVersion,
340
+ activatedAt: event.createdAt,
341
+ } satisfies HeadRow)
342
+ : null;
343
+ return { head, event };
344
+ }
345
+
346
+ function mutationResponse(
347
+ revision: RevisionRow | null,
348
+ result: { head: HeadRow | null; event: EventRow } | null,
349
+ ): CompanyProfileMutationResponse {
350
+ return {
351
+ revision: revision ? revisionFromRow(revision) : null,
352
+ head: result?.head ? headFromRow(result.head) : null,
353
+ event: result ? eventFromRow(result.event) : null,
354
+ };
355
+ }
356
+
357
+ export async function updateCompanyProfile(
358
+ db: Database,
359
+ input: {
360
+ operationId?: string;
361
+ accountId: string;
362
+ workspaceId: string;
363
+ profile: CompanyProfileContentType;
364
+ expectedCurrentRevisionId: string | null;
365
+ expectedActivationVersion: number;
366
+ actorSubjectId: string;
367
+ principalKind: CompanyProfilePrincipalKind;
368
+ reason: string;
369
+ },
370
+ ): Promise<CompanyProfileMutationResponse> {
371
+ const operationId = input.operationId ?? randomUUID();
372
+ const profile = canonicalProfile(input.profile);
373
+ const requestFingerprint = operationFingerprint("admin_update", {
374
+ ...input,
375
+ operationId: undefined,
376
+ profile,
377
+ });
378
+ return await withRlsContext(
379
+ db,
380
+ { accountId: input.accountId, workspaceId: input.workspaceId },
381
+ async (scopedDb) => {
382
+ await lockAccount(scopedDb, input.accountId);
383
+ const existingRevision = await getRevisionByOperation(scopedDb, input.accountId, operationId);
384
+ const existingEvent = await getEventByOperation(scopedDb, input.accountId, operationId);
385
+ if (existingRevision || existingEvent) {
386
+ if (
387
+ !existingRevision ||
388
+ !existingEvent ||
389
+ existingRevision.requestFingerprint !== requestFingerprint ||
390
+ existingEvent.requestFingerprint !== requestFingerprint
391
+ ) {
392
+ throw new CompanyProfileOperationReuseError();
393
+ }
394
+ return mutationResponse(
395
+ existingRevision,
396
+ await activateRevisionInTransaction(scopedDb, {
397
+ operationId,
398
+ requestFingerprint,
399
+ accountId: input.accountId,
400
+ workspaceId: input.workspaceId,
401
+ target: existingRevision,
402
+ expectedCurrentRevisionId: input.expectedCurrentRevisionId,
403
+ expectedActivationVersion: input.expectedActivationVersion,
404
+ actorSubjectId: input.actorSubjectId,
405
+ principalKind: input.principalKind,
406
+ reason: input.reason,
407
+ type: "activate",
408
+ }),
409
+ );
410
+ }
411
+ const current = await getHeadInTransaction(scopedDb, input.accountId);
412
+ if (
413
+ (current?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
414
+ (current?.activationVersion ?? 0) !== input.expectedActivationVersion
415
+ ) {
416
+ throw new CompanyProfileConflictError(current ? headFromRow(current) : null);
417
+ }
418
+ const revision = await createRevisionInTransaction(scopedDb, {
419
+ operationId,
420
+ requestFingerprint,
421
+ accountId: input.accountId,
422
+ intent: "active",
423
+ profile,
424
+ provenanceSource: "human",
425
+ provenanceSourceId: null,
426
+ supersedesRevisionId: current?.revisionId ?? null,
427
+ createdBySubjectId: input.actorSubjectId,
428
+ });
429
+ const activated = await activateRevisionInTransaction(scopedDb, {
430
+ operationId,
431
+ requestFingerprint,
432
+ accountId: input.accountId,
433
+ workspaceId: input.workspaceId,
434
+ target: revision,
435
+ expectedCurrentRevisionId: input.expectedCurrentRevisionId,
436
+ expectedActivationVersion: input.expectedActivationVersion,
437
+ actorSubjectId: input.actorSubjectId,
438
+ principalKind: input.principalKind,
439
+ reason: input.reason,
440
+ type: "activate",
441
+ });
442
+ return mutationResponse(revision, activated);
443
+ },
444
+ );
445
+ }
446
+
447
+ async function changeActiveRevision(
448
+ db: Database,
449
+ input: {
450
+ operationId?: string;
451
+ accountId: string;
452
+ workspaceId: string;
453
+ targetRevisionId: string | null;
454
+ expectedCurrentRevisionId: string | null;
455
+ expectedActivationVersion: number;
456
+ actorSubjectId: string;
457
+ principalKind: CompanyProfilePrincipalKind;
458
+ reason: string;
459
+ type: CompanyProfileActivationType;
460
+ requirePreviouslyActive: boolean;
461
+ requestFingerprintOverride?: string;
462
+ },
463
+ ): Promise<CompanyProfileMutationResponse> {
464
+ const operationId = input.operationId ?? randomUUID();
465
+ const requestFingerprint =
466
+ input.requestFingerprintOverride ??
467
+ operationFingerprint(`change_active:${input.type}`, {
468
+ ...input,
469
+ operationId: undefined,
470
+ requestFingerprintOverride: undefined,
471
+ });
472
+ return await withRlsContext(
473
+ db,
474
+ { accountId: input.accountId, workspaceId: input.workspaceId },
475
+ async (scopedDb) => {
476
+ await lockAccount(scopedDb, input.accountId);
477
+ const revisionOperation = await getRevisionByOperation(
478
+ scopedDb,
479
+ input.accountId,
480
+ operationId,
481
+ );
482
+ if (revisionOperation) throw new CompanyProfileOperationReuseError();
483
+ const existing = await getEventByOperation(scopedDb, input.accountId, operationId);
484
+ if (existing) {
485
+ if (existing.requestFingerprint !== requestFingerprint) {
486
+ throw new CompanyProfileOperationReuseError();
487
+ }
488
+ const target = existing.newRevisionId
489
+ ? await getRevisionInTransaction(scopedDb, input.accountId, existing.newRevisionId)
490
+ : null;
491
+ return mutationResponse(
492
+ target,
493
+ await activateRevisionInTransaction(scopedDb, {
494
+ operationId,
495
+ requestFingerprint,
496
+ accountId: input.accountId,
497
+ workspaceId: input.workspaceId,
498
+ target,
499
+ expectedCurrentRevisionId: input.expectedCurrentRevisionId,
500
+ expectedActivationVersion: input.expectedActivationVersion,
501
+ actorSubjectId: input.actorSubjectId,
502
+ principalKind: input.principalKind,
503
+ reason: input.reason,
504
+ type: input.type,
505
+ }),
506
+ );
507
+ }
508
+ const current = await getHeadInTransaction(scopedDb, input.accountId);
509
+ if (
510
+ (current?.revisionId ?? null) !== input.expectedCurrentRevisionId ||
511
+ (current?.activationVersion ?? 0) !== input.expectedActivationVersion
512
+ ) {
513
+ throw new CompanyProfileConflictError(current ? headFromRow(current) : null);
514
+ }
515
+ const target = input.targetRevisionId
516
+ ? await getRevisionInTransaction(scopedDb, input.accountId, input.targetRevisionId)
517
+ : null;
518
+ if (input.targetRevisionId && !target) throw new CompanyProfileNotFoundError();
519
+ if ((target?.id ?? null) === (current?.revisionId ?? null)) {
520
+ throw new CompanyProfileInvalidOperationError(
521
+ "The requested company-profile revision is already active",
522
+ );
523
+ }
524
+ if (input.requirePreviouslyActive && target) {
525
+ const [priorActivation] = await scopedDb
526
+ .select({ id: schema.companyProfileActivationEvents.id })
527
+ .from(schema.companyProfileActivationEvents)
528
+ .where(
529
+ and(
530
+ eq(schema.companyProfileActivationEvents.accountId, input.accountId),
531
+ eq(schema.companyProfileActivationEvents.newRevisionId, target.id),
532
+ ),
533
+ )
534
+ .limit(1);
535
+ if (!priorActivation) {
536
+ throw new CompanyProfileInvalidOperationError(
537
+ "Rollback targets must have been active previously",
538
+ );
539
+ }
540
+ }
541
+ const activated = await activateRevisionInTransaction(scopedDb, {
542
+ operationId,
543
+ requestFingerprint,
544
+ accountId: input.accountId,
545
+ workspaceId: input.workspaceId,
546
+ target,
547
+ expectedCurrentRevisionId: input.expectedCurrentRevisionId,
548
+ expectedActivationVersion: input.expectedActivationVersion,
549
+ actorSubjectId: input.actorSubjectId,
550
+ principalKind: input.principalKind,
551
+ reason: input.reason,
552
+ type: input.type,
553
+ });
554
+ return mutationResponse(target, activated);
555
+ },
556
+ );
557
+ }
558
+
559
+ export async function activateCompanyProfileRevision(
560
+ db: Database,
561
+ input: {
562
+ operationId?: string;
563
+ accountId: string;
564
+ workspaceId: string;
565
+ revisionId: string;
566
+ expectedCurrentRevisionId: string | null;
567
+ expectedActivationVersion: number;
568
+ actorSubjectId: string;
569
+ principalKind: CompanyProfilePrincipalKind;
570
+ reason: string;
571
+ },
572
+ ) {
573
+ return await changeActiveRevision(db, {
574
+ ...input,
575
+ targetRevisionId: input.revisionId,
576
+ type: "activate",
577
+ requirePreviouslyActive: false,
578
+ });
579
+ }
580
+
581
+ export async function rollbackCompanyProfileRevision(
582
+ db: Database,
583
+ input: {
584
+ operationId?: string;
585
+ accountId: string;
586
+ workspaceId: string;
587
+ targetRevisionId: string;
588
+ expectedCurrentRevisionId: string;
589
+ expectedActivationVersion: number;
590
+ actorSubjectId: string;
591
+ principalKind: CompanyProfilePrincipalKind;
592
+ reason: string;
593
+ },
594
+ ) {
595
+ return await changeActiveRevision(db, {
596
+ ...input,
597
+ type: "rollback",
598
+ requirePreviouslyActive: true,
599
+ });
600
+ }
601
+
602
+ function profileWithLearningSubject(
603
+ current: CompanyProfileContentType | null,
604
+ subject: { kind: CompanyProfileLearningSubjectKind; content: string; stableKey: string | null },
605
+ ): CompanyProfileContentType {
606
+ const profile = current ?? {
607
+ identity: null,
608
+ mission: null,
609
+ products: [],
610
+ customers: [],
611
+ goals: [],
612
+ constraints: [],
613
+ };
614
+ if (subject.kind === "company_identity")
615
+ return canonicalProfile({ ...profile, identity: subject.content });
616
+ if (subject.kind === "company_mission")
617
+ return canonicalProfile({ ...profile, mission: subject.content });
618
+ if (!subject.stableKey) {
619
+ throw new CompanyProfileInvalidOperationError(
620
+ "Repeatable company-profile learning subjects require a stable key",
621
+ );
622
+ }
623
+ const field = {
624
+ company_product: "products",
625
+ company_customer: "customers",
626
+ company_goal: "goals",
627
+ company_constraint: "constraints",
628
+ }[subject.kind] as "products" | "customers" | "goals" | "constraints";
629
+ const entries = profile[field].filter((entry) => entry.key !== subject.stableKey);
630
+ entries.push({ key: subject.stableKey, content: subject.content });
631
+ entries.sort((left, right) => left.key.localeCompare(right.key));
632
+ return canonicalProfile({ ...profile, [field]: entries });
633
+ }
634
+
635
+ export type CompanyProfileLearningWriteResult = {
636
+ outcome: "applied" | "proposed";
637
+ revision: CompanyProfileRevision;
638
+ head: CompanyProfileHead | null;
639
+ effectiveBoundary: "next_accepted_attempt";
640
+ rollbackToken: string | null;
641
+ };
642
+
643
+ function rollbackToken(previousRevisionId: string | null, appliedRevisionId: string): string {
644
+ return `company-profile.v1:${previousRevisionId ?? "none"}:${appliedRevisionId}`;
645
+ }
646
+
647
+ export async function writeCompanyProfileLearning(
648
+ db: Database,
649
+ rawInput: CompanyProfileLearningWrite,
650
+ ): Promise<CompanyProfileLearningWriteResult> {
651
+ const input = CompanyProfileLearningWrite.parse(rawInput);
652
+ const requestFingerprint = operationFingerprint("durable_learning_write", input);
653
+ return await withRlsContext(
654
+ db,
655
+ { accountId: input.accountId, workspaceId: input.workspaceId },
656
+ async (scopedDb) => {
657
+ await lockAccount(scopedDb, input.accountId);
658
+ const existingRevision = await getRevisionByOperation(
659
+ scopedDb,
660
+ input.accountId,
661
+ input.operationId,
662
+ );
663
+ const existingEvent = await getEventByOperation(scopedDb, input.accountId, input.operationId);
664
+ if (existingRevision) {
665
+ if (existingRevision.requestFingerprint !== requestFingerprint) {
666
+ throw new CompanyProfileOperationReuseError();
667
+ }
668
+ if (input.authority === "active" && !existingEvent) {
669
+ throw new CompanyProfileOperationReuseError();
670
+ }
671
+ return {
672
+ outcome: input.authority === "active" ? "applied" : "proposed",
673
+ revision: revisionFromRow(existingRevision),
674
+ head:
675
+ existingEvent?.newRevisionId === existingRevision.id
676
+ ? headFromRow({
677
+ accountId: existingEvent.accountId,
678
+ revisionId: existingRevision.id,
679
+ revision: existingRevision.revision,
680
+ contentHash: existingRevision.contentHash,
681
+ activationVersion: existingEvent.activationVersion,
682
+ activatedAt: existingEvent.createdAt,
683
+ })
684
+ : null,
685
+ effectiveBoundary: "next_accepted_attempt",
686
+ rollbackToken:
687
+ input.authority === "active"
688
+ ? rollbackToken(existingEvent?.oldRevisionId ?? null, existingRevision.id)
689
+ : null,
690
+ };
691
+ }
692
+ if (existingEvent) throw new CompanyProfileOperationReuseError();
693
+ const current = await getHeadInTransaction(scopedDb, input.accountId);
694
+ const currentRevision = current
695
+ ? await getRevisionInTransaction(scopedDb, input.accountId, current.revisionId)
696
+ : null;
697
+ if (current && !currentRevision) {
698
+ throw new CompanyProfileInvalidOperationError(
699
+ "The active company-profile revision is missing",
700
+ );
701
+ }
702
+ const profile = profileWithLearningSubject(
703
+ currentRevision ? revisionFromRow(currentRevision).profile : null,
704
+ input.subject,
705
+ );
706
+ const revision = await createRevisionInTransaction(scopedDb, {
707
+ operationId: input.operationId,
708
+ requestFingerprint,
709
+ accountId: input.accountId,
710
+ intent: input.authority,
711
+ profile,
712
+ provenanceSource: "durable_learning",
713
+ provenanceSourceId: input.sourceId,
714
+ supersedesRevisionId: current?.revisionId ?? null,
715
+ createdBySubjectId: input.actorSubjectId,
716
+ });
717
+ if (input.authority === "proposal") {
718
+ return {
719
+ outcome: "proposed",
720
+ revision: revisionFromRow(revision),
721
+ head: null,
722
+ effectiveBoundary: "next_accepted_attempt",
723
+ rollbackToken: null,
724
+ };
725
+ }
726
+ const activated = await activateRevisionInTransaction(scopedDb, {
727
+ operationId: input.operationId,
728
+ requestFingerprint,
729
+ accountId: input.accountId,
730
+ workspaceId: input.workspaceId,
731
+ target: revision,
732
+ expectedCurrentRevisionId: current?.revisionId ?? null,
733
+ expectedActivationVersion: current?.activationVersion ?? 0,
734
+ actorSubjectId: input.actorSubjectId,
735
+ principalKind: principalKindForActor(input.actorKind),
736
+ reason: `Durable learning attempt ${input.operationId}`,
737
+ type: "activate",
738
+ });
739
+ return {
740
+ outcome: "applied",
741
+ revision: revisionFromRow(revision),
742
+ head: activated.head ? headFromRow(activated.head) : null,
743
+ effectiveBoundary: "next_accepted_attempt",
744
+ rollbackToken: rollbackToken(current?.revisionId ?? null, revision.id),
745
+ };
746
+ },
747
+ );
748
+ }
749
+
750
+ export async function rollbackCompanyProfileLearning(
751
+ db: Database,
752
+ input: {
753
+ operationId: string;
754
+ accountId: string;
755
+ workspaceId: string;
756
+ actorSubjectId: string;
757
+ actorKind: "human" | "agent" | "service";
758
+ token: string;
759
+ reason: string;
760
+ },
761
+ ): Promise<CompanyProfileMutationResponse> {
762
+ const match = /^company-profile\.v1:(none|[0-9a-f-]{36}):([0-9a-f-]{36})$/i.exec(input.token);
763
+ if (!match)
764
+ throw new CompanyProfileInvalidOperationError("Invalid company-profile rollback token");
765
+ const priorRevisionId = match[1] === "none" ? null : match[1]!;
766
+ const appliedRevisionId = match[2]!;
767
+ const requestFingerprint = operationFingerprint("durable_learning_rollback", input);
768
+ return await changeActiveRevision(db, {
769
+ operationId: input.operationId,
770
+ accountId: input.accountId,
771
+ workspaceId: input.workspaceId,
772
+ targetRevisionId: priorRevisionId,
773
+ expectedCurrentRevisionId: appliedRevisionId,
774
+ expectedActivationVersion: await currentActivationVersion(db, input),
775
+ actorSubjectId: input.actorSubjectId,
776
+ principalKind: principalKindForActor(input.actorKind),
777
+ reason: input.reason,
778
+ type: "rollback",
779
+ requirePreviouslyActive: priorRevisionId !== null,
780
+ requestFingerprintOverride: requestFingerprint,
781
+ });
782
+ }
783
+
784
+ async function currentActivationVersion(
785
+ db: Database,
786
+ input: { accountId: string; workspaceId: string },
787
+ ): Promise<number> {
788
+ return await withRlsContext(db, input, async (scopedDb) => {
789
+ const [head] = await scopedDb
790
+ .select({ activationVersion: schema.companyProfileHeads.activationVersion })
791
+ .from(schema.companyProfileHeads)
792
+ .where(eq(schema.companyProfileHeads.accountId, input.accountId))
793
+ .limit(1);
794
+ return head?.activationVersion ?? 0;
795
+ });
796
+ }
797
+
798
+ export async function listCompanyProfile(
799
+ db: Database,
800
+ input: { accountId: string; workspaceId: string; afterRevision?: number; limit: number },
801
+ ): Promise<CompanyProfileListResponse> {
802
+ return await withRlsContext(db, input, async (scopedDb) => {
803
+ const [headRows, revisions, events] = await Promise.all([
804
+ scopedDb
805
+ .select()
806
+ .from(schema.companyProfileHeads)
807
+ .where(eq(schema.companyProfileHeads.accountId, input.accountId))
808
+ .limit(1),
809
+ scopedDb
810
+ .select()
811
+ .from(schema.companyProfileRevisions)
812
+ .where(
813
+ and(
814
+ eq(schema.companyProfileRevisions.accountId, input.accountId),
815
+ input.afterRevision
816
+ ? lt(schema.companyProfileRevisions.revision, input.afterRevision)
817
+ : undefined,
818
+ ),
819
+ )
820
+ .orderBy(desc(schema.companyProfileRevisions.revision))
821
+ .limit(input.limit + 1),
822
+ scopedDb
823
+ .select()
824
+ .from(schema.companyProfileActivationEvents)
825
+ .where(eq(schema.companyProfileActivationEvents.accountId, input.accountId))
826
+ .orderBy(desc(schema.companyProfileActivationEvents.activationVersion))
827
+ .limit(100),
828
+ ]);
829
+ const page = revisions.slice(0, input.limit);
830
+ const head = headRows[0] ?? null;
831
+ const activeRevision = head
832
+ ? await getRevisionInTransaction(scopedDb, input.accountId, head.revisionId)
833
+ : null;
834
+ if (head && !activeRevision) {
835
+ throw new CompanyProfileInvalidOperationError(
836
+ "The active company-profile revision is missing",
837
+ );
838
+ }
839
+ return {
840
+ current: head ? headFromRow(head) : null,
841
+ activeRevision: activeRevision ? revisionFromRow(activeRevision) : null,
842
+ revisions: page.map(revisionFromRow),
843
+ activationEvents: events.map(eventFromRow),
844
+ nextAfterRevision: revisions.length > input.limit ? page.at(-1)!.revision : null,
845
+ };
846
+ });
847
+ }
848
+
849
+ export async function getCompanyProfileRevision(
850
+ db: Database,
851
+ input: { accountId: string; workspaceId: string; revisionId: string },
852
+ ): Promise<CompanyProfileRevision> {
853
+ return await withRlsContext(db, input, async (scopedDb) => {
854
+ const revision = await getRevisionInTransaction(scopedDb, input.accountId, input.revisionId);
855
+ if (!revision) throw new CompanyProfileNotFoundError();
856
+ return revisionFromRow(revision);
857
+ });
858
+ }
859
+
860
+ function unifiedJsonDiff(from: CompanyProfileRevision, to: CompanyProfileRevision): string {
861
+ const before = JSON.stringify(from.profile, null, 2).split("\n");
862
+ const after = JSON.stringify(to.profile, null, 2).split("\n");
863
+ return [
864
+ `--- company-profile-r${from.revision}`,
865
+ `+++ company-profile-r${to.revision}`,
866
+ `@@ -1,${before.length} +1,${after.length} @@`,
867
+ ...before.map((line) => `-${line}`),
868
+ ...after.map((line) => `+${line}`),
869
+ "",
870
+ ].join("\n");
871
+ }
872
+
873
+ export async function diffCompanyProfileRevisions(
874
+ db: Database,
875
+ input: { accountId: string; workspaceId: string; fromRevisionId: string; toRevisionId: string },
876
+ ): Promise<CompanyProfileDiffResponse> {
877
+ const [from, to] = await Promise.all([
878
+ getCompanyProfileRevision(db, {
879
+ accountId: input.accountId,
880
+ workspaceId: input.workspaceId,
881
+ revisionId: input.fromRevisionId,
882
+ }),
883
+ getCompanyProfileRevision(db, {
884
+ accountId: input.accountId,
885
+ workspaceId: input.workspaceId,
886
+ revisionId: input.toRevisionId,
887
+ }),
888
+ ]);
889
+ return { from, to, format: "unified_json", diff: unifiedJsonDiff(from, to) };
890
+ }
891
+
892
+ export async function getOrCreateCompanyProfileSnapshot(
893
+ db: Database,
894
+ claims: CompanyProfileAttemptClaims,
895
+ ) {
896
+ try {
897
+ return await withWorkspaceRls(db, claims.workspaceId, async (scopedDb) => {
898
+ const rows = (await scopedDb.execute(sql`
899
+ SELECT
900
+ snapshot.id,
901
+ snapshot.account_id AS "accountId",
902
+ snapshot.workspace_id AS "workspaceId",
903
+ snapshot.session_id AS "sessionId",
904
+ snapshot.turn_id AS "turnId",
905
+ snapshot.attempt_id AS "attemptId",
906
+ snapshot.execution_generation AS "executionGeneration",
907
+ snapshot.profile,
908
+ snapshot.snapshot_hash AS "snapshotHash",
909
+ snapshot.created_at AS "createdAt"
910
+ FROM company_profile_get_or_create_snapshot(
911
+ ${claims.accountId}::uuid,
912
+ ${claims.workspaceId}::uuid,
913
+ ${claims.sessionId}::uuid,
914
+ ${claims.turnId}::uuid,
915
+ ${claims.attemptId}::uuid,
916
+ ${claims.executionGeneration}
917
+ ) snapshot
918
+ `)) as unknown as Array<{
919
+ id: string;
920
+ accountId: string;
921
+ workspaceId: string;
922
+ sessionId: string;
923
+ turnId: string;
924
+ attemptId: string;
925
+ executionGeneration: number;
926
+ profile: unknown;
927
+ snapshotHash: string;
928
+ createdAt: Date | string;
929
+ }>;
930
+ const row = rows[0];
931
+ if (!row) throw new CompanyProfileSnapshotAuthorityError();
932
+ const snapshot = CompanyProfileSnapshot.parse({ ...row, createdAt: iso(row.createdAt) });
933
+ if (!snapshot.profile) return ResolvedCompanyProfileSnapshot.parse(snapshot);
934
+ const revision = await getRevisionInTransaction(
935
+ scopedDb,
936
+ claims.accountId,
937
+ snapshot.profile.id,
938
+ );
939
+ if (!revision || !snapshotMatchesRevision(snapshot.profile, revision)) {
940
+ throw new CompanyProfileInvalidOperationError(
941
+ "Company-profile snapshot references an inexact immutable revision",
942
+ );
943
+ }
944
+ return ResolvedCompanyProfileSnapshot.parse({
945
+ ...snapshot,
946
+ profile: { ...snapshot.profile, profile: revisionFromRow(revision).profile },
947
+ });
948
+ });
949
+ } catch (error) {
950
+ if (["40001", "42501"].includes(nestedPostgresSqlState(error) ?? "")) {
951
+ throw new CompanyProfileSnapshotAuthorityError(
952
+ "Company-profile snapshot requires the exact current attempt and generation",
953
+ );
954
+ }
955
+ throw error;
956
+ }
957
+ }
958
+
959
+ function snapshotMatchesRevision(
960
+ entry: CompanyProfileSnapshotEntry,
961
+ revision: RevisionRow,
962
+ ): boolean {
963
+ return (
964
+ entry.id === revision.id &&
965
+ entry.revision === revision.revision &&
966
+ entry.contentHash === revision.contentHash
967
+ );
968
+ }