ultra-builder-pro-cli 0.14.0 → 0.16.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 (163) 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 +77 -1
  8. package/README.md +46 -24
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/handbook.cjs +2 -1
  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 +3 -2
  16. package/agents/review-coordinator.md +9 -6
  17. package/commands/ultra-change.md +1 -1
  18. package/commands/ultra-plan.md +1 -1
  19. package/commands/ultra-research.md +1 -1
  20. package/commands/ultra-think.md +2 -2
  21. package/docs/AGENT-CONTEXT.md +43 -19
  22. package/docs/ARCHITECTURE.md +54 -13
  23. package/docs/PLAN.zh-CN.md +4 -2
  24. package/docs/ROADMAP.md +10 -4
  25. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  26. package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
  27. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  28. package/docs/WORKFLOW-LIFECYCLE.md +342 -0
  29. package/hooks/README.md +6 -1
  30. package/hooks/health_check.py +12 -3
  31. package/hooks/subagent_tracker.py +52 -77
  32. package/mcp-server/hook-event.cjs +34 -0
  33. package/mcp-server/lib/archive-journal.cjs +17 -5
  34. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  35. package/mcp-server/lib/change-workflow.cjs +598 -85
  36. package/mcp-server/lib/context-spine.cjs +305 -25
  37. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  38. package/mcp-server/lib/doctor.cjs +42 -3
  39. package/mcp-server/lib/init-project.cjs +210 -16
  40. package/mcp-server/lib/plan-store.cjs +4 -1
  41. package/mcp-server/lib/pricing.cjs +7 -10
  42. package/mcp-server/lib/project-breadcrumb.cjs +15 -0
  43. package/mcp-server/lib/projector.cjs +57 -3
  44. package/mcp-server/lib/spec-learning.cjs +79 -2
  45. package/mcp-server/lib/state-db.cjs +168 -8
  46. package/mcp-server/lib/state-ops.cjs +160 -9
  47. package/mcp-server/lib/workflow-state.cjs +2305 -0
  48. package/mcp-server/server.cjs +215 -15
  49. package/orchestrator/daemon.cjs +1 -2
  50. package/orchestrator/dispatch-rules.cjs +5 -31
  51. package/orchestrator/planner/plan-builder.cjs +21 -12
  52. package/package.json +13 -3
  53. package/skills/ultra-change/SKILL.md +94 -80
  54. package/skills/ultra-change/references/change-contract.md +36 -0
  55. package/skills/ultra-deliver/SKILL.md +76 -79
  56. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  57. package/skills/ultra-dev/SKILL.md +62 -59
  58. package/skills/ultra-doctor/SKILL.md +61 -82
  59. package/skills/ultra-init/SKILL.md +101 -103
  60. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  61. package/skills/ultra-plan/SKILL.md +99 -85
  62. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  63. package/skills/ultra-research/SKILL.md +102 -60
  64. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  65. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  66. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  67. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  68. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  69. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  70. package/skills/ultra-research/references/10-user-personas.md +29 -0
  71. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  72. package/skills/ultra-research/references/20-user-stories.md +30 -0
  73. package/skills/ultra-research/references/21-features-scope.md +29 -0
  74. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  75. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  76. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  77. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  78. package/skills/ultra-research/references/40-deployment.md +29 -0
  79. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  80. package/skills/ultra-research/references/99-synthesis.md +35 -0
  81. package/skills/ultra-review/SKILL.md +63 -73
  82. package/skills/ultra-review/references/review-modes.md +27 -0
  83. package/skills/ultra-review/references/unified-schema.md +25 -0
  84. package/skills/ultra-review/scripts/review_wait.py +40 -3
  85. package/skills/ultra-status/SKILL.md +41 -41
  86. package/skills/ultra-test/SKILL.md +54 -55
  87. package/skills/ultra-think/SKILL.md +26 -12
  88. package/skills/ultra-think/references/decision-dialogue.md +130 -0
  89. package/skills/ultra-verify/SKILL.md +1 -1
  90. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  91. package/spec/cli-protocol.md +17 -3
  92. package/spec/mcp-tools.yaml +683 -64
  93. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  94. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  95. package/spec/schemas/mcp-tools.schema.json +1 -1
  96. package/spec/schemas/state-db.sql +140 -1
  97. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  98. package/spec/schemas/test-report.v1.schema.json +194 -0
  99. package/ultra-tools/cli.cjs +1 -1
  100. package/ultra-tools/commands/migrate.cjs +3 -3
  101. package/ultra-tools/commands/status.cjs +228 -12
  102. package/.ultra-template/delivery-report.json +0 -9
  103. package/.ultra-template/test-report.json +0 -8
  104. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  105. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  106. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  107. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  108. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  109. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  110. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  111. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  112. package/adapters/_shared/tests/validate.test.cjs +0 -30
  113. package/adapters/tests/claude.test.cjs +0 -138
  114. package/adapters/tests/codex-hook.test.cjs +0 -142
  115. package/adapters/tests/codex.test.cjs +0 -430
  116. package/adapters/tests/kimi-hook.test.cjs +0 -149
  117. package/adapters/tests/kimi.test.cjs +0 -259
  118. package/adapters/tests/opencode.test.cjs +0 -357
  119. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  120. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  121. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  122. package/mcp-server/lib/context-spine.test.cjs +0 -93
  123. package/mcp-server/lib/doctor.test.cjs +0 -148
  124. package/mcp-server/lib/plan-store.test.cjs +0 -132
  125. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  126. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  127. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  128. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  129. package/mcp-server/lib/state-db.test.cjs +0 -314
  130. package/mcp-server/lib/task-expander.test.cjs +0 -169
  131. package/mcp-server/lib/topo.test.cjs +0 -158
  132. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  133. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  134. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  135. package/mcp-server/tests/concurrency.test.cjs +0 -112
  136. package/mcp-server/tests/db-ops.test.cjs +0 -121
  137. package/mcp-server/tests/init-project.test.cjs +0 -393
  138. package/mcp-server/tests/migrate.test.cjs +0 -232
  139. package/mcp-server/tests/projector.test.cjs +0 -162
  140. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  141. package/mcp-server/tests/staleness.test.cjs +0 -190
  142. package/mcp-server/tests/state-db.test.cjs +0 -106
  143. package/mcp-server/tests/state-ops.test.cjs +0 -449
  144. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  145. package/mcp-server/tests/telemetry.test.cjs +0 -219
  146. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  147. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  148. package/orchestrator/tests/daemon.test.cjs +0 -311
  149. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  150. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  151. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  152. package/orchestrator/tests/recovery.test.cjs +0 -190
  153. package/orchestrator/tests/session-runner.test.cjs +0 -314
  154. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  155. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  156. package/skills/ultra-research/references/architecture.md +0 -38
  157. package/skills/ultra-research/references/problem-product.md +0 -39
  158. package/skills/ultra-research/references/quality-delivery.md +0 -34
  159. package/skills/ultra-research/references/synthesis.md +0 -38
  160. package/skills/ultra-research/references/users-scope.md +0 -40
  161. package/ultra-tools/legacy-memory.test.cjs +0 -80
  162. package/ultra-tools/status-command.test.cjs +0 -181
  163. package/ultra-tools/system-command.test.cjs +0 -309
