ultra-builder-pro-cli 0.13.0 → 0.15.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.
Files changed (172) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,117 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
- const { spawnSync } = require('node:child_process');
9
-
10
- const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
11
- const ops = require('../lib/state-ops.cjs');
12
- const projector = require('../lib/projector.cjs');
13
-
14
- function gitRun(cwd, args, opts = {}) {
15
- const env = {
16
- ...process.env,
17
- GIT_AUTHOR_NAME: 'ubp-test',
18
- GIT_AUTHOR_EMAIL: 'ubp@example.com',
19
- GIT_COMMITTER_NAME: 'ubp-test',
20
- GIT_COMMITTER_EMAIL: 'ubp@example.com',
21
- };
22
- const r = spawnSync('git', args, { cwd, encoding: 'utf8', env, ...opts });
23
- if (r.status !== 0 && !opts.allowFail) {
24
- throw new Error(`git ${args.join(' ')} failed: ${r.stderr || r.stdout}`);
25
- }
26
- return { code: r.status, stdout: r.stdout.trim(), stderr: r.stderr };
27
- }
28
-
29
- function tmpRepo() {
30
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-hash-flow-'));
31
- gitRun(dir, ['init', '-q', '-b', 'main']);
32
- gitRun(dir, ['config', 'commit.gpgsign', 'false']);
33
- // Ignore the SQLite WAL/SHM sidecars so they don't appear as modifications.
34
- fs.writeFileSync(path.join(dir, '.gitignore'), '.ultra/state.db-shm\n.ultra/state.db-wal\n');
35
- return dir;
36
- }
37
-
38
- test('two-commit flow lands feat then chore with the right SHA in context md', () => {
39
- const dir = tmpRepo();
40
- try {
41
- const dbPath = path.join(dir, '.ultra', 'state.db');
42
- const init = initStateDb(dbPath);
43
- const db = init.db;
44
-
45
- // Task starts pending, no completion_commit yet.
46
- ops.createTask(db, { id: 'task-h1', title: 'commit hash flow', type: 'feature', priority: 'P1' });
47
- projector.projectAll(db, { rootDir: dir });
48
-
49
- // Stage initial state and land the feat commit.
50
- fs.writeFileSync(path.join(dir, 'src.js'), '// implementation\n');
51
- gitRun(dir, ['add', '-A']);
52
- gitRun(dir, ['commit', '-q', '-m', 'feat: task-h1 — commit hash flow']);
53
-
54
- const featSha = gitRun(dir, ['rev-parse', 'HEAD']).stdout;
55
-
56
- // Step 3 — update state.db with the new SHA.
57
- ops.patchTask(db, 'task-h1', { completion_commit: featSha });
58
-
59
- // Step 4 — re-project the context so its YAML carries the hash.
60
- projector.projectContext(db, 'task-h1', {}, { rootDir: dir });
61
-
62
- // Step 5 — chore commit captures the projection update.
63
- gitRun(dir, ['add', '-A']);
64
- gitRun(dir, ['commit', '-q', '-m', 'chore: record task-h1 completion hash']);
65
-
66
- const log = gitRun(dir, ['log', '--oneline', '-2']).stdout.split('\n');
67
- assert.match(log[0], /chore: record task-h1 completion hash/);
68
- assert.match(log[1], /feat: task-h1 — commit hash flow/);
69
-
70
- const ctxFile = path.join(dir, '.ultra', 'tasks', 'contexts', 'task-task-h1.md');
71
- const ctxText = fs.readFileSync(ctxFile, 'utf8');
72
- // The chore commit's working tree must reflect the recorded SHA.
73
- const dbRow = db.prepare("SELECT completion_commit FROM tasks WHERE id = 'task-h1'").get();
74
- assert.equal(dbRow.completion_commit, featSha);
75
- // Header is regenerated; ensure projector wrote v4.5 + the right task_id.
76
- assert.match(ctxText, /^---/);
77
- assert.match(ctxText, /schema_version: 4\.5/);
78
- assert.match(ctxText, /task_id: task-h1/);
79
- closeStateDb(db);
80
- } finally {
81
- fs.rmSync(dir, { recursive: true, force: true });
82
- }
83
- });
84
-
85
- test('rerunning steps 4–5 against the same SHA produces an empty chore commit attempt', () => {
86
- const dir = tmpRepo();
87
- try {
88
- const dbPath = path.join(dir, '.ultra', 'state.db');
89
- const init = initStateDb(dbPath);
90
- const db = init.db;
91
- ops.createTask(db, { id: 'task-h2', title: 'idempotent', type: 'feature', priority: 'P1' });
92
- projector.projectAll(db, { rootDir: dir });
93
-
94
- fs.writeFileSync(path.join(dir, 'a.js'), '// a\n');
95
- gitRun(dir, ['add', '-A']);
96
- gitRun(dir, ['commit', '-q', '-m', 'feat: task-h2 — idempotent']);
97
- const sha = gitRun(dir, ['rev-parse', 'HEAD']).stdout;
98
- ops.patchTask(db, 'task-h2', { completion_commit: sha });
99
- projector.projectContext(db, 'task-h2', {}, { rootDir: dir });
100
- gitRun(dir, ['add', '-A']);
101
- gitRun(dir, ['commit', '-q', '-m', 'chore: record task-h2 completion hash']);
102
-
103
- // Idempotency: re-projecting the same db state must produce byte-identical
104
- // context md (generated_at is derived from tasks.updated_at, not now()).
105
- projector.projectContext(db, 'task-h2', {}, { rootDir: dir });
106
- gitRun(dir, ['add', '-A']);
107
- const status = gitRun(dir, ['status', '--porcelain']).stdout;
108
- assert.equal(status, '', 're-projection of unchanged state must leave nothing to commit');
109
-
110
- // A retry chore commit on an empty diff must be refused by git.
111
- const r = gitRun(dir, ['commit', '-q', '-m', 'chore: idempotent retry'], { allowFail: true });
112
- assert.notEqual(r.code, 0, 'a follow-up empty chore commit must fail');
113
- closeStateDb(db);
114
- } finally {
115
- fs.rmSync(dir, { recursive: true, force: true });
116
- }
117
- });
@@ -1,96 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
- const { Worker, isMainThread, workerData, parentPort } = require('node:worker_threads');
9
-
10
- const { initStateDb, openStateDb, closeStateDb } = require('../lib/state-db.cjs');
11
- const ops = require('../lib/state-ops.cjs');
12
-
13
- const WORKER_COUNT = 20;
14
- const TASKS_PER_WORKER = 50;
15
-
16
- function tmpDb() {
17
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-conc-up-'));
18
- return { dir, file: path.join(dir, 'state.db') };
19
- }
20
-
21
- if (!isMainThread) {
22
- const { dbPath, workerIdx, count } = workerData;
23
- const db = openStateDb(dbPath);
24
- const completed = [];
25
- for (let i = 0; i < count; i++) {
26
- const id = `w${workerIdx}-t${i}`;
27
- ops.updateTaskStatus(db, id, 'in_progress');
28
- ops.updateTaskStatus(db, id, 'completed');
29
- completed.push(id);
30
- }
31
- closeStateDb(db);
32
- parentPort.postMessage({ workerIdx, completed });
33
- } else {
34
- test(`${WORKER_COUNT} workers × ${TASKS_PER_WORKER} updateTaskStatus — no loss, monotonic events`, async () => {
35
- const { dir, file } = tmpDb();
36
- try {
37
- const init = initStateDb(file);
38
- const db = init.db;
39
-
40
- // Pre-seed disjoint tasks so workers never contend on the same row.
41
- for (let w = 0; w < WORKER_COUNT; w++) {
42
- for (let t = 0; t < TASKS_PER_WORKER; t++) {
43
- ops.createTask(db, {
44
- id: `w${w}-t${t}`,
45
- title: `seed w${w}t${t}`,
46
- type: 'feature',
47
- priority: 'P3',
48
- });
49
- }
50
- }
51
- closeStateDb(db);
52
-
53
- const expectedTasks = WORKER_COUNT * TASKS_PER_WORKER;
54
- const results = await Promise.all(
55
- Array.from({ length: WORKER_COUNT }, (_, idx) =>
56
- new Promise((resolve, reject) => {
57
- const w = new Worker(__filename, {
58
- workerData: { dbPath: file, workerIdx: idx, count: TASKS_PER_WORKER },
59
- });
60
- w.on('message', resolve);
61
- w.on('error', reject);
62
- w.on('exit', (code) => {
63
- if (code !== 0) reject(new Error(`worker ${idx} exit ${code}`));
64
- });
65
- }),
66
- ),
67
- );
68
-
69
- const completedClaims = results.flatMap((r) => r.completed);
70
- assert.equal(completedClaims.length, expectedTasks);
71
-
72
- const verify = openStateDb(file);
73
- const completedRows = verify.prepare("SELECT id, status FROM tasks WHERE status = @status").all({ status: 'completed' });
74
- assert.equal(completedRows.length, expectedTasks, 'every task must end in completed');
75
-
76
- // Each task contributes 1 task_created + 1 task_started + 1 task_completed = 3 events.
77
- const totalEvents = verify.prepare('SELECT COUNT(*) AS n FROM events').get().n;
78
- assert.equal(totalEvents, expectedTasks * 3, 'every transition must record an event');
79
-
80
- const ids = verify.prepare('SELECT id FROM events ORDER BY id ASC').all().map((r) => r.id);
81
- assert.equal(new Set(ids).size, ids.length, 'event ids must be unique');
82
- for (let i = 1; i < ids.length; i++) {
83
- assert.ok(ids[i] > ids[i - 1], 'event ids must be strictly monotonic');
84
- }
85
-
86
- const startedCount = verify.prepare("SELECT COUNT(*) AS n FROM events WHERE type = @type").get({ type: 'task_started' }).n;
87
- const completedCount = verify.prepare("SELECT COUNT(*) AS n FROM events WHERE type = @type").get({ type: 'task_completed' }).n;
88
- assert.equal(startedCount, expectedTasks, 'every task got a task_started event');
89
- assert.equal(completedCount, expectedTasks, 'every task got a task_completed event');
90
-
91
- closeStateDb(verify);
92
- } finally {
93
- fs.rmSync(dir, { recursive: true, force: true });
94
- }
95
- });
96
- }
@@ -1,112 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
- const { Worker, isMainThread, workerData, parentPort } = require('node:worker_threads');
9
-
10
- const { initStateDb, openStateDb, closeStateDb } = require('../lib/state-db.cjs');
11
-
12
- function tmpDb() {
13
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-conc-'));
14
- return { dir, file: path.join(dir, 'state.db') };
15
- }
16
-
17
- function appendEventRaw(db, type, payload) {
18
- const stmt = db.prepare(
19
- `INSERT INTO events (type, runtime, payload_json) VALUES (?, ?, ?)`,
20
- );
21
- return stmt.run(type, 'claude', JSON.stringify(payload)).lastInsertRowid;
22
- }
23
-
24
- function withRetry(fn, attempts = 3, baseMs = 25) {
25
- let lastErr;
26
- for (let i = 0; i < attempts; i++) {
27
- try {
28
- return fn();
29
- } catch (err) {
30
- if (!String(err.code || err.message).includes('SQLITE_BUSY')) throw err;
31
- lastErr = err;
32
- const wait = baseMs * Math.pow(2, i);
33
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, wait);
34
- }
35
- }
36
- throw lastErr;
37
- }
38
-
39
- if (!isMainThread) {
40
- const { dbPath, role, count, baseId } = workerData;
41
- const db = openStateDb(dbPath);
42
- const written = [];
43
- for (let i = 0; i < count; i++) {
44
- const id = withRetry(() =>
45
- appendEventRaw(db, 'task_created', { role, baseId, i }),
46
- );
47
- written.push(Number(id));
48
- }
49
- closeStateDb(db);
50
- parentPort.postMessage({ role, written });
51
- } else {
52
- test('three concurrent writers append to events with no loss and monotonic id', async () => {
53
- const { dir, file } = tmpDb();
54
- try {
55
- const init = initStateDb(file);
56
- closeStateDb(init.db);
57
-
58
- const roles = ['mcp', 'cli', 'daemon'];
59
- const COUNT_PER_WORKER = 100;
60
- const expected = roles.length * COUNT_PER_WORKER;
61
-
62
- const results = await Promise.all(
63
- roles.map(
64
- (role, idx) =>
65
- new Promise((resolve, reject) => {
66
- const w = new Worker(__filename, {
67
- workerData: {
68
- dbPath: file,
69
- role,
70
- count: COUNT_PER_WORKER,
71
- baseId: idx * COUNT_PER_WORKER,
72
- },
73
- });
74
- w.on('message', (m) => resolve(m));
75
- w.on('error', reject);
76
- w.on('exit', (code) => {
77
- if (code !== 0) reject(new Error(`worker ${role} exit ${code}`));
78
- });
79
- }),
80
- ),
81
- );
82
-
83
- const verify = openStateDb(file);
84
- const total = verify.prepare('SELECT COUNT(*) AS n FROM events').get().n;
85
- assert.equal(total, expected, 'total event count must match');
86
-
87
- const ids = verify.prepare('SELECT id FROM events ORDER BY id ASC').all().map((r) => r.id);
88
- assert.equal(new Set(ids).size, ids.length, 'event ids must be unique');
89
- for (let i = 1; i < ids.length; i++) {
90
- assert.ok(ids[i] > ids[i - 1], 'event ids must be strictly monotonic');
91
- }
92
- const minId = ids[0];
93
- const maxId = ids[ids.length - 1];
94
- assert.ok(maxId - minId + 1 >= ids.length, 'sequence must not have collisions');
95
-
96
- const perRole = verify
97
- .prepare("SELECT json_extract(payload_json, '$.role') AS role, COUNT(*) AS n FROM events GROUP BY role")
98
- .all();
99
- for (const row of perRole) {
100
- assert.equal(row.n, COUNT_PER_WORKER, `role ${row.role} count`);
101
- }
102
- closeStateDb(verify);
103
-
104
- // Workers should each have observed their own row ids returned cleanly.
105
- const collected = results.flatMap((r) => r.written);
106
- assert.equal(collected.length, expected);
107
- assert.equal(new Set(collected).size, expected, 'no duplicate ids reported by workers');
108
- } finally {
109
- fs.rmSync(dir, { recursive: true, force: true });
110
- }
111
- });
112
- }
@@ -1,121 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('node:fs');
6
- const os = require('node:os');
7
- const path = require('node:path');
8
- const { spawnSync } = require('node:child_process');
9
-
10
- const Database = require('better-sqlite3');
11
-
12
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
13
- const CLI = path.join(REPO_ROOT, 'ultra-tools', 'cli.cjs');
14
- const { EXPECTED_VERSION, initStateDb, closeStateDb } = require('../lib/state-db.cjs');
15
- const ops = require('../lib/state-ops.cjs');
16
-
17
- function tmpDir() {
18
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-db-ops-'));
19
- }
20
-
21
- function runCli(args) {
22
- const r = spawnSync(process.execPath, [CLI, 'db', ...args], { encoding: 'utf8' });
23
- const lines = r.stdout.trim().split('\n').filter(Boolean);
24
- const last = lines[lines.length - 1] || '{}';
25
- return { code: r.status, envelope: JSON.parse(last), stderr: r.stderr };
26
- }
27
-
28
- test('db checkpoint runs PRAGMA wal_checkpoint(TRUNCATE) and reports counters', () => {
29
- const dir = tmpDir();
30
- const dbPath = path.join(dir, 'state.db');
31
- try {
32
- closeStateDb(initStateDb(dbPath).db);
33
- const { code, envelope } = runCli(['checkpoint', '--path', dbPath]);
34
- assert.equal(code, 0);
35
- assert.equal(envelope.ok, true);
36
- assert.equal(typeof envelope.data.busy, 'number');
37
- assert.equal(typeof envelope.data.checkpointed, 'number');
38
- } finally {
39
- fs.rmSync(dir, { recursive: true, force: true });
40
- }
41
- });
42
-
43
- test('db vacuum reports reclaimed bytes after deleting rows', () => {
44
- const dir = tmpDir();
45
- const dbPath = path.join(dir, 'state.db');
46
- try {
47
- const init = initStateDb(dbPath);
48
- for (let i = 0; i < 50; i++) {
49
- ops.appendEvent(init.db, {
50
- type: 'task_created',
51
- task_id: `t-${i}`,
52
- payload: { padding: 'x'.repeat(16 * 1024) },
53
- });
54
- }
55
- init.db.prepare('DELETE FROM events').run();
56
- closeStateDb(init.db);
57
-
58
- const { code, envelope } = runCli(['vacuum', '--path', dbPath]);
59
- assert.equal(code, 0);
60
- assert.equal(envelope.ok, true);
61
- assert.ok(envelope.data.bytes_after <= envelope.data.bytes_before);
62
- assert.equal(typeof envelope.data.reclaimed_bytes, 'number');
63
- } finally {
64
- fs.rmSync(dir, { recursive: true, force: true });
65
- }
66
- });
67
-
68
- test('db integrity returns ok on a healthy database', () => {
69
- const dir = tmpDir();
70
- const dbPath = path.join(dir, 'state.db');
71
- try {
72
- closeStateDb(initStateDb(dbPath).db);
73
- const { code, envelope } = runCli(['integrity', '--path', dbPath]);
74
- assert.equal(code, 0);
75
- assert.equal(envelope.ok, true);
76
- assert.equal(envelope.data.integrity_ok, true);
77
- assert.deepEqual(envelope.data.messages, ['ok']);
78
- } finally {
79
- fs.rmSync(dir, { recursive: true, force: true });
80
- }
81
- });
82
-
83
- test('db backup writes a file that opens independently with the same schema', () => {
84
- const dir = tmpDir();
85
- const dbPath = path.join(dir, 'state.db');
86
- const backupPath = path.join(dir, 'snap.db');
87
- try {
88
- const init = initStateDb(dbPath);
89
- ops.createTask(init.db, { id: 'b-1', title: 'backup', type: 'feature', priority: 'P1' });
90
- closeStateDb(init.db);
91
-
92
- const { code, envelope } = runCli(['backup', '--path', dbPath, '--to', backupPath]);
93
- assert.equal(code, 0);
94
- assert.equal(envelope.ok, true);
95
- assert.equal(envelope.data.target, path.resolve(backupPath));
96
- assert.ok(envelope.data.size_bytes > 0);
97
-
98
- // Independently open the backup with a fresh connection
99
- const snap = new Database(backupPath);
100
- const v = snap.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
101
- assert.equal(v.version, EXPECTED_VERSION);
102
- const t = snap.prepare("SELECT id FROM tasks WHERE id = 'b-1'").get();
103
- assert.equal(t.id, 'b-1');
104
- snap.close();
105
- } finally {
106
- fs.rmSync(dir, { recursive: true, force: true });
107
- }
108
- });
109
-
110
- test('db <verb> on a missing database returns DB_NOT_FOUND with exit 2', () => {
111
- const dir = tmpDir();
112
- try {
113
- const missing = path.join(dir, 'missing.db');
114
- const { code, envelope } = runCli(['integrity', '--path', missing]);
115
- assert.equal(code, 2);
116
- assert.equal(envelope.ok, false);
117
- assert.equal(envelope.error.code, 'DB_NOT_FOUND');
118
- } finally {
119
- fs.rmSync(dir, { recursive: true, force: true });
120
- }
121
- });