@cat-factory/worker 0.110.1 → 0.111.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/infrastructure/repositories/D1BrainstormSessionRepository.d.ts +14 -4
- package/dist/infrastructure/repositories/D1BrainstormSessionRepository.d.ts.map +1 -1
- package/dist/infrastructure/repositories/D1BrainstormSessionRepository.js +71 -13
- package/dist/infrastructure/repositories/D1BrainstormSessionRepository.js.map +1 -1
- package/dist/infrastructure/repositories/D1ClarityReviewRepository.d.ts +13 -3
- package/dist/infrastructure/repositories/D1ClarityReviewRepository.d.ts.map +1 -1
- package/dist/infrastructure/repositories/D1ClarityReviewRepository.js +68 -12
- package/dist/infrastructure/repositories/D1ClarityReviewRepository.js.map +1 -1
- package/dist/infrastructure/repositories/D1RequirementReviewRepository.d.ts +15 -4
- package/dist/infrastructure/repositories/D1RequirementReviewRepository.d.ts.map +1 -1
- package/dist/infrastructure/repositories/D1RequirementReviewRepository.js +77 -13
- package/dist/infrastructure/repositories/D1RequirementReviewRepository.js.map +1 -1
- package/migrations/0065_review_rev.sql +14 -0
- package/migrations/0066_review_block_unique.sql +64 -0
- package/package.json +18 -18
|
@@ -3,9 +3,11 @@ import type { BrainstormSession, BrainstormStage } from '@cat-factory/contracts'
|
|
|
3
3
|
import type { D1Database } from '@cloudflare/workers-types';
|
|
4
4
|
/**
|
|
5
5
|
* Brainstorm (structured-dialogue) sessions, stored one row per session in
|
|
6
|
-
* `brainstorm_sessions`. The mirror of {@link D1ClarityReviewRepository}
|
|
7
|
-
* (block, stage):
|
|
8
|
-
*
|
|
6
|
+
* `brainstorm_sessions`. The mirror of {@link D1ClarityReviewRepository} — same rev-guarded
|
|
7
|
+
* `compareAndSwap` contract — but keyed by (block, stage): a UNIQUE index on
|
|
8
|
+
* (workspace_id, block_id, stage) (migration 0066) keeps one live session per block+stage, which
|
|
9
|
+
* `replaceForBlockStage` upserts against, so `getByBlockStage` returns it and the block's other
|
|
10
|
+
* stage is never disturbed.
|
|
9
11
|
*/
|
|
10
12
|
export declare class D1BrainstormSessionRepository implements BrainstormSessionRepository {
|
|
11
13
|
private readonly db;
|
|
@@ -14,7 +16,15 @@ export declare class D1BrainstormSessionRepository implements BrainstormSessionR
|
|
|
14
16
|
});
|
|
15
17
|
getByBlockStage(workspaceId: string, blockId: string, stage: BrainstormStage): Promise<BrainstormSession | null>;
|
|
16
18
|
get(workspaceId: string, id: string): Promise<BrainstormSession | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Force-write by session id. A fresh insert starts at rev 0; a write over an existing row BUMPS
|
|
21
|
+
* it, so a concurrent compareAndSwap holding the old revision still detects that the row moved.
|
|
22
|
+
* Writing a DIFFERENT id onto a block+stage that already has a session now violates the UNIQUE
|
|
23
|
+
* index (migration 0066) — loudly, which is the point: only {@link replaceForBlockStage} may
|
|
24
|
+
* change which session is a block+stage's live one.
|
|
25
|
+
*/
|
|
17
26
|
upsert(workspaceId: string, session: BrainstormSession): Promise<void>;
|
|
18
|
-
|
|
27
|
+
compareAndSwap(workspaceId: string, session: BrainstormSession): Promise<boolean>;
|
|
28
|
+
replaceForBlockStage(workspaceId: string, session: BrainstormSession): Promise<void>;
|
|
19
29
|
}
|
|
20
30
|
//# sourceMappingURL=D1BrainstormSessionRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1BrainstormSessionRepository.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1BrainstormSessionRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AACtE,OAAO,KAAK,EAAkB,iBAAiB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAChG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"D1BrainstormSessionRepository.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1BrainstormSessionRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AACtE,OAAO,KAAK,EAAkB,iBAAiB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAChG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAyC3D;;;;;;;GAOG;AACH,qBAAa,6BAA8B,YAAW,2BAA2B;IAC/E,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAE/B,YAAY,EAAE,EAAE,EAAE,EAAE;QAAE,EAAE,EAAE,UAAU,CAAA;KAAE,EAErC;IAEK,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAUnC;IAEK,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAM5E;IAED;;;;;;OAMG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC3E;IAEK,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAqCtF;IAEK,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCzF;CACF"}
|
|
@@ -18,15 +18,18 @@ function rowToSession(row) {
|
|
|
18
18
|
convergedDirection: row.converged_direction,
|
|
19
19
|
iteration: row.iteration,
|
|
20
20
|
maxIterations: row.max_iterations,
|
|
21
|
+
rev: row.rev ?? 0,
|
|
21
22
|
createdAt: row.created_at,
|
|
22
23
|
updatedAt: row.updated_at,
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Brainstorm (structured-dialogue) sessions, stored one row per session in
|
|
27
|
-
* `brainstorm_sessions`. The mirror of {@link D1ClarityReviewRepository}
|
|
28
|
-
* (block, stage):
|
|
29
|
-
*
|
|
28
|
+
* `brainstorm_sessions`. The mirror of {@link D1ClarityReviewRepository} — same rev-guarded
|
|
29
|
+
* `compareAndSwap` contract — but keyed by (block, stage): a UNIQUE index on
|
|
30
|
+
* (workspace_id, block_id, stage) (migration 0066) keeps one live session per block+stage, which
|
|
31
|
+
* `replaceForBlockStage` upserts against, so `getByBlockStage` returns it and the block's other
|
|
32
|
+
* stage is never disturbed.
|
|
30
33
|
*/
|
|
31
34
|
export class D1BrainstormSessionRepository {
|
|
32
35
|
db;
|
|
@@ -49,12 +52,19 @@ export class D1BrainstormSessionRepository {
|
|
|
49
52
|
.first();
|
|
50
53
|
return row ? rowToSession(row) : null;
|
|
51
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Force-write by session id. A fresh insert starts at rev 0; a write over an existing row BUMPS
|
|
57
|
+
* it, so a concurrent compareAndSwap holding the old revision still detects that the row moved.
|
|
58
|
+
* Writing a DIFFERENT id onto a block+stage that already has a session now violates the UNIQUE
|
|
59
|
+
* index (migration 0066) — loudly, which is the point: only {@link replaceForBlockStage} may
|
|
60
|
+
* change which session is a block+stage's live one.
|
|
61
|
+
*/
|
|
52
62
|
async upsert(workspaceId, session) {
|
|
53
|
-
await this.db
|
|
63
|
+
const row = await this.db
|
|
54
64
|
.prepare(`INSERT INTO brainstorm_sessions
|
|
55
65
|
(workspace_id, id, block_id, stage, status, items, model, converged_direction,
|
|
56
|
-
iteration, max_iterations, created_at, updated_at)
|
|
57
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
66
|
+
iteration, max_iterations, rev, created_at, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
58
68
|
ON CONFLICT (workspace_id, id) DO UPDATE SET
|
|
59
69
|
block_id = excluded.block_id,
|
|
60
70
|
stage = excluded.stage,
|
|
@@ -64,15 +74,63 @@ export class D1BrainstormSessionRepository {
|
|
|
64
74
|
converged_direction = excluded.converged_direction,
|
|
65
75
|
iteration = excluded.iteration,
|
|
66
76
|
max_iterations = excluded.max_iterations,
|
|
67
|
-
|
|
77
|
+
rev = brainstorm_sessions.rev + 1,
|
|
78
|
+
updated_at = excluded.updated_at
|
|
79
|
+
RETURNING rev`)
|
|
68
80
|
.bind(workspaceId, session.id, session.blockId, session.stage, session.status, JSON.stringify(session.items), session.model, session.convergedDirection, session.iteration ?? 1, session.maxIterations ?? 1, session.createdAt, session.updatedAt)
|
|
69
|
-
.
|
|
81
|
+
.first();
|
|
82
|
+
if (row)
|
|
83
|
+
session.rev = row.rev;
|
|
70
84
|
}
|
|
71
|
-
async
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
85
|
+
async compareAndSwap(workspaceId, session) {
|
|
86
|
+
// Conditional update guarded on the rev last read onto this session; writes only while the
|
|
87
|
+
// stored row is unchanged, and never inserts (a deleted session must stay deleted).
|
|
88
|
+
const row = await this.db
|
|
89
|
+
.prepare(`UPDATE brainstorm_sessions SET
|
|
90
|
+
block_id = ?,
|
|
91
|
+
stage = ?,
|
|
92
|
+
status = ?,
|
|
93
|
+
items = ?,
|
|
94
|
+
model = ?,
|
|
95
|
+
converged_direction = ?,
|
|
96
|
+
iteration = ?,
|
|
97
|
+
max_iterations = ?,
|
|
98
|
+
updated_at = ?,
|
|
99
|
+
rev = rev + 1
|
|
100
|
+
WHERE workspace_id = ? AND id = ? AND rev = ?
|
|
101
|
+
RETURNING rev`)
|
|
102
|
+
.bind(session.blockId, session.stage, session.status, JSON.stringify(session.items), session.model, session.convergedDirection, session.iteration ?? 1, session.maxIterations ?? 1, session.updatedAt, workspaceId, session.id, session.rev ?? 0)
|
|
103
|
+
.first();
|
|
104
|
+
if (!row)
|
|
105
|
+
return false;
|
|
106
|
+
session.rev = row.rev;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
async replaceForBlockStage(workspaceId, session) {
|
|
110
|
+
// ONE conflict-targeted upsert on the (block, STAGE) UNIQUE key (migration 0066), NOT a
|
|
111
|
+
// delete-then-insert pair — see {@link D1RequirementReviewRepository.replaceForBlock} for why
|
|
112
|
+
// a transaction around that pair is not enough. The stage is part of the conflict target, so
|
|
113
|
+
// the block's other stage is untouched by construction rather than by a scoped delete.
|
|
114
|
+
const row = await this.db
|
|
115
|
+
.prepare(`INSERT INTO brainstorm_sessions
|
|
116
|
+
(workspace_id, id, block_id, stage, status, items, model, converged_direction,
|
|
117
|
+
iteration, max_iterations, rev, created_at, updated_at)
|
|
118
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
119
|
+
ON CONFLICT (workspace_id, block_id, stage) DO UPDATE SET
|
|
120
|
+
id = excluded.id,
|
|
121
|
+
status = excluded.status,
|
|
122
|
+
items = excluded.items,
|
|
123
|
+
model = excluded.model,
|
|
124
|
+
converged_direction = excluded.converged_direction,
|
|
125
|
+
iteration = excluded.iteration,
|
|
126
|
+
max_iterations = excluded.max_iterations,
|
|
127
|
+
rev = 0,
|
|
128
|
+
created_at = excluded.created_at,
|
|
129
|
+
updated_at = excluded.updated_at
|
|
130
|
+
RETURNING rev`)
|
|
131
|
+
.bind(workspaceId, session.id, session.blockId, session.stage, session.status, JSON.stringify(session.items), session.model, session.convergedDirection, session.iteration ?? 1, session.maxIterations ?? 1, session.createdAt, session.updatedAt)
|
|
132
|
+
.first();
|
|
133
|
+
session.rev = row?.rev ?? 0;
|
|
76
134
|
}
|
|
77
135
|
}
|
|
78
136
|
//# sourceMappingURL=D1BrainstormSessionRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1BrainstormSessionRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1BrainstormSessionRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"D1BrainstormSessionRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1BrainstormSessionRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,8 +3,10 @@ import type { ClarityReview } from '@cat-factory/contracts';
|
|
|
3
3
|
import type { D1Database } from '@cloudflare/workers-types';
|
|
4
4
|
/**
|
|
5
5
|
* Clarity (bug-report triage) reviews, stored one row per review in `clarity_reviews`.
|
|
6
|
-
* The mirror of {@link D1RequirementReviewRepository}
|
|
7
|
-
*
|
|
6
|
+
* The mirror of {@link D1RequirementReviewRepository}, down to the concurrency contract: items
|
|
7
|
+
* live as a JSON array, a UNIQUE index on (workspace_id, block_id) keeps exactly one live review
|
|
8
|
+
* per block (which `replaceForBlock` upserts against), and every read-modify-write rides the
|
|
9
|
+
* rev-guarded `compareAndSwap`.
|
|
8
10
|
*/
|
|
9
11
|
export declare class D1ClarityReviewRepository implements ClarityReviewRepository {
|
|
10
12
|
private readonly db;
|
|
@@ -13,7 +15,15 @@ export declare class D1ClarityReviewRepository implements ClarityReviewRepositor
|
|
|
13
15
|
});
|
|
14
16
|
getByBlock(workspaceId: string, blockId: string): Promise<ClarityReview | null>;
|
|
15
17
|
get(workspaceId: string, id: string): Promise<ClarityReview | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Force-write by review id. A fresh insert starts at rev 0; a write over an existing row BUMPS
|
|
20
|
+
* it, so a concurrent compareAndSwap holding the old revision still detects that the row moved.
|
|
21
|
+
* Writing a DIFFERENT id onto a block that already has a review now violates the block's UNIQUE
|
|
22
|
+
* index (migration 0066) — loudly, which is the point: only {@link replaceForBlock} may change
|
|
23
|
+
* which review is a block's live one.
|
|
24
|
+
*/
|
|
16
25
|
upsert(workspaceId: string, review: ClarityReview): Promise<void>;
|
|
17
|
-
|
|
26
|
+
compareAndSwap(workspaceId: string, review: ClarityReview): Promise<boolean>;
|
|
27
|
+
replaceForBlock(workspaceId: string, review: ClarityReview): Promise<void>;
|
|
18
28
|
}
|
|
19
29
|
//# sourceMappingURL=D1ClarityReviewRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1ClarityReviewRepository.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1ClarityReviewRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,wBAAwB,CAAA;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"D1ClarityReviewRepository.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1ClarityReviewRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,wBAAwB,CAAA;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAuC3D;;;;;;GAMG;AACH,qBAAa,yBAA0B,YAAW,uBAAuB;IACvE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAE/B,YAAY,EAAE,EAAE,EAAE,EAAE;QAAE,EAAE,EAAE,UAAU,CAAA;KAAE,EAErC;IAEK,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAUpF;IAEK,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAMxE;IAED;;;;;;OAMG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCtE;IAEK,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAmCjF;IAEK,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAsC/E;CACF"}
|
|
@@ -17,14 +17,17 @@ function rowToReview(row) {
|
|
|
17
17
|
clarifiedReport: row.clarified_report,
|
|
18
18
|
iteration: row.iteration,
|
|
19
19
|
maxIterations: row.max_iterations,
|
|
20
|
+
rev: row.rev ?? 0,
|
|
20
21
|
createdAt: row.created_at,
|
|
21
22
|
updatedAt: row.updated_at,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Clarity (bug-report triage) reviews, stored one row per review in `clarity_reviews`.
|
|
26
|
-
* The mirror of {@link D1RequirementReviewRepository}
|
|
27
|
-
*
|
|
27
|
+
* The mirror of {@link D1RequirementReviewRepository}, down to the concurrency contract: items
|
|
28
|
+
* live as a JSON array, a UNIQUE index on (workspace_id, block_id) keeps exactly one live review
|
|
29
|
+
* per block (which `replaceForBlock` upserts against), and every read-modify-write rides the
|
|
30
|
+
* rev-guarded `compareAndSwap`.
|
|
28
31
|
*/
|
|
29
32
|
export class D1ClarityReviewRepository {
|
|
30
33
|
db;
|
|
@@ -47,12 +50,19 @@ export class D1ClarityReviewRepository {
|
|
|
47
50
|
.first();
|
|
48
51
|
return row ? rowToReview(row) : null;
|
|
49
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Force-write by review id. A fresh insert starts at rev 0; a write over an existing row BUMPS
|
|
55
|
+
* it, so a concurrent compareAndSwap holding the old revision still detects that the row moved.
|
|
56
|
+
* Writing a DIFFERENT id onto a block that already has a review now violates the block's UNIQUE
|
|
57
|
+
* index (migration 0066) — loudly, which is the point: only {@link replaceForBlock} may change
|
|
58
|
+
* which review is a block's live one.
|
|
59
|
+
*/
|
|
50
60
|
async upsert(workspaceId, review) {
|
|
51
|
-
await this.db
|
|
61
|
+
const row = await this.db
|
|
52
62
|
.prepare(`INSERT INTO clarity_reviews
|
|
53
63
|
(workspace_id, id, block_id, status, items, model, clarified_report,
|
|
54
|
-
iteration, max_iterations, created_at, updated_at)
|
|
55
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
64
|
+
iteration, max_iterations, rev, created_at, updated_at)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
56
66
|
ON CONFLICT (workspace_id, id) DO UPDATE SET
|
|
57
67
|
block_id = excluded.block_id,
|
|
58
68
|
status = excluded.status,
|
|
@@ -61,15 +71,61 @@ export class D1ClarityReviewRepository {
|
|
|
61
71
|
clarified_report = excluded.clarified_report,
|
|
62
72
|
iteration = excluded.iteration,
|
|
63
73
|
max_iterations = excluded.max_iterations,
|
|
64
|
-
|
|
74
|
+
rev = clarity_reviews.rev + 1,
|
|
75
|
+
updated_at = excluded.updated_at
|
|
76
|
+
RETURNING rev`)
|
|
65
77
|
.bind(workspaceId, review.id, review.blockId, review.status, JSON.stringify(review.items), review.model, review.clarifiedReport, review.iteration ?? 1, review.maxIterations ?? 1, review.createdAt, review.updatedAt)
|
|
66
|
-
.
|
|
78
|
+
.first();
|
|
79
|
+
if (row)
|
|
80
|
+
review.rev = row.rev;
|
|
67
81
|
}
|
|
68
|
-
async
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
82
|
+
async compareAndSwap(workspaceId, review) {
|
|
83
|
+
// Conditional update guarded on the rev last read onto this review; writes only while the
|
|
84
|
+
// stored row is unchanged, and never inserts (a deleted review must stay deleted).
|
|
85
|
+
const row = await this.db
|
|
86
|
+
.prepare(`UPDATE clarity_reviews SET
|
|
87
|
+
block_id = ?,
|
|
88
|
+
status = ?,
|
|
89
|
+
items = ?,
|
|
90
|
+
model = ?,
|
|
91
|
+
clarified_report = ?,
|
|
92
|
+
iteration = ?,
|
|
93
|
+
max_iterations = ?,
|
|
94
|
+
updated_at = ?,
|
|
95
|
+
rev = rev + 1
|
|
96
|
+
WHERE workspace_id = ? AND id = ? AND rev = ?
|
|
97
|
+
RETURNING rev`)
|
|
98
|
+
.bind(review.blockId, review.status, JSON.stringify(review.items), review.model, review.clarifiedReport, review.iteration ?? 1, review.maxIterations ?? 1, review.updatedAt, workspaceId, review.id, review.rev ?? 0)
|
|
99
|
+
.first();
|
|
100
|
+
if (!row)
|
|
101
|
+
return false;
|
|
102
|
+
review.rev = row.rev;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
async replaceForBlock(workspaceId, review) {
|
|
106
|
+
// ONE conflict-targeted upsert on the block's UNIQUE key (migration 0066), NOT a
|
|
107
|
+
// delete-then-insert pair — see {@link D1RequirementReviewRepository.replaceForBlock} for why
|
|
108
|
+
// a transaction around that pair is not enough.
|
|
109
|
+
const row = await this.db
|
|
110
|
+
.prepare(`INSERT INTO clarity_reviews
|
|
111
|
+
(workspace_id, id, block_id, status, items, model, clarified_report,
|
|
112
|
+
iteration, max_iterations, rev, created_at, updated_at)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
114
|
+
ON CONFLICT (workspace_id, block_id) DO UPDATE SET
|
|
115
|
+
id = excluded.id,
|
|
116
|
+
status = excluded.status,
|
|
117
|
+
items = excluded.items,
|
|
118
|
+
model = excluded.model,
|
|
119
|
+
clarified_report = excluded.clarified_report,
|
|
120
|
+
iteration = excluded.iteration,
|
|
121
|
+
max_iterations = excluded.max_iterations,
|
|
122
|
+
rev = 0,
|
|
123
|
+
created_at = excluded.created_at,
|
|
124
|
+
updated_at = excluded.updated_at
|
|
125
|
+
RETURNING rev`)
|
|
126
|
+
.bind(workspaceId, review.id, review.blockId, review.status, JSON.stringify(review.items), review.model, review.clarifiedReport, review.iteration ?? 1, review.maxIterations ?? 1, review.createdAt, review.updatedAt)
|
|
127
|
+
.first();
|
|
128
|
+
review.rev = row?.rev ?? 0;
|
|
73
129
|
}
|
|
74
130
|
}
|
|
75
131
|
//# sourceMappingURL=D1ClarityReviewRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1ClarityReviewRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1ClarityReviewRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"D1ClarityReviewRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1ClarityReviewRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,9 +3,12 @@ import type { RequirementReview } from '@cat-factory/contracts';
|
|
|
3
3
|
import type { D1Database } from '@cloudflare/workers-types';
|
|
4
4
|
/**
|
|
5
5
|
* Requirements reviews, stored one row per review in `requirement_reviews`
|
|
6
|
-
* (migration 0021). The reviewed items live as a JSON array in `items`;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* (migration 0021). The reviewed items live as a JSON array in `items`; a block holds at most
|
|
7
|
+
* ONE live review — a UNIQUE index on (workspace_id, block_id) enforces that (migration 0066),
|
|
8
|
+
* and {@link replaceForBlock} publishes a fresh one as a single upsert against it — so
|
|
9
|
+
* `getByBlock` returns it. Every read-modify-write rides the rev-guarded {@link compareAndSwap}
|
|
10
|
+
* (migration 0065): the whole row is one blob, so a blind write would drop a concurrent editor's
|
|
11
|
+
* answer.
|
|
9
12
|
*/
|
|
10
13
|
export declare class D1RequirementReviewRepository implements RequirementReviewRepository {
|
|
11
14
|
private readonly db;
|
|
@@ -14,7 +17,15 @@ export declare class D1RequirementReviewRepository implements RequirementReviewR
|
|
|
14
17
|
});
|
|
15
18
|
getByBlock(workspaceId: string, blockId: string): Promise<RequirementReview | null>;
|
|
16
19
|
get(workspaceId: string, id: string): Promise<RequirementReview | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Force-write by review id. A fresh insert starts at rev 0; a write over an existing row BUMPS
|
|
22
|
+
* it, so a concurrent compareAndSwap holding the old revision still detects that the row moved.
|
|
23
|
+
* Writing a DIFFERENT id onto a block that already has a review now violates the block's UNIQUE
|
|
24
|
+
* index (migration 0066) — loudly, which is the point: only {@link replaceForBlock} may change
|
|
25
|
+
* which review is a block's live one.
|
|
26
|
+
*/
|
|
17
27
|
upsert(workspaceId: string, review: RequirementReview): Promise<void>;
|
|
18
|
-
|
|
28
|
+
compareAndSwap(workspaceId: string, review: RequirementReview): Promise<boolean>;
|
|
29
|
+
replaceForBlock(workspaceId: string, review: RequirementReview): Promise<void>;
|
|
19
30
|
}
|
|
20
31
|
//# sourceMappingURL=D1RequirementReviewRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1RequirementReviewRepository.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1RequirementReviewRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AACtE,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"D1RequirementReviewRepository.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1RequirementReviewRepository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AACtE,OAAO,KAAK,EAEV,iBAAiB,EAElB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AA4C3D;;;;;;;;GAQG;AACH,qBAAa,6BAA8B,YAAW,2BAA2B;IAC/E,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAE/B,YAAY,EAAE,EAAE,EAAE,EAAE;QAAE,EAAE,EAAE,UAAU,CAAA;KAAE,EAErC;IAEK,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAUxF;IAEK,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAM5E;IAED;;;;;;OAMG;IACG,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC1E;IAEK,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAsCrF;IAEK,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CnF;CACF"}
|
|
@@ -20,15 +20,19 @@ function rowToReview(row) {
|
|
|
20
20
|
iteration: row.iteration,
|
|
21
21
|
maxIterations: row.max_iterations,
|
|
22
22
|
recommendations: parseJsonArray(row.recommendations),
|
|
23
|
+
rev: row.rev ?? 0,
|
|
23
24
|
createdAt: row.created_at,
|
|
24
25
|
updatedAt: row.updated_at,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
29
|
* Requirements reviews, stored one row per review in `requirement_reviews`
|
|
29
|
-
* (migration 0021). The reviewed items live as a JSON array in `items`;
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* (migration 0021). The reviewed items live as a JSON array in `items`; a block holds at most
|
|
31
|
+
* ONE live review — a UNIQUE index on (workspace_id, block_id) enforces that (migration 0066),
|
|
32
|
+
* and {@link replaceForBlock} publishes a fresh one as a single upsert against it — so
|
|
33
|
+
* `getByBlock` returns it. Every read-modify-write rides the rev-guarded {@link compareAndSwap}
|
|
34
|
+
* (migration 0065): the whole row is one blob, so a blind write would drop a concurrent editor's
|
|
35
|
+
* answer.
|
|
32
36
|
*/
|
|
33
37
|
export class D1RequirementReviewRepository {
|
|
34
38
|
db;
|
|
@@ -51,12 +55,19 @@ export class D1RequirementReviewRepository {
|
|
|
51
55
|
.first();
|
|
52
56
|
return row ? rowToReview(row) : null;
|
|
53
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Force-write by review id. A fresh insert starts at rev 0; a write over an existing row BUMPS
|
|
60
|
+
* it, so a concurrent compareAndSwap holding the old revision still detects that the row moved.
|
|
61
|
+
* Writing a DIFFERENT id onto a block that already has a review now violates the block's UNIQUE
|
|
62
|
+
* index (migration 0066) — loudly, which is the point: only {@link replaceForBlock} may change
|
|
63
|
+
* which review is a block's live one.
|
|
64
|
+
*/
|
|
54
65
|
async upsert(workspaceId, review) {
|
|
55
|
-
await this.db
|
|
66
|
+
const row = await this.db
|
|
56
67
|
.prepare(`INSERT INTO requirement_reviews
|
|
57
68
|
(workspace_id, id, block_id, status, items, model, incorporated_requirements,
|
|
58
|
-
iteration, max_iterations, recommendations, created_at, updated_at)
|
|
59
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
69
|
+
iteration, max_iterations, recommendations, rev, created_at, updated_at)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
60
71
|
ON CONFLICT (workspace_id, id) DO UPDATE SET
|
|
61
72
|
block_id = excluded.block_id,
|
|
62
73
|
status = excluded.status,
|
|
@@ -66,15 +77,68 @@ export class D1RequirementReviewRepository {
|
|
|
66
77
|
iteration = excluded.iteration,
|
|
67
78
|
max_iterations = excluded.max_iterations,
|
|
68
79
|
recommendations = excluded.recommendations,
|
|
69
|
-
|
|
80
|
+
rev = requirement_reviews.rev + 1,
|
|
81
|
+
updated_at = excluded.updated_at
|
|
82
|
+
RETURNING rev`)
|
|
70
83
|
.bind(workspaceId, review.id, review.blockId, review.status, JSON.stringify(review.items), review.model, review.incorporatedRequirements, review.iteration ?? 1, review.maxIterations ?? 1, JSON.stringify(review.recommendations ?? []), review.createdAt, review.updatedAt)
|
|
71
|
-
.
|
|
84
|
+
.first();
|
|
85
|
+
if (row)
|
|
86
|
+
review.rev = row.rev;
|
|
72
87
|
}
|
|
73
|
-
async
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
async compareAndSwap(workspaceId, review) {
|
|
89
|
+
// Conditional update guarded on the rev last read onto this review; writes only while the
|
|
90
|
+
// stored row is unchanged, and never inserts (a deleted review must stay deleted).
|
|
91
|
+
const expected = review.rev ?? 0;
|
|
92
|
+
const row = await this.db
|
|
93
|
+
.prepare(`UPDATE requirement_reviews SET
|
|
94
|
+
block_id = ?,
|
|
95
|
+
status = ?,
|
|
96
|
+
items = ?,
|
|
97
|
+
model = ?,
|
|
98
|
+
incorporated_requirements = ?,
|
|
99
|
+
iteration = ?,
|
|
100
|
+
max_iterations = ?,
|
|
101
|
+
recommendations = ?,
|
|
102
|
+
updated_at = ?,
|
|
103
|
+
rev = rev + 1
|
|
104
|
+
WHERE workspace_id = ? AND id = ? AND rev = ?
|
|
105
|
+
RETURNING rev`)
|
|
106
|
+
.bind(review.blockId, review.status, JSON.stringify(review.items), review.model, review.incorporatedRequirements, review.iteration ?? 1, review.maxIterations ?? 1, JSON.stringify(review.recommendations ?? []), review.updatedAt, workspaceId, review.id, expected)
|
|
107
|
+
.first();
|
|
108
|
+
if (!row)
|
|
109
|
+
return false;
|
|
110
|
+
review.rev = row.rev;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
async replaceForBlock(workspaceId, review) {
|
|
114
|
+
// ONE conflict-targeted upsert on the block's UNIQUE key (migration 0066), NOT a
|
|
115
|
+
// delete-then-insert pair: there is no window between two statements for a second review run
|
|
116
|
+
// to interleave into, on either runtime. The predecessor's row IS this row — its `id` is
|
|
117
|
+
// overwritten — so the superseded review is gone rather than left beside the live one, and
|
|
118
|
+
// `rev` restarts at 0 so a writer still holding the predecessor's revision can't match.
|
|
119
|
+
const row = await this.db
|
|
120
|
+
.prepare(`INSERT INTO requirement_reviews
|
|
121
|
+
(workspace_id, id, block_id, status, items, model, incorporated_requirements,
|
|
122
|
+
iteration, max_iterations, recommendations, rev, created_at, updated_at)
|
|
123
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
124
|
+
ON CONFLICT (workspace_id, block_id) DO UPDATE SET
|
|
125
|
+
id = excluded.id,
|
|
126
|
+
status = excluded.status,
|
|
127
|
+
items = excluded.items,
|
|
128
|
+
model = excluded.model,
|
|
129
|
+
incorporated_requirements = excluded.incorporated_requirements,
|
|
130
|
+
iteration = excluded.iteration,
|
|
131
|
+
max_iterations = excluded.max_iterations,
|
|
132
|
+
recommendations = excluded.recommendations,
|
|
133
|
+
rev = 0,
|
|
134
|
+
created_at = excluded.created_at,
|
|
135
|
+
updated_at = excluded.updated_at
|
|
136
|
+
RETURNING rev`)
|
|
137
|
+
.bind(workspaceId, review.id, review.blockId, review.status, JSON.stringify(review.items), review.model, review.incorporatedRequirements, review.iteration ?? 1, review.maxIterations ?? 1, JSON.stringify(review.recommendations ?? []), review.createdAt, review.updatedAt)
|
|
138
|
+
.first();
|
|
139
|
+
// Read the revision back rather than assuming it: the caller keeps mutating this object
|
|
140
|
+
// through `compareAndSwap`, so a `rev` it only believes is right desynchronises it.
|
|
141
|
+
review.rev = row?.rev ?? 0;
|
|
78
142
|
}
|
|
79
143
|
}
|
|
80
144
|
//# sourceMappingURL=D1RequirementReviewRepository.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"D1RequirementReviewRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1RequirementReviewRepository.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"D1RequirementReviewRepository.js","sourceRoot":"","sources":["../../../src/infrastructure/repositories/D1RequirementReviewRepository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Optimistic concurrency for the iterative-review stores (race-condition audit 2.5).
|
|
2
|
+
--
|
|
3
|
+
-- A requirements / clarity / brainstorm review is ONE JSON blob holding every finding, and every
|
|
4
|
+
-- mutation used to load it, edit one item, and write the whole row back. Two writers in that
|
|
5
|
+
-- window (two people answering different findings, a dismissal racing the durable driver's
|
|
6
|
+
-- incorporation pass) left only the last writer's edit — and because incorporation refuses to run
|
|
7
|
+
-- while any finding is still `open`, a lost dismissal wedged the loop on a phantom open item.
|
|
8
|
+
--
|
|
9
|
+
-- `rev` is the monotonic token every read-modify-write now CASes on: the store writes only while
|
|
10
|
+
-- the row still carries the revision the caller loaded, so a lost race reloads and re-applies
|
|
11
|
+
-- instead of clobbering. Existing rows start at 0, which is exactly what an unset column reads as.
|
|
12
|
+
ALTER TABLE requirement_reviews ADD COLUMN rev INTEGER NOT NULL DEFAULT 0;
|
|
13
|
+
ALTER TABLE clarity_reviews ADD COLUMN rev INTEGER NOT NULL DEFAULT 0;
|
|
14
|
+
ALTER TABLE brainstorm_sessions ADD COLUMN rev INTEGER NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- One live review per block, enforced by the DATABASE (race-condition audit 2.5, follow-up).
|
|
2
|
+
--
|
|
3
|
+
-- Migration 0065 gave the review stores a `rev` + `compareAndSwap`, and the service publishes a
|
|
4
|
+
-- fresh review through an atomic replace. But "atomic" was carried by a transaction wrapping a
|
|
5
|
+
-- DELETE-then-INSERT, and a transaction is not a uniqueness constraint: under Postgres' default
|
|
6
|
+
-- READ COMMITTED a DELETE takes no predicate lock, so two review runs for one block both delete
|
|
7
|
+
-- nothing (or each other's already-gone row), both insert, and both commit — leaving TWO live
|
|
8
|
+
-- reviews, which is exactly the hazard the replace was introduced to close. SQLite serializes
|
|
9
|
+
-- writers so D1 happened to be safe; that is a runtime accident, not a guarantee, and the two
|
|
10
|
+
-- facades must not disagree about a domain invariant.
|
|
11
|
+
--
|
|
12
|
+
-- The invariant is therefore expressed where it belongs: a UNIQUE index on the block key. With it
|
|
13
|
+
-- the replace becomes a single conflict-targeted upsert (no delete, nothing to interleave) on both
|
|
14
|
+
-- runtimes, and any future writer that tries to add a second live review fails loudly instead of
|
|
15
|
+
-- silently splitting the block's review in two.
|
|
16
|
+
--
|
|
17
|
+
-- Self-healing, like every constraint-adding migration here: pre-existing duplicates (rows a
|
|
18
|
+
-- delete/insert pair already interleaved into existence) are deleted before the index is created,
|
|
19
|
+
-- newest `created_at` winning — the same row `getByBlock` returns today, so the surviving review is
|
|
20
|
+
-- the one the product is already showing. Dropping the superseded duplicates is correct: they were
|
|
21
|
+
-- never reachable.
|
|
22
|
+
|
|
23
|
+
DELETE FROM requirement_reviews
|
|
24
|
+
WHERE rowid NOT IN (
|
|
25
|
+
SELECT rowid FROM requirement_reviews r
|
|
26
|
+
WHERE r.rowid = (
|
|
27
|
+
SELECT rowid FROM requirement_reviews x
|
|
28
|
+
WHERE x.workspace_id = r.workspace_id AND x.block_id = r.block_id
|
|
29
|
+
ORDER BY x.created_at DESC, x.rowid DESC
|
|
30
|
+
LIMIT 1
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
DROP INDEX IF EXISTS idx_requirement_reviews_block;
|
|
34
|
+
CREATE UNIQUE INDEX idx_requirement_reviews_block
|
|
35
|
+
ON requirement_reviews (workspace_id, block_id);
|
|
36
|
+
|
|
37
|
+
DELETE FROM clarity_reviews
|
|
38
|
+
WHERE rowid NOT IN (
|
|
39
|
+
SELECT rowid FROM clarity_reviews r
|
|
40
|
+
WHERE r.rowid = (
|
|
41
|
+
SELECT rowid FROM clarity_reviews x
|
|
42
|
+
WHERE x.workspace_id = r.workspace_id AND x.block_id = r.block_id
|
|
43
|
+
ORDER BY x.created_at DESC, x.rowid DESC
|
|
44
|
+
LIMIT 1
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
DROP INDEX IF EXISTS idx_clarity_reviews_block;
|
|
48
|
+
CREATE UNIQUE INDEX idx_clarity_reviews_block ON clarity_reviews (workspace_id, block_id);
|
|
49
|
+
|
|
50
|
+
-- Brainstorm is keyed by (block, stage): a block legitimately holds one live `requirements` and
|
|
51
|
+
-- one live `architecture` session at once, so the stage is part of the uniqueness key.
|
|
52
|
+
DELETE FROM brainstorm_sessions
|
|
53
|
+
WHERE rowid NOT IN (
|
|
54
|
+
SELECT rowid FROM brainstorm_sessions r
|
|
55
|
+
WHERE r.rowid = (
|
|
56
|
+
SELECT rowid FROM brainstorm_sessions x
|
|
57
|
+
WHERE x.workspace_id = r.workspace_id AND x.block_id = r.block_id AND x.stage = r.stage
|
|
58
|
+
ORDER BY x.created_at DESC, x.rowid DESC
|
|
59
|
+
LIMIT 1
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
DROP INDEX IF EXISTS idx_brainstorm_sessions_block_stage;
|
|
63
|
+
CREATE UNIQUE INDEX idx_brainstorm_sessions_block_stage
|
|
64
|
+
ON brainstorm_sessions (workspace_id, block_id, stage);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.0",
|
|
4
4
|
"description": "Reusable Cloudflare Worker library (Hono + D1) exposing the Agent Architecture Board core: the `createApp()` Hono factory, the default fetch/scheduled/queue handler, and the Durable Object + Workflow classes. Deployments (see deploy/backend) supply the wrangler.toml + config and re-export these.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,22 +42,22 @@
|
|
|
42
42
|
"pino": "^10.3.1",
|
|
43
43
|
"valibot": "^1.4.2",
|
|
44
44
|
"workers-ai-provider": "^4.0.0",
|
|
45
|
-
"@cat-factory/agents": "0.75.
|
|
46
|
-
"@cat-factory/caching": "0.10.
|
|
47
|
-
"@cat-factory/consensus": "0.11.
|
|
48
|
-
"@cat-factory/contracts": "0.
|
|
49
|
-
"@cat-factory/eks": "0.1.
|
|
50
|
-
"@cat-factory/gates": "0.7.
|
|
51
|
-
"@cat-factory/gitlab": "0.13.
|
|
52
|
-
"@cat-factory/integrations": "0.103.
|
|
53
|
-
"@cat-factory/kernel": "0.
|
|
54
|
-
"@cat-factory/observability-langfuse": "0.7.
|
|
55
|
-
"@cat-factory/observability-otel": "0.2.
|
|
56
|
-
"@cat-factory/orchestration": "0.
|
|
57
|
-
"@cat-factory/prompt-fragments": "0.15.
|
|
58
|
-
"@cat-factory/provider-cloudflare": "0.7.
|
|
59
|
-
"@cat-factory/server": "0.
|
|
60
|
-
"@cat-factory/spend": "0.12.
|
|
45
|
+
"@cat-factory/agents": "0.75.1",
|
|
46
|
+
"@cat-factory/caching": "0.10.56",
|
|
47
|
+
"@cat-factory/consensus": "0.11.53",
|
|
48
|
+
"@cat-factory/contracts": "0.178.0",
|
|
49
|
+
"@cat-factory/eks": "0.1.152",
|
|
50
|
+
"@cat-factory/gates": "0.7.43",
|
|
51
|
+
"@cat-factory/gitlab": "0.13.19",
|
|
52
|
+
"@cat-factory/integrations": "0.103.3",
|
|
53
|
+
"@cat-factory/kernel": "0.171.0",
|
|
54
|
+
"@cat-factory/observability-langfuse": "0.7.274",
|
|
55
|
+
"@cat-factory/observability-otel": "0.2.57",
|
|
56
|
+
"@cat-factory/orchestration": "0.151.0",
|
|
57
|
+
"@cat-factory/prompt-fragments": "0.15.1",
|
|
58
|
+
"@cat-factory/provider-cloudflare": "0.7.303",
|
|
59
|
+
"@cat-factory/server": "0.162.0",
|
|
60
|
+
"@cat-factory/spend": "0.12.100"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@cloudflare/vitest-pool-workers": "^0.18.8",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"typescript": "7.0.2",
|
|
66
66
|
"vitest": "^4.1.10",
|
|
67
67
|
"wrangler": "^4.114.0",
|
|
68
|
-
"@cat-factory/conformance": "0.12.
|
|
68
|
+
"@cat-factory/conformance": "0.12.15"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc -b tsconfig.build.json",
|