@projitive/mcp 2.0.3 → 2.0.4
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/files.js +11 -11
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +10 -10
- package/output/source/common/linter.js +27 -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 +74 -74
- 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/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/index.js +16 -16
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskExecution.js +148 -148
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/index.js +2 -2
- 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/project.js +191 -191
- package/output/source/tools/project.test.js +174 -173
- package/output/source/tools/roadmap.js +110 -95
- package/output/source/tools/roadmap.test.js +54 -46
- package/output/source/tools/task.js +305 -277
- package/output/source/tools/task.test.js +117 -110
- package/output/source/types.js +22 -22
- package/package.json +8 -2
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
const MESSAGE_TEMPLATE_ENV =
|
|
4
|
-
const CONTENT_TEMPLATE_TOKEN =
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MESSAGE_TEMPLATE_ENV = 'PROJITIVE_MESSAGE_TEMPLATE_PATH';
|
|
4
|
+
const CONTENT_TEMPLATE_TOKEN = '{{content}}';
|
|
5
5
|
function baseToolTemplateMarkdown() {
|
|
6
6
|
return [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
].join(
|
|
7
|
+
'# {{tool_name}}',
|
|
8
|
+
'',
|
|
9
|
+
'## Summary',
|
|
10
|
+
'{{summary}}',
|
|
11
|
+
'',
|
|
12
|
+
'## Evidence',
|
|
13
|
+
'{{evidence}}',
|
|
14
|
+
'',
|
|
15
|
+
'## Agent Guidance',
|
|
16
|
+
'{{guidance}}',
|
|
17
|
+
'',
|
|
18
|
+
'## Next Call',
|
|
19
|
+
'{{next_call}}',
|
|
20
|
+
'',
|
|
21
|
+
'## Raw Response',
|
|
22
|
+
'{{content}}',
|
|
23
|
+
].join('\n');
|
|
24
24
|
}
|
|
25
25
|
function contextGuideTemplateExtra() {
|
|
26
26
|
return [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
35
|
];
|
|
36
36
|
}
|
|
37
37
|
function idleDiscoveryTemplateExtra() {
|
|
38
38
|
return [
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
46
|
];
|
|
47
47
|
}
|
|
48
48
|
function commitReminderTemplateExtra() {
|
|
49
49
|
return [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
56
|
];
|
|
57
57
|
}
|
|
58
58
|
export function getDefaultToolTemplateMarkdown(toolName) {
|
|
59
|
-
const base = baseToolTemplateMarkdown().split(
|
|
60
|
-
if (toolName ===
|
|
61
|
-
return [...base, ...idleDiscoveryTemplateExtra()].join(
|
|
59
|
+
const base = baseToolTemplateMarkdown().split('\n');
|
|
60
|
+
if (toolName === 'taskNext') {
|
|
61
|
+
return [...base, ...idleDiscoveryTemplateExtra()].join('\n');
|
|
62
62
|
}
|
|
63
|
-
if (toolName ===
|
|
64
|
-
return [...base, ...contextGuideTemplateExtra()].join(
|
|
63
|
+
if (toolName === 'projectContext' || toolName === 'taskContext' || toolName === 'roadmapContext') {
|
|
64
|
+
return [...base, ...contextGuideTemplateExtra()].join('\n');
|
|
65
65
|
}
|
|
66
|
-
if (toolName ===
|
|
67
|
-
return [...base, ...commitReminderTemplateExtra()].join(
|
|
66
|
+
if (toolName === 'taskUpdate' || toolName === 'roadmapUpdate') {
|
|
67
|
+
return [...base, ...commitReminderTemplateExtra()].join('\n');
|
|
68
68
|
}
|
|
69
|
-
return base.join(
|
|
69
|
+
return base.join('\n');
|
|
70
70
|
}
|
|
71
71
|
function loadTemplateFile(templatePath) {
|
|
72
72
|
try {
|
|
73
|
-
const content = fs.readFileSync(templatePath,
|
|
73
|
+
const content = fs.readFileSync(templatePath, 'utf-8').trim();
|
|
74
74
|
return content.length > 0 ? content : undefined;
|
|
75
75
|
}
|
|
76
76
|
catch {
|
|
@@ -84,13 +84,13 @@ function ensureTemplateFile(templatePath, toolName) {
|
|
|
84
84
|
}
|
|
85
85
|
fs.mkdirSync(path.dirname(templatePath), { recursive: true });
|
|
86
86
|
const generated = getDefaultToolTemplateMarkdown(toolName);
|
|
87
|
-
fs.writeFileSync(templatePath, `${generated}\n`,
|
|
87
|
+
fs.writeFileSync(templatePath, `${generated}\n`, 'utf-8');
|
|
88
88
|
return generated;
|
|
89
89
|
}
|
|
90
90
|
function resolveTemplateTarget(toolName) {
|
|
91
91
|
const configuredPath = process.env[MESSAGE_TEMPLATE_ENV]?.trim();
|
|
92
92
|
if (!configuredPath) {
|
|
93
|
-
return path.resolve(process.cwd(),
|
|
93
|
+
return path.resolve(process.cwd(), '.projitive', 'templates', 'tools', `${toolName}.md`);
|
|
94
94
|
}
|
|
95
95
|
const absolutePath = path.resolve(configuredPath);
|
|
96
96
|
try {
|
|
@@ -102,7 +102,7 @@ function resolveTemplateTarget(toolName) {
|
|
|
102
102
|
}
|
|
103
103
|
catch {
|
|
104
104
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
105
|
-
if (ext ===
|
|
105
|
+
if (ext === '.md') {
|
|
106
106
|
return absolutePath;
|
|
107
107
|
}
|
|
108
108
|
return path.join(absolutePath, `${toolName}.md`);
|
|
@@ -114,17 +114,17 @@ function loadMessageTemplate(toolName) {
|
|
|
114
114
|
}
|
|
115
115
|
export function asText(markdown) {
|
|
116
116
|
return {
|
|
117
|
-
content: [{ type:
|
|
117
|
+
content: [{ type: 'text', text: markdown }],
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
function withFallback(lines) {
|
|
121
|
-
return lines.length > 0 ? lines : [
|
|
121
|
+
return lines.length > 0 ? lines : ['- (none)'];
|
|
122
122
|
}
|
|
123
123
|
function shouldKeepRawLine(trimmed) {
|
|
124
124
|
if (trimmed.length === 0) {
|
|
125
125
|
return true;
|
|
126
126
|
}
|
|
127
|
-
if (trimmed.startsWith(
|
|
127
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('```')) {
|
|
128
128
|
return true;
|
|
129
129
|
}
|
|
130
130
|
if (/^[-*+]\s/.test(trimmed)) {
|
|
@@ -149,25 +149,25 @@ export function section(title, lines) {
|
|
|
149
149
|
return { title, lines: normalizeLines(lines) };
|
|
150
150
|
}
|
|
151
151
|
export function summarySection(lines) {
|
|
152
|
-
return section(
|
|
152
|
+
return section('Summary', lines);
|
|
153
153
|
}
|
|
154
154
|
export function evidenceSection(lines) {
|
|
155
|
-
return section(
|
|
155
|
+
return section('Evidence', lines);
|
|
156
156
|
}
|
|
157
157
|
export function guidanceSection(lines) {
|
|
158
|
-
return section(
|
|
158
|
+
return section('Agent Guidance', lines);
|
|
159
159
|
}
|
|
160
160
|
export function lintSection(lines) {
|
|
161
|
-
return section(
|
|
161
|
+
return section('Lint Suggestions', lines);
|
|
162
162
|
}
|
|
163
163
|
export function nextCallSection(nextCall) {
|
|
164
|
-
return section(
|
|
164
|
+
return section('Next Call', nextCall ? [nextCall] : []);
|
|
165
165
|
}
|
|
166
166
|
function toSectionText(section) {
|
|
167
167
|
if (!section) {
|
|
168
|
-
return
|
|
168
|
+
return '- (none)';
|
|
169
169
|
}
|
|
170
|
-
return withFallback(section.lines).join(
|
|
170
|
+
return withFallback(section.lines).join('\n');
|
|
171
171
|
}
|
|
172
172
|
function resolveSection(payload, title) {
|
|
173
173
|
return payload.sections.find((item) => item.title === title);
|
|
@@ -176,10 +176,10 @@ function buildToolTemplateVariables(payload, classicMarkdown) {
|
|
|
176
176
|
return {
|
|
177
177
|
tool_name: payload.toolName,
|
|
178
178
|
content: classicMarkdown,
|
|
179
|
-
summary: toSectionText(resolveSection(payload,
|
|
180
|
-
evidence: toSectionText(resolveSection(payload,
|
|
181
|
-
guidance: toSectionText(resolveSection(payload,
|
|
182
|
-
next_call: toSectionText(resolveSection(payload,
|
|
179
|
+
summary: toSectionText(resolveSection(payload, 'Summary')),
|
|
180
|
+
evidence: toSectionText(resolveSection(payload, 'Evidence')),
|
|
181
|
+
guidance: toSectionText(resolveSection(payload, 'Agent Guidance')),
|
|
182
|
+
next_call: toSectionText(resolveSection(payload, 'Next Call')),
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
function applyTemplateVariables(template, variables) {
|
|
@@ -196,13 +196,13 @@ export function renderToolResponseMarkdown(payload) {
|
|
|
196
196
|
const body = payload.sections.flatMap((section) => [
|
|
197
197
|
`## ${section.title}`,
|
|
198
198
|
...withFallback(section.lines),
|
|
199
|
-
|
|
199
|
+
'',
|
|
200
200
|
]);
|
|
201
201
|
const classicMarkdown = [
|
|
202
202
|
`# ${payload.toolName}`,
|
|
203
|
-
|
|
203
|
+
'',
|
|
204
204
|
...body,
|
|
205
|
-
].join(
|
|
205
|
+
].join('\n').trimEnd();
|
|
206
206
|
const template = loadMessageTemplate(payload.toolName);
|
|
207
207
|
const variables = buildToolTemplateVariables(payload, classicMarkdown);
|
|
208
208
|
return applyTemplateVariables(template, variables);
|
|
@@ -211,9 +211,9 @@ export function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
|
|
|
211
211
|
return renderToolResponseMarkdown({
|
|
212
212
|
toolName,
|
|
213
213
|
sections: [
|
|
214
|
-
section(
|
|
215
|
-
section(
|
|
216
|
-
section(
|
|
214
|
+
section('Error', [`cause: ${cause}`]),
|
|
215
|
+
section('Next Step', nextSteps),
|
|
216
|
+
section('Retry Example', [retryExample ?? '(none)']),
|
|
217
217
|
],
|
|
218
218
|
});
|
|
219
219
|
}
|
|
@@ -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
|
}
|