@projitive/mcp 1.1.1 → 1.2.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/README.md CHANGED
@@ -84,7 +84,7 @@ MCP client config example (`mcp.json`):
84
84
  "command": "npx",
85
85
  "args": ["-y", "@projitive/mcp"],
86
86
  "env": {
87
- "PROJITIVE_SCAN_ROOT_PATH": "/absolute/path/to/your/workspace",
87
+ "PROJITIVE_SCAN_ROOT_PATHS": "/workspace/a:/workspace/b",
88
88
  "PROJITIVE_SCAN_MAX_DEPTH": "3"
89
89
  }
90
90
  }
@@ -94,7 +94,9 @@ MCP client config example (`mcp.json`):
94
94
 
95
95
  Environment variables (required):
96
96
 
97
- - `PROJITIVE_SCAN_ROOT_PATH`: required scan root for discovery methods.
97
+ - `PROJITIVE_SCAN_ROOT_PATHS`: required scan roots for discovery methods.
98
+ - Use platform-delimiter string (`:` on Linux/macOS, `;` on Windows), e.g. `/workspace/a:/workspace/b`.
99
+ - Fallback: if not set, legacy `PROJITIVE_SCAN_ROOT_PATH` is used.
98
100
  - `PROJITIVE_SCAN_MAX_DEPTH`: required scan depth for discovery methods (integer `0-8`).
99
101
 
100
102
  Local path startup is not the recommended usage mode in this README.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -4,7 +4,6 @@ export * from "./utils.js";
4
4
  export * from "./markdown.js";
5
5
  export * from "./files.js";
6
6
  export * from "./response.js";
7
- export * from "./confidence.js";
8
7
  export * from "./catch.js";
9
8
  export * from "./artifacts.js";
10
9
  export * from "./linter.js";
@@ -6,9 +6,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import packageJson from "../package.json" with { type: "json" };
8
8
  import { registerTools } from "./tools/index.js";
9
- import { registerResources } from "./resources/index.js";
10
9
  import { registerPrompts } from "./prompts/index.js";
11
- import { registerDesignContextResources, registerDesignContextPrompts } from "./design-context.js";
10
+ import { registerResources } from "./resources/index.js";
12
11
  const PROJITIVE_SPEC_VERSION = "1.1.0";
13
12
  const currentFilePath = fileURLToPath(import.meta.url);
14
13
  const sourceDir = path.dirname(currentFilePath);
@@ -21,12 +20,10 @@ const server = new McpServer({
21
20
  version: MCP_RUNTIME_VERSION,
22
21
  description: "Semantic Projitive MCP for project/task discovery and agent guidance with markdown-first outputs",
23
22
  });
24
- // Register all modules
23
+ // 注册所有模块
25
24
  registerTools(server);
26
- registerResources(server, repoRoot);
27
25
  registerPrompts(server);
