ultra-builder-pro-cli 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/delivery-report.json +9 -0
- package/.ultra-template/docs/research/README.md +53 -0
- package/.ultra-template/specs/architecture.md +250 -0
- package/.ultra-template/specs/discovery.md +248 -0
- package/.ultra-template/specs/product.md +223 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
- package/.ultra-template/tasks/tasks.json +33 -0
- package/.ultra-template/test-report.json +8 -0
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/adapters/_shared/codex-assets.cjs +1128 -0
- package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
- package/adapters/_shared/file-ops.cjs +115 -0
- package/adapters/_shared/frontmatter.cjs +52 -0
- package/adapters/_shared/handbook.cjs +144 -0
- package/adapters/_shared/md-to-toml.cjs +54 -0
- package/adapters/_shared/path-rewrite.cjs +79 -0
- package/adapters/_shared/rtk-detect.cjs +86 -0
- package/adapters/_shared/runtime-assets.cjs +70 -0
- package/adapters/_shared/settings-merge.cjs +76 -0
- package/adapters/_shared/tests/file-ops.test.cjs +54 -0
- package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
- package/adapters/_shared/tests/handbook.test.cjs +86 -0
- package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
- package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
- package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
- package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
- package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
- package/adapters/_shared/tests/validate.test.cjs +30 -0
- package/adapters/_shared/validate.cjs +24 -0
- package/adapters/claude.js +182 -0
- package/adapters/codex.js +385 -0
- package/adapters/gemini.js +204 -0
- package/adapters/opencode.js +506 -0
- package/adapters/tests/claude.test.cjs +117 -0
- package/adapters/tests/codex-hook.test.cjs +90 -0
- package/adapters/tests/codex.test.cjs +356 -0
- package/adapters/tests/gemini.test.cjs +112 -0
- package/adapters/tests/opencode.test.cjs +246 -0
- package/agents/code-reviewer.md +102 -0
- package/agents/debugger.md +127 -0
- package/agents/review-code.md +125 -0
- package/agents/review-comments.md +98 -0
- package/agents/review-coordinator.md +146 -0
- package/agents/review-design.md +133 -0
- package/agents/review-errors.md +87 -0
- package/agents/review-tests.md +93 -0
- package/agents/tdd-runner.md +92 -0
- package/bin/handbook.js +54 -0
- package/bin/install.js +254 -0
- package/bin/orchestrator.js +214 -0
- package/commands/learn.md +41 -0
- package/commands/ultra-deliver.md +45 -0
- package/commands/ultra-dev.md +50 -0
- package/commands/ultra-init.md +51 -0
- package/commands/ultra-plan.md +49 -0
- package/commands/ultra-research.md +59 -0
- package/commands/ultra-status.md +46 -0
- package/commands/ultra-test.md +48 -0
- package/commands/ultra-think.md +45 -0
- package/docs/AGENT-CONTEXT.md +143 -0
- package/docs/ARCHITECTURE.md +197 -0
- package/docs/COMMIT-HASH-BACKFILL.md +101 -0
- package/docs/LEGACY-HERMES.md +1175 -0
- package/docs/PLAN.zh-CN.md +1683 -0
- package/docs/ROADMAP.md +102 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
- package/hooks/README.md +24 -0
- package/hooks/active_task_context.py +44 -0
- package/hooks/adapters/codex.py +302 -0
- package/hooks/health_check.py +61 -0
- package/hooks/pre_stop_check.py +56 -0
- package/hooks/subagent_tracker.py +116 -0
- package/hooks/workflow_checkpoint.py +56 -0
- package/hooks/workflow_context.py +63 -0
- package/hooks/workflow_resume.py +50 -0
- package/mcp-server/lib/init-project.cjs +162 -0
- package/mcp-server/lib/llm-client.cjs +157 -0
- package/mcp-server/lib/llm-client.test.cjs +156 -0
- package/mcp-server/lib/plan-store.cjs +96 -0
- package/mcp-server/lib/plan-store.test.cjs +132 -0
- package/mcp-server/lib/prd-parser.cjs +110 -0
- package/mcp-server/lib/prd-parser.test.cjs +171 -0
- package/mcp-server/lib/pricing.cjs +53 -0
- package/mcp-server/lib/projector.cjs +158 -0
- package/mcp-server/lib/state-db.cjs +113 -0
- package/mcp-server/lib/state-db.test.cjs +116 -0
- package/mcp-server/lib/state-ops.cjs +725 -0
- package/mcp-server/lib/task-expander.cjs +123 -0
- package/mcp-server/lib/task-expander.test.cjs +253 -0
- package/mcp-server/lib/telemetry.cjs +72 -0
- package/mcp-server/lib/topo.cjs +131 -0
- package/mcp-server/lib/topo.test.cjs +158 -0
- package/mcp-server/server.cjs +482 -0
- package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
- package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
- package/mcp-server/tests/concurrency-update.test.cjs +96 -0
- package/mcp-server/tests/concurrency.test.cjs +112 -0
- package/mcp-server/tests/db-ops.test.cjs +117 -0
- package/mcp-server/tests/init-project.test.cjs +141 -0
- package/mcp-server/tests/migrate.test.cjs +144 -0
- package/mcp-server/tests/projector.test.cjs +130 -0
- package/mcp-server/tests/server-contract.test.cjs +750 -0
- package/mcp-server/tests/staleness.test.cjs +190 -0
- package/mcp-server/tests/state-ops.test.cjs +286 -0
- package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
- package/mcp-server/tests/telemetry.test.cjs +205 -0
- package/orchestrator/auto-merge.cjs +109 -0
- package/orchestrator/code-graph-watcher.cjs +121 -0
- package/orchestrator/daemon.cjs +112 -0
- package/orchestrator/dispatch-rules.cjs +93 -0
- package/orchestrator/parallel-orchestrator.cjs +195 -0
- package/orchestrator/planner/expand-prompt.cjs +62 -0
- package/orchestrator/planner/expand-prompt.test.cjs +52 -0
- package/orchestrator/planner/plan-builder.cjs +116 -0
- package/orchestrator/planner/plan-builder.test.cjs +165 -0
- package/orchestrator/planner/prd-prompt.cjs +44 -0
- package/orchestrator/planner/prd-prompt.test.cjs +30 -0
- package/orchestrator/recovery.cjs +70 -0
- package/orchestrator/session-runner.cjs +224 -0
- package/orchestrator/skill-miner.cjs +133 -0
- package/orchestrator/tests/auto-merge.test.cjs +216 -0
- package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
- package/orchestrator/tests/daemon.test.cjs +260 -0
- package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
- package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
- package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
- package/orchestrator/tests/recovery.test.cjs +190 -0
- package/orchestrator/tests/session-runner.test.cjs +314 -0
- package/orchestrator/tests/skill-miner.test.cjs +147 -0
- package/orchestrator/tests/worktree-manager.test.cjs +148 -0
- package/orchestrator/worktree-manager.cjs +131 -0
- package/output-styles/coding-vibes.md +67 -0
- package/output-styles/structural-thinking.md +61 -0
- package/package.json +109 -0
- package/settings.json +8 -0
- package/skills/cc-collab/SKILL.md +41 -0
- package/skills/code-review-expert/SKILL.md +118 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
- package/skills/code-review-expert/references/integration-checklist.md +46 -0
- package/skills/code-review-expert/references/removal-plan.md +52 -0
- package/skills/code-review-expert/references/security-checklist.md +100 -0
- package/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/skills/codex-collab/SKILL.md +52 -0
- package/skills/codex-collab/evals/evals.json +40 -0
- package/skills/gemini-collab/SKILL.md +51 -0
- package/skills/gemini-collab/evals/evals.json +42 -0
- package/skills/integration-rules/SKILL.md +71 -0
- package/skills/learn/SKILL.md +117 -0
- package/skills/security-rules/SKILL.md +60 -0
- package/skills/testing-rules/SKILL.md +56 -0
- package/skills/ultra-deliver/SKILL.md +171 -0
- package/skills/ultra-dev/SKILL.md +324 -0
- package/skills/ultra-init/SKILL.md +148 -0
- package/skills/ultra-plan/SKILL.md +314 -0
- package/skills/ultra-research/SKILL.md +156 -0
- package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
- package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
- package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
- package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
- package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
- package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
- package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
- package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
- package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
- package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
- package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
- package/skills/ultra-research/steps/step-40-deployment.md +123 -0
- package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
- package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
- package/skills/ultra-review/SKILL.md +437 -0
- package/skills/ultra-review/references/unified-schema.md +127 -0
- package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
- package/skills/ultra-review/scripts/review_wait.py +120 -0
- package/skills/ultra-status/SKILL.md +162 -0
- package/skills/ultra-test/SKILL.md +195 -0
- package/skills/ultra-think/SKILL.md +109 -0
- package/skills/ultra-verify/SKILL.md +111 -0
- package/skills/ultra-verify/evals/evals.json +51 -0
- package/skills/ultra-verify/references/confidence-system.md +38 -0
- package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
- package/skills/ultra-verify/scripts/verify_wait.py +131 -0
- package/spec/cli-protocol.md +118 -0
- package/spec/mcp-tools.yaml +658 -0
- package/spec/schemas/command-manifest.schema.json +47 -0
- package/spec/schemas/context-file.v4.5.schema.json +54 -0
- package/spec/schemas/mcp-tools.schema.json +136 -0
- package/spec/schemas/skill-manifest.schema.json +94 -0
- package/spec/schemas/state-db.sql +154 -0
- package/spec/schemas/tasks.v4.5.schema.json +66 -0
- package/ultra-tools/cli.cjs +121 -0
- package/ultra-tools/commands/db.cjs +175 -0
- package/ultra-tools/commands/legacy-memory.cjs +159 -0
- package/ultra-tools/commands/migrate.cjs +354 -0
- package/ultra-tools/commands/session.cjs +180 -0
- package/ultra-tools/commands/status.cjs +158 -0
- package/ultra-tools/commands/task.cjs +95 -0
- package/ultra-tools/legacy-memory.test.cjs +80 -0
- package/ultra-tools/status-command.test.cjs +132 -0
|
@@ -0,0 +1,141 @@
|
|
|
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 { initProject, InitProjectError, DEFAULT_TEMPLATE } = require('../lib/init-project.cjs');
|
|
10
|
+
|
|
11
|
+
function mkTempDir(prefix = 'ubp-init-test-') {
|
|
12
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanup(dir) {
|
|
16
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* best effort */ }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('initProject copies bundled template into .ultra/', () => {
|
|
20
|
+
const target = mkTempDir();
|
|
21
|
+
try {
|
|
22
|
+
const r = initProject({
|
|
23
|
+
target_dir: target,
|
|
24
|
+
project_name: 'demo',
|
|
25
|
+
project_type: 'cli',
|
|
26
|
+
stack: 'node',
|
|
27
|
+
});
|
|
28
|
+
assert.equal(r.status, 'created');
|
|
29
|
+
assert.equal(r.created_path, path.join(target, '.ultra'));
|
|
30
|
+
assert.ok(r.copied_files.includes('tasks/tasks.json'));
|
|
31
|
+
assert.ok(r.copied_files.includes('specs/product.md'));
|
|
32
|
+
assert.ok(!r.copied_files.some((f) => f.endsWith('.DS_Store')));
|
|
33
|
+
} finally { cleanup(target); }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('initProject injects project metadata into tasks.json', () => {
|
|
37
|
+
const target = mkTempDir();
|
|
38
|
+
try {
|
|
39
|
+
initProject({
|
|
40
|
+
target_dir: target,
|
|
41
|
+
project_name: 'meta-demo',
|
|
42
|
+
project_type: 'web',
|
|
43
|
+
stack: 'next',
|
|
44
|
+
});
|
|
45
|
+
const tasksJson = JSON.parse(fs.readFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
|
|
46
|
+
assert.equal(tasksJson.project.name, 'meta-demo');
|
|
47
|
+
assert.equal(tasksJson.project.type, 'web');
|
|
48
|
+
assert.equal(tasksJson.project.stack, 'next');
|
|
49
|
+
assert.ok(tasksJson.created.length > 0);
|
|
50
|
+
assert.ok(tasksJson.updated.length > 0);
|
|
51
|
+
} finally { cleanup(target); }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('initProject refuses when .ultra/ already exists and overwrite=false', () => {
|
|
55
|
+
const target = mkTempDir();
|
|
56
|
+
try {
|
|
57
|
+
initProject({ target_dir: target, project_name: 'first' });
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => initProject({ target_dir: target, project_name: 'again' }),
|
|
60
|
+
(err) => err instanceof InitProjectError && err.code === 'ULTRA_DIR_EXISTS',
|
|
61
|
+
);
|
|
62
|
+
} finally { cleanup(target); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('initProject with overwrite=true backs up existing .ultra/', () => {
|
|
66
|
+
const target = mkTempDir();
|
|
67
|
+
try {
|
|
68
|
+
initProject({ target_dir: target, project_name: 'first' });
|
|
69
|
+
const sentinel = path.join(target, '.ultra', 'sentinel.txt');
|
|
70
|
+
fs.writeFileSync(sentinel, 'before-backup');
|
|
71
|
+
|
|
72
|
+
const r = initProject({ target_dir: target, project_name: 'second', overwrite: true });
|
|
73
|
+
assert.equal(r.status, 'overwritten');
|
|
74
|
+
assert.ok(r.backup_path);
|
|
75
|
+
assert.ok(fs.existsSync(path.join(r.backup_path, 'sentinel.txt')));
|
|
76
|
+
assert.equal(
|
|
77
|
+
fs.readFileSync(path.join(r.backup_path, 'sentinel.txt'), 'utf8'),
|
|
78
|
+
'before-backup',
|
|
79
|
+
);
|
|
80
|
+
assert.ok(!fs.existsSync(sentinel));
|
|
81
|
+
} finally { cleanup(target); }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('initProject rejects empty project_name', () => {
|
|
85
|
+
const target = mkTempDir();
|
|
86
|
+
try {
|
|
87
|
+
assert.throws(
|
|
88
|
+
() => initProject({ target_dir: target, project_name: '' }),
|
|
89
|
+
(err) => err instanceof InitProjectError && err.code === 'VALIDATION_ERROR',
|
|
90
|
+
);
|
|
91
|
+
} finally { cleanup(target); }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('initProject rejects missing source_template', () => {
|
|
95
|
+
const target = mkTempDir();
|
|
96
|
+
try {
|
|
97
|
+
assert.throws(
|
|
98
|
+
() => initProject({
|
|
99
|
+
target_dir: target,
|
|
100
|
+
project_name: 'demo',
|
|
101
|
+
source_template: '/tmp/does-not-exist-ubp-init',
|
|
102
|
+
}),
|
|
103
|
+
(err) => err instanceof InitProjectError && err.code === 'TEMPLATE_MISSING',
|
|
104
|
+
);
|
|
105
|
+
} finally { cleanup(target); }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('initProject surface TARGET_NOT_DIR when target_dir is a file', () => {
|
|
109
|
+
const parent = mkTempDir();
|
|
110
|
+
const filePath = path.join(parent, 'not-a-dir.txt');
|
|
111
|
+
fs.writeFileSync(filePath, 'x');
|
|
112
|
+
try {
|
|
113
|
+
assert.throws(
|
|
114
|
+
() => initProject({ target_dir: filePath, project_name: 'demo' }),
|
|
115
|
+
(err) => err instanceof InitProjectError && err.code === 'TARGET_NOT_DIR',
|
|
116
|
+
);
|
|
117
|
+
} finally { cleanup(parent); }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('copied tree matches bundled templates/.ultra/ (diff-equal excluding .DS_Store)', () => {
|
|
121
|
+
const target = mkTempDir();
|
|
122
|
+
try {
|
|
123
|
+
const r = initProject({ target_dir: target, project_name: 'diff-equal' });
|
|
124
|
+
const templateFiles = [];
|
|
125
|
+
(function walk(dir, prefix = '') {
|
|
126
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
127
|
+
if (e.name === '.DS_Store') continue;
|
|
128
|
+
const rel = prefix ? path.join(prefix, e.name) : e.name;
|
|
129
|
+
if (e.isDirectory()) walk(path.join(dir, e.name), rel);
|
|
130
|
+
else if (e.isFile()) templateFiles.push(rel);
|
|
131
|
+
}
|
|
132
|
+
})(DEFAULT_TEMPLATE);
|
|
133
|
+
assert.deepEqual(r.copied_files.sort(), templateFiles.sort());
|
|
134
|
+
for (const rel of templateFiles) {
|
|
135
|
+
if (rel === 'tasks/tasks.json') continue; // metadata injected by design
|
|
136
|
+
const src = fs.readFileSync(path.join(DEFAULT_TEMPLATE, rel));
|
|
137
|
+
const dst = fs.readFileSync(path.join(r.created_path, rel));
|
|
138
|
+
assert.deepEqual(dst, src, `content diff in ${rel}`);
|
|
139
|
+
}
|
|
140
|
+
} finally { cleanup(target); }
|
|
141
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
// Status comes from tasks.json (task-3 = pending), not the context md (blocked)
|
|
77
|
+
const t3 = db.prepare("SELECT status FROM tasks WHERE id = 'task-3'").get();
|
|
78
|
+
assert.equal(t3.status, 'pending');
|
|
79
|
+
|
|
80
|
+
const eventCount = db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
|
|
81
|
+
assert.equal(eventCount, 6);
|
|
82
|
+
|
|
83
|
+
const mig = db.prepare("SELECT direction, status FROM migration_history ORDER BY id").all();
|
|
84
|
+
assert.equal(mig.length, 1);
|
|
85
|
+
assert.equal(mig[0].direction, 'forward');
|
|
86
|
+
assert.equal(mig[0].status, 'success');
|
|
87
|
+
closeStateDb(db);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('migrate --rollback restores .ultra contents and writes a rollback row', () => {
|
|
94
|
+
const dir = tmpProject();
|
|
95
|
+
try {
|
|
96
|
+
const dbPath = path.join(dir, '.ultra', 'state.db');
|
|
97
|
+
const fwd = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
|
|
98
|
+
assert.equal(fwd.code, 0);
|
|
99
|
+
|
|
100
|
+
// Capture rollback audit BEFORE the db file is removed by reading from the
|
|
101
|
+
// backup snapshot we'll restore from.
|
|
102
|
+
const backupDir = fwd.envelope.data.backup_dir;
|
|
103
|
+
|
|
104
|
+
const back = runCli(['migrate', '--from=4.4', '--to=4.5', '--rollback', '--source-dir', dir]);
|
|
105
|
+
assert.equal(back.code, 0);
|
|
106
|
+
assert.equal(back.envelope.ok, true);
|
|
107
|
+
assert.equal(back.envelope.data.mode, 'rollback');
|
|
108
|
+
assert.equal(back.envelope.data.backup_dir, backupDir);
|
|
109
|
+
|
|
110
|
+
// state.db is gone
|
|
111
|
+
assert.equal(fs.existsSync(dbPath), false);
|
|
112
|
+
|
|
113
|
+
// .ultra/tasks/tasks.json restored to original v4.4 content
|
|
114
|
+
const tasksJson = JSON.parse(fs.readFileSync(path.join(dir, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
|
|
115
|
+
assert.equal(tasksJson.schema_version, '4.4');
|
|
116
|
+
assert.equal(tasksJson.tasks.length, 3);
|
|
117
|
+
closeStateDb(undefined);
|
|
118
|
+
} finally {
|
|
119
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('migrate rejects unsupported --from / --to versions', () => {
|
|
124
|
+
const r1 = runCli(['migrate', '--from=3.0', '--to=4.5', '--dry', '--source-dir', FIXTURE]);
|
|
125
|
+
assert.equal(r1.code, 1);
|
|
126
|
+
assert.equal(r1.envelope.error.code, 'UNSUPPORTED_VERSION');
|
|
127
|
+
|
|
128
|
+
const r2 = runCli(['migrate', '--from=4.4', '--to=5.0', '--dry', '--source-dir', FIXTURE]);
|
|
129
|
+
assert.equal(r2.code, 1);
|
|
130
|
+
assert.equal(r2.envelope.error.code, 'UNSUPPORTED_VERSION');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('migrate forward fails cleanly when tasks.json is missing', () => {
|
|
134
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-mig-empty-'));
|
|
135
|
+
fs.mkdirSync(path.join(dir, '.ultra'), { recursive: true });
|
|
136
|
+
try {
|
|
137
|
+
const r = runCli(['migrate', '--from=4.4', '--to=4.5', '--source-dir', dir]);
|
|
138
|
+
assert.equal(r.code, 2);
|
|
139
|
+
assert.equal(r.envelope.error.code, 'MIGRATE_FAILED');
|
|
140
|
+
assert.match(r.envelope.error.message, /tasks\.json missing/);
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
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' });
|
|
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
|
+
closeStateDb(db);
|
|
53
|
+
} finally {
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('projectContext rebuilds header but preserves the body', () => {
|
|
59
|
+
const { dir, db } = tmpProject();
|
|
60
|
+
try {
|
|
61
|
+
ops.createTask(db, { id: 'cx-1', title: 'context test', type: 'feature', priority: 'P1' });
|
|
62
|
+
const ctxFile = path.join(dir, '.ultra', 'tasks', 'contexts', 'task-cx-1.md');
|
|
63
|
+
fs.mkdirSync(path.dirname(ctxFile), { recursive: true });
|
|
64
|
+
fs.writeFileSync(ctxFile, '---\nstale: header\n---\n\n# body that must survive\n\nUser notes go here.\n');
|
|
65
|
+
|
|
66
|
+
ops.updateTaskStatus(db, 'cx-1', 'in_progress');
|
|
67
|
+
projector.projectContext(db, 'cx-1', {}, { rootDir: dir });
|
|
68
|
+
|
|
69
|
+
const text = fs.readFileSync(ctxFile, 'utf8');
|
|
70
|
+
assert.match(text, /^---\n/, 'must start with frontmatter');
|
|
71
|
+
assert.match(text, /status: in_progress/);
|
|
72
|
+
assert.match(text, /schema_version: 4\.5/);
|
|
73
|
+
assert.match(text, /# body that must survive/);
|
|
74
|
+
assert.match(text, /User notes go here\./);
|
|
75
|
+
closeStateDb(db);
|
|
76
|
+
} finally {
|
|
77
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('manual edits to tasks.json are overwritten on the next projectAll', () => {
|
|
82
|
+
const { dir, db } = tmpProject();
|
|
83
|
+
try {
|
|
84
|
+
ops.createTask(db, { id: 'ow-1', title: 'overwrite', type: 'feature', priority: 'P0' });
|
|
85
|
+
projector.projectAll(db, { rootDir: dir });
|
|
86
|
+
|
|
87
|
+
const tasksJson = path.join(dir, '.ultra', 'tasks', 'tasks.json');
|
|
88
|
+
fs.writeFileSync(tasksJson, JSON.stringify({ tampered: true }));
|
|
89
|
+
assert.deepEqual(readJson(tasksJson), { tampered: true });
|
|
90
|
+
|
|
91
|
+
ops.patchTask(db, 'ow-1', { tag: 'main' });
|
|
92
|
+
projector.projectAll(db, { rootDir: dir });
|
|
93
|
+
|
|
94
|
+
const restored = readJson(tasksJson);
|
|
95
|
+
assert.equal(restored.schema_version, '4.5');
|
|
96
|
+
assert.equal(restored.tasks[0].id, 'ow-1');
|
|
97
|
+
closeStateDb(db);
|
|
98
|
+
} finally {
|
|
99
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('projectAll completes within 1s after a state.db write (PLAN AC)', () => {
|
|
104
|
+
const { dir, db } = tmpProject();
|
|
105
|
+
try {
|
|
106
|
+
for (let i = 0; i < 30; i++) {
|
|
107
|
+
ops.createTask(db, { id: `s-${i}`, title: `s${i}`, type: 'feature', priority: 'P3' });
|
|
108
|
+
}
|
|
109
|
+
const start = Date.now();
|
|
110
|
+
const out = projector.projectAll(db, { rootDir: dir });
|
|
111
|
+
const elapsed = Date.now() - start;
|
|
112
|
+
assert.ok(elapsed <= 1000, `projectAll took ${elapsed}ms, must be <= 1000ms`);
|
|
113
|
+
assert.equal(out.tasks_json.count, 30);
|
|
114
|
+
assert.equal(out.contexts.length, 30);
|
|
115
|
+
closeStateDb(db);
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('projectContext is a no-op for unknown task ids', () => {
|
|
122
|
+
const { dir, db } = tmpProject();
|
|
123
|
+
try {
|
|
124
|
+
const r = projector.projectContext(db, 'no-such-task', {}, { rootDir: dir });
|
|
125
|
+
assert.equal(r, null);
|
|
126
|
+
closeStateDb(db);
|
|
127
|
+
} finally {
|
|
128
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|