ultra-builder-pro-cli 0.14.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.
- package/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +44 -1
- package/README.md +34 -19
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +15 -8
- package/docs/ARCHITECTURE.md +33 -12
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +8 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +6 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +161 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +57 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +384 -63
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +82 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +213 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -169
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1467
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -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
|
-
});
|
|
@@ -1,393 +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 { execFileSync } = require('node:child_process');
|
|
9
|
-
const Database = require('better-sqlite3');
|
|
10
|
-
|
|
11
|
-
const {
|
|
12
|
-
initProject, InitProjectError, DEFAULT_TEMPLATE, classifyRepository,
|
|
13
|
-
} = require('../lib/init-project.cjs');
|
|
14
|
-
const { EXPECTED_VERSION } = require('../lib/state-db.cjs');
|
|
15
|
-
|
|
16
|
-
function mkTempDir(prefix = 'ubp-init-test-') {
|
|
17
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function cleanup(dir) {
|
|
21
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* best effort */ }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test('initProject copies bundled template into .ultra/', () => {
|
|
25
|
-
const target = mkTempDir();
|
|
26
|
-
try {
|
|
27
|
-
const r = initProject({
|
|
28
|
-
target_dir: target,
|
|
29
|
-
project_name: 'demo',
|
|
30
|
-
project_type: 'cli',
|
|
31
|
-
stack: 'node',
|
|
32
|
-
});
|
|
33
|
-
assert.equal(r.status, 'created');
|
|
34
|
-
assert.equal(r.mode, 'greenfield');
|
|
35
|
-
assert.equal(r.created_path, path.join(target, '.ultra'));
|
|
36
|
-
assert.ok(r.copied_files.includes('tasks/tasks.json'));
|
|
37
|
-
assert.ok(r.copied_files.includes('specs/product.md'));
|
|
38
|
-
assert.ok(r.copied_files.includes('changes/active/.gitkeep'));
|
|
39
|
-
assert.ok(r.copied_files.includes('changes/archive/.gitkeep'));
|
|
40
|
-
assert.equal(r.state_db_path, path.join(target, '.ultra', 'state.db'));
|
|
41
|
-
assert.ok(fs.existsSync(r.state_db_path));
|
|
42
|
-
const db = new Database(r.state_db_path, { readonly: true });
|
|
43
|
-
try {
|
|
44
|
-
const version = db.prepare('SELECT version FROM schema_version WHERE version = ?').get(EXPECTED_VERSION);
|
|
45
|
-
assert.equal(version.version, EXPECTED_VERSION);
|
|
46
|
-
assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE name = 'changes'").get());
|
|
47
|
-
const baseline = db.prepare('SELECT mode, status, project_name FROM baselines').get();
|
|
48
|
-
assert.deepEqual(baseline, { mode: 'greenfield', status: 'draft', project_name: 'demo' });
|
|
49
|
-
} finally {
|
|
50
|
-
db.close();
|
|
51
|
-
}
|
|
52
|
-
assert.ok(!r.copied_files.some((f) => f.endsWith('.DS_Store')));
|
|
53
|
-
} finally { cleanup(target); }
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('initProject auto-detects existing source as brownfield and starts adoption', () => {
|
|
57
|
-
const target = mkTempDir();
|
|
58
|
-
try {
|
|
59
|
-
fs.mkdirSync(path.join(target, 'src'));
|
|
60
|
-
fs.writeFileSync(path.join(target, 'package.json'), '{"name":"legacy-app"}\n');
|
|
61
|
-
fs.writeFileSync(path.join(target, 'src', 'index.js'), 'module.exports = true;\n');
|
|
62
|
-
const result = initProject({ target_dir: target, project_name: 'legacy-app' });
|
|
63
|
-
assert.equal(result.mode, 'brownfield');
|
|
64
|
-
const db = new Database(result.state_db_path, { readonly: true });
|
|
65
|
-
try {
|
|
66
|
-
const baseline = db.prepare('SELECT mode, status, scope_json FROM baselines').get();
|
|
67
|
-
assert.equal(baseline.mode, 'brownfield');
|
|
68
|
-
assert.equal(baseline.status, 'adopting');
|
|
69
|
-
assert.deepEqual(JSON.parse(baseline.scope_json), ['.']);
|
|
70
|
-
} finally {
|
|
71
|
-
db.close();
|
|
72
|
-
}
|
|
73
|
-
} finally { cleanup(target); }
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('auto classification keeps a documentation-and-manifest-only skeleton greenfield', () => {
|
|
77
|
-
const target = mkTempDir();
|
|
78
|
-
try {
|
|
79
|
-
fs.writeFileSync(path.join(target, 'README.md'), '# New service skeleton\n');
|
|
80
|
-
fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({
|
|
81
|
-
name: 'new-service', private: true, scripts: { test: 'node --test' },
|
|
82
|
-
}));
|
|
83
|
-
const profile = classifyRepository(target);
|
|
84
|
-
assert.equal(profile.mode, 'greenfield');
|
|
85
|
-
assert.equal(profile.repository_kind, 'single');
|
|
86
|
-
assert.deepEqual(profile.source_signals, []);
|
|
87
|
-
|
|
88
|
-
const result = initProject({ target_dir: target, project_name: 'new-service' });
|
|
89
|
-
assert.equal(result.mode, 'greenfield');
|
|
90
|
-
assert.deepEqual(result.repository_profile.verification_commands, ['npm test']);
|
|
91
|
-
} finally { cleanup(target); }
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('auto classification records bounded monorepo evidence for brownfield adoption', () => {
|
|
95
|
-
const target = mkTempDir();
|
|
96
|
-
try {
|
|
97
|
-
fs.mkdirSync(path.join(target, 'packages', 'api', 'src'), { recursive: true });
|
|
98
|
-
fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({
|
|
99
|
-
name: 'legacy-suite', private: true, workspaces: ['packages/*'],
|
|
100
|
-
scripts: { build: 'turbo build', test: 'turbo test', lint: 'turbo lint' },
|
|
101
|
-
}));
|
|
102
|
-
fs.writeFileSync(path.join(target, 'packages', 'api', 'package.json'), '{"name":"api"}\n');
|
|
103
|
-
fs.writeFileSync(path.join(target, 'packages', 'api', 'src', 'index.js'), 'module.exports = true;\n');
|
|
104
|
-
fs.writeFileSync(path.join(target, 'Dockerfile'), 'FROM node:22\n');
|
|
105
|
-
|
|
106
|
-
const result = initProject({ target_dir: target, project_name: 'legacy-suite' });
|
|
107
|
-
assert.equal(result.mode, 'brownfield');
|
|
108
|
-
assert.equal(result.repository_profile.repository_kind, 'monorepo');
|
|
109
|
-
assert.deepEqual(result.repository_profile.workspace_roots, ['packages/api']);
|
|
110
|
-
assert.ok(result.repository_profile.source_signals.includes('packages/api/src/index.js'));
|
|
111
|
-
assert.ok(result.repository_profile.deployment_signals.includes('Dockerfile'));
|
|
112
|
-
assert.deepEqual(
|
|
113
|
-
result.repository_profile.verification_commands,
|
|
114
|
-
['npm run build', 'npm test', 'npm run lint'],
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const db = new Database(result.state_db_path, { readonly: true });
|
|
118
|
-
try {
|
|
119
|
-
const row = db.prepare(
|
|
120
|
-
'SELECT repository_branch, worktree_state, classification_json FROM baselines',
|
|
121
|
-
).get();
|
|
122
|
-
assert.equal(row.repository_branch, null);
|
|
123
|
-
assert.equal(row.worktree_state, 'unavailable');
|
|
124
|
-
assert.equal(JSON.parse(row.classification_json).repository_kind, 'monorepo');
|
|
125
|
-
} finally { db.close(); }
|
|
126
|
-
} finally { cleanup(target); }
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('brownfield initialization persists an explicitly selected monorepo scope', () => {
|
|
130
|
-
const target = mkTempDir();
|
|
131
|
-
try {
|
|
132
|
-
fs.mkdirSync(path.join(target, 'packages', 'api', 'src'), { recursive: true });
|
|
133
|
-
fs.mkdirSync(path.join(target, 'packages', 'web', 'src'), { recursive: true });
|
|
134
|
-
fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify({
|
|
135
|
-
name: 'legacy-suite', private: true, workspaces: ['packages/*'],
|
|
136
|
-
}));
|
|
137
|
-
fs.writeFileSync(path.join(target, 'packages', 'api', 'src', 'index.js'), 'module.exports = true;\n');
|
|
138
|
-
fs.writeFileSync(path.join(target, 'packages', 'web', 'src', 'index.js'), 'module.exports = true;\n');
|
|
139
|
-
|
|
140
|
-
const result = initProject({
|
|
141
|
-
target_dir: target,
|
|
142
|
-
project_name: 'legacy-suite',
|
|
143
|
-
scope: ['packages/api'],
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const db = new Database(result.state_db_path, { readonly: true });
|
|
147
|
-
try {
|
|
148
|
-
const baseline = db.prepare('SELECT scope_json FROM baselines').get();
|
|
149
|
-
assert.deepEqual(JSON.parse(baseline.scope_json), ['packages/api']);
|
|
150
|
-
assert.deepEqual(result.baseline.scope, ['packages/api']);
|
|
151
|
-
} finally { db.close(); }
|
|
152
|
-
} finally { cleanup(target); }
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('maintenance CLI forwards an explicit initialization mode', () => {
|
|
156
|
-
const target = mkTempDir();
|
|
157
|
-
try {
|
|
158
|
-
fs.writeFileSync(path.join(target, 'README.md'), '# Intentional greenfield notes\n');
|
|
159
|
-
const output = execFileSync(process.execPath, [
|
|
160
|
-
path.resolve(__dirname, '..', '..', 'ultra-tools', 'cli.cjs'),
|
|
161
|
-
'task', 'init-project', '--target-dir', target, '--project-name', 'explicit-mode',
|
|
162
|
-
'--mode', 'greenfield',
|
|
163
|
-
], { encoding: 'utf8' });
|
|
164
|
-
const envelope = JSON.parse(output);
|
|
165
|
-
assert.equal(envelope.ok, true);
|
|
166
|
-
assert.equal(envelope.data.mode, 'greenfield');
|
|
167
|
-
} finally { cleanup(target); }
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('initProject keeps project metadata in authoritative baseline state and tasks.json as a projection', () => {
|
|
171
|
-
const target = mkTempDir();
|
|
172
|
-
try {
|
|
173
|
-
initProject({
|
|
174
|
-
target_dir: target,
|
|
175
|
-
project_name: 'meta-demo',
|
|
176
|
-
project_type: 'web',
|
|
177
|
-
stack: 'next',
|
|
178
|
-
});
|
|
179
|
-
const tasksJson = JSON.parse(fs.readFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), 'utf8'));
|
|
180
|
-
assert.equal(tasksJson.schema_version, '4.5');
|
|
181
|
-
assert.equal(tasksJson.source, '.ultra/state.db');
|
|
182
|
-
assert.deepEqual(tasksJson.tasks, []);
|
|
183
|
-
assert.equal('project' in tasksJson, false);
|
|
184
|
-
const db = new Database(path.join(target, '.ultra', 'state.db'), { readonly: true });
|
|
185
|
-
try {
|
|
186
|
-
const baseline = db.prepare('SELECT project_name, project_type, stack FROM baselines').get();
|
|
187
|
-
assert.deepEqual(baseline, { project_name: 'meta-demo', project_type: 'web', stack: 'next' });
|
|
188
|
-
} finally {
|
|
189
|
-
db.close();
|
|
190
|
-
}
|
|
191
|
-
} finally { cleanup(target); }
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('initProject records the declared project_initialized lifecycle event', () => {
|
|
195
|
-
const rootDir = mkTempDir();
|
|
196
|
-
try {
|
|
197
|
-
const result = initProject({ target_dir: rootDir, project_name: 'events-fixture' });
|
|
198
|
-
const db = new Database(result.state_db_path, { readonly: true });
|
|
199
|
-
try {
|
|
200
|
-
const events = db.prepare('SELECT type, payload_json FROM events ORDER BY id').all();
|
|
201
|
-
assert.deepEqual(events.map((row) => row.type), ['baseline_started', 'project_initialized']);
|
|
202
|
-
assert.deepEqual(JSON.parse(events[1].payload_json), {
|
|
203
|
-
project_name: 'events-fixture', mode: 'greenfield', baseline_id: 'project-baseline',
|
|
204
|
-
});
|
|
205
|
-
} finally {
|
|
206
|
-
db.close();
|
|
207
|
-
}
|
|
208
|
-
} finally {
|
|
209
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('initProject refuses when .ultra/ already exists and overwrite=false', () => {
|
|
214
|
-
const target = mkTempDir();
|
|
215
|
-
try {
|
|
216
|
-
initProject({ target_dir: target, project_name: 'first' });
|
|
217
|
-
assert.throws(
|
|
218
|
-
() => initProject({ target_dir: target, project_name: 'again' }),
|
|
219
|
-
(err) => err instanceof InitProjectError && err.code === 'ULTRA_DIR_EXISTS',
|
|
220
|
-
);
|
|
221
|
-
} finally { cleanup(target); }
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('initProject resume preserves existing artifacts and installs only missing current scaffold files', () => {
|
|
225
|
-
const target = mkTempDir();
|
|
226
|
-
try {
|
|
227
|
-
fs.mkdirSync(path.join(target, '.ultra', 'specs'), { recursive: true });
|
|
228
|
-
fs.writeFileSync(path.join(target, '.ultra', 'specs', 'product.md'), '# Existing product contract\n');
|
|
229
|
-
|
|
230
|
-
const result = initProject({
|
|
231
|
-
target_dir: target, project_name: 'existing-project', mode: 'brownfield', resume: true,
|
|
232
|
-
});
|
|
233
|
-
assert.equal(result.status, 'resumed');
|
|
234
|
-
assert.equal(
|
|
235
|
-
fs.readFileSync(path.join(target, '.ultra', 'specs', 'product.md'), 'utf8'),
|
|
236
|
-
'# Existing product contract\n',
|
|
237
|
-
);
|
|
238
|
-
assert.ok(fs.existsSync(path.join(target, '.ultra', 'specs', 'architecture.md')));
|
|
239
|
-
assert.ok(result.copied_files.includes('specs/architecture.md'));
|
|
240
|
-
assert.equal(result.copied_files.includes('specs/product.md'), false);
|
|
241
|
-
|
|
242
|
-
const second = initProject({
|
|
243
|
-
target_dir: target, project_name: 'existing-project', resume: true,
|
|
244
|
-
});
|
|
245
|
-
assert.equal(second.status, 'resumed');
|
|
246
|
-
assert.deepEqual(second.copied_files, []);
|
|
247
|
-
const db = new Database(result.state_db_path, { readonly: true });
|
|
248
|
-
try {
|
|
249
|
-
assert.equal(db.prepare('SELECT COUNT(*) AS count FROM baselines').get().count, 1);
|
|
250
|
-
} finally { db.close(); }
|
|
251
|
-
} finally { cleanup(target); }
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test('initProject resume refuses projection-only tasks until the supported import runs', () => {
|
|
255
|
-
const target = mkTempDir();
|
|
256
|
-
try {
|
|
257
|
-
fs.mkdirSync(path.join(target, '.ultra', 'tasks'), { recursive: true });
|
|
258
|
-
fs.writeFileSync(path.join(target, '.ultra', 'tasks', 'tasks.json'), JSON.stringify({
|
|
259
|
-
schema_version: '4.5', source: '.ultra/state.db',
|
|
260
|
-
tasks: [{ id: 'legacy', title: 'Legacy task', type: 'feature', priority: 'P1', status: 'pending' }],
|
|
261
|
-
}));
|
|
262
|
-
assert.throws(
|
|
263
|
-
() => initProject({ target_dir: target, project_name: 'legacy', resume: true }),
|
|
264
|
-
(error) => error.code === 'LEGACY_STATE_MIGRATION_REQUIRED'
|
|
265
|
-
&& /--from=4\.5 --to=12\.0/.test(error.message),
|
|
266
|
-
);
|
|
267
|
-
assert.equal(fs.existsSync(path.join(target, '.ultra', 'state.db')), false);
|
|
268
|
-
assert.equal(fs.existsSync(path.join(target, '.ultra', 'specs', 'product.md')), false);
|
|
269
|
-
} finally { cleanup(target); }
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test('initProject rejects conflicting resume and overwrite modes', () => {
|
|
273
|
-
const target = mkTempDir();
|
|
274
|
-
try {
|
|
275
|
-
fs.mkdirSync(path.join(target, '.ultra'));
|
|
276
|
-
assert.throws(
|
|
277
|
-
() => initProject({
|
|
278
|
-
target_dir: target, project_name: 'conflict', resume: true, overwrite: true,
|
|
279
|
-
}),
|
|
280
|
-
(error) => error.code === 'VALIDATION_ERROR' && /resume.*overwrite/i.test(error.message),
|
|
281
|
-
);
|
|
282
|
-
} finally { cleanup(target); }
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('initProject with overwrite=true backs up existing .ultra/', () => {
|
|
286
|
-
const target = mkTempDir();
|
|
287
|
-
try {
|
|
288
|
-
initProject({ target_dir: target, project_name: 'first' });
|
|
289
|
-
const sentinel = path.join(target, '.ultra', 'sentinel.txt');
|
|
290
|
-
fs.writeFileSync(sentinel, 'before-backup');
|
|
291
|
-
|
|
292
|
-
const r = initProject({ target_dir: target, project_name: 'second', overwrite: true });
|
|
293
|
-
assert.equal(r.status, 'overwritten');
|
|
294
|
-
assert.ok(r.backup_path);
|
|
295
|
-
assert.ok(fs.existsSync(path.join(r.backup_path, 'sentinel.txt')));
|
|
296
|
-
assert.equal(
|
|
297
|
-
fs.readFileSync(path.join(r.backup_path, 'sentinel.txt'), 'utf8'),
|
|
298
|
-
'before-backup',
|
|
299
|
-
);
|
|
300
|
-
assert.ok(!fs.existsSync(sentinel));
|
|
301
|
-
} finally { cleanup(target); }
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test('initProject restores the prior .ultra when overwrite initialization fails', () => {
|
|
305
|
-
const target = mkTempDir();
|
|
306
|
-
const badTemplate = mkTempDir('ubp-bad-template-');
|
|
307
|
-
try {
|
|
308
|
-
initProject({ target_dir: target, project_name: 'first' });
|
|
309
|
-
fs.writeFileSync(path.join(target, '.ultra', 'sentinel.txt'), 'restore-me');
|
|
310
|
-
fs.cpSync(DEFAULT_TEMPLATE, badTemplate, { recursive: true });
|
|
311
|
-
fs.writeFileSync(path.join(badTemplate, 'tasks', 'tasks.json'), '{not-json\n');
|
|
312
|
-
|
|
313
|
-
assert.throws(
|
|
314
|
-
() => initProject({
|
|
315
|
-
target_dir: target,
|
|
316
|
-
project_name: 'broken-overwrite',
|
|
317
|
-
overwrite: true,
|
|
318
|
-
source_template: badTemplate,
|
|
319
|
-
}),
|
|
320
|
-
(error) => error instanceof InitProjectError && error.code === 'IO_ERROR',
|
|
321
|
-
);
|
|
322
|
-
assert.equal(
|
|
323
|
-
fs.readFileSync(path.join(target, '.ultra', 'sentinel.txt'), 'utf8'),
|
|
324
|
-
'restore-me',
|
|
325
|
-
);
|
|
326
|
-
assert.equal(
|
|
327
|
-
fs.readdirSync(target).some((name) => name.startsWith('.ultra.backup.')),
|
|
328
|
-
false,
|
|
329
|
-
);
|
|
330
|
-
} finally {
|
|
331
|
-
cleanup(target);
|
|
332
|
-
cleanup(badTemplate);
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test('initProject rejects empty project_name', () => {
|
|
337
|
-
const target = mkTempDir();
|
|
338
|
-
try {
|
|
339
|
-
assert.throws(
|
|
340
|
-
() => initProject({ target_dir: target, project_name: '' }),
|
|
341
|
-
(err) => err instanceof InitProjectError && err.code === 'VALIDATION_ERROR',
|
|
342
|
-
);
|
|
343
|
-
} finally { cleanup(target); }
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test('initProject rejects missing source_template', () => {
|
|
347
|
-
const target = mkTempDir();
|
|
348
|
-
try {
|
|
349
|
-
assert.throws(
|
|
350
|
-
() => initProject({
|
|
351
|
-
target_dir: target,
|
|
352
|
-
project_name: 'demo',
|
|
353
|
-
source_template: '/tmp/does-not-exist-ubp-init',
|
|
354
|
-
}),
|
|
355
|
-
(err) => err instanceof InitProjectError && err.code === 'TEMPLATE_MISSING',
|
|
356
|
-
);
|
|
357
|
-
} finally { cleanup(target); }
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test('initProject surface TARGET_NOT_DIR when target_dir is a file', () => {
|
|
361
|
-
const parent = mkTempDir();
|
|
362
|
-
const filePath = path.join(parent, 'not-a-dir.txt');
|
|
363
|
-
fs.writeFileSync(filePath, 'x');
|
|
364
|
-
try {
|
|
365
|
-
assert.throws(
|
|
366
|
-
() => initProject({ target_dir: filePath, project_name: 'demo' }),
|
|
367
|
-
(err) => err instanceof InitProjectError && err.code === 'TARGET_NOT_DIR',
|
|
368
|
-
);
|
|
369
|
-
} finally { cleanup(parent); }
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('copied tree matches bundled templates/.ultra/ (diff-equal excluding .DS_Store)', () => {
|
|
373
|
-
const target = mkTempDir();
|
|
374
|
-
try {
|
|
375
|
-
const r = initProject({ target_dir: target, project_name: 'diff-equal' });
|
|
376
|
-
const templateFiles = [];
|
|
377
|
-
(function walk(dir, prefix = '') {
|
|
378
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
379
|
-
if (e.name === '.DS_Store') continue;
|
|
380
|
-
const rel = prefix ? path.join(prefix, e.name) : e.name;
|
|
381
|
-
if (e.isDirectory()) walk(path.join(dir, e.name), rel);
|
|
382
|
-
else if (e.isFile()) templateFiles.push(rel);
|
|
383
|
-
}
|
|
384
|
-
})(DEFAULT_TEMPLATE);
|
|
385
|
-
assert.deepEqual(r.copied_files.sort(), templateFiles.sort());
|
|
386
|
-
for (const rel of templateFiles) {
|
|
387
|
-
if (rel === 'tasks/tasks.json') continue; // metadata injected by design
|
|
388
|
-
const src = fs.readFileSync(path.join(DEFAULT_TEMPLATE, rel));
|
|
389
|
-
const dst = fs.readFileSync(path.join(r.created_path, rel));
|
|
390
|
-
assert.deepEqual(dst, src, `content diff in ${rel}`);
|
|
391
|
-
}
|
|
392
|
-
} finally { cleanup(target); }
|
|
393
|
-
});
|