feat: add quality_review task status and qualityReview initiative flag
Adds two new fields to the database and propagates them through the repository layer: - Task status enum gains 'quality_review' (between in_progress and completed), enabling a QA gate before tasks are marked complete. - initiatives.quality_review (INTEGER DEFAULT 0) lets an initiative be flagged for quality-review workflow without a data migration (existing rows default to false). Includes: - Schema changes in schema.ts - Migration 0037 (ALTER TABLE initiatives ADD quality_review) - Snapshot chain repaired: deleted stale 0036 snapshot, fixed 0035 prevId to create a linear chain (0032 → 0034 → 0035), then generated clean 0037 snapshot - Repository adapter already uses SELECT * / spread-update pattern so no adapter code changes were needed - Initiative and task repository tests extended with qualityReview / quality_review_status describe blocks (7 new tests) - docs/database.md updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -147,4 +147,32 @@ describe('DrizzleInitiativeRepository', () => {
|
||||
expect(archived[0].name).toBe('Archived');
|
||||
});
|
||||
});
|
||||
|
||||
describe('qualityReview', () => {
|
||||
it('defaults to false on create', async () => {
|
||||
const initiative = await repo.create({ name: 'QR Test' });
|
||||
expect(initiative.qualityReview).toBe(false);
|
||||
});
|
||||
|
||||
it('can be set to true via update', async () => {
|
||||
const created = await repo.create({ name: 'QR Toggle' });
|
||||
const updated = await repo.update(created.id, { qualityReview: true });
|
||||
expect(updated.qualityReview).toBe(true);
|
||||
});
|
||||
|
||||
it('round-trips through findById', async () => {
|
||||
const created = await repo.create({ name: 'QR Round-trip' });
|
||||
await repo.update(created.id, { qualityReview: true });
|
||||
const found = await repo.findById(created.id);
|
||||
expect(found!.qualityReview).toBe(true);
|
||||
});
|
||||
|
||||
it('round-trips false value', async () => {
|
||||
const created = await repo.create({ name: 'QR False' });
|
||||
await repo.update(created.id, { qualityReview: true });
|
||||
await repo.update(created.id, { qualityReview: false });
|
||||
const found = await repo.findById(created.id);
|
||||
expect(found!.qualityReview).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,4 +196,38 @@ describe('DrizzleTaskRepository', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quality_review status', () => {
|
||||
it('can set status to quality_review', async () => {
|
||||
const task = await taskRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
name: 'QR Status Task',
|
||||
order: 99,
|
||||
});
|
||||
const updated = await taskRepo.update(task.id, { status: 'quality_review' });
|
||||
expect(updated.status).toBe('quality_review');
|
||||
});
|
||||
|
||||
it('round-trips quality_review through findById', async () => {
|
||||
const task = await taskRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
name: 'QR Round-trip Task',
|
||||
order: 100,
|
||||
});
|
||||
await taskRepo.update(task.id, { status: 'quality_review' });
|
||||
const found = await taskRepo.findById(task.id);
|
||||
expect(found!.status).toBe('quality_review');
|
||||
});
|
||||
|
||||
it('can transition from quality_review to completed', async () => {
|
||||
const task = await taskRepo.create({
|
||||
phaseId: testPhaseId,
|
||||
name: 'QR to Completed',
|
||||
order: 101,
|
||||
});
|
||||
await taskRepo.update(task.id, { status: 'quality_review' });
|
||||
const completed = await taskRepo.update(task.id, { status: 'completed' });
|
||||
expect(completed.status).toBe('completed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ export const initiatives = sqliteTable('initiatives', {
|
||||
executionMode: text('execution_mode', { enum: ['yolo', 'review_per_phase'] })
|
||||
.notNull()
|
||||
.default('review_per_phase'),
|
||||
qualityReview: integer('quality_review', { mode: 'boolean' }).notNull().default(false),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
@@ -151,7 +152,7 @@ export const tasks = sqliteTable('tasks', {
|
||||
.notNull()
|
||||
.default('medium'),
|
||||
status: text('status', {
|
||||
enum: ['pending', 'in_progress', 'completed', 'blocked'],
|
||||
enum: ['pending', 'in_progress', 'quality_review', 'completed', 'blocked'],
|
||||
})
|
||||
.notNull()
|
||||
.default('pending'),
|
||||
|
||||
1
apps/server/drizzle/0037_worthless_princess_powerful.sql
Normal file
1
apps/server/drizzle/0037_worthless_princess_powerful.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `initiatives` ADD `quality_review` integer DEFAULT false NOT NULL;
|
||||
@@ -2,7 +2,7 @@
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "c84e499f-7df8-4091-b2a5-6b12847898bd",
|
||||
"prevId": "5fbe1151-1dfb-4b0c-a7fa-2177369543fd",
|
||||
"prevId": "443071fe-31d6-498a-9f4a-4a3ff24a46fc",
|
||||
"tables": {
|
||||
"accounts": {
|
||||
"name": "accounts",
|
||||
@@ -238,6 +238,13 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exit_code": {
|
||||
"name": "exit_code",
|
||||
"type": "integer",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -260,6 +260,13 @@
|
||||
"when": 1772798869413,
|
||||
"tag": "0036_icy_silvermane",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 37,
|
||||
"version": "6",
|
||||
"when": 1772829916655,
|
||||
"tag": "0037_worthless_princess_powerful",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user