@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.
Files changed (52) hide show
  1. package/output/package.json +8 -2
  2. package/output/source/common/artifacts.js +1 -1
  3. package/output/source/common/artifacts.test.js +11 -11
  4. package/output/source/common/errors.js +19 -19
  5. package/output/source/common/errors.test.js +59 -0
  6. package/output/source/common/files.js +30 -19
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +11 -10
  9. package/output/source/common/linter.js +29 -27
  10. package/output/source/common/linter.test.js +9 -9
  11. package/output/source/common/markdown.js +3 -3
  12. package/output/source/common/markdown.test.js +15 -15
  13. package/output/source/common/response.js +91 -107
  14. package/output/source/common/response.test.js +30 -30
  15. package/output/source/common/store.js +40 -40
  16. package/output/source/common/store.test.js +72 -72
  17. package/output/source/common/tool.js +43 -0
  18. package/output/source/common/types.js +3 -3
  19. package/output/source/common/utils.js +8 -8
  20. package/output/source/common/utils.test.js +48 -0
  21. package/output/source/index.js +16 -16
  22. package/output/source/index.runtime.test.js +57 -0
  23. package/output/source/index.test.js +64 -64
  24. package/output/source/prompts/index.js +3 -3
  25. package/output/source/prompts/index.test.js +23 -0
  26. package/output/source/prompts/quickStart.js +96 -96
  27. package/output/source/prompts/quickStart.test.js +24 -0
  28. package/output/source/prompts/taskDiscovery.js +184 -184
  29. package/output/source/prompts/taskDiscovery.test.js +24 -0
  30. package/output/source/prompts/taskExecution.js +164 -148
  31. package/output/source/prompts/taskExecution.test.js +27 -0
  32. package/output/source/resources/designs.js +26 -26
  33. package/output/source/resources/designs.resources.test.js +52 -0
  34. package/output/source/resources/designs.test.js +88 -88
  35. package/output/source/resources/governance.js +19 -19
  36. package/output/source/resources/governance.test.js +35 -0
  37. package/output/source/resources/index.js +2 -2
  38. package/output/source/resources/index.test.js +18 -0
  39. package/output/source/resources/readme.js +7 -7
  40. package/output/source/resources/readme.test.js +113 -113
  41. package/output/source/resources/reports.js +10 -10
  42. package/output/source/resources/reports.test.js +83 -83
  43. package/output/source/tools/index.js +3 -3
  44. package/output/source/tools/index.test.js +23 -0
  45. package/output/source/tools/project.js +330 -377
  46. package/output/source/tools/project.test.js +308 -175
  47. package/output/source/tools/roadmap.js +236 -255
  48. package/output/source/tools/roadmap.test.js +241 -46
  49. package/output/source/tools/task.js +770 -652
  50. package/output/source/tools/task.test.js +433 -105
  51. package/output/source/types.js +28 -22
  52. package/package.json +8 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -16,7 +16,9 @@
16
16
  "test": "vitest run",
17
17
  "test:coverage": "vitest run --coverage",
18
18
  "benchmark": "vitest bench --run",
19
- "lint": "tsc -p tsconfig.json --noEmit",
19
+ "lint": "npm run lint:types && npm run lint:eslint",
20
+ "lint:types": "tsc -p tsconfig.json --noEmit",
21
+ "lint:eslint": "eslint .",
20
22
  "build": "rm -rf output && tsc -p tsconfig.json",
21
23
  "prepublishOnly": "npm run build",
22
24
  "dev": "tsc -p tsconfig.json --watch"
@@ -31,8 +33,12 @@
31
33
  "devDependencies": {
32
34
  "@types/node": "^24.3.0",
33
35
  "@vitest/coverage-v8": "^3.2.4",
36
+ "@eslint/js": "^9.37.0",
37
+ "eslint": "^9.37.0",
38
+ "globals": "^16.4.0",
34
39
  "tsx": "^4.20.5",
35
40
  "typescript": "^5.9.2",
41
+ "typescript-eslint": "^8.45.0",
36
42
  "vitest": "^3.2.4"
37
43
  }
38
44
  }
