@projitive/mcp 2.0.3 → 2.1.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/output/package.json +8 -2
- package/output/source/common/artifacts.js +1 -1
- package/output/source/common/artifacts.test.js +11 -11
- package/output/source/common/errors.js +19 -19
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +30 -19
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +11 -10
- package/output/source/common/linter.js +29 -27
- package/output/source/common/linter.test.js +9 -9
- package/output/source/common/markdown.js +3 -3
- package/output/source/common/markdown.test.js +15 -15
- package/output/source/common/response.js +91 -107
- package/output/source/common/response.test.js +30 -30
- package/output/source/common/store.js +40 -40
- package/output/source/common/store.test.js +72 -72
- package/output/source/common/tool.js +43 -0
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.js +16 -16
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +164 -148
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/index.test.js +18 -0
- package/output/source/resources/readme.js +7 -7
- package/output/source/resources/readme.test.js +113 -113
- package/output/source/resources/reports.js +10 -10
- package/output/source/resources/reports.test.js +83 -83
- package/output/source/tools/index.js +3 -3
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +330 -377
- package/output/source/tools/project.test.js +308 -175
- package/output/source/tools/roadmap.js +236 -255
- package/output/source/tools/roadmap.test.js +241 -46
- package/output/source/tools/task.js +770 -652
- package/output/source/tools/task.test.js +433 -105
- package/output/source/types.js +28 -22
- package/package.json +8 -2
|
@@ -1,76 +1,70 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
const MESSAGE_TEMPLATE_ENV =
|
|
4
|
-
const CONTENT_TEMPLATE_TOKEN = "{{content}}";
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MESSAGE_TEMPLATE_ENV = 'PROJITIVE_MESSAGE_TEMPLATE_PATH';
|
|
5
4
|
function baseToolTemplateMarkdown() {
|
|
6
5
|
return [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
].join(
|
|
24
|
-
}
|
|
25
|
-
function contextGuideTemplateExtra() {
|
|
26
|
-
return [
|
|
27
|
-
"",
|
|
28
|
-
"## Common Tool Guides To Read First",
|
|
29
|
-
"- ./CLAUDE.md",
|
|
30
|
-
"- ./AGENTS.md",
|
|
31
|
-
"- ./.github/copilot-instructions.md",
|
|
32
|
-
"- ./.cursorrules",
|
|
33
|
-
"- ./.github/instructions/*",
|
|
34
|
-
"- ./.cursor/rules/*",
|
|
35
|
-
];
|
|
36
|
-
}
|
|
37
|
-
function idleDiscoveryTemplateExtra() {
|
|
38
|
-
return [
|
|
39
|
-
"",
|
|
40
|
-
"## Idle Discovery Checklist (When No Actionable Task)",
|
|
41
|
-
"- Scan backlog comments: TODO / FIXME / HACK / XXX.",
|
|
42
|
-
"- Check lint gaps and create executable fix tasks.",
|
|
43
|
-
"- Check test quality gaps (missing tests, flaky tests, low-value coverage).",
|
|
44
|
-
"- Learn current project architecture and consolidate/update design docs in designs/.",
|
|
45
|
-
"- Re-run {{tool_name}} after creating 1-3 focused TODO tasks.",
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
function commitReminderTemplateExtra() {
|
|
49
|
-
return [
|
|
50
|
-
"",
|
|
51
|
-
"## Commit Reminder",
|
|
52
|
-
"- After this update, create a commit to keep progress auditable.",
|
|
53
|
-
"- Recommended format: type(scope): summary",
|
|
54
|
-
"- Example: feat(task): complete TASK-0007 validation flow",
|
|
55
|
-
"- Footer suggestion: Refs: TASK-0007, ROADMAP-0002",
|
|
56
|
-
];
|
|
6
|
+
'# {{tool_name}}',
|
|
7
|
+
'',
|
|
8
|
+
'## Summary',
|
|
9
|
+
'{{summary}}',
|
|
10
|
+
'',
|
|
11
|
+
'## Evidence',
|
|
12
|
+
'{{evidence}}',
|
|
13
|
+
'',
|
|
14
|
+
'## Agent Guidance',
|
|
15
|
+
'{{guidance}}',
|
|
16
|
+
'',
|
|
17
|
+
'## Lint Suggestions',
|
|
18
|
+
'{{suggestions}}',
|
|
19
|
+
'',
|
|
20
|
+
'## Next Call',
|
|
21
|
+
'{{next_call}}',
|
|
22
|
+
].join('\n');
|
|
57
23
|
}
|
|
58
24
|
export function getDefaultToolTemplateMarkdown(toolName) {
|
|
59
|
-
const base = baseToolTemplateMarkdown()
|
|
60
|
-
if (toolName ===
|
|
61
|
-
return [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
25
|
+
const base = baseToolTemplateMarkdown();
|
|
26
|
+
if (toolName === 'taskNext') {
|
|
27
|
+
return [
|
|
28
|
+
base,
|
|
29
|
+
'',
|
|
30
|
+
'## Idle Discovery Checklist (When No Actionable Task)',
|
|
31
|
+
'- Scan backlog comments: TODO / FIXME / HACK / XXX.',
|
|
32
|
+
'- Check lint gaps and create executable fix tasks.',
|
|
33
|
+
'- Check test quality gaps (missing tests, flaky tests, low-value coverage).',
|
|
34
|
+
'- Learn current project architecture and consolidate/update design docs in designs/.',
|
|
35
|
+
'- Review and update architecture docs under designs/core/ (architecture.md, style-guide.md) if missing or outdated.',
|
|
36
|
+
'- Re-run {{tool_name}} after creating 1-3 focused TODO tasks.',
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
if (toolName === 'projectContext' || toolName === 'taskContext' || toolName === 'roadmapContext') {
|
|
40
|
+
return [
|
|
41
|
+
base,
|
|
42
|
+
'',
|
|
43
|
+
'## Common Tool Guides To Read First',
|
|
44
|
+
'- ./CLAUDE.md',
|
|
45
|
+
'- ./AGENTS.md',
|
|
46
|
+
'- ./.github/copilot-instructions.md',
|
|
47
|
+
'- ./.cursorrules',
|
|
48
|
+
'- ./.github/instructions/*',
|
|
49
|
+
'- ./.cursor/rules/*',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
if (toolName === 'taskUpdate' || toolName === 'roadmapUpdate') {
|
|
53
|
+
return [
|
|
54
|
+
base,
|
|
55
|
+
'',
|
|
56
|
+
'## Commit Reminder',
|
|
57
|
+
'- After this update, create a commit to keep progress auditable.',
|
|
58
|
+
'- Recommended format: type(scope): summary',
|
|
59
|
+
'- Example: feat(task): complete TASK-0007 validation flow',
|
|
60
|
+
'- Footer suggestion: Refs: TASK-0007, ROADMAP-0002',
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
return base;
|
|
70
64
|
}
|
|
71
65
|
function loadTemplateFile(templatePath) {
|
|
72
66
|
try {
|
|
73
|
-
const content = fs.readFileSync(templatePath,
|
|
67
|
+
const content = fs.readFileSync(templatePath, 'utf-8').trim();
|
|
74
68
|
return content.length > 0 ? content : undefined;
|
|
75
69
|
}
|
|
76
70
|
catch {
|
|
@@ -84,13 +78,13 @@ function ensureTemplateFile(templatePath, toolName) {
|
|
|
84
78
|
}
|
|
85
79
|
fs.mkdirSync(path.dirname(templatePath), { recursive: true });
|
|
86
80
|
const generated = getDefaultToolTemplateMarkdown(toolName);
|
|
87
|
-
fs.writeFileSync(templatePath, `${generated}\n`,
|
|
81
|
+
fs.writeFileSync(templatePath, `${generated}\n`, 'utf-8');
|
|
88
82
|
return generated;
|
|
89
83
|
}
|
|
90
84
|
function resolveTemplateTarget(toolName) {
|
|
91
85
|
const configuredPath = process.env[MESSAGE_TEMPLATE_ENV]?.trim();
|
|
92
86
|
if (!configuredPath) {
|
|
93
|
-
return path.resolve(process.cwd(),
|
|
87
|
+
return path.resolve(process.cwd(), '.projitive', 'templates', 'tools', `${toolName}.md`);
|
|
94
88
|
}
|
|
95
89
|
const absolutePath = path.resolve(configuredPath);
|
|
96
90
|
try {
|
|
@@ -102,7 +96,7 @@ function resolveTemplateTarget(toolName) {
|
|
|
102
96
|
}
|
|
103
97
|
catch {
|
|
104
98
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
105
|
-
if (ext ===
|
|
99
|
+
if (ext === '.md') {
|
|
106
100
|
return absolutePath;
|
|
107
101
|
}
|
|
108
102
|
return path.join(absolutePath, `${toolName}.md`);
|
|
@@ -114,17 +108,17 @@ function loadMessageTemplate(toolName) {
|
|
|
114
108
|
}
|
|
115
109
|
export function asText(markdown) {
|
|
116
110
|
return {
|
|
117
|
-
content: [{ type:
|
|
111
|
+
content: [{ type: 'text', text: markdown }],
|
|
118
112
|
};
|
|
119
113
|
}
|
|
120
114
|
function withFallback(lines) {
|
|
121
|
-
return lines.length > 0 ? lines : [
|
|
115
|
+
return lines.length > 0 ? lines : ['- (none)'];
|
|
122
116
|
}
|
|
123
117
|
function shouldKeepRawLine(trimmed) {
|
|
124
118
|
if (trimmed.length === 0) {
|
|
125
119
|
return true;
|
|
126
120
|
}
|
|
127
|
-
if (trimmed.startsWith(
|
|
121
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('```')) {
|
|
128
122
|
return true;
|
|
129
123
|
}
|
|
130
124
|
if (/^[-*+]\s/.test(trimmed)) {
|
|
@@ -149,37 +143,37 @@ export function section(title, lines) {
|
|
|
149
143
|
return { title, lines: normalizeLines(lines) };
|
|
150
144
|
}
|
|
151
145
|
export function summarySection(lines) {
|
|
152
|
-
return section(
|
|
146
|
+
return section('Summary', lines);
|
|
153
147
|
}
|
|
154
148
|
export function evidenceSection(lines) {
|
|
155
|
-
return section(
|
|
149
|
+
return section('Evidence', lines);
|
|
156
150
|
}
|
|
157
151
|
export function guidanceSection(lines) {
|
|
158
|
-
return section(
|
|
152
|
+
return section('Agent Guidance', lines);
|
|
159
153
|
}
|
|
160
154
|
export function lintSection(lines) {
|
|
161
|
-
return section(
|
|
155
|
+
return section('Lint Suggestions', lines);
|
|
162
156
|
}
|
|
163
157
|
export function nextCallSection(nextCall) {
|
|
164
|
-
return section(
|
|
158
|
+
return section('Next Call', nextCall ? [nextCall] : []);
|
|
165
159
|
}
|
|
166
160
|
function toSectionText(section) {
|
|
167
161
|
if (!section) {
|
|
168
|
-
return
|
|
162
|
+
return '- (none)';
|
|
169
163
|
}
|
|
170
|
-
return withFallback(section.lines).join(
|
|
164
|
+
return withFallback(section.lines).join('\n');
|
|
171
165
|
}
|
|
172
166
|
function resolveSection(payload, title) {
|
|
173
167
|
return payload.sections.find((item) => item.title === title);
|
|
174
168
|
}
|
|
175
|
-
function buildToolTemplateVariables(payload
|
|
169
|
+
function buildToolTemplateVariables(payload) {
|
|
176
170
|
return {
|
|
177
171
|
tool_name: payload.toolName,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
next_call: toSectionText(resolveSection(payload,
|
|
172
|
+
summary: toSectionText(resolveSection(payload, 'Summary')),
|
|
173
|
+
evidence: toSectionText(resolveSection(payload, 'Evidence')),
|
|
174
|
+
guidance: toSectionText(resolveSection(payload, 'Agent Guidance')),
|
|
175
|
+
suggestions: toSectionText(resolveSection(payload, 'Lint Suggestions')),
|
|
176
|
+
next_call: toSectionText(resolveSection(payload, 'Next Call')),
|
|
183
177
|
};
|
|
184
178
|
}
|
|
185
179
|
function applyTemplateVariables(template, variables) {
|
|
@@ -187,33 +181,23 @@ function applyTemplateVariables(template, variables) {
|
|
|
187
181
|
for (const [key, value] of Object.entries(variables)) {
|
|
188
182
|
rendered = rendered.split(`{{${key}}}`).join(value);
|
|
189
183
|
}
|
|
190
|
-
if (!rendered.includes(variables.content) && !template.includes(CONTENT_TEMPLATE_TOKEN)) {
|
|
191
|
-
rendered = `${rendered}\n\n${variables.content}`;
|
|
192
|
-
}
|
|
193
184
|
return rendered.trimEnd();
|
|
194
185
|
}
|
|
195
186
|
export function renderToolResponseMarkdown(payload) {
|
|
196
|
-
const body = payload.sections.flatMap((section) => [
|
|
197
|
-
`## ${section.title}`,
|
|
198
|
-
...withFallback(section.lines),
|
|
199
|
-
"",
|
|
200
|
-
]);
|
|
201
|
-
const classicMarkdown = [
|
|
202
|
-
`# ${payload.toolName}`,
|
|
203
|
-
"",
|
|
204
|
-
...body,
|
|
205
|
-
].join("\n").trimEnd();
|
|
206
187
|
const template = loadMessageTemplate(payload.toolName);
|
|
207
|
-
const variables = buildToolTemplateVariables(payload
|
|
188
|
+
const variables = buildToolTemplateVariables(payload);
|
|
208
189
|
return applyTemplateVariables(template, variables);
|
|
209
190
|
}
|
|
210
191
|
export function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
192
|
+
const sections = [
|
|
193
|
+
section('Error', [`cause: ${cause}`]),
|
|
194
|
+
section('Next Step', nextSteps),
|
|
195
|
+
section('Retry Example', [retryExample ?? '(none)']),
|
|
196
|
+
];
|
|
197
|
+
const body = sections.flatMap((sec) => [
|
|
198
|
+
`## ${sec.title}`,
|
|
199
|
+
...withFallback(sec.lines),
|
|
200
|
+
'',
|
|
201
|
+
]);
|
|
202
|
+
return [`# ${toolName}`, '', ...body].join('\n').trimEnd();
|
|
219
203
|
}
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { asText, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from './response.js';
|
|
3
|
-
describe(
|
|
4
|
-
it(
|
|
5
|
-
const result = asText(
|
|
6
|
-
expect(result.content).toEqual([{ type:
|
|
3
|
+
describe('response helpers', () => {
|
|
4
|
+
it('wraps markdown text as MCP text content', () => {
|
|
5
|
+
const result = asText('# hello');
|
|
6
|
+
expect(result.content).toEqual([{ type: 'text', text: '# hello' }]);
|
|
7
7
|
});
|
|
8
|
-
it(
|
|
9
|
-
const markdown = renderErrorMarkdown(
|
|
10
|
-
expect(markdown).toContain(
|
|
11
|
-
expect(markdown).toContain(
|
|
12
|
-
expect(markdown).toContain(
|
|
13
|
-
expect(markdown).toContain(
|
|
14
|
-
expect(markdown).toContain(
|
|
8
|
+
it('renders error markdown sections', () => {
|
|
9
|
+
const markdown = renderErrorMarkdown('taskContext', 'bad id', ['retry'], 'taskContext(...)');
|
|
10
|
+
expect(markdown).toContain('# taskContext');
|
|
11
|
+
expect(markdown).toContain('## Error');
|
|
12
|
+
expect(markdown).toContain('- cause: bad id');
|
|
13
|
+
expect(markdown).toContain('- retry');
|
|
14
|
+
expect(markdown).toContain('## Retry Example');
|
|
15
15
|
});
|
|
16
|
-
it(
|
|
16
|
+
it('renders standard tool response sections with fallback', () => {
|
|
17
17
|
const markdown = renderToolResponseMarkdown({
|
|
18
|
-
toolName:
|
|
18
|
+
toolName: 'taskList',
|
|
19
19
|
sections: [
|
|
20
|
-
{ title:
|
|
21
|
-
{ title:
|
|
20
|
+
{ title: 'Summary', lines: ['- governanceDir: /tmp/.projitive'] },
|
|
21
|
+
{ title: 'Evidence', lines: [] },
|
|
22
22
|
],
|
|
23
23
|
});
|
|
24
|
-
expect(markdown).toContain(
|
|
25
|
-
expect(markdown).toContain(
|
|
26
|
-
expect(markdown).toContain(
|
|
27
|
-
expect(markdown).toContain(
|
|
24
|
+
expect(markdown).toContain('# taskList');
|
|
25
|
+
expect(markdown).toContain('## Summary');
|
|
26
|
+
expect(markdown).toContain('## Evidence');
|
|
27
|
+
expect(markdown).toContain('- (none)');
|
|
28
28
|
});
|
|
29
|
-
it(
|
|
29
|
+
it('auto-prefixes plain lines in section helpers', () => {
|
|
30
30
|
const markdown = renderToolResponseMarkdown({
|
|
31
|
-
toolName:
|
|
31
|
+
toolName: 'taskList',
|
|
32
32
|
sections: [
|
|
33
|
-
summarySection([
|
|
33
|
+
summarySection(['governanceDir: /tmp/.projitive']),
|
|
34
34
|
],
|
|
35
35
|
});
|
|
36
|
-
expect(markdown).toContain(
|
|
36
|
+
expect(markdown).toContain('- governanceDir: /tmp/.projitive');
|
|
37
37
|
});
|
|
38
|
-
it(
|
|
38
|
+
it('nextCallSection accepts optional call and falls back when missing', () => {
|
|
39
39
|
const withCall = renderToolResponseMarkdown({
|
|
40
|
-
toolName:
|
|
41
|
-
sections: [nextCallSection(
|
|
40
|
+
toolName: 'taskList',
|
|
41
|
+
sections: [nextCallSection('taskContext(projectPath="/tmp", taskId="TASK-0001")')],
|
|
42
42
|
});
|
|
43
|
-
expect(withCall).toContain(
|
|
43
|
+
expect(withCall).toContain('- taskContext(projectPath="/tmp", taskId="TASK-0001")');
|
|
44
44
|
const withoutCall = renderToolResponseMarkdown({
|
|
45
|
-
toolName:
|
|
45
|
+
toolName: 'taskList',
|
|
46
46
|
sections: [nextCallSection(undefined)],
|
|
47
47
|
});
|
|
48
|
-
expect(withoutCall).toContain(
|
|
48
|
+
expect(withoutCall).toContain('- (none)');
|
|
49
49
|
});
|
|
50
50
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
const STORE_SCHEMA_VERSION = 3;
|
|
4
4
|
const storeCache = new Map();
|
|
5
5
|
const storeLocks = new Map();
|
|
@@ -8,13 +8,13 @@ function defaultViewState(name) {
|
|
|
8
8
|
name,
|
|
9
9
|
dirty: true,
|
|
10
10
|
lastSourceVersion: 0,
|
|
11
|
-
lastBuiltAt:
|
|
11
|
+
lastBuiltAt: '',
|
|
12
12
|
recordVersion: 1,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
function defaultStore() {
|
|
16
16
|
return {
|
|
17
|
-
schema:
|
|
17
|
+
schema: 'projitive-json-store',
|
|
18
18
|
tasks: [],
|
|
19
19
|
roadmaps: [],
|
|
20
20
|
meta: {
|
|
@@ -23,8 +23,8 @@ function defaultStore() {
|
|
|
23
23
|
store_schema_version: STORE_SCHEMA_VERSION,
|
|
24
24
|
},
|
|
25
25
|
view_state: {
|
|
26
|
-
tasks_markdown: defaultViewState(
|
|
27
|
-
roadmaps_markdown: defaultViewState(
|
|
26
|
+
tasks_markdown: defaultViewState('tasks_markdown'),
|
|
27
|
+
roadmaps_markdown: defaultViewState('roadmaps_markdown'),
|
|
28
28
|
},
|
|
29
29
|
migration_history: [],
|
|
30
30
|
};
|
|
@@ -33,24 +33,24 @@ function nowIso() {
|
|
|
33
33
|
return new Date().toISOString();
|
|
34
34
|
}
|
|
35
35
|
function normalizeTaskStatus(status) {
|
|
36
|
-
if (status ===
|
|
36
|
+
if (status === 'IN_PROGRESS' || status === 'BLOCKED' || status === 'DONE') {
|
|
37
37
|
return status;
|
|
38
38
|
}
|
|
39
|
-
return
|
|
39
|
+
return 'TODO';
|
|
40
40
|
}
|
|
41
41
|
function normalizeRoadmapStatus(status) {
|
|
42
|
-
return status ===
|
|
42
|
+
return status === 'done' ? 'done' : 'active';
|
|
43
43
|
}
|
|
44
44
|
function normalizeStore(input) {
|
|
45
45
|
const base = defaultStore();
|
|
46
46
|
const meta = input.meta ?? {};
|
|
47
47
|
const tasks = Array.isArray(input.tasks)
|
|
48
48
|
? input.tasks.map((task) => ({
|
|
49
|
-
id: String(task.id ??
|
|
50
|
-
title: String(task.title ??
|
|
51
|
-
status: normalizeTaskStatus(String(task.status ??
|
|
52
|
-
owner: String(task.owner ??
|
|
53
|
-
summary: String(task.summary ??
|
|
49
|
+
id: String(task.id ?? ''),
|
|
50
|
+
title: String(task.title ?? ''),
|
|
51
|
+
status: normalizeTaskStatus(String(task.status ?? 'TODO')),
|
|
52
|
+
owner: String(task.owner ?? ''),
|
|
53
|
+
summary: String(task.summary ?? ''),
|
|
54
54
|
updatedAt: String(task.updatedAt ?? nowIso()),
|
|
55
55
|
links: Array.isArray(task.links) ? task.links.map((item) => String(item)) : [],
|
|
56
56
|
roadmapRefs: Array.isArray(task.roadmapRefs) ? task.roadmapRefs.map((item) => String(item)) : [],
|
|
@@ -61,10 +61,10 @@ function normalizeStore(input) {
|
|
|
61
61
|
: [];
|
|
62
62
|
const roadmaps = Array.isArray(input.roadmaps)
|
|
63
63
|
? input.roadmaps.map((milestone) => ({
|
|
64
|
-
id: String(milestone.id ??
|
|
65
|
-
title: String(milestone.title ??
|
|
66
|
-
status: normalizeRoadmapStatus(String(milestone.status ??
|
|
67
|
-
time: typeof milestone.time ===
|
|
64
|
+
id: String(milestone.id ?? ''),
|
|
65
|
+
title: String(milestone.title ?? ''),
|
|
66
|
+
status: normalizeRoadmapStatus(String(milestone.status ?? 'active')),
|
|
67
|
+
time: typeof milestone.time === 'string' && milestone.time.length > 0 ? milestone.time : undefined,
|
|
68
68
|
updatedAt: String(milestone.updatedAt ?? nowIso()),
|
|
69
69
|
recordVersion: Number.isFinite(Number(milestone.recordVersion)) ? Number(milestone.recordVersion) : 1,
|
|
70
70
|
}))
|
|
@@ -72,7 +72,7 @@ function normalizeStore(input) {
|
|
|
72
72
|
const tasksView = input.view_state?.tasks_markdown;
|
|
73
73
|
const roadmapsView = input.view_state?.roadmaps_markdown;
|
|
74
74
|
return {
|
|
75
|
-
schema:
|
|
75
|
+
schema: 'projitive-json-store',
|
|
76
76
|
tasks,
|
|
77
77
|
roadmaps,
|
|
78
78
|
meta: {
|
|
@@ -83,22 +83,22 @@ function normalizeStore(input) {
|
|
|
83
83
|
view_state: {
|
|
84
84
|
tasks_markdown: {
|
|
85
85
|
...base.view_state.tasks_markdown,
|
|
86
|
-
dirty: typeof tasksView?.dirty ===
|
|
86
|
+
dirty: typeof tasksView?.dirty === 'boolean' ? tasksView.dirty : base.view_state.tasks_markdown.dirty,
|
|
87
87
|
lastSourceVersion: Number.isFinite(Number(tasksView?.lastSourceVersion))
|
|
88
88
|
? Number(tasksView?.lastSourceVersion)
|
|
89
89
|
: base.view_state.tasks_markdown.lastSourceVersion,
|
|
90
|
-
lastBuiltAt: typeof tasksView?.lastBuiltAt ===
|
|
90
|
+
lastBuiltAt: typeof tasksView?.lastBuiltAt === 'string' ? tasksView.lastBuiltAt : base.view_state.tasks_markdown.lastBuiltAt,
|
|
91
91
|
recordVersion: Number.isFinite(Number(tasksView?.recordVersion))
|
|
92
92
|
? Number(tasksView?.recordVersion)
|
|
93
93
|
: base.view_state.tasks_markdown.recordVersion,
|
|
94
94
|
},
|
|
95
95
|
roadmaps_markdown: {
|
|
96
96
|
...base.view_state.roadmaps_markdown,
|
|
97
|
-
dirty: typeof roadmapsView?.dirty ===
|
|
97
|
+
dirty: typeof roadmapsView?.dirty === 'boolean' ? roadmapsView.dirty : base.view_state.roadmaps_markdown.dirty,
|
|
98
98
|
lastSourceVersion: Number.isFinite(Number(roadmapsView?.lastSourceVersion))
|
|
99
99
|
? Number(roadmapsView?.lastSourceVersion)
|
|
100
100
|
: base.view_state.roadmaps_markdown.lastSourceVersion,
|
|
101
|
-
lastBuiltAt: typeof roadmapsView?.lastBuiltAt ===
|
|
101
|
+
lastBuiltAt: typeof roadmapsView?.lastBuiltAt === 'string' ? roadmapsView.lastBuiltAt : base.view_state.roadmaps_markdown.lastBuiltAt,
|
|
102
102
|
recordVersion: Number.isFinite(Number(roadmapsView?.recordVersion))
|
|
103
103
|
? Number(roadmapsView?.recordVersion)
|
|
104
104
|
: base.view_state.roadmaps_markdown.recordVersion,
|
|
@@ -111,7 +111,7 @@ async function persistStore(dbPath, store) {
|
|
|
111
111
|
await fs.mkdir(path.dirname(dbPath), { recursive: true });
|
|
112
112
|
const tempPath = `${dbPath}.tmp-${process.pid}-${Date.now()}`;
|
|
113
113
|
const body = `${JSON.stringify(store, null, 2)}\n`;
|
|
114
|
-
await fs.writeFile(tempPath, body,
|
|
114
|
+
await fs.writeFile(tempPath, body, 'utf8');
|
|
115
115
|
await fs.rename(tempPath, dbPath);
|
|
116
116
|
}
|
|
117
117
|
async function loadStoreFromDisk(dbPath) {
|
|
@@ -119,7 +119,7 @@ async function loadStoreFromDisk(dbPath) {
|
|
|
119
119
|
if (!file || file.length === 0) {
|
|
120
120
|
return { store: defaultStore(), shouldPersist: true };
|
|
121
121
|
}
|
|
122
|
-
const text = file.toString(
|
|
122
|
+
const text = file.toString('utf8').trim();
|
|
123
123
|
if (text.length === 0) {
|
|
124
124
|
return { store: defaultStore(), shouldPersist: true };
|
|
125
125
|
}
|
|
@@ -164,7 +164,7 @@ async function withStoreLock(dbPath, action) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
function bumpVersionAndDirtyView(store, kind) {
|
|
167
|
-
if (kind ===
|
|
167
|
+
if (kind === 'tasks') {
|
|
168
168
|
store.meta.tasks_version += 1;
|
|
169
169
|
const view = store.view_state.tasks_markdown;
|
|
170
170
|
view.dirty = true;
|
|
@@ -205,9 +205,9 @@ function safeTime(value) {
|
|
|
205
205
|
return Number.isFinite(t) ? t : 0;
|
|
206
206
|
}
|
|
207
207
|
function normalizeStatusForSort(status) {
|
|
208
|
-
if (status ===
|
|
208
|
+
if (status === 'IN_PROGRESS')
|
|
209
209
|
return 2;
|
|
210
|
-
if (status ===
|
|
210
|
+
if (status === 'TODO')
|
|
211
211
|
return 1;
|
|
212
212
|
return 0;
|
|
213
213
|
}
|
|
@@ -216,7 +216,7 @@ export async function ensureStore(dbPath) {
|
|
|
216
216
|
}
|
|
217
217
|
export async function getStoreVersion(dbPath, kind) {
|
|
218
218
|
const store = await openStore(dbPath);
|
|
219
|
-
return kind ===
|
|
219
|
+
return kind === 'tasks' ? store.meta.tasks_version : store.meta.roadmaps_version;
|
|
220
220
|
}
|
|
221
221
|
export async function getMarkdownViewState(dbPath, viewName) {
|
|
222
222
|
const store = await openStore(dbPath);
|
|
@@ -253,20 +253,20 @@ export async function loadTasksFromStore(dbPath) {
|
|
|
253
253
|
}
|
|
254
254
|
export async function loadTaskStatusStatsFromStore(dbPath) {
|
|
255
255
|
const tasks = await loadTasksFromStore(dbPath);
|
|
256
|
-
const todo = tasks.filter((task) => task.status ===
|
|
257
|
-
const inProgress = tasks.filter((task) => task.status ===
|
|
258
|
-
const blocked = tasks.filter((task) => task.status ===
|
|
259
|
-
const done = tasks.filter((task) => task.status ===
|
|
256
|
+
const todo = tasks.filter((task) => task.status === 'TODO').length;
|
|
257
|
+
const inProgress = tasks.filter((task) => task.status === 'IN_PROGRESS').length;
|
|
258
|
+
const blocked = tasks.filter((task) => task.status === 'BLOCKED').length;
|
|
259
|
+
const done = tasks.filter((task) => task.status === 'DONE').length;
|
|
260
260
|
const total = tasks.length;
|
|
261
261
|
const latestUpdatedAt = tasks
|
|
262
262
|
.map((task) => task.updatedAt)
|
|
263
|
-
.sort((a, b) => safeTime(b) - safeTime(a))[0] ??
|
|
263
|
+
.sort((a, b) => safeTime(b) - safeTime(a))[0] ?? '';
|
|
264
264
|
return { todo, inProgress, blocked, done, total, latestUpdatedAt };
|
|
265
265
|
}
|
|
266
266
|
export async function loadActionableTasksFromStore(dbPath, limit) {
|
|
267
267
|
const tasks = await loadTasksFromStore(dbPath);
|
|
268
268
|
const sorted = tasks
|
|
269
|
-
.filter((task) => task.status ===
|
|
269
|
+
.filter((task) => task.status === 'IN_PROGRESS' || task.status === 'TODO')
|
|
270
270
|
.sort((a, b) => {
|
|
271
271
|
const ap = normalizeStatusForSort(a.status);
|
|
272
272
|
const bp = normalizeStatusForSort(b.status);
|
|
@@ -279,7 +279,7 @@ export async function loadActionableTasksFromStore(dbPath, limit) {
|
|
|
279
279
|
}
|
|
280
280
|
return b.id.localeCompare(a.id);
|
|
281
281
|
});
|
|
282
|
-
if (typeof limit ===
|
|
282
|
+
if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0) {
|
|
283
283
|
return sorted.slice(0, Math.floor(limit));
|
|
284
284
|
}
|
|
285
285
|
return sorted;
|
|
@@ -307,7 +307,7 @@ export async function upsertTaskInStore(dbPath, task) {
|
|
|
307
307
|
recordVersion: 1,
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
|
-
bumpVersionAndDirtyView(store,
|
|
310
|
+
bumpVersionAndDirtyView(store, 'tasks');
|
|
311
311
|
await persistStore(dbPath, store);
|
|
312
312
|
});
|
|
313
313
|
}
|
|
@@ -321,7 +321,7 @@ export async function replaceTasksInStore(dbPath, tasks) {
|
|
|
321
321
|
roadmapRefs: [...(task.roadmapRefs ?? [])],
|
|
322
322
|
recordVersion: 1,
|
|
323
323
|
}));
|
|
324
|
-
bumpVersionAndDirtyView(store,
|
|
324
|
+
bumpVersionAndDirtyView(store, 'tasks');
|
|
325
325
|
await persistStore(dbPath, store);
|
|
326
326
|
});
|
|
327
327
|
}
|
|
@@ -349,7 +349,7 @@ export async function replaceRoadmapsInStore(dbPath, milestones) {
|
|
|
349
349
|
status: normalizeRoadmapStatus(milestone.status),
|
|
350
350
|
recordVersion: 1,
|
|
351
351
|
}));
|
|
352
|
-
bumpVersionAndDirtyView(store,
|
|
352
|
+
bumpVersionAndDirtyView(store, 'roadmaps');
|
|
353
353
|
await persistStore(dbPath, store);
|
|
354
354
|
});
|
|
355
355
|
}
|
|
@@ -372,7 +372,7 @@ export async function upsertRoadmapInStore(dbPath, milestone) {
|
|
|
372
372
|
recordVersion: 1,
|
|
373
373
|
});
|
|
374
374
|
}
|
|
375
|
-
bumpVersionAndDirtyView(store,
|
|
375
|
+
bumpVersionAndDirtyView(store, 'roadmaps');
|
|
376
376
|
await persistStore(dbPath, store);
|
|
377
377
|
});
|
|
378
378
|
}
|