gsd-pi 2.45.0-dev.fdcf73c → 2.46.0-dev.cc9d310
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/resources/extensions/gsd/auto/phases.js +14 -35
- package/dist/resources/extensions/gsd/auto/session.js +0 -11
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +112 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +25 -96
- package/dist/resources/extensions/gsd/auto-start.js +2 -3
- package/dist/resources/extensions/gsd/auto.js +8 -52
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +18 -0
- package/dist/resources/extensions/gsd/commands/context.js +0 -4
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +1 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +2 -4
- package/dist/resources/extensions/gsd/dashboard-overlay.js +0 -44
- package/dist/resources/extensions/gsd/doctor-checks.js +166 -1
- package/dist/resources/extensions/gsd/doctor.js +3 -1
- package/dist/resources/extensions/gsd/gsd-db.js +11 -2
- package/dist/resources/extensions/gsd/guided-flow.js +1 -2
- package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +5 -18
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -15
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -1
- package/dist/resources/extensions/gsd/session-lock.js +1 -3
- package/dist/resources/extensions/gsd/state.js +7 -0
- package/dist/resources/extensions/gsd/sync-lock.js +89 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +58 -12
- package/dist/resources/extensions/gsd/tools/complete-slice.js +56 -11
- package/dist/resources/extensions/gsd/tools/complete-task.js +50 -2
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +37 -1
- package/dist/resources/extensions/gsd/tools/plan-slice.js +30 -1
- package/dist/resources/extensions/gsd/tools/plan-task.js +27 -1
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +32 -2
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +86 -0
- package/dist/resources/extensions/gsd/tools/reopen-task.js +90 -0
- package/dist/resources/extensions/gsd/tools/replan-slice.js +32 -2
- package/dist/resources/extensions/gsd/unit-ownership.js +85 -0
- package/dist/resources/extensions/gsd/workflow-events.js +102 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +56 -1
- package/dist/resources/extensions/gsd/workflow-manifest.js +244 -0
- package/dist/resources/extensions/gsd/workflow-migration.js +280 -0
- package/dist/resources/extensions/gsd/workflow-projections.js +373 -0
- package/dist/resources/extensions/gsd/workflow-reconcile.js +411 -0
- package/dist/resources/extensions/gsd/write-intercept.js +84 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -19
- package/src/resources/extensions/gsd/auto/phases.ts +11 -35
- package/src/resources/extensions/gsd/auto/session.ts +0 -18
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +131 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +0 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +25 -106
- package/src/resources/extensions/gsd/auto-start.ts +1 -3
- package/src/resources/extensions/gsd/auto.ts +4 -80
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
- package/src/resources/extensions/gsd/commands/context.ts +0 -5
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +1 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +1 -5
- package/src/resources/extensions/gsd/dashboard-overlay.ts +0 -50
- package/src/resources/extensions/gsd/doctor-checks.ts +179 -1
- package/src/resources/extensions/gsd/doctor-types.ts +7 -1
- package/src/resources/extensions/gsd/doctor.ts +4 -1
- package/src/resources/extensions/gsd/gsd-db.ts +11 -2
- package/src/resources/extensions/gsd/guided-flow.ts +1 -2
- package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +5 -21
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -23
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +5 -15
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/prompts/quick-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -1
- package/src/resources/extensions/gsd/session-lock.ts +0 -4
- package/src/resources/extensions/gsd/state.ts +8 -0
- package/src/resources/extensions/gsd/sync-lock.ts +94 -0
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +5 -13
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +6 -10
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +264 -228
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +317 -250
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +2 -8
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration-proof.test.ts +15 -24
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +0 -7
- package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +7 -8
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +20 -24
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +0 -2
- package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/projection-regression.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +15 -14
- package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/reopen-task.test.ts +165 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +1 -4
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/sync-lock.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/unit-ownership.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/workflow-events.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +76 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +70 -13
- package/src/resources/extensions/gsd/tools/complete-slice.ts +68 -11
- package/src/resources/extensions/gsd/tools/complete-task.ts +63 -1
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +45 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +38 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +35 -1
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +39 -1
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +125 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +129 -0
- package/src/resources/extensions/gsd/tools/replan-slice.ts +38 -1
- package/src/resources/extensions/gsd/types.ts +8 -0
- package/src/resources/extensions/gsd/unit-ownership.ts +104 -0
- package/src/resources/extensions/gsd/workflow-events.ts +154 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +51 -1
- package/src/resources/extensions/gsd/workflow-manifest.ts +334 -0
- package/src/resources/extensions/gsd/workflow-migration.ts +345 -0
- package/src/resources/extensions/gsd/workflow-projections.ts +425 -0
- package/src/resources/extensions/gsd/workflow-reconcile.ts +503 -0
- package/src/resources/extensions/gsd/write-intercept.ts +90 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{zWYDSwB-terOjfhmWzqk1 → ZIDqryyYDroh_8AnaAOSG}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import assert from "node:assert/strict";
|
|
1
|
+
import { createTestContext } from './test-helpers.ts';
|
|
3
2
|
import * as fs from 'node:fs';
|
|
4
3
|
import * as path from 'node:path';
|
|
5
4
|
import * as os from 'node:os';
|
|
@@ -18,6 +17,8 @@ import {
|
|
|
18
17
|
import { handleCompleteSlice } from '../tools/complete-slice.ts';
|
|
19
18
|
import type { CompleteSliceParams } from '../types.ts';
|
|
20
19
|
|
|
20
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
21
|
+
|
|
21
22
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
23
|
// Helpers
|
|
23
24
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -114,262 +115,297 @@ Run the test suite and verify all assertions pass.
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
117
|
-
//
|
|
118
|
+
// complete-slice: Schema v6 migration
|
|
118
119
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const adapter = _getAdapter()!;
|
|
126
|
-
|
|
127
|
-
// Verify schema version is current (v10 after M001 planning migrations)
|
|
128
|
-
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
129
|
-
assert.strictEqual(versionRow?.['v'], 10, 'schema version should be 10');
|
|
130
|
-
|
|
131
|
-
// Verify slices table has full_summary_md and full_uat_md columns
|
|
132
|
-
const cols = adapter.prepare("PRAGMA table_info(slices)").all();
|
|
133
|
-
const colNames = cols.map(c => c['name'] as string);
|
|
134
|
-
assert.ok(colNames.includes('full_summary_md'), 'slices table should have full_summary_md column');
|
|
135
|
-
assert.ok(colNames.includes('full_uat_md'), 'slices table should have full_uat_md column');
|
|
136
|
-
|
|
137
|
-
cleanup(dbPath);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe("complete-slice: getSlice/updateSliceStatus accessors", () => {
|
|
142
|
-
test("getSlice and updateSliceStatus work correctly", () => {
|
|
143
|
-
const dbPath = tempDbPath();
|
|
144
|
-
openDatabase(dbPath);
|
|
145
|
-
|
|
146
|
-
// Insert milestone and slice
|
|
147
|
-
insertMilestone({ id: 'M001' });
|
|
148
|
-
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', risk: 'high' });
|
|
149
|
-
|
|
150
|
-
// getSlice returns correct row
|
|
151
|
-
const slice = getSlice('M001', 'S01');
|
|
152
|
-
assert.ok(slice !== null, 'getSlice should return non-null for existing slice');
|
|
153
|
-
assert.strictEqual(slice!.id, 'S01', 'slice id');
|
|
154
|
-
assert.strictEqual(slice!.milestone_id, 'M001', 'slice milestone_id');
|
|
155
|
-
assert.strictEqual(slice!.title, 'Test Slice', 'slice title');
|
|
156
|
-
assert.strictEqual(slice!.risk, 'high', 'slice risk');
|
|
157
|
-
assert.strictEqual(slice!.status, 'pending', 'slice default status should be pending');
|
|
158
|
-
assert.strictEqual(slice!.completed_at, null, 'slice completed_at should be null initially');
|
|
159
|
-
assert.strictEqual(slice!.full_summary_md, '', 'slice full_summary_md should be empty initially');
|
|
160
|
-
assert.strictEqual(slice!.full_uat_md, '', 'slice full_uat_md should be empty initially');
|
|
161
|
-
|
|
162
|
-
// getSlice returns null for non-existent
|
|
163
|
-
const noSlice = getSlice('M001', 'S99');
|
|
164
|
-
assert.strictEqual(noSlice, null, 'non-existent slice should return null');
|
|
165
|
-
|
|
166
|
-
// updateSliceStatus changes status and completed_at
|
|
167
|
-
const now = new Date().toISOString();
|
|
168
|
-
updateSliceStatus('M001', 'S01', 'complete', now);
|
|
169
|
-
const updated = getSlice('M001', 'S01');
|
|
170
|
-
assert.strictEqual(updated!.status, 'complete', 'slice status should be updated to complete');
|
|
171
|
-
assert.strictEqual(updated!.completed_at, now, 'slice completed_at should be set');
|
|
172
|
-
|
|
173
|
-
cleanup(dbPath);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe("complete-slice: handler", () => {
|
|
178
|
-
test("happy path", async () => {
|
|
179
|
-
const dbPath = tempDbPath();
|
|
180
|
-
openDatabase(dbPath);
|
|
181
|
-
|
|
182
|
-
const { basePath, roadmapPath } = createTempProject();
|
|
183
|
-
|
|
184
|
-
// Set up DB state: milestone, slice, 2 complete tasks
|
|
185
|
-
insertMilestone({ id: 'M001' });
|
|
186
|
-
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
187
|
-
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
188
|
-
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 2' });
|
|
189
|
-
|
|
190
|
-
const params = makeValidSliceParams();
|
|
191
|
-
const result = await handleCompleteSlice(params, basePath);
|
|
192
|
-
|
|
193
|
-
assert.ok(!('error' in result), 'handler should succeed without error');
|
|
194
|
-
if (!('error' in result)) {
|
|
195
|
-
assert.strictEqual(result.sliceId, 'S01', 'result sliceId');
|
|
196
|
-
assert.strictEqual(result.milestoneId, 'M001', 'result milestoneId');
|
|
197
|
-
assert.ok(result.summaryPath.endsWith('S01-SUMMARY.md'), 'summaryPath should end with S01-SUMMARY.md');
|
|
198
|
-
assert.ok(result.uatPath.endsWith('S01-UAT.md'), 'uatPath should end with S01-UAT.md');
|
|
199
|
-
|
|
200
|
-
// (a) Verify SUMMARY.md exists on disk with correct YAML frontmatter
|
|
201
|
-
assert.ok(fs.existsSync(result.summaryPath), 'summary file should exist on disk');
|
|
202
|
-
const summaryContent = fs.readFileSync(result.summaryPath, 'utf-8');
|
|
203
|
-
assert.match(summaryContent, /^---\n/, 'summary should start with YAML frontmatter');
|
|
204
|
-
assert.match(summaryContent, /id: S01/, 'summary should contain id: S01');
|
|
205
|
-
assert.match(summaryContent, /parent: M001/, 'summary should contain parent: M001');
|
|
206
|
-
assert.match(summaryContent, /milestone: M001/, 'summary should contain milestone: M001');
|
|
207
|
-
assert.match(summaryContent, /blocker_discovered: false/, 'summary should contain blocker_discovered');
|
|
208
|
-
assert.match(summaryContent, /verification_result: passed/, 'summary should contain verification_result');
|
|
209
|
-
assert.match(summaryContent, /key_files:/, 'summary should contain key_files');
|
|
210
|
-
assert.match(summaryContent, /patterns_established:/, 'summary should contain patterns_established');
|
|
211
|
-
assert.match(summaryContent, /observability_surfaces:/, 'summary should contain observability_surfaces');
|
|
212
|
-
assert.match(summaryContent, /provides:/, 'summary should contain provides');
|
|
213
|
-
assert.match(summaryContent, /# S01: Test Slice/, 'summary should have H1 with slice ID and title');
|
|
214
|
-
assert.match(summaryContent, /\*\*Implemented test slice with full coverage\*\*/, 'summary should have one-liner in bold');
|
|
215
|
-
assert.match(summaryContent, /## What Happened/, 'summary should have What Happened section');
|
|
216
|
-
assert.match(summaryContent, /## Verification/, 'summary should have Verification section');
|
|
217
|
-
assert.match(summaryContent, /## Requirements Advanced/, 'summary should have Requirements Advanced section');
|
|
218
|
-
|
|
219
|
-
// (b) Verify UAT.md exists on disk
|
|
220
|
-
assert.ok(fs.existsSync(result.uatPath), 'UAT file should exist on disk');
|
|
221
|
-
const uatContent = fs.readFileSync(result.uatPath, 'utf-8');
|
|
222
|
-
assert.match(uatContent, /# S01: Test Slice — UAT/, 'UAT should have correct title');
|
|
223
|
-
assert.match(uatContent, /Milestone:\*\* M001/, 'UAT should reference milestone');
|
|
224
|
-
assert.match(uatContent, /Smoke Test/, 'UAT should contain smoke test from params');
|
|
225
|
-
|
|
226
|
-
// (c) Verify roadmap checkbox toggled to [x]
|
|
227
|
-
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
228
|
-
assert.match(roadmapContent, /\[x\]\s+\*\*S01:/, 'S01 should be checked in roadmap');
|
|
229
|
-
assert.match(roadmapContent, /\[ \]\s+\*\*S02:/, 'S02 should still be unchecked in roadmap');
|
|
230
|
-
|
|
231
|
-
// (d) Verify full_summary_md and full_uat_md stored in DB for D004 recovery
|
|
232
|
-
const sliceAfter = getSlice('M001', 'S01');
|
|
233
|
-
assert.ok(sliceAfter !== null, 'slice should exist in DB after handler');
|
|
234
|
-
assert.ok(sliceAfter!.full_summary_md.length > 0, 'full_summary_md should be non-empty in DB');
|
|
235
|
-
assert.match(sliceAfter!.full_summary_md, /id: S01/, 'full_summary_md should contain frontmatter');
|
|
236
|
-
assert.ok(sliceAfter!.full_uat_md.length > 0, 'full_uat_md should be non-empty in DB');
|
|
237
|
-
assert.match(sliceAfter!.full_uat_md, /S01: Test Slice — UAT/, 'full_uat_md should contain UAT title');
|
|
238
|
-
|
|
239
|
-
// (e) Verify slice status is complete in DB
|
|
240
|
-
assert.strictEqual(sliceAfter!.status, 'complete', 'slice status should be complete in DB');
|
|
241
|
-
assert.ok(sliceAfter!.completed_at !== null, 'completed_at should be set in DB');
|
|
242
|
-
}
|
|
121
|
+
console.log('\n=== complete-slice: schema v6 migration ===');
|
|
122
|
+
{
|
|
123
|
+
const dbPath = tempDbPath();
|
|
124
|
+
openDatabase(dbPath);
|
|
243
125
|
|
|
244
|
-
|
|
245
|
-
cleanup(dbPath);
|
|
246
|
-
});
|
|
126
|
+
const adapter = _getAdapter()!;
|
|
247
127
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
128
|
+
// Verify schema version is current (v10 after M001 planning migrations)
|
|
129
|
+
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
130
|
+
assertEq(versionRow?.['v'], 11, 'schema version should be 11');
|
|
251
131
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
132
|
+
// Verify slices table has full_summary_md and full_uat_md columns
|
|
133
|
+
const cols = adapter.prepare("PRAGMA table_info(slices)").all();
|
|
134
|
+
const colNames = cols.map(c => c['name'] as string);
|
|
135
|
+
assertTrue(colNames.includes('full_summary_md'), 'slices table should have full_summary_md column');
|
|
136
|
+
assertTrue(colNames.includes('full_uat_md'), 'slices table should have full_uat_md column');
|
|
257
137
|
|
|
258
|
-
|
|
259
|
-
|
|
138
|
+
cleanup(dbPath);
|
|
139
|
+
}
|
|
260
140
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
assert.match(result.error, /T02/, 'error should mention the specific incomplete task ID');
|
|
265
|
-
}
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
// complete-slice: getSlice/updateSliceStatus accessors
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
266
144
|
|
|
267
|
-
|
|
268
|
-
|
|
145
|
+
console.log('\n=== complete-slice: getSlice/updateSliceStatus accessors ===');
|
|
146
|
+
{
|
|
147
|
+
const dbPath = tempDbPath();
|
|
148
|
+
openDatabase(dbPath);
|
|
149
|
+
|
|
150
|
+
// Insert milestone and slice
|
|
151
|
+
insertMilestone({ id: 'M001' });
|
|
152
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', risk: 'high' });
|
|
153
|
+
|
|
154
|
+
// getSlice returns correct row
|
|
155
|
+
const slice = getSlice('M001', 'S01');
|
|
156
|
+
assertTrue(slice !== null, 'getSlice should return non-null for existing slice');
|
|
157
|
+
assertEq(slice!.id, 'S01', 'slice id');
|
|
158
|
+
assertEq(slice!.milestone_id, 'M001', 'slice milestone_id');
|
|
159
|
+
assertEq(slice!.title, 'Test Slice', 'slice title');
|
|
160
|
+
assertEq(slice!.risk, 'high', 'slice risk');
|
|
161
|
+
assertEq(slice!.status, 'pending', 'slice default status should be pending');
|
|
162
|
+
assertEq(slice!.completed_at, null, 'slice completed_at should be null initially');
|
|
163
|
+
assertEq(slice!.full_summary_md, '', 'slice full_summary_md should be empty initially');
|
|
164
|
+
assertEq(slice!.full_uat_md, '', 'slice full_uat_md should be empty initially');
|
|
165
|
+
|
|
166
|
+
// getSlice returns null for non-existent
|
|
167
|
+
const noSlice = getSlice('M001', 'S99');
|
|
168
|
+
assertEq(noSlice, null, 'non-existent slice should return null');
|
|
169
|
+
|
|
170
|
+
// updateSliceStatus changes status and completed_at
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
updateSliceStatus('M001', 'S01', 'complete', now);
|
|
173
|
+
const updated = getSlice('M001', 'S01');
|
|
174
|
+
assertEq(updated!.status, 'complete', 'slice status should be updated to complete');
|
|
175
|
+
assertEq(updated!.completed_at, now, 'slice completed_at should be set');
|
|
176
|
+
|
|
177
|
+
cleanup(dbPath);
|
|
178
|
+
}
|
|
269
179
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
180
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
181
|
+
// complete-slice: Handler happy path
|
|
182
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
273
183
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
184
|
+
console.log('\n=== complete-slice: handler happy path ===');
|
|
185
|
+
{
|
|
186
|
+
const dbPath = tempDbPath();
|
|
187
|
+
openDatabase(dbPath);
|
|
188
|
+
|
|
189
|
+
const { basePath, roadmapPath } = createTempProject();
|
|
190
|
+
|
|
191
|
+
// Set up DB state: milestone, slices (S01 + S02), 2 complete tasks
|
|
192
|
+
insertMilestone({ id: 'M001' });
|
|
193
|
+
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
194
|
+
insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second Slice' });
|
|
195
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
196
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 2' });
|
|
197
|
+
|
|
198
|
+
const params = makeValidSliceParams();
|
|
199
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
200
|
+
|
|
201
|
+
assertTrue(!('error' in result), 'handler should succeed without error');
|
|
202
|
+
if (!('error' in result)) {
|
|
203
|
+
assertEq(result.sliceId, 'S01', 'result sliceId');
|
|
204
|
+
assertEq(result.milestoneId, 'M001', 'result milestoneId');
|
|
205
|
+
assertTrue(result.summaryPath.endsWith('S01-SUMMARY.md'), 'summaryPath should end with S01-SUMMARY.md');
|
|
206
|
+
assertTrue(result.uatPath.endsWith('S01-UAT.md'), 'uatPath should end with S01-UAT.md');
|
|
207
|
+
|
|
208
|
+
// (a) Verify SUMMARY.md exists on disk with correct YAML frontmatter
|
|
209
|
+
assertTrue(fs.existsSync(result.summaryPath), 'summary file should exist on disk');
|
|
210
|
+
const summaryContent = fs.readFileSync(result.summaryPath, 'utf-8');
|
|
211
|
+
assertMatch(summaryContent, /^---\n/, 'summary should start with YAML frontmatter');
|
|
212
|
+
assertMatch(summaryContent, /id: S01/, 'summary should contain id: S01');
|
|
213
|
+
assertMatch(summaryContent, /parent: M001/, 'summary should contain parent: M001');
|
|
214
|
+
assertMatch(summaryContent, /milestone: M001/, 'summary should contain milestone: M001');
|
|
215
|
+
assertMatch(summaryContent, /blocker_discovered: false/, 'summary should contain blocker_discovered');
|
|
216
|
+
assertMatch(summaryContent, /verification_result: passed/, 'summary should contain verification_result');
|
|
217
|
+
assertMatch(summaryContent, /key_files:/, 'summary should contain key_files');
|
|
218
|
+
assertMatch(summaryContent, /patterns_established:/, 'summary should contain patterns_established');
|
|
219
|
+
assertMatch(summaryContent, /observability_surfaces:/, 'summary should contain observability_surfaces');
|
|
220
|
+
assertMatch(summaryContent, /provides:/, 'summary should contain provides');
|
|
221
|
+
assertMatch(summaryContent, /# S01: Test Slice/, 'summary should have H1 with slice ID and title');
|
|
222
|
+
assertMatch(summaryContent, /\*\*Implemented test slice with full coverage\*\*/, 'summary should have one-liner in bold');
|
|
223
|
+
assertMatch(summaryContent, /## What Happened/, 'summary should have What Happened section');
|
|
224
|
+
assertMatch(summaryContent, /## Verification/, 'summary should have Verification section');
|
|
225
|
+
assertMatch(summaryContent, /## Requirements Advanced/, 'summary should have Requirements Advanced section');
|
|
226
|
+
|
|
227
|
+
// (b) Verify UAT.md exists on disk
|
|
228
|
+
assertTrue(fs.existsSync(result.uatPath), 'UAT file should exist on disk');
|
|
229
|
+
const uatContent = fs.readFileSync(result.uatPath, 'utf-8');
|
|
230
|
+
assertMatch(uatContent, /# S01: Test Slice — UAT/, 'UAT should have correct title');
|
|
231
|
+
assertMatch(uatContent, /Milestone:\*\* M001/, 'UAT should reference milestone');
|
|
232
|
+
assertMatch(uatContent, /Smoke Test/, 'UAT should contain smoke test from params');
|
|
233
|
+
|
|
234
|
+
// (c) Verify roadmap shows S01 complete (✅) and S02 pending (⬜) in table format
|
|
235
|
+
// Projection renders roadmap as a Slice Overview table, not checkbox list
|
|
236
|
+
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
237
|
+
assertMatch(roadmapContent, /\| S01 \|/, 'S01 should appear in roadmap table');
|
|
238
|
+
assertTrue(roadmapContent.includes('✅'), 'completed S01 should show ✅ in roadmap table');
|
|
239
|
+
assertMatch(roadmapContent, /\| S02 \|/, 'S02 should appear in roadmap table');
|
|
240
|
+
assertTrue(roadmapContent.includes('⬜'), 'pending S02 should show ⬜ in roadmap table');
|
|
241
|
+
|
|
242
|
+
// (d) Verify full_summary_md and full_uat_md stored in DB for D004 recovery
|
|
243
|
+
const sliceAfter = getSlice('M001', 'S01');
|
|
244
|
+
assertTrue(sliceAfter !== null, 'slice should exist in DB after handler');
|
|
245
|
+
assertTrue(sliceAfter!.full_summary_md.length > 0, 'full_summary_md should be non-empty in DB');
|
|
246
|
+
assertMatch(sliceAfter!.full_summary_md, /id: S01/, 'full_summary_md should contain frontmatter');
|
|
247
|
+
assertTrue(sliceAfter!.full_uat_md.length > 0, 'full_uat_md should be non-empty in DB');
|
|
248
|
+
assertMatch(sliceAfter!.full_uat_md, /S01: Test Slice — UAT/, 'full_uat_md should contain UAT title');
|
|
249
|
+
|
|
250
|
+
// (e) Verify slice status is complete in DB
|
|
251
|
+
assertEq(sliceAfter!.status, 'complete', 'slice status should be complete in DB');
|
|
252
|
+
assertTrue(sliceAfter!.completed_at !== null, 'completed_at should be set in DB');
|
|
253
|
+
}
|
|
277
254
|
|
|
278
|
-
|
|
279
|
-
|
|
255
|
+
cleanupDir(basePath);
|
|
256
|
+
cleanup(dbPath);
|
|
257
|
+
}
|
|
280
258
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
259
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
260
|
+
// complete-slice: Handler rejects incomplete tasks
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
285
262
|
|
|
286
|
-
|
|
287
|
-
|
|
263
|
+
console.log('\n=== complete-slice: handler rejects incomplete tasks ===');
|
|
264
|
+
{
|
|
265
|
+
const dbPath = tempDbPath();
|
|
266
|
+
openDatabase(dbPath);
|
|
288
267
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
268
|
+
// Insert milestone, slice, 2 tasks — one complete, one pending
|
|
269
|
+
insertMilestone({ id: 'M001' });
|
|
270
|
+
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
271
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
272
|
+
insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'pending', title: 'Task 2' });
|
|
292
273
|
|
|
293
|
-
|
|
274
|
+
const params = makeValidSliceParams();
|
|
275
|
+
const result = await handleCompleteSlice(params, '/tmp/fake');
|
|
294
276
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
277
|
+
assertTrue('error' in result, 'should return error when tasks are incomplete');
|
|
278
|
+
if ('error' in result) {
|
|
279
|
+
assertMatch(result.error, /incomplete tasks/, 'error should mention incomplete tasks');
|
|
280
|
+
assertMatch(result.error, /T02/, 'error should mention the specific incomplete task ID');
|
|
281
|
+
}
|
|
301
282
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
assert.ok('error' in r2, 'should return error for empty milestoneId');
|
|
305
|
-
if ('error' in r2) {
|
|
306
|
-
assert.match(r2.error, /milestoneId/, 'error should mention milestoneId');
|
|
307
|
-
}
|
|
283
|
+
cleanup(dbPath);
|
|
284
|
+
}
|
|
308
285
|
|
|
309
|
-
|
|
310
|
-
|
|
286
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
287
|
+
// complete-slice: Handler rejects no tasks
|
|
288
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
311
289
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
290
|
+
console.log('\n=== complete-slice: handler rejects no tasks ===');
|
|
291
|
+
{
|
|
292
|
+
const dbPath = tempDbPath();
|
|
293
|
+
openDatabase(dbPath);
|
|
315
294
|
|
|
316
|
-
|
|
295
|
+
// Insert milestone and slice but NO tasks
|
|
296
|
+
insertMilestone({ id: 'M001' });
|
|
297
|
+
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
317
298
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
321
|
-
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
299
|
+
const params = makeValidSliceParams();
|
|
300
|
+
const result = await handleCompleteSlice(params, '/tmp/fake');
|
|
322
301
|
|
|
323
|
-
|
|
302
|
+
assertTrue('error' in result, 'should return error when no tasks exist');
|
|
303
|
+
if ('error' in result) {
|
|
304
|
+
assertMatch(result.error, /no tasks found/, 'error should say no tasks found');
|
|
305
|
+
}
|
|
324
306
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
assert.ok(!('error' in r1), 'first call should succeed');
|
|
307
|
+
cleanup(dbPath);
|
|
308
|
+
}
|
|
328
309
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
311
|
+
// complete-slice: Handler validation errors
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
332
313
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
314
|
+
console.log('\n=== complete-slice: handler validation errors ===');
|
|
315
|
+
{
|
|
316
|
+
const dbPath = tempDbPath();
|
|
317
|
+
openDatabase(dbPath);
|
|
337
318
|
|
|
338
|
-
|
|
339
|
-
if (!('error' in r2)) {
|
|
340
|
-
assert.ok(fs.existsSync(r2.summaryPath), 'summary should still exist after second call');
|
|
341
|
-
assert.ok(fs.existsSync(r2.uatPath), 'UAT should still exist after second call');
|
|
342
|
-
}
|
|
319
|
+
const params = makeValidSliceParams();
|
|
343
320
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
321
|
+
// Empty sliceId
|
|
322
|
+
const r1 = await handleCompleteSlice({ ...params, sliceId: '' }, '/tmp/fake');
|
|
323
|
+
assertTrue('error' in r1, 'should return error for empty sliceId');
|
|
324
|
+
if ('error' in r1) {
|
|
325
|
+
assertMatch(r1.error, /sliceId/, 'error should mention sliceId');
|
|
326
|
+
}
|
|
347
327
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
328
|
+
// Empty milestoneId
|
|
329
|
+
const r2 = await handleCompleteSlice({ ...params, milestoneId: '' }, '/tmp/fake');
|
|
330
|
+
assertTrue('error' in r2, 'should return error for empty milestoneId');
|
|
331
|
+
if ('error' in r2) {
|
|
332
|
+
assertMatch(r2.error, /milestoneId/, 'error should mention milestoneId');
|
|
333
|
+
}
|
|
351
334
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const sliceDir = path.join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01');
|
|
355
|
-
fs.mkdirSync(sliceDir, { recursive: true });
|
|
335
|
+
cleanup(dbPath);
|
|
336
|
+
}
|
|
356
337
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
338
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
339
|
+
// complete-slice: Handler idempotency
|
|
340
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
361
341
|
|
|
362
|
-
|
|
363
|
-
|
|
342
|
+
console.log('\n=== complete-slice: handler idempotency ===');
|
|
343
|
+
{
|
|
344
|
+
const dbPath = tempDbPath();
|
|
345
|
+
openDatabase(dbPath);
|
|
364
346
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
347
|
+
const { basePath, roadmapPath } = createTempProject();
|
|
348
|
+
|
|
349
|
+
// Set up DB state
|
|
350
|
+
insertMilestone({ id: 'M001' });
|
|
351
|
+
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
352
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
353
|
+
|
|
354
|
+
const params = makeValidSliceParams();
|
|
355
|
+
|
|
356
|
+
// First call
|
|
357
|
+
const r1 = await handleCompleteSlice(params, basePath);
|
|
358
|
+
assertTrue(!('error' in r1), 'first call should succeed');
|
|
359
|
+
|
|
360
|
+
// Second call — state machine guard rejects (slice is already complete)
|
|
361
|
+
const r2 = await handleCompleteSlice(params, basePath);
|
|
362
|
+
assertTrue('error' in r2, 'second call should return error (slice already complete)');
|
|
363
|
+
if ('error' in r2) {
|
|
364
|
+
assertMatch(r2.error, /already complete/, 'error should mention already complete');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Verify only 1 slice row (not duplicated)
|
|
368
|
+
const adapter = _getAdapter()!;
|
|
369
|
+
const sliceRows = adapter.prepare("SELECT * FROM slices WHERE milestone_id = 'M001' AND id = 'S01'").all();
|
|
370
|
+
assertEq(sliceRows.length, 1, 'should have exactly 1 slice row after calls');
|
|
371
|
+
|
|
372
|
+
cleanupDir(basePath);
|
|
373
|
+
cleanup(dbPath);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
377
|
+
// complete-slice: Handler with missing roadmap (graceful)
|
|
378
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
379
|
+
|
|
380
|
+
console.log('\n=== complete-slice: handler with missing roadmap ===');
|
|
381
|
+
{
|
|
382
|
+
const dbPath = tempDbPath();
|
|
383
|
+
openDatabase(dbPath);
|
|
384
|
+
|
|
385
|
+
// Create a temp dir WITHOUT a roadmap file
|
|
386
|
+
const basePath = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-no-roadmap-'));
|
|
387
|
+
const sliceDir = path.join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01');
|
|
388
|
+
fs.mkdirSync(sliceDir, { recursive: true });
|
|
389
|
+
|
|
390
|
+
// Set up DB state
|
|
391
|
+
insertMilestone({ id: 'M001' });
|
|
392
|
+
insertSlice({ id: 'S01', milestoneId: 'M001' });
|
|
393
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete', title: 'Task 1' });
|
|
394
|
+
|
|
395
|
+
const params = makeValidSliceParams();
|
|
396
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
397
|
+
|
|
398
|
+
// Should succeed even without roadmap file — just skip checkbox toggle
|
|
399
|
+
assertTrue(!('error' in result), 'handler should succeed without roadmap file');
|
|
400
|
+
if (!('error' in result)) {
|
|
401
|
+
assertTrue(fs.existsSync(result.summaryPath), 'summary should be written even without roadmap');
|
|
402
|
+
assertTrue(fs.existsSync(result.uatPath), 'UAT should be written even without roadmap');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
cleanupDir(basePath);
|
|
406
|
+
cleanup(dbPath);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
371
410
|
|
|
372
|
-
|
|
373
|
-
cleanup(dbPath);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
411
|
+
report();
|