@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.
- package/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +299 -0
- package/dist/browser-downloads.d.ts +51 -0
- package/dist/browser-identities.d.ts +137 -0
- package/dist/browser-sessions.d.ts +354 -0
- package/dist/browser-state-artifacts.d.ts +91 -0
- package/dist/canonical-human-identities.d.ts +48 -0
- package/dist/canonical-human-identities.js +28 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +194 -0
- package/dist/chunk-5KTIEDX7.js +13200 -0
- package/dist/chunk-5KTIEDX7.js.map +1 -0
- package/dist/chunk-CQZ4QSP5.js +995 -0
- package/dist/chunk-CQZ4QSP5.js.map +1 -0
- package/dist/chunk-F32YT7MP.js +117 -0
- package/dist/chunk-F32YT7MP.js.map +1 -0
- package/dist/chunk-JC5QPLUM.js +666 -0
- package/dist/chunk-JC5QPLUM.js.map +1 -0
- package/dist/chunk-P3RE4D2B.js +3535 -0
- package/dist/chunk-P3RE4D2B.js.map +1 -0
- package/dist/chunk-TAUTWOS5.js +259 -0
- package/dist/chunk-TAUTWOS5.js.map +1 -0
- package/dist/chunk-VKLB2ZGI.js +2389 -0
- package/dist/chunk-VKLB2ZGI.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +57 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +1122 -126
- package/dist/index.js +42329 -18407
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +73 -10
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-facets.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +14 -0
- package/dist/interaction-schema.d.ts +6036 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +3 -3
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +19 -6
- package/dist/schema.d.ts +22654 -9359
- package/dist/schema.js +209 -3
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +33 -7
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +1 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tenancy.d.ts +44 -0
- package/dist/session-tenancy.js +16 -0
- package/dist/session-tenancy.js.map +1 -0
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-task-policy-schema.d.ts +847 -0
- package/dist/slack-task-policy.d.ts +65 -0
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/task-notes-schema.d.ts +924 -0
- package/dist/task-notes.d.ts +85 -0
- package/dist/transcription-recordings.d.ts +2 -2
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +225 -0
- package/dist/video-generation.js +68 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/dist/xai-subscription.d.ts +326 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_browser_interaction_authority.sql +67 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_browser_download_saves.sql +34 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_browser_controller_host.sql +385 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/drizzle/0217_external_browser_auth_operations.sql +22 -0
- package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
- package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
- package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
- package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
- package/drizzle/0220_session_channels.sql +54 -0
- package/drizzle/0221_sessions_channel_index.sql +5 -0
- package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
- package/drizzle/0222_sessions_channel_fk.sql +89 -0
- package/drizzle/0223_pending_tool_event_output.sql +55 -0
- package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
- package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
- package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
- package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
- package/drizzle/0227_slack_native_actions.sql +145 -0
- package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
- package/drizzle/0228_slack_task_policy.sql +400 -0
- package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
- package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
- package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
- package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
- package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
- package/drizzle/0234_xai_subscription_authority.sql +1139 -0
- package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
- package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
- package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
- package/drizzle/0237_interaction_transition_reaper.sql +263 -0
- package/drizzle/0238_goal_persistence_policy.sql +471 -0
- package/drizzle/0238_supergrok_realtime_model.sql +29 -0
- package/drizzle/0239_supergrok_video_funding.sql +50 -0
- package/drizzle/0239_task_tree_notes.sql +990 -0
- package/package.json +26 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +4373 -0
- package/src/browser-downloads.ts +517 -0
- package/src/browser-identities.ts +1385 -0
- package/src/browser-sessions.ts +2294 -0
- package/src/browser-state-artifacts.ts +449 -0
- package/src/canonical-human-identities.ts +317 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1571 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +468 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/generated-images.ts +740 -0
- package/src/index.ts +9963 -1465
- package/src/insights.ts +251 -23
- package/src/integration-bindings.ts +455 -0
- package/src/integration-facets.ts +862 -0
- package/src/interaction-revisions.ts +70 -0
- package/src/interaction-schema.ts +1606 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +886 -0
- package/src/persistence-errors.ts +4 -7
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +534 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +721 -5
- package/src/schema.ts +6270 -3159
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +130 -42
- package/src/session-queue-commands.ts +265 -15
- package/src/session-realtime-context.ts +2 -2
- package/src/session-realtime-ledger.ts +3 -3
- package/src/session-realtime.ts +7 -7
- package/src/session-tenancy.ts +188 -0
- package/src/session-tool-call-settlement.ts +18 -3
- package/src/slack-task-policy-schema.ts +130 -0
- package/src/slack-task-policy.ts +341 -0
- package/src/slack-user-link-access.ts +702 -0
- package/src/task-notes-schema.ts +135 -0
- package/src/task-notes.ts +178 -0
- package/src/video-generation.ts +1530 -0
- package/src/workspace-artifacts.ts +1 -1
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/src/xai-subscription.ts +1887 -0
- package/dist/chunk-P6CUHXQZ.js +0 -7248
- package/dist/chunk-P6CUHXQZ.js.map +0 -1
- package/dist/chunk-VHBFHMPC.js +0 -1219
- 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
|
+
}
|