@@ -2,7 +2,7 @@ export function candidateFilesFromArtifacts(artifacts) {
2
2
  return artifacts
3
3
  .filter((item) => item.exists)
4
4
  .flatMap((item) => {
5
- if (item.kind === "file") {
5
+ if (item.kind === 'file') {
6
6
  return [item.path];
7
7
  }
8
8
  return (item.markdownFiles ?? []).map((entry) => entry.path);
@@ -1,18 +1,18 @@
1
- import { describe, expect, it } from "vitest";
2
- import { candidateFilesFromArtifacts } from "./artifacts.js";
3
- describe("candidateFilesFromArtifacts", () => {
4
- it("collects existing file artifacts and markdown files from existing directories", () => {
1
+ import { describe, expect, it } from 'vitest';
2
+ import { candidateFilesFromArtifacts } from './artifacts.js';
3
+ describe('candidateFilesFromArtifacts', () => {
4
+ it('collects existing file artifacts and markdown files from existing directories', () => {
5
5
  const candidates = candidateFilesFromArtifacts([
6
- { name: "README.md", kind: "file", path: "/a/README.md", exists: true, lineCount: 3 },
7
- { name: "tasks.md", kind: "file", path: "/a/tasks.md", exists: false },
6
+ { name: 'README.md', kind: 'file', path: '/a/README.md', exists: true, lineCount: 3 },
7
+ { name: 'tasks.md', kind: 'file', path: '/a/tasks.md', exists: false },
8
8
  {
9
- name: "designs",
10
- kind: "directory",
11
- path: "/a/designs",
9
+ name: 'designs',
10
+ kind: 'directory',
11
+ path: '/a/designs',
12
12
  exists: true,
13
- markdownFiles: [{ path: "/a/designs/d1.md", lineCount: 10 }],
13
+ markdownFiles: [{ path: '/a/designs/d1.md', lineCount: 10 }],
14
14
  },
15
15
  ]);
16
- expect(candidates).toEqual(["/a/README.md", "/a/designs/d1.md"]);
16
+ expect(candidates).toEqual(['/a/README.md', '/a/designs/d1.md']);
17
17
  });
18
18
  });
@@ -6,49 +6,49 @@ export class ProjitiveError extends Error {
6
6
  super(message);
7
7
  this.code = code;
8
8
  this.details = details;
9
- this.name = "ProjitiveError";
9
+ this.name = 'ProjitiveError';
10
10
  }
11
11
  }
12
12
  // Project related errors
13
13
  export class ProjectError extends ProjitiveError {
14
14
  constructor(message, code, details) {
15
15
  super(message, code, details);
16
- this.name = "ProjectError";
16
+ this.name = 'ProjectError';
17
17
  }
18
18
  }
19
19
  export class ProjectNotFoundError extends ProjectError {
20
20
  constructor(inputPath) {
21
- super(`Project not found at path: ${inputPath}`, "PROJECT_NOT_FOUND", {
21
+ super(`Project not found at path: ${inputPath}`, 'PROJECT_NOT_FOUND', {
22
22
  inputPath,
23
23
  });
24
24
  }
25
25
  }
26
26
  export class GovernanceRootNotFoundError extends ProjectError {
27
27
  constructor(projectPath) {
28
- super(`Governance root not found for project: ${projectPath}`, "GOVERNANCE_ROOT_NOT_FOUND", { projectPath });
28
+ super(`Governance root not found for project: ${projectPath}`, 'GOVERNANCE_ROOT_NOT_FOUND', { projectPath });
29
29
  }
30
30
  }
31
31
  // Task related errors
32
32
  export class TaskError extends ProjitiveError {
33
33
  constructor(message, code, details) {
34
34
  super(message, code, details);
35
- this.name = "TaskError";
35
+ this.name = 'TaskError';
36
36
  }
37
37
  }
38
38
  export class TaskNotFoundError extends TaskError {
39
39
  constructor(taskId) {
40
- super(`Task not found: ${taskId}`, "TASK_NOT_FOUND", { taskId });
40
+ super(`Task not found: ${taskId}`, 'TASK_NOT_FOUND', { taskId });
41
41
  }
42
42
  }
43
43
  export class InvalidTaskIdError extends TaskError {
44
44
  constructor(taskId) {
45
- super(`Invalid task ID: ${taskId}`, "INVALID_TASK_ID", { taskId });
45
+ super(`Invalid task ID: ${taskId}`, 'INVALID_TASK_ID', { taskId });
46
46
  }
47
47
  }
48
48
  export class TaskValidationError extends TaskError {
49
49
  errors;
50
50
  constructor(taskId, errors) {
51
- super(`Task validation failed for ${taskId}: ${errors.join(", ")}`, "TASK_VALIDATION_FAILED", { taskId, errors });
51
+ super(`Task validation failed for ${taskId}: ${errors.join(', ')}`, 'TASK_VALIDATION_FAILED', { taskId, errors });
52
52
  this.errors = errors;
53
53
  }
54
54
  }
@@ -56,26 +56,26 @@ export class TaskValidationError extends TaskError {
56
56
  export class FileError extends ProjitiveError {
57
57
  filePath;
58
58
  constructor(message, filePath, code, details) {
59
- super(message, code || "FILE_ERROR", { filePath, ...details });
59
+ super(message, code || 'FILE_ERROR', { filePath, ...details });
60
60
  this.filePath = filePath;
61
- this.name = "FileError";
61
+ this.name = 'FileError';
62
62
  }
63
63
  }
64
64
  export class FileNotFoundError extends FileError {
65
65
  constructor(filePath) {
66
- super(`File not found: ${filePath}`, filePath, "FILE_NOT_FOUND");
66
+ super(`File not found: ${filePath}`, filePath, 'FILE_NOT_FOUND');
67
67
  }
68
68
  }
69
69
  export class FileReadError extends FileError {
70
70
  constructor(filePath, cause) {
71
- super(`Failed to read file: ${filePath}`, filePath, "FILE_READ_ERROR", {
71
+ super(`Failed to read file: ${filePath}`, filePath, 'FILE_READ_ERROR', {
72
72
  cause: cause?.message,
73
73
  });
74
74
  }
75
75
  }
76
76
  export class FileWriteError extends FileError {
77
77
  constructor(filePath, cause) {
78
- super(`Failed to write file: ${filePath}`, filePath, "FILE_WRITE_ERROR", {
78
+ super(`Failed to write file: ${filePath}`, filePath, 'FILE_WRITE_ERROR', {
79
79
  cause: cause?.message,
80
80
  });
81
81
  }
@@ -84,15 +84,15 @@ export class FileWriteError extends FileError {
84
84
  export class ValidationError extends ProjitiveError {
85
85
  errors;
86
86
  constructor(message, errors = [], code) {
87
- super(message, code || "VALIDATION_FAILED", { errors });
87
+ super(message, code || 'VALIDATION_FAILED', { errors });
88
88
  this.errors = errors;
89
- this.name = "ValidationError";
89
+ this.name = 'ValidationError';
90
90
  }
91
91
  }
92
92
  export class ConfidenceScoreError extends ValidationError {
93
93
  score;
94
94
  constructor(message, score, errors = []) {
95
- super(message, errors, "CONFIDENCE_SCORE_ERROR");
95
+ super(message, errors, 'CONFIDENCE_SCORE_ERROR');
96
96
  this.score = score;
97
97
  this.score = score;
98
98
  }
@@ -101,19 +101,19 @@ export class ConfidenceScoreError extends ValidationError {
101
101
  export class MCPError extends ProjitiveError {
102
102
  constructor(message, code, details) {
103
103
  super(message, code, details);
104
- this.name = "MCPError";
104
+ this.name = 'MCPError';
105
105
  }
106
106
  }
107
107
  export class ResourceNotFoundError extends MCPError {
108
108
  constructor(resourceUri) {
109
- super(`Resource not found: ${resourceUri}`, "RESOURCE_NOT_FOUND", {
109
+ super(`Resource not found: ${resourceUri}`, 'RESOURCE_NOT_FOUND', {
110
110
  resourceUri,
111
111
  });
112
112
  }
113
113
  }
114
114
  export class PromptNotFoundError extends MCPError {
115
115
  constructor(promptName) {
116
- super(`Prompt not found: ${promptName}`, "PROMPT_NOT_FOUND", {
116
+ super(`Prompt not found: ${promptName}`, 'PROMPT_NOT_FOUND', {
117
117
  promptName,
118
118
  });
119
119
  }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ConfidenceScoreError, FileNotFoundError, FileReadError, FileWriteError, GovernanceRootNotFoundError, InvalidTaskIdError, MCPError, ProjectError, ProjectNotFoundError, PromptNotFoundError, ProjitiveError, ResourceNotFoundError, TaskError, TaskNotFoundError, TaskValidationError, ValidationError, } from './errors.js';
3
+ describe('errors module', () => {
4
+ it('constructs base and project/task hierarchy errors with metadata', () => {
5
+ const base = new ProjitiveError('base failure', 'BASE', { a: 1 });
6
+ const project = new ProjectError('project failure', 'PROJECT', { projectPath: '/tmp/project' });
7
+ const task = new TaskError('task failure', 'TASK', { taskId: 'TASK-0001' });
8
+ expect(base.name).toBe('ProjitiveError');
9
+ expect(base.code).toBe('BASE');
10
+ expect(base.details).toEqual({ a: 1 });
11
+ expect(project.name).toBe('ProjectError');
12
+ expect(project.code).toBe('PROJECT');
13
+ expect(task.name).toBe('TaskError');
14
+ expect(task.code).toBe('TASK');
15
+ });
16
+ it('constructs specific project and task lookup errors', () => {
17
+ const projectNotFound = new ProjectNotFoundError('/tmp/missing');
18
+ const governanceNotFound = new GovernanceRootNotFoundError('/tmp/project');
19
+ const taskNotFound = new TaskNotFoundError('TASK-0001');
20
+ const invalidTaskId = new InvalidTaskIdError('BAD');
21
+ const taskValidation = new TaskValidationError('TASK-0001', ['owner required', 'links required']);
22
+ expect(projectNotFound.code).toBe('PROJECT_NOT_FOUND');
23
+ expect(projectNotFound.message).toContain('/tmp/missing');
24
+ expect(governanceNotFound.code).toBe('GOVERNANCE_ROOT_NOT_FOUND');
25
+ expect(taskNotFound.code).toBe('TASK_NOT_FOUND');
26
+ expect(invalidTaskId.code).toBe('INVALID_TASK_ID');
27
+ expect(taskValidation.code).toBe('TASK_VALIDATION_FAILED');
28
+ expect(taskValidation.errors).toEqual(['owner required', 'links required']);
29
+ expect(taskValidation.message).toContain('owner required, links required');
30
+ });
31
+ it('constructs file and validation related errors', () => {
32
+ const cause = new Error('disk offline');
33
+ const notFound = new FileNotFoundError('/tmp/a.md');
34
+ const readError = new FileReadError('/tmp/b.md', cause);
35
+ const writeError = new FileWriteError('/tmp/c.md', cause);
36
+ const validation = new ValidationError('invalid input', ['field missing']);
37
+ const confidence = new ConfidenceScoreError('bad confidence', 1.5, ['must be <= 1']);
38
+ expect(notFound.name).toBe('FileError');
39
+ expect(notFound.code).toBe('FILE_NOT_FOUND');
40
+ expect(readError.details).toMatchObject({ filePath: '/tmp/b.md', cause: 'disk offline' });
41
+ expect(writeError.details).toMatchObject({ filePath: '/tmp/c.md', cause: 'disk offline' });
42
+ expect(validation.name).toBe('ValidationError');
43
+ expect(validation.code).toBe('VALIDATION_FAILED');
44
+ expect(validation.errors).toEqual(['field missing']);
45
+ expect(confidence.code).toBe('CONFIDENCE_SCORE_ERROR');
46
+ expect(confidence.score).toBe(1.5);
47
+ });
48
+ it('constructs MCP related not-found errors', () => {
49
+ const mcp = new MCPError('mcp failed', 'MCP_GENERIC', { retry: true });
50
+ const resource = new ResourceNotFoundError('projitive://missing');
51
+ const prompt = new PromptNotFoundError('taskExecution');
52
+ expect(mcp.name).toBe('MCPError');
53
+ expect(mcp.details).toEqual({ retry: true });
54
+ expect(resource.code).toBe('RESOURCE_NOT_FOUND');
55
+ expect(resource.message).toContain('projitive://missing');
56
+ expect(prompt.code).toBe('PROMPT_NOT_FOUND');
57
+ expect(prompt.message).toContain('taskExecution');
58
+ });
59
+ });
@@ -1,26 +1,37 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { catchIt } from "./catch.js";
4
- const FILE_ARTIFACTS = ["README.md", "roadmap.md", "tasks.md"];
5
- const DIRECTORY_ARTIFACTS = ["designs", "reports", "templates"];
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { catchIt } from './catch.js';
4
+ const FILE_ARTIFACTS = ['README.md', 'roadmap.md', 'tasks.md'];
5
+ const DIRECTORY_ARTIFACTS = ['designs', 'reports', 'templates'];
6
6
  async function fileLineCount(filePath) {
7
- const content = await fs.readFile(filePath, "utf-8");
7
+ const content = await fs.readFile(filePath, 'utf-8');
8
8
  if (!content) {
9
9
  return 0;
10
10
  }
11
11
  return content.split(/\r?\n/).length;
12
12
  }
13
13
  async function listMarkdownFiles(dirPath) {
14
- const entriesResult = await catchIt(fs.readdir(dirPath, { withFileTypes: true }));
15
- if (entriesResult.isError()) {
16
- return [];
17
- }
18
- const entries = entriesResult.value;
19
- const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"));
20
14
  const result = [];
21
- for (const file of files) {
22
- const fullPath = path.join(dirPath, file.name);
23
- result.push({ path: fullPath, lineCount: await fileLineCount(fullPath) });
15
+ const stack = [dirPath];
16
+ while (stack.length > 0) {
17
+ const currentDir = stack.pop();
18
+ if (!currentDir) {
19
+ continue;
20
+ }
21
+ const entriesResult = await catchIt(fs.readdir(currentDir, { withFileTypes: true }));
22
+ if (entriesResult.isError()) {
23
+ continue;
24
+ }
25
+ for (const entry of entriesResult.value) {
26
+ const entryPath = path.join(currentDir, entry.name);
27
+ if (entry.isDirectory()) {
28
+ stack.push(entryPath);
29
+ continue;
30
+ }
31
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
32
+ result.push({ path: entryPath, lineCount: await fileLineCount(entryPath) });
33
+ }
34
+ }
24
35
  }
25
36
  return result.sort((a, b) => a.path.localeCompare(b.path));
26
37
  }
@@ -32,14 +43,14 @@ export async function discoverGovernanceArtifacts(governanceDir) {
32
43
  if (!accessResult.isError()) {
33
44
  result.push({
34
45
  name: artifact,
35
- kind: "file",
46
+ kind: 'file',
36
47
  path: artifactPath,
37
48
  exists: true,
38
49
  lineCount: await fileLineCount(artifactPath),
39
50
  });
40
51
  }
41
52
  else {
42
- result.push({ name: artifact, kind: "file", path: artifactPath, exists: false });
53
+ result.push({ name: artifact, kind: 'file', path: artifactPath, exists: false });
43
54
  }
44
55
  }
45
56
  for (const artifact of DIRECTORY_ARTIFACTS) {
@@ -48,14 +59,14 @@ export async function discoverGovernanceArtifacts(governanceDir) {
48
59
  if (!accessResult.isError()) {
49
60
  result.push({
50
61
  name: artifact,
51
- kind: "directory",
62
+ kind: 'directory',
52
63
  path: artifactPath,
53
64
  exists: true,
54
65
  markdownFiles: await listMarkdownFiles(artifactPath),
55
66
  });
56
67
  }
57
68
  else {
58
- result.push({ name: artifact, kind: "directory", path: artifactPath, exists: false, markdownFiles: [] });
69
+ result.push({ name: artifact, kind: 'directory', path: artifactPath, exists: false, markdownFiles: [] });
59
70
  }
60
71
  }
61
72
  return result;
@@ -1,11 +1,11 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it } from "vitest";
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
5
  import { discoverGovernanceArtifacts } from './files.js';
6
6
  const tempPaths = [];
7
7
  async function createTempDir() {
8
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
9
9
  tempPaths.push(dir);
10
10
  return dir;
11
11
  }
@@ -14,19 +14,19 @@ afterEach(async () => {
14
14
  await fs.rm(dir, { recursive: true, force: true });
15
15
  }));
16
16
  });
17
- describe("files module", () => {
18
- it("discovers governance artifacts with paths and line counts", async () => {
17
+ describe('files module', () => {
18
+ it('discovers governance artifacts with paths and line counts', async () => {
19
19
  const root = await createTempDir();
20
- await fs.writeFile(path.join(root, "README.md"), "# Readme\n", "utf-8");
21
- await fs.writeFile(path.join(root, "tasks.md"), "# Tasks\n## TODO\n", "utf-8");
22
- await fs.mkdir(path.join(root, "designs"), { recursive: true });
23
- await fs.writeFile(path.join(root, "designs", "feature-design.md"), "# Design\n", "utf-8");
20
+ await fs.writeFile(path.join(root, 'README.md'), '# Readme\n', 'utf-8');
21
+ await fs.writeFile(path.join(root, 'tasks.md'), '# Tasks\n## TODO\n', 'utf-8');
22
+ await fs.mkdir(path.join(root, 'designs'), { recursive: true });
23
+ await fs.writeFile(path.join(root, 'designs', 'feature-design.md'), '# Design\n', 'utf-8');
24
24
  const artifacts = await discoverGovernanceArtifacts(root);
25
- const readme = artifacts.find((item) => item.name === "README.md");
26
- const designs = artifacts.find((item) => item.name === "designs");
25
+ const readme = artifacts.find((item) => item.name === 'README.md');
26
+ const designs = artifacts.find((item) => item.name === 'designs');
27
27
  expect(readme?.exists).toBe(true);
28
28
  expect(readme?.lineCount).toBe(2);
29
29
  expect(designs?.exists).toBe(true);
30
- expect(designs?.markdownFiles?.[0].path.endsWith("feature-design.md")).toBe(true);
30
+ expect(designs?.markdownFiles?.[0].path.endsWith('feature-design.md')).toBe(true);
31
31
  });
32
32
  });
@@ -1,10 +1,11 @@
1
- export * from "./errors.js";
2
- export * from "./types.js";
3
- export * from "./utils.js";
4
- export * from "./markdown.js";
5
- export * from "./files.js";
6
- export * from "./response.js";
7
- export * from "./catch.js";
8
- export * from "./artifacts.js";
9
- export * from "./linter.js";
10
- export * from "./store.js";
1
+ export * from './errors.js';
2
+ export * from './types.js';
3
+ export * from './utils.js';
4
+ export * from './markdown.js';
5
+ export * from './files.js';
6
+ export * from './response.js';
7
+ export * from './catch.js';
8
+ export * from './artifacts.js';
9
+ export * from './linter.js';
10
+ export * from './store.js';
11
+ export * from './tool.js';
@@ -1,39 +1,41 @@
1
1
  export function renderLintSuggestions(suggestions) {
2
2
  return suggestions.map((item) => {
3
- const suffix = item.fixHint ? ` ${item.fixHint}` : "";
3
+ const suffix = item.fixHint ? ` ${item.fixHint}` : '';
4
4
  return `- [${item.code}] ${item.message}${suffix}`;
5
5
  });
6
6
  }
7
7
  export const TASK_LINT_CODES = {
8
- DUPLICATE_ID: "TASK_DUPLICATE_ID",
9
- IN_PROGRESS_OWNER_EMPTY: "TASK_IN_PROGRESS_OWNER_EMPTY",
10
- DONE_LINKS_MISSING: "TASK_DONE_LINKS_MISSING",
11
- BLOCKED_SUMMARY_EMPTY: "TASK_BLOCKED_SUMMARY_EMPTY",
12
- UPDATED_AT_INVALID: "TASK_UPDATED_AT_INVALID",
13
- ROADMAP_REFS_EMPTY: "TASK_ROADMAP_REFS_EMPTY",
14
- OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
15
- LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
16
- LINK_PATH_FORMAT_INVALID: "TASK_LINK_PATH_FORMAT_INVALID",
17
- HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
18
- FILTER_EMPTY: "TASK_FILTER_EMPTY",
19
- CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
20
- CONTEXT_HOOK_FOOTER_MISSING: "TASK_CONTEXT_HOOK_FOOTER_MISSING",
8
+ DUPLICATE_ID: 'TASK_DUPLICATE_ID',
9
+ IN_PROGRESS_OWNER_EMPTY: 'TASK_IN_PROGRESS_OWNER_EMPTY',
10
+ DONE_LINKS_MISSING: 'TASK_DONE_LINKS_MISSING',
11
+ BLOCKED_SUMMARY_EMPTY: 'TASK_BLOCKED_SUMMARY_EMPTY',
12
+ UPDATED_AT_INVALID: 'TASK_UPDATED_AT_INVALID',
13
+ ROADMAP_REFS_EMPTY: 'TASK_ROADMAP_REFS_EMPTY',
14
+ OUTSIDE_MARKER: 'TASK_OUTSIDE_MARKER',
15
+ LINK_PATH_FORMAT_INVALID: 'TASK_LINK_PATH_FORMAT_INVALID',
16
+ HOOK_FILE_MISSING: 'TASK_HOOK_FILE_MISSING',
17
+ FILTER_EMPTY: 'TASK_FILTER_EMPTY',
18
+ CONTEXT_HOOK_HEAD_MISSING: 'TASK_CONTEXT_HOOK_HEAD_MISSING',
19
+ CONTEXT_HOOK_FOOTER_MISSING: 'TASK_CONTEXT_HOOK_FOOTER_MISSING',
21
20
  // Spec v1.1.0 - Blocker Categorization
22
- BLOCKED_WITHOUT_BLOCKER: "TASK_BLOCKED_WITHOUT_BLOCKER",
23
- BLOCKER_TYPE_INVALID: "TASK_BLOCKER_TYPE_INVALID",
24
- BLOCKER_DESCRIPTION_EMPTY: "TASK_BLOCKER_DESCRIPTION_EMPTY",
25
- IN_PROGRESS_WITHOUT_SUBSTATE: "TASK_IN_PROGRESS_WITHOUT_SUBSTATE",
26
- SUBSTATE_PHASE_INVALID: "TASK_SUBSTATE_PHASE_INVALID",
27
- SUBSTATE_CONFIDENCE_INVALID: "TASK_SUBSTATE_CONFIDENCE_INVALID",
21
+ BLOCKED_WITHOUT_BLOCKER: 'TASK_BLOCKED_WITHOUT_BLOCKER',
22
+ BLOCKER_TYPE_INVALID: 'TASK_BLOCKER_TYPE_INVALID',
23
+ BLOCKER_DESCRIPTION_EMPTY: 'TASK_BLOCKER_DESCRIPTION_EMPTY',
24
+ IN_PROGRESS_WITHOUT_SUBSTATE: 'TASK_IN_PROGRESS_WITHOUT_SUBSTATE',
25
+ SUBSTATE_PHASE_INVALID: 'TASK_SUBSTATE_PHASE_INVALID',
26
+ SUBSTATE_CONFIDENCE_INVALID: 'TASK_SUBSTATE_CONFIDENCE_INVALID',
27
+ RESEARCH_BRIEF_MISSING: 'TASK_RESEARCH_BRIEF_MISSING',
28
28
  };
29
29
  export const ROADMAP_LINT_CODES = {
30
- IDS_EMPTY: "ROADMAP_IDS_EMPTY",
31
- TASKS_EMPTY: "ROADMAP_TASKS_EMPTY",
32
- TASK_REFS_EMPTY: "ROADMAP_TASK_REFS_EMPTY",
33
- UNKNOWN_REFS: "ROADMAP_UNKNOWN_REFS",
34
- ZERO_LINKED_TASKS: "ROADMAP_ZERO_LINKED_TASKS",
35
- CONTEXT_RELATED_TASKS_EMPTY: "ROADMAP_CONTEXT_RELATED_TASKS_EMPTY",
30
+ IDS_EMPTY: 'ROADMAP_IDS_EMPTY',
31
+ TASKS_EMPTY: 'ROADMAP_TASKS_EMPTY',
32
+ TASK_REFS_EMPTY: 'ROADMAP_TASK_REFS_EMPTY',
33
+ UNKNOWN_REFS: 'ROADMAP_UNKNOWN_REFS',
34
+ ZERO_LINKED_TASKS: 'ROADMAP_ZERO_LINKED_TASKS',
35
+ CONTEXT_RELATED_TASKS_EMPTY: 'ROADMAP_CONTEXT_RELATED_TASKS_EMPTY',
36
36
  };
37
37
  export const PROJECT_LINT_CODES = {
38
- TASKS_FILE_MISSING: "PROJECT_TASKS_FILE_MISSING",
38
+ TASKS_FILE_MISSING: 'PROJECT_TASKS_FILE_MISSING',
39
+ ARCHITECTURE_DOC_MISSING: 'PROJECT_ARCHITECTURE_DOC_MISSING',
40
+ STYLE_DOC_MISSING: 'PROJECT_STYLE_DOC_MISSING',
39
41
  };
@@ -1,16 +1,16 @@
1
- import { describe, expect, it } from "vitest";
2
- import { renderLintSuggestions } from "./linter.js";
3
- describe("renderLintSuggestions", () => {
4
- it("renders lint lines with code and message", () => {
1
+ import { describe, expect, it } from 'vitest';
2
+ import { renderLintSuggestions } from './linter.js';
3
+ describe('renderLintSuggestions', () => {
4
+ it('renders lint lines with code and message', () => {
5
5
  const lines = renderLintSuggestions([
6
- { code: "TASK_001", message: "Example lint" },
6
+ { code: 'TASK_001', message: 'Example lint' },
7
7
  ]);
8
- expect(lines).toEqual(["- [TASK_001] Example lint"]);
8
+ expect(lines).toEqual(['- [TASK_001] Example lint']);
9
9
  });
10
- it("appends fixHint when provided", () => {
10
+ it('appends fixHint when provided', () => {
11
11
  const lines = renderLintSuggestions([
12
- { code: "TASK_002", message: "Missing field.", fixHint: "Set owner." },
12
+ { code: 'TASK_002', message: 'Missing field.', fixHint: 'Set owner.' },
13
13
  ]);
14
- expect(lines).toEqual(["- [TASK_002] Missing field. Set owner."]);
14
+ expect(lines).toEqual(['- [TASK_002] Missing field. Set owner.']);
15
15
  });
16
16
  });
@@ -1,6 +1,6 @@
1
- import fs from "node:fs/promises";
1
+ import fs from 'node:fs/promises';
2
2
  export async function readMarkdownSections(filePath) {
3
- const content = await fs.readFile(filePath, "utf-8");
3
+ const content = await fs.readFile(filePath, 'utf-8');
4
4
  const lines = content.split(/\r?\n/);
5
5
  const headers = [];
6
6
  lines.forEach((line, index) => {
@@ -21,7 +21,7 @@ export async function readMarkdownSections(filePath) {
21
21
  return { filePath, lineCount: lines.length, sections };
22
22
  }
23
23
  export async function findTextReferences(filePath, needle) {
24
- const content = await fs.readFile(filePath, "utf-8");
24
+ const content = await fs.readFile(filePath, 'utf-8');
25
25
  const lines = content.split(/\r?\n/);
26
26
  const result = [];
27
27
  lines.forEach((line, index) => {
@@ -1,11 +1,11 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it } from "vitest";
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
5
  import { findTextReferences, readMarkdownSections } from './markdown.js';
6
6
  const tempPaths = [];
7
7
  async function createTempDir() {
8
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
9
9
  tempPaths.push(dir);
10
10
  return dir;
11
11
  }
@@ -14,22 +14,22 @@ afterEach(async () => {
14
14
  await fs.rm(dir, { recursive: true, force: true });
15
15
  }));
16
16
  });
17
- describe("markdown module", () => {
18
- it("locates markdown sections with line ranges", async () => {
17
+ describe('markdown module', () => {
18
+ it('locates markdown sections with line ranges', async () => {
19
19
  const root = await createTempDir();
20
- const file = path.join(root, "tasks.md");
21
- await fs.writeFile(file, ["# Tasks", "", "## TODO", "- TASK-0001", "## DONE", "- TASK-0002"].join("\n"), "utf-8");
20
+ const file = path.join(root, 'tasks.md');
21
+ await fs.writeFile(file, ['# Tasks', '', '## TODO', '- TASK-0001', '## DONE', '- TASK-0002'].join('\n'), 'utf-8');
22
22
  const located = await readMarkdownSections(file);
23
23
  expect(located.lineCount).toBe(6);
24
- expect(located.sections[0].heading).toBe("Tasks");
25
- expect(located.sections[1].heading).toBe("TODO");
24
+ expect(located.sections[0].heading).toBe('Tasks');
25
+ expect(located.sections[1].heading).toBe('TODO');
26
26
  expect(located.sections[1].startLine).toBe(3);
27
27
  });
28
- it("finds ID references with exact line number", async () => {
28
+ it('finds ID references with exact line number', async () => {
29
29
  const root = await createTempDir();
30
- const file = path.join(root, "reports.md");
31
- await fs.writeFile(file, ["Task: TASK-0001", "Roadmap: ROADMAP-0001"].join("\n"), "utf-8");
32
- const refs = await findTextReferences(file, "TASK-0001");
30
+ const file = path.join(root, 'reports.md');
31
+ await fs.writeFile(file, ['Task: TASK-0001', 'Roadmap: ROADMAP-0001'].join('\n'), 'utf-8');
32
+ const refs = await findTextReferences(file, 'TASK-0001');
33
33
  expect(refs).toHaveLength(1);
34
34
  expect(refs[0].line).toBe(1);
35
35
  });