28
- registerDesignContextResources(server);
29
- registerDesignContextPrompts(server);
26
+ registerResources(server, repoRoot);
30
27
  async function main() {
31
28
  console.error(`[projitive-mcp] starting server`);
32
29
  console.error(`[projitive-mcp] version=${MCP_RUNTIME_VERSION} spec=${PROJITIVE_SPEC_VERSION} transport=stdio pid=${process.pid}`);
@@ -26,10 +26,6 @@ vi.mock("./tools/task.js", () => ({
26
26
  vi.mock("./tools/roadmap.js", () => ({
27
27
  registerRoadmapTools: vi.fn(),
28
28
  }));
29
- vi.mock("./design-context.js", () => ({
30
- registerDesignContextResources: vi.fn(),
31
- registerDesignContextPrompts: vi.fn(),
32
- }));
33
29
  vi.mock("./resources/governance.js", () => ({
34
30
  registerGovernanceResources: vi.fn(),
35
31
  }));
@@ -87,8 +83,6 @@ describe("index module", () => {
87
83
  expect(content).toContain("registerTools(server)");
88
84
  expect(content).toContain("registerResources(server, repoRoot)");
89
85
  expect(content).toContain("registerPrompts(server)");
90
- expect(content).toContain("registerDesignContextResources(server)");
91
- expect(content).toContain("registerDesignContextPrompts(server)");
92
86
  });
93
87
  it("should have proper error handling in main", async () => {
94
88
  const indexPath = path.join(sourceDir, "index.ts");
@@ -46,9 +46,43 @@ function requireEnvVar(name) {
46
46
  }
47
47
  return value.trim();
48
48
  }
49
+ function normalizeScanRoots(rootPaths) {
50
+ const normalized = rootPaths
51
+ .map((entry) => entry.trim())
52
+ .filter((entry) => entry.length > 0)
53
+ .map((entry) => normalizePath(entry));
54
+ return Array.from(new Set(normalized));
55
+ }
56
+ function parseScanRoots(rawValue) {
57
+ const trimmed = rawValue.trim();
58
+ if (trimmed.length === 0) {
59
+ return [];
60
+ }
61
+ return trimmed.split(path.delimiter);
62
+ }
63
+ export function resolveScanRoots(inputPaths) {
64
+ const normalizedInputPaths = normalizeScanRoots(inputPaths ?? []);
65
+ if (normalizedInputPaths.length > 0) {
66
+ return normalizedInputPaths;
67
+ }
68
+ const configuredRoots = process.env.PROJITIVE_SCAN_ROOT_PATHS;
69
+ const rootsFromMultiEnv = typeof configuredRoots === "string"
70
+ ? normalizeScanRoots(parseScanRoots(configuredRoots))
71
+ : [];
72
+ if (rootsFromMultiEnv.length > 0) {
73
+ return rootsFromMultiEnv;
74
+ }
75
+ const legacyRoot = process.env.PROJITIVE_SCAN_ROOT_PATH;
76
+ const rootsFromLegacyEnv = typeof legacyRoot === "string"
77
+ ? normalizeScanRoots([legacyRoot])
78
+ : [];
79
+ if (rootsFromLegacyEnv.length > 0) {
80
+ return rootsFromLegacyEnv;
81
+ }
82
+ throw new Error("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS (or legacy PROJITIVE_SCAN_ROOT_PATH)");
83
+ }
49
84
  export function resolveScanRoot(inputPath) {
50
- const configuredRoot = requireEnvVar("PROJITIVE_SCAN_ROOT_PATH");
51
- return normalizePath(inputPath ?? configuredRoot);
85
+ return resolveScanRoots(inputPath ? [inputPath] : undefined)[0];
52
86
  }
53
87
  export function resolveScanDepth(inputDepth) {
54
88
  const configuredDepthRaw = requireEnvVar("PROJITIVE_SCAN_MAX_DEPTH");
@@ -210,6 +244,10 @@ export async function discoverProjects(rootPath, maxDepth) {
210
244
  await walk(rootPath, 0);
211
245
  return Array.from(new Set(results)).sort();
212
246
  }
247
+ export async function discoverProjectsAcrossRoots(rootPaths, maxDepth) {
248
+ const perRootResults = await Promise.all(rootPaths.map((rootPath) => discoverProjects(rootPath, maxDepth)));
249
+ return Array.from(new Set(perRootResults.flat())).sort();
250
+ }
213
251
  async function pathExists(targetPath) {
214
252
  const accessResult = await catchIt(fs.access(targetPath));
215
253
  return !accessResult.isError();
@@ -362,14 +400,15 @@ export function registerProjectTools(server) {
362
400
  description: "Start here when project path is unknown; discover all governance roots",
363
401
  inputSchema: {},
364
402
  }, async () => {
365
- const root = resolveScanRoot();
403
+ const roots = resolveScanRoots();
366
404
  const depth = resolveScanDepth();
367
- const projects = await discoverProjects(root, depth);
405
+ const projects = await discoverProjectsAcrossRoots(roots, depth);
368
406
  const markdown = renderToolResponseMarkdown({
369
407
  toolName: "projectScan",
370
408
  sections: [
371
409
  summarySection([
372
- `- rootPath: ${root}`,
410
+ `- rootPaths: ${roots.join(", ")}`,
411
+ `- rootCount: ${roots.length}`,
373
412
  `- maxDepth: ${depth}`,
374
413
  `- discoveredCount: ${projects.length}`,
375
414
  ]),
@@ -398,9 +437,9 @@ export function registerProjectTools(server) {
398
437
  limit: z.number().int().min(1).max(50).optional(),
399
438
  },
400
439
  }, async ({ limit }) => {
401
- const root = resolveScanRoot();
440
+ const roots = resolveScanRoots();
402
441
  const depth = resolveScanDepth();
403
- const projects = await discoverProjects(root, depth);
442
+ const projects = await discoverProjectsAcrossRoots(roots, depth);
404
443
  const snapshots = await Promise.all(projects.map(async (governanceDir) => {
405
444
  const snapshot = await readTasksSnapshot(governanceDir);
406
445
  const inProgress = snapshot.tasks.filter((task) => task.status === "IN_PROGRESS").length;
@@ -435,7 +474,8 @@ export function registerProjectTools(server) {
435
474
  toolName: "projectNext",
436
475
  sections: [
437
476
  summarySection([
438
- `- rootPath: ${root}`,
477
+ `- rootPaths: ${roots.join(", ")}`,
478
+ `- rootCount: ${roots.length}`,
439
479
  `- maxDepth: ${depth}`,
440
480
  `- matchedProjects: ${projects.length}`,
441
481
  `- actionableProjects: ${ranked.length}`,
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
- import { discoverProjects, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoot, resolveScanDepth, toProjectPath, registerProjectTools } from "./project.js";
5
+ import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from "./project.js";
6
6
  const tempPaths = [];
7
7
  async function createTempDir() {
8
8
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
@@ -186,6 +186,15 @@ describe("projitive module", () => {
186
186
  const projects = await discoverProjects(root, 3);
187
187
  expect(projects).toEqual([]);
188
188
  });
189
+ it("ignores non-existent roots when scanning across multiple roots", async () => {
190
+ const validRoot = await createTempDir();
191
+ const validProject = path.join(validRoot, "project-a");
192
+ const missingRoot = path.join(validRoot, "__missing_root__");
193
+ await fs.mkdir(validProject, { recursive: true });
194
+ await fs.writeFile(path.join(validProject, ".projitive"), "", "utf-8");
195
+ const projects = await discoverProjectsAcrossRoots([missingRoot, validRoot], 3);
196
+ expect(projects).toContain(validProject);
197
+ });
189
198
  });
190
199
  describe("initializeProjectStructure", () => {
191
200
  it("initializes governance structure under default .projitive directory", async () => {
@@ -267,20 +276,30 @@ describe("projitive module", () => {
267
276
  expect(toProjectPath("/a/b/c")).toBe("/a/b");
268
277
  });
269
278
  });
270
- describe("resolveScanRoot", () => {
271
- it("uses environment variable when no input path", () => {
279
+ describe("resolveScanRoots", () => {
280
+ it("uses legacy environment variable when no multi-root env is provided", () => {
272
281
  vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
273
- expect(resolveScanRoot()).toBe("/test/root");
282
+ expect(resolveScanRoots()).toEqual(["/test/root"]);
274
283
  vi.unstubAllEnvs();
275
284
  });
276
- it("uses input path when provided", () => {
285
+ it("uses input paths when provided", () => {
277
286
  vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
278
- expect(resolveScanRoot("/custom/path")).toBe("/custom/path");
287
+ expect(resolveScanRoots(["/custom/path", " /custom/path ", "/second/path"])).toEqual(["/custom/path", "/second/path"]);
288
+ vi.unstubAllEnvs();
289
+ });
290
+ it("uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter", () => {
291
+ vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", ["/root/a", "/root/b", "", " /root/c "].join(path.delimiter));
292
+ expect(resolveScanRoots()).toEqual(["/root/a", "/root/b", "/root/c"]);
293
+ vi.unstubAllEnvs();
294
+ });
295
+ it("treats JSON-like string as plain delimiter input", () => {
296
+ vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", JSON.stringify(["/json/a", "/json/b"]));
297
+ expect(resolveScanRoots()).toHaveLength(1);
279
298
  vi.unstubAllEnvs();
280
299
  });
281
- it("throws error when required environment variable missing", () => {
300
+ it("throws error when no root environment variables are configured", () => {
282
301
  vi.unstubAllEnvs();
283
- expect(() => resolveScanRoot()).toThrow("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATH");
302
+ expect(() => resolveScanRoots()).toThrow("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS");
284
303
  });
285
304
  });
286
305
  describe("resolveScanDepth", () => {
@@ -4,10 +4,9 @@ import { z } from "zod";
4
4
  import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences } from "../common/index.js";
5
5
  import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
6
6
  import { catchIt, TASK_LINT_CODES, renderLintSuggestions } from "../common/index.js";
7
- import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects, toProjectPath } from "./project.js";
7
+ import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from "./project.js";
8
8
  import { isValidRoadmapId } from "./roadmap.js";
9
9
  import { SUB_STATE_PHASES, BLOCKER_TYPES } from "../types.js";
10
- import { calculateConfidenceScore, calculateContextCompleteness, calculateSimilarTaskHistory, calculateSpecificationClarity, getOrCreateTaskAutoCreateValidationHook, runPreCreationValidation, generateConfidenceReport } from "../common/index.js";
11
10
  export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
12
11
  export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
13
12
  export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
@@ -51,7 +50,7 @@ const NO_TASK_DISCOVERY_HOOK_FILE = "task_no_actionable.md";
51
50
  const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
52
51
  "- Check whether current code violates project guide/spec conventions; create TODO tasks for each actionable gap.",
53
52
  "- Check unit/integration test coverage and identify high-value missing tests; create TODO tasks for meaningful coverage improvements.",
54
- "- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create TODO tasks to improve reliability.",
53
+ "- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create tasks to improve reliability.",
55
54
  "- Scan for TODO/FIXME/HACK comments and convert feasible items into governed TODO tasks with evidence links.",
56
55
  "- Check dependency freshness and security advisories; create tasks for safe upgrades when needed.",
57
56
  "- Check repeated manual operations that can be automated (lint/test/release checks); create tasks to reduce operational toil.",
@@ -821,9 +820,9 @@ export function registerTaskTools(server) {
821
820
  limit: z.number().int().min(1).max(20).optional(),
822
821
  },
823
822
  }, async ({ limit }) => {
824
- const root = resolveScanRoot();
823
+ const roots = resolveScanRoots();
825
824
  const depth = resolveScanDepth();
826
- const projects = await discoverProjects(root, depth);
825
+ const projects = await discoverProjectsAcrossRoots(roots, depth);
827
826
  const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
828
827
  if (rankedCandidates.length === 0) {
829
828
  const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
@@ -851,7 +850,8 @@ export function registerTaskTools(server) {
851
850
  toolName: "taskNext",
852
851
  sections: [
853
852
  summarySection([
854
- `- rootPath: ${root}`,
853
+ `- rootPaths: ${roots.join(", ")}`,
854
+ `- rootCount: ${roots.length}`,
855
855
  `- maxDepth: ${depth}`,
856
856
  `- matchedProjects: ${projects.length}`,
857
857
  "- actionableTasks: 0",
@@ -901,7 +901,8 @@ export function registerTaskTools(server) {
901
901
  toolName: "taskNext",
902
902
  sections: [
903
903
  summarySection([
904
- `- rootPath: ${root}`,
904
+ `- rootPaths: ${roots.join(", ")}`,
905
+ `- rootCount: ${roots.length}`,
905
906
  `- maxDepth: ${depth}`,
906
907
  `- matchedProjects: ${projects.length}`,
907
908
  `- actionableTasks: ${rankedCandidates.length}`,
@@ -1205,111 +1206,4 @@ export function registerTaskTools(server) {
1205
1206
  });
1206
1207
  return asText(markdown);
1207
1208
  });
1208
- // ============================================================================
1209
- // Spec v1.1.0 - Confidence Scoring Tools
1210
- // ============================================================================
1211
- server.registerTool("taskCalculateConfidence", {
1212
- title: "Calculate Task Confidence",
1213
- description: "Calculate confidence score for auto-creating a new task (Spec v1.1.0)",
1214
- inputSchema: {
1215
- projectPath: z.string(),
1216
- candidateTaskSummary: z.string(),
1217
- contextCompleteness: z.number().min(0).max(1).optional(),
1218
- similarTaskHistory: z.number().min(0).max(1).optional(),
1219
- specificationClarity: z.number().min(0).max(1).optional(),
1220
- },
1221
- }, async ({ projectPath, candidateTaskSummary, contextCompleteness, similarTaskHistory, specificationClarity }) => {
1222
- const governanceDir = await resolveGovernanceDir(projectPath);
1223
- const { tasks } = await loadTasks(governanceDir);
1224
- // Calculate factors if not provided
1225
- const calculatedContextCompleteness = contextCompleteness ?? await calculateContextCompleteness(governanceDir);
1226
- const calculatedSimilarTaskHistory = similarTaskHistory ?? calculateSimilarTaskHistory(tasks, candidateTaskSummary);
1227
- const calculatedSpecificationClarity = specificationClarity ?? calculateSpecificationClarity({
1228
- hasRoadmap: true, // Assume roadmap exists for simplicity
1229
- hasDesignDocs: true,
1230
- hasClearAcceptanceCriteria: candidateTaskSummary.length > 50,
1231
- });
1232
- const factors = {
1233
- contextCompleteness: calculatedContextCompleteness,
1234
- similarTaskHistory: calculatedSimilarTaskHistory,
1235
- specificationClarity: calculatedSpecificationClarity,
1236
- };
1237
- const confidenceScore = calculateConfidenceScore(factors);
1238
- const validationResult = await runPreCreationValidation(governanceDir, confidenceScore);
1239
- const hookContent = await getOrCreateTaskAutoCreateValidationHook(governanceDir);
1240
- const markdown = renderToolResponseMarkdown({
1241
- toolName: "taskCalculateConfidence",
1242
- sections: [
1243
- summarySection([
1244
- `- governanceDir: ${governanceDir}`,
1245
- `- confidenceScore: ${(confidenceScore.score * 100).toFixed(0)}%`,
1246
- `- recommendation: ${confidenceScore.recommendation}`,
1247
- `- validationPassed: ${validationResult.passed}`,
1248
- ]),
1249
- evidenceSection([
1250
- "### Confidence Report",
1251
- ...generateConfidenceReport(confidenceScore).split("\n"),
1252
- "",
1253
- "### Validation Issues",
1254
- ...(validationResult.issues.length > 0
1255
- ? validationResult.issues.map(issue => `- ${issue}`)
1256
- : ["- (none)"]),
1257
- "",
1258
- "### Validation Hook",
1259
- "- hook created/verified at: hooks/task_auto_create_validation.md",
1260
- ]),
1261
- guidanceSection([
1262
- confidenceScore.recommendation === "auto_create"
1263
- ? "✅ Confidence is high - you can auto-create this task."
1264
- : confidenceScore.recommendation === "review_required"
1265
- ? "⚠️ Confidence is medium - review recommended before creating."
1266
- : "❌ Confidence is low - do not auto-create this task.",
1267
- "",
1268
- "### Next Steps",
1269
- "- If recommendation is auto_create: use taskUpdate or manually add the task",
1270
- "- If review_required: review the factors and improve context before creating",
1271
- "- If do_not_create: gather more requirements or context before attempting",
1272
- ]),
1273
- lintSection([]),
1274
- nextCallSection(confidenceScore.recommendation !== "do_not_create"
1275
- ? `projectContext(projectPath=\"${toProjectPath(governanceDir)}\")`
1276
- : undefined),
1277
- ],
1278
- });
1279
- return asText(markdown);
1280
- });
1281
- server.registerTool("taskCreateValidationHook", {
1282
- title: "Create Validation Hook",
1283
- description: "Create or update the task auto-create validation hook (Spec v1.1.0)",
1284
- inputSchema: {
1285
- projectPath: z.string(),
1286
- },
1287
- }, async ({ projectPath }) => {
1288
- const governanceDir = await resolveGovernanceDir(projectPath);
1289
- const hookContent = await getOrCreateTaskAutoCreateValidationHook(governanceDir);
1290
- const markdown = renderToolResponseMarkdown({
1291
- toolName: "taskCreateValidationHook",
1292
- sections: [
1293
- summarySection([
1294
- `- governanceDir: ${governanceDir}`,
1295
- `- hookPath: ${governanceDir}/hooks/task_auto_create_validation.md`,
1296
- `- status: created/verified`,
1297
- ]),
1298
- evidenceSection([
1299
- "### Hook Content",
1300
- "```markdown",
1301
- ...hookContent.split("\n"),
1302
- "```",
1303
- ]),
1304
- guidanceSection([
1305
- "Validation hook created successfully.",
1306
- "Edit the hook file to customize pre-creation and post-creation actions.",
1307
- "The hook will be used by taskCalculateConfidence for validation.",
1308
- ]),
1309
- lintSection([]),
1310
- nextCallSection(`taskCalculateConfidence(projectPath=\"${toProjectPath(governanceDir)}\", candidateTaskSummary=\"Your task summary here\")`),
1311
- ],
1312
- });
1313
- return asText(markdown);
1314
- });
1315
1209
  }
@@ -26,17 +26,6 @@ export const BLOCKER_TYPES = [
26
26
  "resource",
27
27
  "approval"
28
28
  ];
29
- // Weight factors for confidence calculation
30
- export const CONFIDENCE_WEIGHTS = {
31
- contextCompleteness: 0.4,
32
- similarTaskHistory: 0.3,
33
- specificationClarity: 0.3,
34
- };
35
- // Confidence thresholds
36
- export const CONFIDENCE_THRESHOLDS = {
37
- autoCreate: 0.85,
38
- reviewRequired: 0.6,
39
- };
40
29
  // ============================================================================
41
30
  // Parser Constants
42
31
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -1,231 +0,0 @@
1
- /**
2
- * Projitive MCP - Confidence Scoring for Spec v1.1.0
3
- *
4
- * This module implements the confidence scoring algorithm for auto-discovery
5
- * and task creation, along with validation hooks integration.
6
- */
7
- import fs from "node:fs/promises";
8
- import path from "node:path";
9
- import { CONFIDENCE_WEIGHTS, CONFIDENCE_THRESHOLDS } from "../types.js";
10
- // ============================================================================
11
- // Confidence Scoring Algorithm
12
- // ============================================================================
13
- /**
14
- * Calculate confidence score based on the three factors
15
- * Formula: context_completeness * 0.4 + similar_task_history * 0.3 + specification_clarity * 0.3
16
- */
17
- export function calculateConfidenceScore(factors) {
18
- // Validate input ranges
19
- const validateFactor = (value, name) => {
20
- if (value < 0 || value > 1) {
21
- console.warn(`[Confidence] ${name} value ${value} is outside [0, 1] range, clamping`);
22
- return Math.max(0, Math.min(1, value));
23
- }
24
- return value;
25
- };
26
- const contextCompleteness = validateFactor(factors.contextCompleteness, "contextCompleteness");
27
- const similarTaskHistory = validateFactor(factors.similarTaskHistory, "similarTaskHistory");
28
- const specificationClarity = validateFactor(factors.specificationClarity, "specificationClarity");
29
- // Calculate weighted score
30
- const score = contextCompleteness * CONFIDENCE_WEIGHTS.contextCompleteness +
31
- similarTaskHistory * CONFIDENCE_WEIGHTS.similarTaskHistory +
32
- specificationClarity * CONFIDENCE_WEIGHTS.specificationClarity;
33
- // Determine recommendation
34
- let recommendation;
35
- if (score >= CONFIDENCE_THRESHOLDS.autoCreate) {
36
- recommendation = "auto_create";
37
- }
38
- else if (score >= CONFIDENCE_THRESHOLDS.reviewRequired) {
39
- recommendation = "review_required";
40
- }
41
- else {
42
- recommendation = "do_not_create";
43
- }
44
- return {
45
- score,
46
- factors: {
47
- contextCompleteness,
48
- similarTaskHistory,
49
- specificationClarity,
50
- },
51
- recommendation,
52
- };
53
- }
54
- // ============================================================================
55
- // Factor Calculation Helpers
56
- // ============================================================================
57
- /**
58
- * Calculate context completeness factor by checking available governance artifacts
59
- */
60
- export async function calculateContextCompleteness(governanceDir) {
61
- const requiredFiles = [
62
- "tasks.md",
63
- "roadmap.md",
64
- "README.md"
65
- ];
66
- const optionalFiles = [
67
- "hooks/task_no_actionable.md",
68
- "hooks/task_auto_create_validation.md"
69
- ];
70
- let availableCount = 0;
71
- const totalFiles = requiredFiles.length + optionalFiles.length;
72
- // Check required files
73
- for (const file of requiredFiles) {
74
- const filePath = path.join(governanceDir, file);
75
- try {
76
- await fs.access(filePath);
77
- availableCount++;
78
- }
79
- catch {
80
- // File doesn't exist
81
- }
82
- }
83
- // Check optional files (weighted 0.5 each)
84
- for (const file of optionalFiles) {
85
- const filePath = path.join(governanceDir, file);
86
- try {
87
- await fs.access(filePath);
88
- availableCount += 0.5;
89
- }
90
- catch {
91
- // File doesn't exist
92
- }
93
- }
94
- return Math.min(1, availableCount / totalFiles);
95
- }
96
- /**
97
- * Calculate similar task history success rate
98
- */
99
- export function calculateSimilarTaskHistory(tasks, candidateTaskSummary) {
100
- // Filter similar tasks based on keyword matching
101
- const keywords = candidateTaskSummary.toLowerCase().split(/\s+/).filter(w => w.length > 3);
102
- const similarTasks = tasks.filter(task => {
103
- const taskText = `${task.title} ${task.summary}`.toLowerCase();
104
- return keywords.some(keyword => taskText.includes(keyword));
105
- });
106
- if (similarTasks.length === 0) {
107
- return 0.5; // Neutral score when no history
108
- }
109
- // Calculate success rate
110
- const completedTasks = similarTasks.filter(task => task.status === "DONE");
111
- return completedTasks.length / similarTasks.length;
112
- }
113
- /**
114
- * Calculate specification clarity factor
115
- */
116
- export function calculateSpecificationClarity(projectContext) {
117
- let clarity = 0.3; // Base clarity
118
- if (projectContext.hasRoadmap)
119
- clarity += 0.2;
120
- if (projectContext.hasDesignDocs)
121
- clarity += 0.2;
122
- if (projectContext.hasClearAcceptanceCriteria)
123
- clarity += 0.3;
124
- return Math.min(1, clarity);
125
- }
126
- // ============================================================================
127
- // Validation Hooks Integration
128
- // ============================================================================
129
- const TASK_AUTO_CREATE_VALIDATION_HOOK = "task_auto_create_validation.md";
130
- const DEFAULT_TASK_AUTO_CREATE_VALIDATION_HOOK = `# Task Auto-Create Validation Hook
131
-
132
- ## Pre-Creation Checklist
133
- - [ ] Context files exist and are readable
134
- - [ ] Similar tasks have been completed successfully
135
- - [ ] Acceptance criteria are clear and testable
136
- - [ ] Dependencies are identified and available
137
-
138
- ## Post-Creation Actions
139
- - [ ] Add evidence link to analysis document
140
- - [ ] Notify relevant stakeholders (if configured)
141
- - [ ] Schedule validation review (24h for high-confidence)
142
- `;
143
- /**
144
- * Check if task auto-create validation hook exists
145
- */
146
- export async function hasTaskAutoCreateValidationHook(governanceDir) {
147
- const hookPath = path.join(governanceDir, "hooks", TASK_AUTO_CREATE_VALIDATION_HOOK);
148
- try {
149
- await fs.access(hookPath);
150
- return true;
151
- }
152
- catch {
153
- return false;
154
- }
155
- }
156
- /**
157
- * Get or create task auto-create validation hook
158
- */
159
- export async function getOrCreateTaskAutoCreateValidationHook(governanceDir) {
160
- const hooksDir = path.join(governanceDir, "hooks");
161
- const hookPath = path.join(hooksDir, TASK_AUTO_CREATE_VALIDATION_HOOK);
162
- try {
163
- // Try to read existing hook
164
- const content = await fs.readFile(hookPath, "utf-8");
165
- return content;
166
- }
167
- catch {
168
- // Create hooks directory if it doesn't exist
169
- try {
170
- await fs.mkdir(hooksDir, { recursive: true });
171
- }
172
- catch {
173
- // Directory already exists or creation failed silently
174
- }
175
- // Create default hook
176
- await fs.writeFile(hookPath, DEFAULT_TASK_AUTO_CREATE_VALIDATION_HOOK, "utf-8");
177
- return DEFAULT_TASK_AUTO_CREATE_VALIDATION_HOOK;
178
- }
179
- }
180
- /**
181
- * Run pre-creation validation checklist
182
- */
183
- export async function runPreCreationValidation(governanceDir, confidenceScore) {
184
- const issues = [];
185
- // Check confidence score first
186
- if (confidenceScore.recommendation === "do_not_create") {
187
- issues.push(`Confidence score ${confidenceScore.score.toFixed(2)} is below threshold (${CONFIDENCE_THRESHOLDS.reviewRequired})`);
188
- }
189
- // Check context completeness
190
- if (confidenceScore.factors.contextCompleteness < 0.6) {
191
- issues.push(`Context completeness (${confidenceScore.factors.contextCompleteness.toFixed(2)}) is low - more governance artifacts recommended`);
192
- }
193
- // Check for validation hook
194
- const hasHook = await hasTaskAutoCreateValidationHook(governanceDir);
195
- if (!hasHook) {
196
- issues.push("Task auto-create validation hook not found - will create default hook");
197
- }
198
- return {
199
- passed: issues.length === 0 || confidenceScore.recommendation === "auto_create",
200
- issues,
201
- };
202
- }
203
- // ============================================================================
204
- // Confidence Report Generation
205
- // ============================================================================
206
- /**
207
- * Generate a human-readable confidence report
208
- */
209
- export function generateConfidenceReport(confidenceScore) {
210
- const lines = [
211
- "# Confidence Score Report",
212
- "",
213
- `## Final Score: ${(confidenceScore.score * 100).toFixed(0)}%`,
214
- `**Recommendation**: ${confidenceScore.recommendation.replace(/_/g, " ")}`,
215
- "",
216
- "## Factor Breakdown",
217
- "",
218
- `| Factor | Score | Weight | Contribution |`,
219
- `|--------|-------|--------|--------------|`,
220
- `| Context Completeness | ${(confidenceScore.factors.contextCompleteness * 100).toFixed(0)}% | ${(CONFIDENCE_WEIGHTS.contextCompleteness * 100).toFixed(0)}% | ${(confidenceScore.factors.contextCompleteness * CONFIDENCE_WEIGHTS.contextCompleteness * 100).toFixed(0)}% |`,
221
- `| Similar Task History | ${(confidenceScore.factors.similarTaskHistory * 100).toFixed(0)}% | ${(CONFIDENCE_WEIGHTS.similarTaskHistory * 100).toFixed(0)}% | ${(confidenceScore.factors.similarTaskHistory * CONFIDENCE_WEIGHTS.similarTaskHistory * 100).toFixed(0)}% |`,
222
- `| Specification Clarity | ${(confidenceScore.factors.specificationClarity * 100).toFixed(0)}% | ${(CONFIDENCE_WEIGHTS.specificationClarity * 100).toFixed(0)}% | ${(confidenceScore.factors.specificationClarity * CONFIDENCE_WEIGHTS.specificationClarity * 100).toFixed(0)}% |`,
223
- "",
224
- "## Thresholds",
225
- "",
226
- `- Auto-create: >= ${(CONFIDENCE_THRESHOLDS.autoCreate * 100).toFixed(0)}%`,
227
- `- Review required: ${(CONFIDENCE_THRESHOLDS.reviewRequired * 100).toFixed(0)}% - ${(CONFIDENCE_THRESHOLDS.autoCreate * 100).toFixed(0)}%`,
228
- `- Do not create: < ${(CONFIDENCE_THRESHOLDS.reviewRequired * 100).toFixed(0)}%`,
229
- ];
230
- return lines.join("\n");
231
- }
@@ -1,205 +0,0 @@
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
- import { calculateConfidenceScore, calculateContextCompleteness, calculateSimilarTaskHistory, calculateSpecificationClarity, hasTaskAutoCreateValidationHook, getOrCreateTaskAutoCreateValidationHook, runPreCreationValidation, generateConfidenceReport, } from "./confidence.js";
6
- const tempPaths = [];
7
- async function createTempDir() {
8
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-confidence-test-"));
9
- tempPaths.push(dir);
10
- return dir;
11
- }
12
- afterEach(async () => {
13
- await Promise.all(tempPaths.splice(0).map(async (dir) => {
14
- await fs.rm(dir, { recursive: true, force: true });
15
- }));
16
- });
17
- describe("confidence module", () => {
18
- describe("calculateConfidenceScore", () => {
19
- it("calculates confidence score with valid inputs", () => {
20
- const result = calculateConfidenceScore({
21
- contextCompleteness: 0.8,
22
- similarTaskHistory: 0.7,
23
- specificationClarity: 0.9,
24
- });
25
- expect(result.score).toBeGreaterThan(0);
26
- expect(result.score).toBeLessThanOrEqual(1);
27
- expect(result.recommendation).toBeDefined();
28
- });
29
- it("recommends auto_create for high confidence scores", () => {
30
- const result = calculateConfidenceScore({
31
- contextCompleteness: 1.0,
32
- similarTaskHistory: 1.0,
33
- specificationClarity: 1.0,
34
- });
35
- expect(result.recommendation).toBe("auto_create");
36
- });
37
- it("recommends review_required for medium confidence scores", () => {
38
- const result = calculateConfidenceScore({
39
- contextCompleteness: 0.7,
40
- similarTaskHistory: 0.7,
41
- specificationClarity: 0.7,
42
- });
43
- expect(result.recommendation).toBe("review_required");
44
- });
45
- it("recommends do_not_create for low confidence scores", () => {
46
- const result = calculateConfidenceScore({
47
- contextCompleteness: 0.0,
48
- similarTaskHistory: 0.0,
49
- specificationClarity: 0.0,
50
- });
51
- expect(result.recommendation).toBe("do_not_create");
52
- });
53
- it("clamps values outside [0, 1] range", () => {
54
- const result = calculateConfidenceScore({
55
- contextCompleteness: 1.5,
56
- similarTaskHistory: -0.5,
57
- specificationClarity: 2.0,
58
- });
59
- expect(result.factors.contextCompleteness).toBe(1);
60
- expect(result.factors.similarTaskHistory).toBe(0);
61
- expect(result.factors.specificationClarity).toBe(1);
62
- });
63
- });
64
- describe("calculateContextCompleteness", () => {
65
- it("returns 0 for empty directory", async () => {
66
- const root = await createTempDir();
67
- const completeness = await calculateContextCompleteness(root);
68
- expect(completeness).toBe(0);
69
- });
70
- it("returns higher score for more complete context", async () => {
71
- const root = await createTempDir();
72
- await fs.writeFile(path.join(root, "tasks.md"), "", "utf-8");
73
- await fs.writeFile(path.join(root, "roadmap.md"), "", "utf-8");
74
- await fs.writeFile(path.join(root, "README.md"), "", "utf-8");
75
- const completeness = await calculateContextCompleteness(root);
76
- expect(completeness).toBeGreaterThan(0.5);
77
- });
78
- });
79
- describe("calculateSimilarTaskHistory", () => {
80
- const sampleTasks = [
81
- {
82
- id: "TASK-0001",
83
- title: "Implement feature X",
84
- status: "DONE",
85
- summary: "Build core functionality for feature X",
86
- owner: "ai-copilot",
87
- updatedAt: "2026-02-22T00:00:00.000Z",
88
- links: [],
89
- roadmapRefs: [],
90
- },
91
- {
92
- id: "TASK-0002",
93
- title: "Test feature Y",
94
- status: "TODO",
95
- summary: "Write tests for feature Y",
96
- owner: "ai-copilot",
97
- updatedAt: "2026-02-22T00:00:00.000Z",
98
- links: [],
99
- roadmapRefs: [],
100
- },
101
- {
102
- id: "TASK-0003",
103
- title: "Implement feature Z",
104
- status: "DONE",
105
- summary: "Build core functionality for feature Z",
106
- owner: "ai-copilot",
107
- updatedAt: "2026-02-22T00:00:00.000Z",
108
- links: [],
109
- roadmapRefs: [],
110
- },
111
- ];
112
- it("returns 0.5 when no similar tasks", () => {
113
- const result = calculateSimilarTaskHistory([], "Build something new");
114
- expect(result).toBe(0.5);
115
- });
116
- it("calculates success rate for similar tasks", () => {
117
- const result = calculateSimilarTaskHistory(sampleTasks, "Implement feature A with core functionality");
118
- expect(result).toBeGreaterThan(0);
119
- expect(result).toBeLessThanOrEqual(1);
120
- });
121
- it("handles tasks with no matching keywords", () => {
122
- const result = calculateSimilarTaskHistory(sampleTasks, "Something completely different unrelated");
123
- expect(result).toBe(0.5);
124
- });
125
- });
126
- describe("calculateSpecificationClarity", () => {
127
- it("returns base clarity with no context", () => {
128
- const result = calculateSpecificationClarity({});
129
- expect(result).toBe(0.3);
130
- });
131
- it("increases clarity with roadmap", () => {
132
- const result = calculateSpecificationClarity({ hasRoadmap: true });
133
- expect(result).toBeGreaterThan(0.3);
134
- });
135
- it("increases clarity with design docs", () => {
136
- const result = calculateSpecificationClarity({ hasDesignDocs: true });
137
- expect(result).toBeGreaterThan(0.3);
138
- });
139
- it("reaches maximum clarity with all factors", () => {
140
- const result = calculateSpecificationClarity({
141
- hasRoadmap: true,
142
- hasDesignDocs: true,
143
- hasClearAcceptanceCriteria: true,
144
- });
145
- expect(result).toBe(1);
146
- });
147
- });
148
- describe("validation hooks", () => {
149
- it("detects missing validation hook", async () => {
150
- const root = await createTempDir();
151
- const hasHook = await hasTaskAutoCreateValidationHook(root);
152
- expect(hasHook).toBe(false);
153
- });
154
- it("creates default validation hook", async () => {
155
- const root = await createTempDir();
156
- const hookContent = await getOrCreateTaskAutoCreateValidationHook(root);
157
- expect(hookContent).toContain("Task Auto-Create Validation Hook");
158
- });
159
- it("reads existing validation hook", async () => {
160
- const root = await createTempDir();
161
- const hooksDir = path.join(root, "hooks");
162
- await fs.mkdir(hooksDir, { recursive: true });
163
- const hookPath = path.join(hooksDir, "task_auto_create_validation.md");
164
- await fs.writeFile(hookPath, "custom hook content", "utf-8");
165
- const hookContent = await getOrCreateTaskAutoCreateValidationHook(root);
166
- expect(hookContent).toBe("custom hook content");
167
- });
168
- });
169
- describe("runPreCreationValidation", () => {
170
- it("validates confidence score", async () => {
171
- const root = await createTempDir();
172
- const confidenceScore = calculateConfidenceScore({
173
- contextCompleteness: 0.9,
174
- similarTaskHistory: 0.9,
175
- specificationClarity: 0.9,
176
- });
177
- const result = await runPreCreationValidation(root, confidenceScore);
178
- expect(result.passed).toBeDefined();
179
- expect(result.issues).toBeInstanceOf(Array);
180
- });
181
- });
182
- describe("generateConfidenceReport", () => {
183
- it("generates human-readable report", () => {
184
- const confidenceScore = calculateConfidenceScore({
185
- contextCompleteness: 0.8,
186
- similarTaskHistory: 0.7,
187
- specificationClarity: 0.9,
188
- });
189
- const report = generateConfidenceReport(confidenceScore);
190
- expect(report).toContain("Confidence Score Report");
191
- expect(report).toContain("Final Score");
192
- expect(report).toContain("Factor Breakdown");
193
- expect(report).toContain("Thresholds");
194
- });
195
- it("includes recommendation in report", () => {
196
- const confidenceScore = calculateConfidenceScore({
197
- contextCompleteness: 1.0,
198
- similarTaskHistory: 1.0,
199
- specificationClarity: 1.0,
200
- });
201
- const report = generateConfidenceReport(confidenceScore);
202
- expect(report).toContain("auto create");
203
- });
204
- });
205
- });
@@ -1,66 +0,0 @@
1
- import { z } from "zod";
2
- function asUserPrompt(text) {
3
- return {
4
- messages: [
5
- {
6
- role: "user",
7
- content: {
8
- type: "text",
9
- text,
10
- },
11
- },
12
- ],
13
- };
14
- }
15
- export function registerDesignContextResources(server) {
16
- // Register design context resources
17
- server.registerResource("designContext", "projitive://design-context/designs", {
18
- title: "Design Context",
19
- description: "Design context and resources for the project",
20
- mimeType: "text/markdown",
21
- }, async () => ({
22
- contents: [
23
- {
24
- uri: "projitive://design-context/designs",
25
- text: `# Design Context\n\nThis is the design context for the project.`,
26
- },
27
- ],
28
- }));
29
- }
30
- export function registerDesignContextPrompts(server) {
31
- // Register design context prompts
32
- server.registerPrompt("designContext", {
33
- title: "Design Context",
34
- description: "Get design context for the project",
35
- argsSchema: {
36
- projectPath: z.string().optional(),
37
- },
38
- }, async ({ projectPath }) => {
39
- const text = [
40
- "You are exploring the design context for the project.",
41
- "",
42
- projectPath
43
- ? [
44
- `1) Project path is known: "${projectPath}"`,
45
- "2) Review the design context resources.",
46
- "3) Identify design patterns and guidelines.",
47
- ].join("\n")
48
- : [
49
- "1) Project path is unknown: Run projectScan() to discover all governance roots.",
50
- "2) Select a project to work on.",
51
- "3) Run projectContext() to load project summary.",
52
- "4) Review the design context resources.",
53
- ].join("\n"),
54
- "",
55
- "Key resources to read:",
56
- "- projitive://design-context/designs - Design context and resources",
57
- "- projitive://governance/workspace - Project overview",
58
- "- projitive://governance/roadmap - Project roadmap",
59
- "",
60
- "Hard rules:",
61
- "- Follow the design patterns and guidelines.",
62
- "- Keep design documents in the .projitive/designs/ directory.",
63
- ].join("\n");
64
- return asUserPrompt(text);
65
- });
66
- }