@@ -1,232 +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 REPO_ROOT = path.resolve(__dirname, '..', '..');
11
- const CLI = path.join(REPO_ROOT, 'ultra-tools', 'cli.cjs');
12
- const FIXTURE = path.join(REPO_ROOT, 'spec', 'fixtures', 'v4.4-project');
13
-
14
- const { openStateDb, closeStateDb } = require('../lib/state-db.cjs');
15
-
16
- function tmpProject() {
17
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mig-'));
18
- copyRecursive(FIXTURE, dir);
19
- return dir;
20
- }
21
-
22
- function copyRecursive(src, dst) {
23
- fs.mkdirSync(dst, { recursive: true });
24
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
25
- const s = path.join(src, entry.name);
26
- const d = path.join(dst, entry.name);
27
- if (entry.isDirectory()) copyRecursive(s, d);
28
- else fs.copyFileSync(s, d);
29
- }
30
- }
31
-
32
- function runCli(args, opts = {}) {
33
- const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', ...opts });
34
- const lines = r.stdout.trim().split('\n').filter(Boolean);
35
- const last = lines[lines.length - 1] || '{}';
36
- return { code: r.status, envelope: JSON.parse(last), stderr: r.stderr };
37
- }
38
-
39
- test('migrate --dry prints the plan without writing state.db or backups', () => {
40
- const dir = tmpProject();
41
- try {
42
- const dbPath = path.join(dir, '.ultra', 'state.db');
43
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--dry', '--source-dir', dir]);
44
- assert.equal(r.code, 0);
45
- assert.equal(r.envelope.ok, true);
46
- assert.equal(r.envelope.data.mode, 'dry');
47
- assert.equal(r.envelope.data.tasks_to_insert, 3);
48
- assert.equal(r.envelope.data.events_to_insert, 6);
49
- assert.equal(r.envelope.data.warnings.length, 1, 'task-3 status mismatch must be flagged');
50
- assert.equal(r.envelope.data.warnings[0].task_id, 'task-3');
51
- assert.equal(fs.existsSync(dbPath), false, 'dry must not create state.db');
52
- const ultraEntries = fs.readdirSync(path.join(dir, '.ultra'));
53
- assert.equal(ultraEntries.some((n) => n.startsWith('backup-v4.4-')), false, 'dry must not create a backup');
54
- } finally {
55
- fs.rmSync(dir, { recursive: true, force: true });
56
- }
57
- });
58
-
59
- test('migrate forward inserts tasks + events, records migration_history, creates backup', () => {
60
- const dir = tmpProject();
61
- try {
62
- const dbPath = path.join(dir, '.ultra', 'state.db');
63
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
64
- assert.equal(r.code, 0);
65
- assert.equal(r.envelope.ok, true);
66
- assert.equal(r.envelope.data.mode, 'apply');
67
- assert.equal(r.envelope.data.tasks_inserted, 3);
68
- assert.equal(r.envelope.data.events_inserted, 6);
69
- assert.ok(fs.existsSync(r.envelope.data.backup_dir), 'backup dir must exist');
70
- assert.ok(fs.existsSync(dbPath), 'state.db must be written');
71
-
72
- const db = openStateDb(dbPath);
73
- const taskIds = db.prepare('SELECT id FROM tasks ORDER BY id').all().map((r) => r.id);
74
- assert.deepEqual(taskIds, ['task-1', 'task-2', 'task-3']);
75
-
76
- const migrated = db.prepare(
77
- 'SELECT id, deps, estimated_days, context_file, created_at, updated_at FROM tasks ORDER BY id',
78
- ).all();
79
- assert.deepEqual(JSON.parse(migrated[1].deps), ['task-1']);
80
- assert.equal(migrated[1].estimated_days, 1.5);
81
- assert.equal(migrated[1].context_file, '.ultra/tasks/contexts/task-2.md');
82
- for (const task of migrated) {
83
- assert.match(task.created_at, /^2026-04-15T00:00:00\.000Z$/);
84
- assert.match(task.updated_at, /^2026-04-16T00:00:00\.000Z$/);
85
- }
86
-
87
- // Status comes from tasks.json (task-3 = pending), not the context md (blocked)
88
- const t3 = db.prepare("SELECT status FROM tasks WHERE id = 'task-3'").get();
89
- assert.equal(t3.status, 'pending');
90
-
91
- const eventCount = db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
92
- assert.equal(eventCount, 6);
93
-
94
- const mig = db.prepare("SELECT direction, status FROM migration_history ORDER BY id").all();
95
- assert.equal(mig.length, 1);
96
- assert.equal(mig[0].direction, 'forward');
97
- assert.equal(mig[0].status, 'success');
98
- closeStateDb(db);
99
-
100
- const projected = JSON.parse(fs.readFileSync(
101
- path.join(dir, '.ultra', 'tasks', 'tasks.json'),
102
- 'utf8',
103
- ));
104
- assert.equal(projected.schema_version, '4.5');
105
- assert.equal(projected.source, '.ultra/state.db');
106
- assert.equal(projected.tasks.length, 3);
107
- assert.equal(projected.tasks[1].estimated_days, 1.5);
108
- assert.deepEqual(projected.tasks[1].deps, ['task-1']);
109
- assert.match(
110
- fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'contexts', 'task-2.md'), 'utf8'),
111
- /Wire JWT validation into the request pipeline/,
112
- );
113
- const contextTemplate = fs.readFileSync(
114
- path.join(dir, '.ultra', 'tasks', 'contexts', 'TEMPLATE.md'),
115
- 'utf8',
116
- );
117
- assert.doesNotMatch(contextTemplate, /> \*\*Status\*\*:/);
118
- assert.doesNotMatch(contextTemplate, /mid_workflow_recall|session_context/);
119
- assert.match(contextTemplate, /Preserve this acceptance item/);
120
- } finally {
121
- fs.rmSync(dir, { recursive: true, force: true });
122
- }
123
- });
124
-
125
- test('migrate imports a v4.5 projection into schema 12 authority and requires evidence-backed re-adoption', () => {
126
- const dir = tmpProject();
127
- try {
128
- const tasksPath = path.join(dir, '.ultra', 'tasks', 'tasks.json');
129
- const projection = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
130
- delete projection.version;
131
- projection.schema_version = '4.5';
132
- projection.source = '.ultra/state.db';
133
- fs.writeFileSync(tasksPath, `${JSON.stringify(projection, null, 2)}\n`);
134
-
135
- const r = runCli(['migrate', '--from=4.5', '--to=12.0', '--source-dir', dir]);
136
- assert.equal(r.code, 0);
137
- assert.equal(r.envelope.data.from, '4.5');
138
- assert.equal(r.envelope.data.to, '12.0');
139
- assert.match(path.basename(r.envelope.data.backup_dir), /^backup-v4\.5-/);
140
-
141
- const db = openStateDb(path.join(dir, '.ultra', 'state.db'));
142
- try {
143
- assert.equal(db.prepare('SELECT COUNT(*) AS count FROM tasks').get().count, 3);
144
- const baseline = db.prepare(
145
- "SELECT mode, status, gaps_json FROM baselines WHERE id = 'migrated-baseline'",
146
- ).get();
147
- assert.deepEqual(
148
- { mode: baseline.mode, status: baseline.status },
149
- { mode: 'migrated', status: 'adopting' },
150
- );
151
- assert.equal(JSON.parse(baseline.gaps_json)[0].id, 'legacy-rebaseline-required');
152
- } finally { closeStateDb(db); }
153
- } finally {
154
- fs.rmSync(dir, { recursive: true, force: true });
155
- }
156
- });
157
-
158
- test('migrate --rollback restores .ultra contents and writes a rollback row', () => {
159
- const dir = tmpProject();
160
- try {
161
- const dbPath = path.join(dir, '.ultra', 'state.db');
162
- const fwd = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
163
- assert.equal(fwd.code, 0);
164
-
165
- // Capture rollback audit BEFORE the db file is removed by reading from the
166
- // backup snapshot we'll restore from.
167
- const backupDir = fwd.envelope.data.backup_dir;
168
-
169
- const back = runCli(['migrate', '--from=4.4', '--to=4.5', '--rollback', '--source-dir', dir]);
170
- assert.equal(back.code, 0);
171
- assert.equal(back.envelope.ok, true);
172
- assert.equal(back.envelope.data.mode, 'rollback');
173
- assert.equal(back.envelope.data.backup_dir, backupDir);
174
-
175
- // state.db is gone
176
- assert.equal(fs.existsSync(dbPath), false);
177
-
178
- // .ultra/tasks/tasks.json restored to original v4.4 content
179
- const tasksJson = JSON.parse(fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
180
- assert.equal(tasksJson.version, '4.4');
181
- assert.equal(tasksJson.tasks.length, 3);
182
- closeStateDb(undefined);
183
- } finally {
184
- fs.rmSync(dir, { recursive: true, force: true });
185
- }
186
- });
187
-
188
- test('migrate refuses to merge a v4.4 projection into a non-empty state.db', () => {
189
- const dir = tmpProject();
190
- try {
191
- const dbPath = path.join(dir, '.ultra', 'state.db');
192
- const init = require('../lib/state-db.cjs').initStateDb(dbPath);
193
- require('../lib/state-ops.cjs').createTask(init.db, {
194
- id: 'existing', title: 'existing task', type: 'feature', priority: 'P1',
195
- });
196
- closeStateDb(init.db);
197
-
198
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
199
- assert.equal(r.code, 2);
200
- assert.equal(r.envelope.error.code, 'MIGRATE_FAILED');
201
- assert.match(r.envelope.error.message, /non-empty state\.db/);
202
-
203
- const db = openStateDb(dbPath);
204
- assert.deepEqual(db.prepare('SELECT id FROM tasks ORDER BY id').all(), [{ id: 'existing' }]);
205
- closeStateDb(db);
206
- } finally {
207
- fs.rmSync(dir, { recursive: true, force: true });
208
- }
209
- });
210
-
211
- test('migrate rejects unsupported --from / --to versions', () => {
212
- const r1 = runCli(['migrate', '--from=3.0', '--to=4.5', '--dry', '--source-dir', FIXTURE]);
213
- assert.equal(r1.code, 1);
214
- assert.equal(r1.envelope.error.code, 'UNSUPPORTED_VERSION');
215
-
216
- const r2 = runCli(['migrate', '--from=4.4', '--to=5.0', '--dry', '--source-dir', FIXTURE]);
217
- assert.equal(r2.code, 1);
218
- assert.equal(r2.envelope.error.code, 'UNSUPPORTED_VERSION');
219
- });
220
-
221
- test('migrate forward fails cleanly when tasks.json is missing', () => {
222
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mig-empty-'));
223
- fs.mkdirSync(path.join(dir, '.ultra'), { recursive: true });
224
- try {
225
- const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
226
- assert.equal(r.code, 2);
227
- assert.equal(r.envelope.error.code, 'MIGRATE_FAILED');
228
- assert.match(r.envelope.error.message, /tasks\.json missing/);
229
- } finally {
230
- fs.rmSync(dir, { recursive: true, force: true });
231
- }
232
- });
@@ -1,162 +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
-
9
- const Ajv = require('ajv/dist/2020');
10
- const addFormats = require('ajv-formats');
11
-
12
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
13
- const TASKS_SCHEMA = path.join(REPO_ROOT, 'spec', 'schemas', 'tasks.v4.5.schema.json');
14
-
15
- const { initStateDb, closeStateDb } = require('../lib/state-db.cjs');
16
- const ops = require('../lib/state-ops.cjs');
17
- const projector = require('../lib/projector.cjs');
18
-
19
- const ajv = new Ajv({ allErrors: true, strict: false });
20
- addFormats(ajv);
21
- const validateTasksJson = ajv.compile(JSON.parse(fs.readFileSync(TASKS_SCHEMA, 'utf8')));
22
-
23
- function tmpProject() {
24
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-proj-'));
25
- const dbPath = path.join(dir, '.ultra', 'state.db');
26
- const init = initStateDb(dbPath);
27
- return { dir, dbPath, db: init.db };
28
- }
29
-
30
- function readJson(p) {
31
- return JSON.parse(fs.readFileSync(p, 'utf8'));
32
- }
33
-
34
- test('projectTasks emits a v4.5-conformant tasks.json', () => {
35
- const { dir, db } = tmpProject();
36
- try {
37
- ops.createTask(db, { id: 'p-1', title: 'first', type: 'feature', priority: 'P1', tag: 'main', estimated_days: 2.5 });
38
- ops.createTask(db, { id: 'p-2', title: 'second', type: 'bugfix', priority: 'P2' });
39
- ops.updateTaskStatus(db, 'p-1', 'in_progress');
40
-
41
- const out = projector.projectTasks(db, {}, { rootDir: dir });
42
- assert.equal(out.count, 2);
43
-
44
- const projection = readJson(out.path);
45
- assert.equal(projection.schema_version, '4.5');
46
- assert.equal(projection.source, '.ultra/state.db');
47
- assert.equal(projection.tasks.length, 2);
48
- assert.ok(validateTasksJson(projection), `ajv failed: ${ajv.errorsText(validateTasksJson.errors)}`);
49
-
50
- const p1 = projection.tasks.find((t) => t.id === 'p-1');
51
- assert.equal(p1.status, 'in_progress');
52
- assert.equal(p1.estimated_days, 2.5);
53
- closeStateDb(db);
54
- } finally {
55
- fs.rmSync(dir, { recursive: true, force: true });
56
- }
57
- });
58
-
59
- test('projectContext rebuilds header but preserves the body', () => {
60
- const { dir, db } = tmpProject();
61
- try {
62
- ops.createTask(db, { id: 'cx-1', title: 'context test', type: 'feature', priority: 'P1' });
63
- const ctxFile = path.join(dir, '.ultra', 'tasks', 'contexts', 'task-cx-1.md');
64
- fs.mkdirSync(path.dirname(ctxFile), { recursive: true });
65
- fs.writeFileSync(ctxFile, '---\nstale: header\n---\n\n# body that must survive\n\nUser notes go here.\n');
66
-
67
- ops.updateTaskStatus(db, 'cx-1', 'in_progress');
68
- projector.projectContext(db, 'cx-1', {}, { rootDir: dir });
69
-
70
- const text = fs.readFileSync(ctxFile, 'utf8');
71
- assert.match(text, /^---\n/, 'must start with frontmatter');
72
- assert.match(text, /status: in_progress/);
73
- assert.match(text, /schema_version: 4\.5/);
74
- assert.match(text, /# body that must survive/);
75
- assert.match(text, /User notes go here\./);
76
- closeStateDb(db);
77
- } finally {
78
- fs.rmSync(dir, { recursive: true, force: true });
79
- }
80
- });
81
-
82
- test('projectContext removes the legacy body status banner while preserving task content', () => {
83
- const { dir, db } = tmpProject();
84
- try {
85
- ops.createTask(db, {
86
- id: 'legacy-body', title: 'legacy context body', type: 'feature', priority: 'P1',
87
- context_file: '.ultra/tasks/contexts/task-legacy-body.md',
88
- });
89
- const ctxFile = path.join(dir, '.ultra', 'tasks', 'contexts', 'task-legacy-body.md');
90
- fs.mkdirSync(path.dirname(ctxFile), { recursive: true });
91
- fs.writeFileSync(ctxFile, [
92
- '# Task legacy-body',
93
- '',
94
- '> **Status**: pending | **Priority**: P1 | **Complexity**: 4',
95
- '',
96
- '## Context',
97
- 'Keep this body.',
98
- '',
99
- ].join('\n'));
100
-
101
- projector.projectContext(db, 'legacy-body', {}, { rootDir: dir });
102
- const text = fs.readFileSync(ctxFile, 'utf8');
103
- assert.match(text, /status: pending/);
104
- assert.doesNotMatch(text, /> \*\*Status\*\*:/);
105
- assert.match(text, /# Task legacy-body/);
106
- assert.match(text, /Keep this body\./);
107
- closeStateDb(db);
108
- } finally {
109
- fs.rmSync(dir, { recursive: true, force: true });
110
- }
111
- });
112
-
113
- test('manual edits to tasks.json are overwritten on the next projectAll', () => {
114
- const { dir, db } = tmpProject();
115
- try {
116
- ops.createTask(db, { id: 'ow-1', title: 'overwrite', type: 'feature', priority: 'P0' });
117
- projector.projectAll(db, { rootDir: dir });
118
-
119
- const tasksJson = path.join(dir, '.ultra', 'tasks', 'tasks.json');
120
- fs.writeFileSync(tasksJson, JSON.stringify({ tampered: true }));
121
- assert.deepEqual(readJson(tasksJson), { tampered: true });
122
-
123
- ops.patchTask(db, 'ow-1', { tag: 'main' });
124
- projector.projectAll(db, { rootDir: dir });
125
-
126
- const restored = readJson(tasksJson);
127
- assert.equal(restored.schema_version, '4.5');
128
- assert.equal(restored.tasks[0].id, 'ow-1');
129
- closeStateDb(db);
130
- } finally {
131
- fs.rmSync(dir, { recursive: true, force: true });
132
- }
133
- });
134
-
135
- test('projectAll completes within 1s after a state.db write (PLAN AC)', () => {
136
- const { dir, db } = tmpProject();
137
- try {
138
- for (let i = 0; i < 30; i++) {
139
- ops.createTask(db, { id: `s-${i}`, title: `s${i}`, type: 'feature', priority: 'P3' });
140
- }
141
- const start = Date.now();
142
- const out = projector.projectAll(db, { rootDir: dir });
143
- const elapsed = Date.now() - start;
144
- assert.ok(elapsed <= 1000, `projectAll took ${elapsed}ms, must be <= 1000ms`);
145
- assert.equal(out.tasks_json.count, 30);
146
- assert.equal(out.contexts.length, 30);
147
- closeStateDb(db);
148
- } finally {
149
- fs.rmSync(dir, { recursive: true, force: true });
150
- }
151
- });
152
-
153
- test('projectContext is a no-op for unknown task ids', () => {
154
- const { dir, db } = tmpProject();
155
- try {
156
- const r = projector.projectContext(db, 'no-such-task', {}, { rootDir: dir });
157
- assert.equal(r, null);
158
- closeStateDb(db);
159
- } finally {
160
- fs.rmSync(dir, { recursive: true, force: true });
161
- }
162
- });