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,132 @@
|
|
|
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 {
|
|
10
|
+
savePlanArtifact,
|
|
11
|
+
loadPlanArtifact,
|
|
12
|
+
selectSection,
|
|
13
|
+
renderPlanMd,
|
|
14
|
+
DEFAULT_ARTIFACT_RELPATH,
|
|
15
|
+
} = require('./plan-store.cjs');
|
|
16
|
+
|
|
17
|
+
function tmpRoot() {
|
|
18
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-plan-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SAMPLE_PLAN = Object.freeze({
|
|
22
|
+
waves: [
|
|
23
|
+
{ id: 1, tasks: ['a', 'b'], parallel: true },
|
|
24
|
+
{ id: 2, tasks: ['c'], parallel: false },
|
|
25
|
+
],
|
|
26
|
+
ownership_forecast: { a: ['src/a.ts'], b: ['src/b.ts'], c: ['src/c.ts'] },
|
|
27
|
+
conflict_surface: [],
|
|
28
|
+
estimated_cost_usd: 0.12,
|
|
29
|
+
estimated_duration_min: 35,
|
|
30
|
+
cycles: [],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('savePlanArtifact: json format writes a file we can JSON.parse back', () => {
|
|
34
|
+
const root = tmpRoot();
|
|
35
|
+
try {
|
|
36
|
+
const outPath = path.join(root, '.ultra', 'execution-plan.json');
|
|
37
|
+
const { plan_path } = savePlanArtifact(SAMPLE_PLAN, outPath, 'json');
|
|
38
|
+
assert.equal(plan_path, path.resolve(outPath));
|
|
39
|
+
assert.ok(fs.existsSync(plan_path));
|
|
40
|
+
const parsed = JSON.parse(fs.readFileSync(plan_path, 'utf8'));
|
|
41
|
+
assert.deepEqual(parsed.waves, SAMPLE_PLAN.waves);
|
|
42
|
+
} finally {
|
|
43
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('savePlanArtifact: md format writes a human-readable report', () => {
|
|
48
|
+
const root = tmpRoot();
|
|
49
|
+
try {
|
|
50
|
+
const outPath = path.join(root, '.ultra', 'execution-plan.md');
|
|
51
|
+
savePlanArtifact(SAMPLE_PLAN, outPath, 'md');
|
|
52
|
+
const text = fs.readFileSync(outPath, 'utf8');
|
|
53
|
+
assert.match(text, /# Execution Plan/);
|
|
54
|
+
assert.match(text, /Wave 1/);
|
|
55
|
+
assert.match(text, /parallel/);
|
|
56
|
+
} finally {
|
|
57
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('savePlanArtifact: missing out_path → WRITE_FAILED', () => {
|
|
62
|
+
assert.throws(
|
|
63
|
+
() => savePlanArtifact(SAMPLE_PLAN, '', 'json'),
|
|
64
|
+
(err) => err.code === 'WRITE_FAILED',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('loadPlanArtifact: no file → null', () => {
|
|
69
|
+
const root = tmpRoot();
|
|
70
|
+
try {
|
|
71
|
+
assert.equal(loadPlanArtifact(root), null);
|
|
72
|
+
} finally {
|
|
73
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('loadPlanArtifact: round-trip via DEFAULT_ARTIFACT_RELPATH', () => {
|
|
78
|
+
const root = tmpRoot();
|
|
79
|
+
try {
|
|
80
|
+
const target = path.join(root, DEFAULT_ARTIFACT_RELPATH);
|
|
81
|
+
savePlanArtifact(SAMPLE_PLAN, target, 'json');
|
|
82
|
+
const loaded = loadPlanArtifact(root);
|
|
83
|
+
assert.deepEqual(loaded.waves, SAMPLE_PLAN.waves);
|
|
84
|
+
assert.equal(loaded.estimated_cost_usd, 0.12);
|
|
85
|
+
} finally {
|
|
86
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('loadPlanArtifact: corrupt file → null (no throw)', () => {
|
|
91
|
+
const root = tmpRoot();
|
|
92
|
+
try {
|
|
93
|
+
const target = path.join(root, DEFAULT_ARTIFACT_RELPATH);
|
|
94
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
95
|
+
fs.writeFileSync(target, '{not json');
|
|
96
|
+
assert.equal(loadPlanArtifact(root), null);
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('selectSection: tasks returns ownership_forecast only', () => {
|
|
103
|
+
const r = selectSection(SAMPLE_PLAN, 'tasks');
|
|
104
|
+
assert.deepEqual(Object.keys(r), ['ownership_forecast']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('selectSection: topo returns waves only', () => {
|
|
108
|
+
const r = selectSection(SAMPLE_PLAN, 'topo');
|
|
109
|
+
assert.deepEqual(Object.keys(r), ['waves']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('selectSection: conflicts returns conflict_surface only', () => {
|
|
113
|
+
const r = selectSection(SAMPLE_PLAN, 'conflicts');
|
|
114
|
+
assert.deepEqual(Object.keys(r), ['conflict_surface']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('selectSection: all (default) returns whole plan', () => {
|
|
118
|
+
assert.deepEqual(selectSection(SAMPLE_PLAN, 'all'), SAMPLE_PLAN);
|
|
119
|
+
assert.deepEqual(selectSection(SAMPLE_PLAN), SAMPLE_PLAN);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('renderPlanMd: reports cycles + conflicts counts when present', () => {
|
|
123
|
+
const planWithConflicts = {
|
|
124
|
+
...SAMPLE_PLAN,
|
|
125
|
+
conflict_surface: [{ files: ['x.ts'], tasks: ['a', 'b'], recommend: 'sequentialize' }],
|
|
126
|
+
cycles: [['z', 'y']],
|
|
127
|
+
};
|
|
128
|
+
const md = renderPlanMd(planWithConflicts);
|
|
129
|
+
assert.match(md, /Conflicts: 1/);
|
|
130
|
+
assert.match(md, /Cycles: 1/);
|
|
131
|
+
assert.match(md, /## Conflict Surface/);
|
|
132
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Phase 8A.1 — PRD → task[] parser. Delegates LLM call to an injected
|
|
4
|
+
// llmClient (see lib/llm-client.cjs). The parser itself is pure logic:
|
|
5
|
+
// prompt assembly → schema validation → id normalization. Keeping the
|
|
6
|
+
// llmClient as a parameter makes unit tests a Test Double exercise without
|
|
7
|
+
// mocking network I/O.
|
|
8
|
+
|
|
9
|
+
const Ajv = require('ajv/dist/2020');
|
|
10
|
+
const {
|
|
11
|
+
buildSystemPrompt,
|
|
12
|
+
buildUserPrompt,
|
|
13
|
+
} = require('../../orchestrator/planner/prd-prompt.cjs');
|
|
14
|
+
|
|
15
|
+
const TASK_LIST_SCHEMA = Object.freeze({
|
|
16
|
+
type: 'object',
|
|
17
|
+
required: ['tasks'],
|
|
18
|
+
properties: {
|
|
19
|
+
tasks: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
minItems: 1,
|
|
22
|
+
items: {
|
|
23
|
+
type: 'object',
|
|
24
|
+
required: ['id', 'title', 'type', 'priority'],
|
|
25
|
+
properties: {
|
|
26
|
+
id: { type: 'string', pattern: '^[a-zA-Z0-9_\\-]+$' },
|
|
27
|
+
title: { type: 'string', minLength: 3, maxLength: 200 },
|
|
28
|
+
type: { type: 'string', enum: ['architecture', 'feature', 'bugfix'] },
|
|
29
|
+
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
|
|
30
|
+
complexity: { type: 'integer', minimum: 1, maximum: 10 },
|
|
31
|
+
deps: { type: 'array', items: { type: 'string' } },
|
|
32
|
+
files_modified: { type: 'array', items: { type: 'string' } },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
40
|
+
const validateTaskList = ajv.compile(TASK_LIST_SCHEMA);
|
|
41
|
+
|
|
42
|
+
class PrdParseError extends Error {
|
|
43
|
+
constructor(code, message, cause) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'PrdParseError';
|
|
46
|
+
this.code = code;
|
|
47
|
+
if (cause) this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function parsePrd(prdText, { llmClient, tag } = {}) {
|
|
52
|
+
if (!prdText || typeof prdText !== 'string' || !prdText.trim()) {
|
|
53
|
+
throw new PrdParseError('NO_INPUT', 'prd text is empty');
|
|
54
|
+
}
|
|
55
|
+
if (!llmClient || typeof llmClient.completeJson !== 'function') {
|
|
56
|
+
throw new PrdParseError('NO_LLM_CLIENT', 'llmClient.completeJson is required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const system = buildSystemPrompt();
|
|
60
|
+
const user = buildUserPrompt(prdText);
|
|
61
|
+
|
|
62
|
+
let reply;
|
|
63
|
+
try {
|
|
64
|
+
reply = await llmClient.completeJson({ system, user });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new PrdParseError(
|
|
67
|
+
'PARSE_FAILED',
|
|
68
|
+
`LLM call failed: ${err.message}`,
|
|
69
|
+
err,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!validateTaskList(reply.json)) {
|
|
74
|
+
throw new PrdParseError(
|
|
75
|
+
'PARSE_FAILED',
|
|
76
|
+
`LLM output failed schema: ${ajv.errorsText(validateTaskList.errors)}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ids = new Set();
|
|
81
|
+
const tasks = reply.json.tasks.map((t) => {
|
|
82
|
+
if (ids.has(t.id)) {
|
|
83
|
+
throw new PrdParseError('PARSE_FAILED', `duplicate task id "${t.id}" in LLM output`);
|
|
84
|
+
}
|
|
85
|
+
ids.add(t.id);
|
|
86
|
+
return {
|
|
87
|
+
id: t.id,
|
|
88
|
+
title: t.title,
|
|
89
|
+
type: t.type,
|
|
90
|
+
priority: t.priority,
|
|
91
|
+
complexity: t.complexity ?? null,
|
|
92
|
+
deps: Array.isArray(t.deps) ? t.deps : [],
|
|
93
|
+
files_modified: Array.isArray(t.files_modified) ? t.files_modified : [],
|
|
94
|
+
tag: tag || null,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
tasks,
|
|
100
|
+
usage: reply.usage || {},
|
|
101
|
+
model: reply.model,
|
|
102
|
+
provider: reply.provider,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
parsePrd,
|
|
108
|
+
PrdParseError,
|
|
109
|
+
TASK_LIST_SCHEMA,
|
|
110
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Test Double rationale: external LLM API — deterministic fixtures prove
|
|
4
|
+
// parsing logic (prompt assembly, schema validation, id normalization)
|
|
5
|
+
// without egress cost or key dependence. Integration coverage against a
|
|
6
|
+
// real LLM lives behind an env gate and is exercised separately.
|
|
7
|
+
|
|
8
|
+
const test = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
|
|
11
|
+
const { parsePrd, PrdParseError, TASK_LIST_SCHEMA } = require('./prd-parser.cjs');
|
|
12
|
+
|
|
13
|
+
function fakeClient(jsonFn, { usage = { input_tokens: 100, output_tokens: 50 }, model = 'claude-sonnet-4-6', provider = 'anthropic' } = {}) {
|
|
14
|
+
return {
|
|
15
|
+
provider,
|
|
16
|
+
model,
|
|
17
|
+
async completeJson({ system, user }) {
|
|
18
|
+
const json = typeof jsonFn === 'function' ? await jsonFn({ system, user }) : jsonFn;
|
|
19
|
+
return { json, raw: JSON.stringify(json), usage, model, provider };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const HAPPY_TASKS = {
|
|
25
|
+
tasks: [
|
|
26
|
+
{
|
|
27
|
+
id: 'task-1',
|
|
28
|
+
title: 'Design auth schema',
|
|
29
|
+
type: 'architecture',
|
|
30
|
+
priority: 'P1',
|
|
31
|
+
complexity: 5,
|
|
32
|
+
deps: [],
|
|
33
|
+
files_modified: ['src/auth/schema.ts'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'task-2',
|
|
37
|
+
title: 'Implement login endpoint',
|
|
38
|
+
type: 'feature',
|
|
39
|
+
priority: 'P1',
|
|
40
|
+
complexity: 6,
|
|
41
|
+
deps: ['task-1'],
|
|
42
|
+
files_modified: ['src/auth/login.ts'],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
test('parsePrd: happy path normalizes tasks and returns usage metadata', async () => {
|
|
48
|
+
const client = fakeClient(HAPPY_TASKS);
|
|
49
|
+
const result = await parsePrd('Build login', { llmClient: client, tag: 'feat-auth' });
|
|
50
|
+
assert.equal(result.tasks.length, 2);
|
|
51
|
+
assert.equal(result.tasks[0].id, 'task-1');
|
|
52
|
+
assert.equal(result.tasks[0].tag, 'feat-auth');
|
|
53
|
+
assert.deepEqual(result.tasks[1].deps, ['task-1']);
|
|
54
|
+
assert.equal(result.model, 'claude-sonnet-4-6');
|
|
55
|
+
assert.equal(result.provider, 'anthropic');
|
|
56
|
+
assert.equal(result.usage.input_tokens, 100);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('parsePrd: empty text → NO_INPUT', async () => {
|
|
60
|
+
await assert.rejects(
|
|
61
|
+
parsePrd('', { llmClient: fakeClient(HAPPY_TASKS) }),
|
|
62
|
+
(err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
|
|
63
|
+
);
|
|
64
|
+
await assert.rejects(
|
|
65
|
+
parsePrd(' ', { llmClient: fakeClient(HAPPY_TASKS) }),
|
|
66
|
+
(err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('parsePrd: missing llmClient → NO_LLM_CLIENT', async () => {
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
parsePrd('Build something', {}),
|
|
73
|
+
(err) => err instanceof PrdParseError && err.code === 'NO_LLM_CLIENT',
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('parsePrd: llmClient error bubbles up as PARSE_FAILED', async () => {
|
|
78
|
+
const brokenClient = {
|
|
79
|
+
provider: 'anthropic',
|
|
80
|
+
model: 'claude-sonnet-4-6',
|
|
81
|
+
async completeJson() { throw new Error('upstream LLM 500'); },
|
|
82
|
+
};
|
|
83
|
+
await assert.rejects(
|
|
84
|
+
parsePrd('Build something', { llmClient: brokenClient }),
|
|
85
|
+
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /upstream LLM 500/.test(err.message),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('parsePrd: invalid JSON shape rejected — missing required "priority"', async () => {
|
|
90
|
+
const bad = {
|
|
91
|
+
tasks: [{
|
|
92
|
+
id: 'task-1', title: 'Foo', type: 'feature', // priority missing
|
|
93
|
+
complexity: 3, deps: [], files_modified: [],
|
|
94
|
+
}],
|
|
95
|
+
};
|
|
96
|
+
await assert.rejects(
|
|
97
|
+
parsePrd('Build Foo', { llmClient: fakeClient(bad) }),
|
|
98
|
+
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /priority/.test(err.message),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('parsePrd: invalid enum value rejected — bad type', async () => {
|
|
103
|
+
const bad = {
|
|
104
|
+
tasks: [{
|
|
105
|
+
id: 'task-1', title: 'Foo', type: 'chore', priority: 'P1',
|
|
106
|
+
}],
|
|
107
|
+
};
|
|
108
|
+
await assert.rejects(
|
|
109
|
+
parsePrd('Build Foo', { llmClient: fakeClient(bad) }),
|
|
110
|
+
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('parsePrd: duplicate task id rejected', async () => {
|
|
115
|
+
const dup = {
|
|
116
|
+
tasks: [
|
|
117
|
+
{ id: 'task-1', title: 'First copy', type: 'feature', priority: 'P2' },
|
|
118
|
+
{ id: 'task-1', title: 'Second copy', type: 'feature', priority: 'P2' },
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
await assert.rejects(
|
|
122
|
+
parsePrd('Build dupes', { llmClient: fakeClient(dup) }),
|
|
123
|
+
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /duplicate/i.test(err.message),
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('parsePrd: missing optional fields are normalized to defaults', async () => {
|
|
128
|
+
const minimal = {
|
|
129
|
+
tasks: [{
|
|
130
|
+
id: 'task-1', title: 'Minimal task', type: 'feature', priority: 'P3',
|
|
131
|
+
}],
|
|
132
|
+
};
|
|
133
|
+
const result = await parsePrd('anything', { llmClient: fakeClient(minimal) });
|
|
134
|
+
assert.equal(result.tasks[0].complexity, null);
|
|
135
|
+
assert.deepEqual(result.tasks[0].deps, []);
|
|
136
|
+
assert.deepEqual(result.tasks[0].files_modified, []);
|
|
137
|
+
assert.equal(result.tasks[0].tag, null);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('parsePrd: prompt contains PRD text passed to llmClient', async () => {
|
|
141
|
+
let captured;
|
|
142
|
+
const client = {
|
|
143
|
+
provider: 'anthropic',
|
|
144
|
+
model: 'claude-sonnet-4-6',
|
|
145
|
+
async completeJson({ system, user }) {
|
|
146
|
+
captured = { system, user };
|
|
147
|
+
return {
|
|
148
|
+
json: HAPPY_TASKS,
|
|
149
|
+
raw: '',
|
|
150
|
+
usage: {},
|
|
151
|
+
model: 'claude-sonnet-4-6',
|
|
152
|
+
provider: 'anthropic',
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
await parsePrd('Build the thing with acceptance criteria X', { llmClient: client });
|
|
157
|
+
assert.match(captured.system, /engineering planner/i);
|
|
158
|
+
assert.match(captured.user, /Build the thing with acceptance criteria X/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('parsePrd: empty tasks array rejected (schema requires minItems:1)', async () => {
|
|
162
|
+
await assert.rejects(
|
|
163
|
+
parsePrd('Build nothing', { llmClient: fakeClient({ tasks: [] }) }),
|
|
164
|
+
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('TASK_LIST_SCHEMA: exported for downstream consumers', () => {
|
|
169
|
+
assert.equal(TASK_LIST_SCHEMA.required[0], 'tasks');
|
|
170
|
+
assert.equal(TASK_LIST_SCHEMA.properties.tasks.minItems, 1);
|
|
171
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Phase 6.2 — Runtime × model pricing table (USD per token).
|
|
4
|
+
//
|
|
5
|
+
// Last updated: 2026-04-17. Public rate cards change — re-check before
|
|
6
|
+
// trusting the cost panel for billing. Numbers below are per token (not
|
|
7
|
+
// per million), so a 1000-token input at 3e-6 costs $0.003.
|
|
8
|
+
//
|
|
9
|
+
// Sources (2026-04):
|
|
10
|
+
// Claude: https://www.anthropic.com/pricing
|
|
11
|
+
// OpenAI: https://platform.openai.com/docs/pricing
|
|
12
|
+
// Gemini: https://ai.google.dev/pricing
|
|
13
|
+
//
|
|
14
|
+
// `default` inside each runtime is the fallback when a session did not
|
|
15
|
+
// record which specific model it used.
|
|
16
|
+
|
|
17
|
+
const PRICING = Object.freeze({
|
|
18
|
+
claude: Object.freeze({
|
|
19
|
+
'claude-opus-4-7': { input: 15e-6, output: 75e-6 },
|
|
20
|
+
'claude-sonnet-4-6': { input: 3e-6, output: 15e-6 },
|
|
21
|
+
'claude-haiku-4-5': { input: 0.8e-6, output: 4e-6 },
|
|
22
|
+
default: { input: 3e-6, output: 15e-6 }, // assume sonnet
|
|
23
|
+
}),
|
|
24
|
+
codex: Object.freeze({
|
|
25
|
+
'gpt-5.4': { input: 2e-6, output: 10e-6 },
|
|
26
|
+
default: { input: 2e-6, output: 10e-6 },
|
|
27
|
+
}),
|
|
28
|
+
opencode: Object.freeze({
|
|
29
|
+
// OpenCode is a frontend over whichever model the user configured;
|
|
30
|
+
// treat as sonnet-equivalent unless a model name is passed in.
|
|
31
|
+
default: { input: 3e-6, output: 15e-6 },
|
|
32
|
+
}),
|
|
33
|
+
gemini: Object.freeze({
|
|
34
|
+
'gemini-2.5-pro': { input: 1.25e-6, output: 10e-6 },
|
|
35
|
+
'gemini-2.5-flash': { input: 0.15e-6, output: 0.6e-6 },
|
|
36
|
+
default: { input: 1.25e-6, output: 10e-6 },
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function computeCost(runtime, model, tokens_input, tokens_output) {
|
|
41
|
+
const r = PRICING[runtime];
|
|
42
|
+
if (!r) return null;
|
|
43
|
+
const p = (model && r[model]) || r.default;
|
|
44
|
+
if (!p) return null;
|
|
45
|
+
const ti = Number(tokens_input) || 0;
|
|
46
|
+
const to = Number(tokens_output) || 0;
|
|
47
|
+
return ti * p.input + to * p.output;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
PRICING,
|
|
52
|
+
computeCost,
|
|
53
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Projector: regenerates the read-only file views from .ultra/state.db.
|
|
4
|
+
//
|
|
5
|
+
// state.db is the only authority (D32). tasks.json and contexts/task-*.md
|
|
6
|
+
// are projections — humans may read them, never write them. The projector
|
|
7
|
+
// is trigger-based: state-ops calls projectAll() after each successful
|
|
8
|
+
// write transaction. Projection output passes the v4.5 schemas under
|
|
9
|
+
// spec/schemas/ — see mcp-server/tests/projector.test.cjs for the
|
|
10
|
+
// round-trip check.
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
const SCHEMA_VERSION = '4.5';
|
|
16
|
+
const SOURCE_TAG = '.ultra/state.db';
|
|
17
|
+
|
|
18
|
+
// Frozen SELECTs — values bind through @placeholders.
|
|
19
|
+
const LIST_TASKS_FOR_PROJECTION_SQL = "SELECT id, title, type, priority, complexity, status, deps, files_modified, session_id, stale, complexity_hint, tag, trace_to, context_file, completion_commit, created_at, updated_at FROM tasks ORDER BY created_at ASC";
|
|
20
|
+
const READ_TASK_FOR_PROJECTION_SQL = "SELECT id, title, type, priority, complexity, status, deps, files_modified, session_id, stale, complexity_hint, tag, trace_to, context_file, completion_commit, created_at, updated_at FROM tasks WHERE id = @id";
|
|
21
|
+
|
|
22
|
+
function rowToProjection(row) {
|
|
23
|
+
if (!row) return null;
|
|
24
|
+
const out = { ...row };
|
|
25
|
+
for (const k of ['deps', 'files_modified']) {
|
|
26
|
+
if (typeof out[k] === 'string') {
|
|
27
|
+
try { out[k] = JSON.parse(out[k]); } catch { out[k] = null; }
|
|
28
|
+
}
|
|
29
|
+
if (out[k] === null || out[k] === undefined) delete out[k];
|
|
30
|
+
}
|
|
31
|
+
if (out.stale !== undefined && out.stale !== null) out.stale = Boolean(out.stale);
|
|
32
|
+
for (const k of Object.keys(out)) {
|
|
33
|
+
if (out[k] === null || out[k] === undefined) delete out[k];
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function defaultPaths(rootDir) {
|
|
39
|
+
return {
|
|
40
|
+
tasksJson: path.join(rootDir, '.ultra', 'tasks', 'tasks.json'),
|
|
41
|
+
contextsDir: path.join(rootDir, '.ultra', 'tasks', 'contexts'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeAtomic(file, contents) {
|
|
46
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
48
|
+
fs.writeFileSync(tmp, contents);
|
|
49
|
+
fs.renameSync(tmp, file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dbContentTimestamp(db) {
|
|
53
|
+
const row = db.prepare("SELECT MAX(latest) AS ts FROM (SELECT MAX(updated_at) AS latest FROM tasks UNION ALL SELECT MAX(ts) AS latest FROM events UNION ALL SELECT MAX(applied_at) AS latest FROM schema_version)").get();
|
|
54
|
+
return (row && row.ts) || new Date(0).toISOString();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectTasks(db, { tasksJson } = {}, opts = {}) {
|
|
58
|
+
const rootDir = opts.rootDir;
|
|
59
|
+
const target = tasksJson || defaultPaths(rootDir || '.').tasksJson;
|
|
60
|
+
const rows = db.prepare(LIST_TASKS_FOR_PROJECTION_SQL).all();
|
|
61
|
+
// generated_at = max(content timestamps) so identical state → identical
|
|
62
|
+
// bytes → idempotent commits (PLAN Phase 2.6 / Phase 2.8 chore commit).
|
|
63
|
+
const payload = {
|
|
64
|
+
schema_version: SCHEMA_VERSION,
|
|
65
|
+
generated_at: dbContentTimestamp(db),
|
|
66
|
+
source: SOURCE_TAG,
|
|
67
|
+
tasks: rows.map(rowToProjection),
|
|
68
|
+
};
|
|
69
|
+
writeAtomic(target, JSON.stringify(payload, null, 2) + '\n');
|
|
70
|
+
return { path: target, count: rows.length };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildContextDoc(taskRow, existingBody) {
|
|
74
|
+
const headerLines = [
|
|
75
|
+
'---',
|
|
76
|
+
`task_id: ${taskRow.id}`,
|
|
77
|
+
`title: ${escapeYaml(taskRow.title)}`,
|
|
78
|
+
`status: ${taskRow.status}`,
|
|
79
|
+
];
|
|
80
|
+
if (taskRow.priority) headerLines.push(`priority: ${taskRow.priority}`);
|
|
81
|
+
if (taskRow.type) headerLines.push(`type: ${taskRow.type}`);
|
|
82
|
+
if (taskRow.session_id) headerLines.push(`session_id: ${taskRow.session_id}`);
|
|
83
|
+
if (taskRow.completion_commit) headerLines.push(`completion_commit: ${taskRow.completion_commit}`);
|
|
84
|
+
if (taskRow.stale) headerLines.push('stale: true');
|
|
85
|
+
headerLines.push(`schema_version: ${SCHEMA_VERSION}`);
|
|
86
|
+
headerLines.push(`generated_at: ${taskRow.updated_at || new Date().toISOString()}`);
|
|
87
|
+
headerLines.push('---', '');
|
|
88
|
+
|
|
89
|
+
let body = existingBody || '';
|
|
90
|
+
// Strip any previous STALE banner so re-projection after reset is clean.
|
|
91
|
+
body = body.replace(/^> ⚠️\s*STALE[^\n]*\n(?:>[^\n]*\n)*\n?/m, '');
|
|
92
|
+
if (taskRow.stale) {
|
|
93
|
+
const banner = [
|
|
94
|
+
`> ⚠️ STALE since ${taskRow.updated_at || new Date().toISOString()}`,
|
|
95
|
+
'> spec 改动可能让此 task 上下文失效;重新跑 spec 对齐后再继续。',
|
|
96
|
+
'',
|
|
97
|
+
].join('\n');
|
|
98
|
+
body = banner + body;
|
|
99
|
+
}
|
|
100
|
+
return headerLines.join('\n') + body;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function escapeYaml(str) {
|
|
104
|
+
if (str == null) return '';
|
|
105
|
+
if (/[:#\[\]\{\}&\*!|>'"%@`]/.test(str) || /^\s|\s$/.test(str)) {
|
|
106
|
+
return JSON.stringify(str);
|
|
107
|
+
}
|
|
108
|
+
return str;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractBodyFromExistingFile(file) {
|
|
112
|
+
if (!fs.existsSync(file)) return '';
|
|
113
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
114
|
+
if (!text.startsWith('---')) return text;
|
|
115
|
+
const end = text.indexOf('\n---', 3);
|
|
116
|
+
if (end === -1) return text;
|
|
117
|
+
return text.slice(end + 4).replace(/^\n/, '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function projectContext(db, taskId, { contextsDir } = {}, opts = {}) {
|
|
121
|
+
const row = db.prepare(READ_TASK_FOR_PROJECTION_SQL).get({ id: taskId });
|
|
122
|
+
if (!row) return null;
|
|
123
|
+
const projection = rowToProjection(row);
|
|
124
|
+
const dir = contextsDir || defaultPaths(opts.rootDir || '.').contextsDir;
|
|
125
|
+
const target = projection.context_file
|
|
126
|
+
? path.resolve(opts.rootDir || '.', projection.context_file)
|
|
127
|
+
: path.join(dir, `task-${taskId}.md`);
|
|
128
|
+
const body = extractBodyFromExistingFile(target);
|
|
129
|
+
const next = buildContextDoc(projection, body);
|
|
130
|
+
writeAtomic(target, next);
|
|
131
|
+
return { path: target, task_id: taskId };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function projectAll(db, { rootDir = '.', tasksJson, contextsDir } = {}) {
|
|
135
|
+
const paths = defaultPaths(rootDir);
|
|
136
|
+
const tasksTarget = tasksJson || paths.tasksJson;
|
|
137
|
+
const contextsTarget = contextsDir || paths.contextsDir;
|
|
138
|
+
|
|
139
|
+
const tasksResult = projectTasks(db, { tasksJson: tasksTarget });
|
|
140
|
+
const contextResults = [];
|
|
141
|
+
const rows = db.prepare("SELECT id FROM tasks").all();
|
|
142
|
+
for (const r of rows) {
|
|
143
|
+
contextResults.push(projectContext(db, r.id, { contextsDir: contextsTarget }, { rootDir }));
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
tasks_json: tasksResult,
|
|
147
|
+
contexts: contextResults.filter(Boolean),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
SCHEMA_VERSION,
|
|
153
|
+
defaultPaths,
|
|
154
|
+
projectTasks,
|
|
155
|
+
projectContext,
|
|
156
|
+
projectAll,
|
|
157
|
+
rowToProjection,
|
|
158
|
+
};
|