@opengeni/db 0.12.6 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XRVNI2GB.js → chunk-ELMUCYZF.js} +12 -2
- package/dist/chunk-ELMUCYZF.js.map +1 -0
- package/dist/chunk-N4WTE6SB.js +4033 -0
- package/dist/chunk-N4WTE6SB.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2500 -1866
- package/dist/index.js.map +1 -1
- package/dist/provision-roles.d.ts +156 -12
- package/dist/provision-roles.js +1 -1
- package/dist/{schema-CeXWlYcG.d.ts → schema-D-hyrsFt.d.ts} +2050 -942
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +9 -1
- package/drizzle/0130_workspace_instruction_policies.sql +330 -0
- package/drizzle/0131_slack_bot_install_and_post_idempotency.sql +133 -0
- package/package.json +3 -3
- package/src/index.ts +358 -7
- package/src/runtime-posture.ts +12 -2
- package/src/schema.ts +87 -0
- package/src/workspace-instruction-policies-schema.ts +140 -0
- package/src/workspace-instruction-policies.ts +624 -0
- package/dist/chunk-CYA6BHV6.js +0 -3822
- package/dist/chunk-CYA6BHV6.js.map +0 -1
- package/dist/chunk-XRVNI2GB.js.map +0 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS } from "@opengeni/contracts";
|
|
3
|
+
import type {
|
|
4
|
+
WorkspaceInstructionPolicyActivationEvent,
|
|
5
|
+
WorkspaceInstructionPolicyActivationResponse,
|
|
6
|
+
WorkspaceInstructionPolicyActivationType,
|
|
7
|
+
WorkspaceInstructionPolicyDiffResponse,
|
|
8
|
+
WorkspaceInstructionPolicyDraftProvenanceSource,
|
|
9
|
+
WorkspaceInstructionPolicyHead,
|
|
10
|
+
WorkspaceInstructionPolicyKind,
|
|
11
|
+
WorkspaceInstructionPolicyListQuery,
|
|
12
|
+
WorkspaceInstructionPolicyListResponse,
|
|
13
|
+
WorkspaceInstructionPolicyProvenanceSource,
|
|
14
|
+
WorkspaceInstructionPolicyRevision,
|
|
15
|
+
WorkspaceInstructionPolicyScope,
|
|
16
|
+
WorkspaceInstructionPolicyTarget,
|
|
17
|
+
} from "@opengeni/contracts";
|
|
18
|
+
import { and, asc, desc, eq, isNull, lt, type SQL } from "drizzle-orm";
|
|
19
|
+
import type { Database } from "./index";
|
|
20
|
+
import { withRlsContext, withWorkspaceRls } from "./index";
|
|
21
|
+
import * as schema from "./schema";
|
|
22
|
+
|
|
23
|
+
type DraftInput = WorkspaceInstructionPolicyTarget & {
|
|
24
|
+
accountId: string;
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
content: string;
|
|
27
|
+
provenanceSource: WorkspaceInstructionPolicyProvenanceSource;
|
|
28
|
+
provenanceSourceId: string | null;
|
|
29
|
+
supersedesRevisionId: string | null;
|
|
30
|
+
createdBySubjectId: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type CallerDraftInput = Omit<DraftInput, "provenanceSource"> & {
|
|
34
|
+
provenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class WorkspaceInstructionPolicyConflictError extends Error {
|
|
38
|
+
readonly name = "WorkspaceInstructionPolicyConflictError";
|
|
39
|
+
readonly code = "WORKSPACE_INSTRUCTION_POLICY_CONFLICT";
|
|
40
|
+
|
|
41
|
+
constructor(readonly currentHead: WorkspaceInstructionPolicyHead | null) {
|
|
42
|
+
super("The active workspace instruction policy changed in another request");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class WorkspaceInstructionPolicyNotFoundError extends Error {
|
|
47
|
+
readonly name = "WorkspaceInstructionPolicyNotFoundError";
|
|
48
|
+
|
|
49
|
+
constructor(message = "Workspace instruction-policy revision was not found") {
|
|
50
|
+
super(message);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class WorkspaceInstructionPolicyInvalidOperationError extends Error {
|
|
55
|
+
readonly name = "WorkspaceInstructionPolicyInvalidOperationError";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class WorkspaceInstructionPolicyLegacyUnavailableError extends Error {
|
|
59
|
+
readonly name = "WorkspaceInstructionPolicyLegacyUnavailableError";
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
super("This workspace has no legacy agent instructions to import");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function contentHash(content: string): string {
|
|
67
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function iso(value: Date | string): string {
|
|
71
|
+
return (value instanceof Date ? value : new Date(value)).toISOString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type RevisionRow = typeof schema.workspaceInstructionPolicyRevisions.$inferSelect;
|
|
75
|
+
type HeadRow = typeof schema.workspaceInstructionPolicyHeads.$inferSelect;
|
|
76
|
+
type EventRow = typeof schema.workspaceInstructionPolicyActivationEvents.$inferSelect;
|
|
77
|
+
|
|
78
|
+
function revisionFromRow(row: RevisionRow): WorkspaceInstructionPolicyRevision {
|
|
79
|
+
return {
|
|
80
|
+
id: row.id,
|
|
81
|
+
accountId: row.accountId,
|
|
82
|
+
workspaceId: row.workspaceId,
|
|
83
|
+
revision: row.revision,
|
|
84
|
+
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
85
|
+
scope: row.scope as WorkspaceInstructionPolicyScope,
|
|
86
|
+
roleKey: row.roleKey,
|
|
87
|
+
content: row.content,
|
|
88
|
+
contentHash: row.contentHash,
|
|
89
|
+
provenance: {
|
|
90
|
+
source: row.provenanceSource as WorkspaceInstructionPolicyProvenanceSource,
|
|
91
|
+
sourceId: row.provenanceSourceId,
|
|
92
|
+
},
|
|
93
|
+
supersedesRevisionId: row.supersedesRevisionId,
|
|
94
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
95
|
+
createdAt: iso(row.createdAt),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function headFromRow(row: HeadRow): WorkspaceInstructionPolicyHead {
|
|
100
|
+
return {
|
|
101
|
+
workspaceId: row.workspaceId,
|
|
102
|
+
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
103
|
+
scope: row.scope as WorkspaceInstructionPolicyScope,
|
|
104
|
+
roleKey: row.roleKey,
|
|
105
|
+
revisionId: row.revisionId,
|
|
106
|
+
revision: row.revision,
|
|
107
|
+
contentHash: row.contentHash,
|
|
108
|
+
activationVersion: row.activationVersion,
|
|
109
|
+
activatedAt: iso(row.activatedAt),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function eventFromRow(row: EventRow): WorkspaceInstructionPolicyActivationEvent {
|
|
114
|
+
return {
|
|
115
|
+
id: row.id,
|
|
116
|
+
accountId: row.accountId,
|
|
117
|
+
workspaceId: row.workspaceId,
|
|
118
|
+
kind: row.kind as WorkspaceInstructionPolicyKind,
|
|
119
|
+
scope: row.scope as WorkspaceInstructionPolicyScope,
|
|
120
|
+
roleKey: row.roleKey,
|
|
121
|
+
type: row.type as WorkspaceInstructionPolicyActivationType,
|
|
122
|
+
activationVersion: row.activationVersion,
|
|
123
|
+
oldRevision:
|
|
124
|
+
row.oldRevisionId === null
|
|
125
|
+
? null
|
|
126
|
+
: {
|
|
127
|
+
id: row.oldRevisionId,
|
|
128
|
+
revision: row.oldRevision!,
|
|
129
|
+
contentHash: row.oldContentHash!,
|
|
130
|
+
},
|
|
131
|
+
newRevision: {
|
|
132
|
+
id: row.newRevisionId,
|
|
133
|
+
revision: row.newRevision,
|
|
134
|
+
contentHash: row.newContentHash,
|
|
135
|
+
},
|
|
136
|
+
actorSubjectId: row.actorSubjectId,
|
|
137
|
+
reason: row.reason,
|
|
138
|
+
createdAt: iso(row.createdAt),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function headTargetConditions(
|
|
143
|
+
workspaceId: string,
|
|
144
|
+
target: WorkspaceInstructionPolicyTarget,
|
|
145
|
+
): SQL[] {
|
|
146
|
+
return [
|
|
147
|
+
eq(schema.workspaceInstructionPolicyHeads.workspaceId, workspaceId),
|
|
148
|
+
eq(schema.workspaceInstructionPolicyHeads.kind, target.kind),
|
|
149
|
+
eq(schema.workspaceInstructionPolicyHeads.scope, target.scope),
|
|
150
|
+
target.roleKey === null
|
|
151
|
+
? isNull(schema.workspaceInstructionPolicyHeads.roleKey)
|
|
152
|
+
: eq(schema.workspaceInstructionPolicyHeads.roleKey, target.roleKey),
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function eventTargetConditions(
|
|
157
|
+
workspaceId: string,
|
|
158
|
+
target: WorkspaceInstructionPolicyTarget,
|
|
159
|
+
): SQL[] {
|
|
160
|
+
return [
|
|
161
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId),
|
|
162
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.kind, target.kind),
|
|
163
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.scope, target.scope),
|
|
164
|
+
target.roleKey === null
|
|
165
|
+
? isNull(schema.workspaceInstructionPolicyActivationEvents.roleKey)
|
|
166
|
+
: eq(schema.workspaceInstructionPolicyActivationEvents.roleKey, target.roleKey),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function lockWorkspace(
|
|
171
|
+
db: Database,
|
|
172
|
+
input: { accountId: string; workspaceId: string },
|
|
173
|
+
): Promise<{ agentInstructions: string | null }> {
|
|
174
|
+
const [workspace] = await db
|
|
175
|
+
.select({ agentInstructions: schema.workspaces.agentInstructions })
|
|
176
|
+
.from(schema.workspaces)
|
|
177
|
+
.where(
|
|
178
|
+
and(
|
|
179
|
+
eq(schema.workspaces.id, input.workspaceId),
|
|
180
|
+
eq(schema.workspaces.accountId, input.accountId),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
.for("update")
|
|
184
|
+
.limit(1);
|
|
185
|
+
if (!workspace) {
|
|
186
|
+
throw new WorkspaceInstructionPolicyNotFoundError("Workspace was not found");
|
|
187
|
+
}
|
|
188
|
+
return workspace;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function getRevisionInTransaction(
|
|
192
|
+
db: Database,
|
|
193
|
+
workspaceId: string,
|
|
194
|
+
revisionId: string,
|
|
195
|
+
): Promise<RevisionRow | null> {
|
|
196
|
+
const [row] = await db
|
|
197
|
+
.select()
|
|
198
|
+
.from(schema.workspaceInstructionPolicyRevisions)
|
|
199
|
+
.where(
|
|
200
|
+
and(
|
|
201
|
+
eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
|
|
202
|
+
eq(schema.workspaceInstructionPolicyRevisions.id, revisionId),
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
.limit(1);
|
|
206
|
+
return row ?? null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function getHeadInTransaction(
|
|
210
|
+
db: Database,
|
|
211
|
+
workspaceId: string,
|
|
212
|
+
target: WorkspaceInstructionPolicyTarget,
|
|
213
|
+
): Promise<HeadRow | null> {
|
|
214
|
+
const [row] = await db
|
|
215
|
+
.select()
|
|
216
|
+
.from(schema.workspaceInstructionPolicyHeads)
|
|
217
|
+
.where(and(...headTargetConditions(workspaceId, target)))
|
|
218
|
+
.for("update")
|
|
219
|
+
.limit(1);
|
|
220
|
+
return row ?? null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function sameTarget(
|
|
224
|
+
left: Pick<WorkspaceInstructionPolicyRevision, "kind" | "scope" | "roleKey">,
|
|
225
|
+
right: WorkspaceInstructionPolicyTarget,
|
|
226
|
+
): boolean {
|
|
227
|
+
return left.kind === right.kind && left.scope === right.scope && left.roleKey === right.roleKey;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function createDraftInTransaction(
|
|
231
|
+
db: Database,
|
|
232
|
+
input: DraftInput,
|
|
233
|
+
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
234
|
+
if (input.supersedesRevisionId !== null) {
|
|
235
|
+
const superseded = await getRevisionInTransaction(
|
|
236
|
+
db,
|
|
237
|
+
input.workspaceId,
|
|
238
|
+
input.supersedesRevisionId,
|
|
239
|
+
);
|
|
240
|
+
if (!superseded || !sameTarget(revisionFromRow(superseded), input)) {
|
|
241
|
+
throw new WorkspaceInstructionPolicyInvalidOperationError(
|
|
242
|
+
"A superseded revision must exist in the same workspace and instruction-policy target",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const [created] = await db
|
|
247
|
+
.insert(schema.workspaceInstructionPolicyRevisions)
|
|
248
|
+
.values({
|
|
249
|
+
accountId: input.accountId,
|
|
250
|
+
workspaceId: input.workspaceId,
|
|
251
|
+
kind: input.kind,
|
|
252
|
+
scope: input.scope,
|
|
253
|
+
roleKey: input.roleKey,
|
|
254
|
+
content: input.content,
|
|
255
|
+
contentHash: contentHash(input.content),
|
|
256
|
+
provenanceSource: input.provenanceSource,
|
|
257
|
+
provenanceSourceId: input.provenanceSourceId,
|
|
258
|
+
supersedesRevisionId: input.supersedesRevisionId,
|
|
259
|
+
createdBySubjectId: input.createdBySubjectId,
|
|
260
|
+
})
|
|
261
|
+
.returning();
|
|
262
|
+
if (!created) throw new Error("Workspace instruction-policy draft was not created");
|
|
263
|
+
return revisionFromRow(created);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function createWorkspaceInstructionPolicyDraft(
|
|
267
|
+
db: Database,
|
|
268
|
+
input: CallerDraftInput,
|
|
269
|
+
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
270
|
+
return await withRlsContext(
|
|
271
|
+
db,
|
|
272
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
273
|
+
async (scopedDb) => {
|
|
274
|
+
await lockWorkspace(scopedDb, input);
|
|
275
|
+
return await createDraftInTransaction(scopedDb, input);
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Import only the stored legacy override; never materialize a deployment default or activate it. */
|
|
281
|
+
export async function importLegacyWorkspaceInstructionPolicyDraft(
|
|
282
|
+
db: Database,
|
|
283
|
+
input: {
|
|
284
|
+
accountId: string;
|
|
285
|
+
workspaceId: string;
|
|
286
|
+
createdBySubjectId: string;
|
|
287
|
+
supersedesRevisionId: string | null;
|
|
288
|
+
},
|
|
289
|
+
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
290
|
+
return await withRlsContext(
|
|
291
|
+
db,
|
|
292
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
293
|
+
async (scopedDb) => {
|
|
294
|
+
const workspace = await lockWorkspace(scopedDb, input);
|
|
295
|
+
if (workspace.agentInstructions === null) {
|
|
296
|
+
throw new WorkspaceInstructionPolicyLegacyUnavailableError();
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
workspace.agentInstructions.trim().length === 0 ||
|
|
300
|
+
workspace.agentInstructions.length > WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS
|
|
301
|
+
) {
|
|
302
|
+
throw new WorkspaceInstructionPolicyInvalidOperationError(
|
|
303
|
+
"Legacy agent instructions do not fit the workspace instruction-policy draft contract",
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return await createDraftInTransaction(scopedDb, {
|
|
307
|
+
...input,
|
|
308
|
+
kind: "charter",
|
|
309
|
+
scope: "global",
|
|
310
|
+
roleKey: null,
|
|
311
|
+
content: workspace.agentInstructions,
|
|
312
|
+
provenanceSource: "legacy_import",
|
|
313
|
+
provenanceSourceId: "workspaces.agent_instructions",
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function listFilterConditions(
|
|
320
|
+
workspaceId: string,
|
|
321
|
+
query: WorkspaceInstructionPolicyListQuery,
|
|
322
|
+
): SQL[] {
|
|
323
|
+
const conditions: SQL[] = [
|
|
324
|
+
eq(schema.workspaceInstructionPolicyRevisions.workspaceId, workspaceId),
|
|
325
|
+
];
|
|
326
|
+
if (query.kind !== undefined) {
|
|
327
|
+
conditions.push(eq(schema.workspaceInstructionPolicyRevisions.kind, query.kind));
|
|
328
|
+
}
|
|
329
|
+
if (query.scope !== undefined) {
|
|
330
|
+
conditions.push(eq(schema.workspaceInstructionPolicyRevisions.scope, query.scope));
|
|
331
|
+
}
|
|
332
|
+
if (query.roleKey !== undefined) {
|
|
333
|
+
conditions.push(
|
|
334
|
+
query.roleKey === null
|
|
335
|
+
? isNull(schema.workspaceInstructionPolicyRevisions.roleKey)
|
|
336
|
+
: eq(schema.workspaceInstructionPolicyRevisions.roleKey, query.roleKey),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (query.afterRevision !== undefined) {
|
|
340
|
+
conditions.push(lt(schema.workspaceInstructionPolicyRevisions.revision, query.afterRevision));
|
|
341
|
+
}
|
|
342
|
+
return conditions;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function listWorkspaceInstructionPolicyRevisions(
|
|
346
|
+
db: Database,
|
|
347
|
+
workspaceId: string,
|
|
348
|
+
query: WorkspaceInstructionPolicyListQuery,
|
|
349
|
+
): Promise<WorkspaceInstructionPolicyListResponse> {
|
|
350
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
351
|
+
const rows = await scopedDb
|
|
352
|
+
.select()
|
|
353
|
+
.from(schema.workspaceInstructionPolicyRevisions)
|
|
354
|
+
.where(and(...listFilterConditions(workspaceId, query)))
|
|
355
|
+
.orderBy(desc(schema.workspaceInstructionPolicyRevisions.revision))
|
|
356
|
+
.limit(query.limit + 1);
|
|
357
|
+
const hasMore = rows.length > query.limit;
|
|
358
|
+
const page = rows.slice(0, query.limit);
|
|
359
|
+
const [heads, events] = await Promise.all([
|
|
360
|
+
scopedDb
|
|
361
|
+
.select()
|
|
362
|
+
.from(schema.workspaceInstructionPolicyHeads)
|
|
363
|
+
.where(eq(schema.workspaceInstructionPolicyHeads.workspaceId, workspaceId))
|
|
364
|
+
.orderBy(
|
|
365
|
+
asc(schema.workspaceInstructionPolicyHeads.kind),
|
|
366
|
+
asc(schema.workspaceInstructionPolicyHeads.scope),
|
|
367
|
+
asc(schema.workspaceInstructionPolicyHeads.roleKey),
|
|
368
|
+
),
|
|
369
|
+
scopedDb
|
|
370
|
+
.select()
|
|
371
|
+
.from(schema.workspaceInstructionPolicyActivationEvents)
|
|
372
|
+
.where(eq(schema.workspaceInstructionPolicyActivationEvents.workspaceId, workspaceId))
|
|
373
|
+
.orderBy(
|
|
374
|
+
desc(schema.workspaceInstructionPolicyActivationEvents.createdAt),
|
|
375
|
+
desc(schema.workspaceInstructionPolicyActivationEvents.id),
|
|
376
|
+
)
|
|
377
|
+
.limit(query.limit),
|
|
378
|
+
]);
|
|
379
|
+
return {
|
|
380
|
+
revisions: page.map(revisionFromRow),
|
|
381
|
+
activeHeads: heads.map(headFromRow),
|
|
382
|
+
activationEvents: events.map(eventFromRow),
|
|
383
|
+
nextAfterRevision: hasMore ? page.at(-1)!.revision : null,
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function getWorkspaceInstructionPolicyRevision(
|
|
389
|
+
db: Database,
|
|
390
|
+
workspaceId: string,
|
|
391
|
+
revisionId: string,
|
|
392
|
+
): Promise<WorkspaceInstructionPolicyRevision> {
|
|
393
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
394
|
+
const row = await getRevisionInTransaction(scopedDb, workspaceId, revisionId);
|
|
395
|
+
if (!row) throw new WorkspaceInstructionPolicyNotFoundError();
|
|
396
|
+
return revisionFromRow(row);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function splitLines(content: string): string[] {
|
|
401
|
+
return content.length === 0 ? [] : content.split("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Deterministic, dependency-free unified line diff; large inputs degrade to a full replacement. */
|
|
405
|
+
export function diffWorkspaceInstructionPolicyContent(
|
|
406
|
+
from: WorkspaceInstructionPolicyRevision,
|
|
407
|
+
to: WorkspaceInstructionPolicyRevision,
|
|
408
|
+
): string {
|
|
409
|
+
const header = `--- revision-${from.revision}\n+++ revision-${to.revision}\n`;
|
|
410
|
+
if (from.content === to.content) return header;
|
|
411
|
+
const before = splitLines(from.content);
|
|
412
|
+
const after = splitLines(to.content);
|
|
413
|
+
const operations: Array<{ prefix: " " | "+" | "-"; line: string }> = [];
|
|
414
|
+
if ((before.length + 1) * (after.length + 1) <= 1_000_000) {
|
|
415
|
+
const width = after.length + 1;
|
|
416
|
+
const table = new Uint32Array((before.length + 1) * width);
|
|
417
|
+
for (let left = before.length - 1; left >= 0; left -= 1) {
|
|
418
|
+
for (let right = after.length - 1; right >= 0; right -= 1) {
|
|
419
|
+
const offset = left * width + right;
|
|
420
|
+
table[offset] =
|
|
421
|
+
before[left] === after[right]
|
|
422
|
+
? table[(left + 1) * width + right + 1]! + 1
|
|
423
|
+
: Math.max(table[(left + 1) * width + right]!, table[left * width + right + 1]!);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
let left = 0;
|
|
427
|
+
let right = 0;
|
|
428
|
+
while (left < before.length || right < after.length) {
|
|
429
|
+
if (left < before.length && right < after.length && before[left] === after[right]) {
|
|
430
|
+
operations.push({ prefix: " ", line: before[left]! });
|
|
431
|
+
left += 1;
|
|
432
|
+
right += 1;
|
|
433
|
+
} else if (
|
|
434
|
+
right < after.length &&
|
|
435
|
+
(left === before.length ||
|
|
436
|
+
table[left * width + right + 1]! >= table[(left + 1) * width + right]!)
|
|
437
|
+
) {
|
|
438
|
+
operations.push({ prefix: "+", line: after[right]! });
|
|
439
|
+
right += 1;
|
|
440
|
+
} else {
|
|
441
|
+
operations.push({ prefix: "-", line: before[left]! });
|
|
442
|
+
left += 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
operations.push(...before.map((line) => ({ prefix: "-" as const, line })));
|
|
447
|
+
operations.push(...after.map((line) => ({ prefix: "+" as const, line })));
|
|
448
|
+
}
|
|
449
|
+
const hunk = `@@ -1,${before.length} +1,${after.length} @@\n`;
|
|
450
|
+
return `${header}${hunk}${operations.map(({ prefix, line }) => `${prefix}${line}\n`).join("")}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function diffWorkspaceInstructionPolicyRevisions(
|
|
454
|
+
db: Database,
|
|
455
|
+
workspaceId: string,
|
|
456
|
+
input: { fromRevisionId: string; toRevisionId: string },
|
|
457
|
+
): Promise<WorkspaceInstructionPolicyDiffResponse> {
|
|
458
|
+
return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
|
|
459
|
+
const [fromRow, toRow] = await Promise.all([
|
|
460
|
+
getRevisionInTransaction(scopedDb, workspaceId, input.fromRevisionId),
|
|
461
|
+
getRevisionInTransaction(scopedDb, workspaceId, input.toRevisionId),
|
|
462
|
+
]);
|
|
463
|
+
if (!fromRow || !toRow) throw new WorkspaceInstructionPolicyNotFoundError();
|
|
464
|
+
const from = revisionFromRow(fromRow);
|
|
465
|
+
const to = revisionFromRow(toRow);
|
|
466
|
+
if (!sameTarget(from, to)) {
|
|
467
|
+
throw new WorkspaceInstructionPolicyInvalidOperationError(
|
|
468
|
+
"Instruction-policy diffs require revisions from the same target",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return { from, to, format: "unified", diff: diffWorkspaceInstructionPolicyContent(from, to) };
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function changeActiveRevision(
|
|
476
|
+
db: Database,
|
|
477
|
+
input: {
|
|
478
|
+
accountId: string;
|
|
479
|
+
workspaceId: string;
|
|
480
|
+
targetRevisionId: string;
|
|
481
|
+
expectedCurrentRevisionId: string | null;
|
|
482
|
+
actorSubjectId: string;
|
|
483
|
+
reason: string;
|
|
484
|
+
type: WorkspaceInstructionPolicyActivationType;
|
|
485
|
+
},
|
|
486
|
+
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
487
|
+
return await withRlsContext(
|
|
488
|
+
db,
|
|
489
|
+
{ accountId: input.accountId, workspaceId: input.workspaceId },
|
|
490
|
+
async (scopedDb) => {
|
|
491
|
+
// This row lock serializes both an absent-head first activation and later
|
|
492
|
+
// updates, so every loser can return the authoritative typed conflict.
|
|
493
|
+
await lockWorkspace(scopedDb, input);
|
|
494
|
+
const targetRow = await getRevisionInTransaction(
|
|
495
|
+
scopedDb,
|
|
496
|
+
input.workspaceId,
|
|
497
|
+
input.targetRevisionId,
|
|
498
|
+
);
|
|
499
|
+
if (!targetRow) throw new WorkspaceInstructionPolicyNotFoundError();
|
|
500
|
+
const target = revisionFromRow(targetRow);
|
|
501
|
+
const targetIdentity: WorkspaceInstructionPolicyTarget = {
|
|
502
|
+
kind: target.kind,
|
|
503
|
+
scope: target.scope,
|
|
504
|
+
roleKey: target.roleKey,
|
|
505
|
+
};
|
|
506
|
+
const currentRow = await getHeadInTransaction(scopedDb, input.workspaceId, targetIdentity);
|
|
507
|
+
const currentHead = currentRow ? headFromRow(currentRow) : null;
|
|
508
|
+
if ((currentHead?.revisionId ?? null) !== input.expectedCurrentRevisionId) {
|
|
509
|
+
throw new WorkspaceInstructionPolicyConflictError(currentHead);
|
|
510
|
+
}
|
|
511
|
+
if (currentHead?.revisionId === target.id) {
|
|
512
|
+
throw new WorkspaceInstructionPolicyInvalidOperationError(
|
|
513
|
+
"The requested instruction-policy revision is already active",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
if (input.type === "rollback") {
|
|
517
|
+
if (!currentHead) {
|
|
518
|
+
throw new WorkspaceInstructionPolicyInvalidOperationError(
|
|
519
|
+
"A workspace instruction policy with no active head cannot be rolled back",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const [previousActivation] = await scopedDb
|
|
523
|
+
.select({ id: schema.workspaceInstructionPolicyActivationEvents.id })
|
|
524
|
+
.from(schema.workspaceInstructionPolicyActivationEvents)
|
|
525
|
+
.where(
|
|
526
|
+
and(
|
|
527
|
+
...eventTargetConditions(input.workspaceId, targetIdentity),
|
|
528
|
+
eq(schema.workspaceInstructionPolicyActivationEvents.newRevisionId, target.id),
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
.limit(1);
|
|
532
|
+
if (!previousActivation) {
|
|
533
|
+
throw new WorkspaceInstructionPolicyInvalidOperationError(
|
|
534
|
+
"Rollback targets must have been active previously",
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const activationVersion = (currentHead?.activationVersion ?? 0) + 1;
|
|
539
|
+
const createdAt = new Date();
|
|
540
|
+
const [eventRow] = await scopedDb
|
|
541
|
+
.insert(schema.workspaceInstructionPolicyActivationEvents)
|
|
542
|
+
.values({
|
|
543
|
+
accountId: input.accountId,
|
|
544
|
+
workspaceId: input.workspaceId,
|
|
545
|
+
...targetIdentity,
|
|
546
|
+
type: input.type,
|
|
547
|
+
activationVersion,
|
|
548
|
+
oldRevisionId: currentHead?.revisionId ?? null,
|
|
549
|
+
oldRevision: currentHead?.revision ?? null,
|
|
550
|
+
oldContentHash: currentHead?.contentHash ?? null,
|
|
551
|
+
newRevisionId: target.id,
|
|
552
|
+
newRevision: target.revision,
|
|
553
|
+
newContentHash: target.contentHash,
|
|
554
|
+
actorSubjectId: input.actorSubjectId,
|
|
555
|
+
reason: input.reason,
|
|
556
|
+
createdAt,
|
|
557
|
+
})
|
|
558
|
+
.returning();
|
|
559
|
+
if (!eventRow) throw new Error("Instruction-policy activation event was not recorded");
|
|
560
|
+
let headRow: HeadRow | undefined;
|
|
561
|
+
if (currentRow) {
|
|
562
|
+
[headRow] = await scopedDb
|
|
563
|
+
.update(schema.workspaceInstructionPolicyHeads)
|
|
564
|
+
.set({
|
|
565
|
+
revisionId: target.id,
|
|
566
|
+
revision: target.revision,
|
|
567
|
+
contentHash: target.contentHash,
|
|
568
|
+
activationVersion,
|
|
569
|
+
activatedAt: createdAt,
|
|
570
|
+
})
|
|
571
|
+
.where(eq(schema.workspaceInstructionPolicyHeads.id, currentRow.id))
|
|
572
|
+
.returning();
|
|
573
|
+
} else {
|
|
574
|
+
[headRow] = await scopedDb
|
|
575
|
+
.insert(schema.workspaceInstructionPolicyHeads)
|
|
576
|
+
.values({
|
|
577
|
+
accountId: input.accountId,
|
|
578
|
+
workspaceId: input.workspaceId,
|
|
579
|
+
...targetIdentity,
|
|
580
|
+
revisionId: target.id,
|
|
581
|
+
revision: target.revision,
|
|
582
|
+
contentHash: target.contentHash,
|
|
583
|
+
activationVersion,
|
|
584
|
+
activatedAt: createdAt,
|
|
585
|
+
})
|
|
586
|
+
.returning();
|
|
587
|
+
}
|
|
588
|
+
if (!headRow) throw new Error("Instruction-policy activation head was not recorded");
|
|
589
|
+
return { head: headFromRow(headRow), event: eventFromRow(eventRow) };
|
|
590
|
+
},
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function activateWorkspaceInstructionPolicyRevision(
|
|
595
|
+
db: Database,
|
|
596
|
+
input: {
|
|
597
|
+
accountId: string;
|
|
598
|
+
workspaceId: string;
|
|
599
|
+
revisionId: string;
|
|
600
|
+
expectedCurrentRevisionId: string | null;
|
|
601
|
+
actorSubjectId: string;
|
|
602
|
+
reason: string;
|
|
603
|
+
},
|
|
604
|
+
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
605
|
+
return await changeActiveRevision(db, {
|
|
606
|
+
...input,
|
|
607
|
+
targetRevisionId: input.revisionId,
|
|
608
|
+
type: "activate",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export async function rollbackWorkspaceInstructionPolicyRevision(
|
|
613
|
+
db: Database,
|
|
614
|
+
input: {
|
|
615
|
+
accountId: string;
|
|
616
|
+
workspaceId: string;
|
|
617
|
+
targetRevisionId: string;
|
|
618
|
+
expectedCurrentRevisionId: string;
|
|
619
|
+
actorSubjectId: string;
|
|
620
|
+
reason: string;
|
|
621
|
+
},
|
|
622
|
+
): Promise<WorkspaceInstructionPolicyActivationResponse> {
|
|
623
|
+
return await changeActiveRevision(db, { ...input, type: "rollback" });
|
|
624
|
+
}
|