@projitive/mcp 1.0.0 → 1.0.2

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 (33) hide show
  1. package/README.md +29 -1
  2. package/output/helpers/artifacts/artifacts.js +10 -0
  3. package/output/helpers/artifacts/artifacts.test.js +18 -0
  4. package/output/helpers/artifacts/index.js +1 -0
  5. package/output/helpers/index.js +3 -0
  6. package/output/helpers/linter/codes.js +25 -0
  7. package/output/helpers/linter/index.js +2 -0
  8. package/output/helpers/linter/linter.js +6 -0
  9. package/output/helpers/linter/linter.test.js +16 -0
  10. package/output/helpers/response/index.js +1 -0
  11. package/output/helpers/response/response.js +73 -0
  12. package/output/helpers/response/response.test.js +50 -0
  13. package/output/index.js +1 -0
  14. package/output/projitive.js +252 -97
  15. package/output/projitive.test.js +29 -1
  16. package/output/rendering-input-guard.test.js +20 -0
  17. package/output/roadmap.js +106 -80
  18. package/output/roadmap.test.js +11 -0
  19. package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectContext.md +48 -0
  20. package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectInit.md +40 -0
  21. package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectLocate.md +22 -0
  22. package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectNext.md +31 -0
  23. package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectScan.md +28 -0
  24. package/output/smoke-reports/2026-02-18T13-18-19-740Z/roadmapContext.md +33 -0
  25. package/output/smoke-reports/2026-02-18T13-18-19-740Z/roadmapList.md +25 -0
  26. package/output/smoke-reports/2026-02-18T13-18-19-740Z/summary.json +90 -0
  27. package/output/smoke-reports/2026-02-18T13-18-19-740Z/summary.md +17 -0
  28. package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskContext.md +47 -0
  29. package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskList.md +27 -0
  30. package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskNext.md +64 -0
  31. package/output/tasks.js +341 -162
  32. package/output/tasks.test.js +51 -1
  33. package/package.json +1 -1
@@ -4,19 +4,33 @@ import process from "node:process";
4
4
  import { z } from "zod";
5
5
  import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
6
6
  import { catchIt } from "./helpers/catch/index.js";
7
- import { loadTasks } from "./tasks.js";
7
+ import { PROJECT_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
8
+ import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
9
+ import { collectTaskLintSuggestions, loadTasksDocument } from "./tasks.js";
8
10
  export const PROJECT_MARKER = ".projitive";
11
+ const DEFAULT_GOVERNANCE_DIR = ".projitive";
9
12
  const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
10
13
  const DEFAULT_SCAN_DEPTH = 3;
11
14
  const MAX_SCAN_DEPTH = 8;
12
- function asText(markdown) {
13
- return {
14
- content: [{ type: "text", text: markdown }],
15
- };
16
- }
17
15
  function normalizePath(inputPath) {
18
16
  return inputPath ? path.resolve(inputPath) : process.cwd();
19
17
  }
18
+ function normalizeGovernanceDirName(input) {
19
+ const name = input?.trim() || DEFAULT_GOVERNANCE_DIR;
20
+ if (!name) {
21
+ throw new Error("governanceDir cannot be empty");
22
+ }
23
+ if (path.isAbsolute(name)) {
24
+ throw new Error("governanceDir must be a relative directory name");
25
+ }
26
+ if (name.includes("/") || name.includes("\\")) {
27
+ throw new Error("governanceDir must not contain path separators");
28
+ }
29
+ if (name === "." || name === "..") {
30
+ throw new Error("governanceDir must be a normal directory name");
31
+ }
32
+ return name;
33
+ }
20
34
  function parseDepthFromEnv(rawDepth) {
21
35
  if (typeof rawDepth !== "string" || rawDepth.trim().length === 0) {
22
36
  return undefined;
@@ -54,11 +68,22 @@ async function readTasksSnapshot(governanceDir) {
54
68
  const tasksPath = path.join(governanceDir, "tasks.md");
55
69
  const markdown = await fs.readFile(tasksPath, "utf-8").catch(() => undefined);
56
70
  if (typeof markdown !== "string") {
57
- return { tasksPath, exists: false, tasks: [] };
71
+ return {
72
+ tasksPath,
73
+ exists: false,
74
+ tasks: [],
75
+ lintSuggestions: renderLintSuggestions([
76
+ {
77
+ code: PROJECT_LINT_CODES.TASKS_FILE_MISSING,
78
+ message: "tasks.md is missing.",
79
+ fixHint: "Initialize governance tasks structure first.",
80
+ },
81
+ ]),
82
+ };
58
83
  }
59
84
  const { parseTasksBlock } = await import("./tasks.js");
60
85
  const tasks = await parseTasksBlock(markdown);
61
- return { tasksPath, exists: true, tasks };
86
+ return { tasksPath, exists: true, tasks, lintSuggestions: collectTaskLintSuggestions(tasks, markdown) };
62
87
  }
63
88
  function latestTaskUpdatedAt(tasks) {
64
89
  const timestamps = tasks
@@ -134,7 +159,134 @@ export async function discoverProjects(rootPath, maxDepth) {
134
159
  await walk(rootPath, 0);
135
160
  return Array.from(new Set(results)).sort();
136
161
  }
162
+ async function pathExists(targetPath) {
163
+ const accessResult = await catchIt(fs.access(targetPath));
164
+ return !accessResult.isError();
165
+ }
166
+ async function writeTextFile(targetPath, content, force) {
167
+ const exists = await pathExists(targetPath);
168
+ if (exists && !force) {
169
+ return { path: targetPath, action: "skipped" };
170
+ }
171
+ await fs.writeFile(targetPath, content, "utf-8");
172
+ return { path: targetPath, action: exists ? "updated" : "created" };
173
+ }
174
+ function defaultReadmeMarkdown(governanceDirName) {
175
+ return [
176
+ "# Projitive Governance Workspace",
177
+ "",
178
+ `This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
179
+ "",
180
+ "## Conventions",
181
+ "- Keep roadmap/task/design/report files in markdown.",
182
+ "- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
183
+ "- Update report evidence before status transitions.",
184
+ ].join("\n");
185
+ }
186
+ function defaultRoadmapMarkdown() {
187
+ return [
188
+ "# Roadmap",
189
+ "",
190
+ "## Active Milestones",
191
+ "- [ ] ROADMAP-0001: Bootstrap governance baseline (time: 2026-Q1)",
192
+ ].join("\n");
193
+ }
194
+ function defaultTasksMarkdown() {
195
+ const updatedAt = new Date().toISOString();
196
+ return [
197
+ "# Tasks",
198
+ "",
199
+ "<!-- PROJITIVE:TASKS:START -->",
200
+ "## TASK-0001 | TODO | Bootstrap governance workspace",
201
+ "- owner: unassigned",
202
+ "- summary: Create initial governance artifacts and confirm task execution loop.",
203
+ `- updatedAt: ${updatedAt}`,
204
+ "- links:",
205
+ "- roadmapRefs: ROADMAP-0001",
206
+ "- hooks:",
207
+ "<!-- PROJITIVE:TASKS:END -->",
208
+ ].join("\n");
209
+ }
210
+ export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
211
+ const rootPath = normalizePath(inputPath);
212
+ const governanceDirName = normalizeGovernanceDirName(governanceDir);
213
+ const rootStat = await catchIt(fs.stat(rootPath));
214
+ if (rootStat.isError()) {
215
+ throw new Error(`Path not found: ${rootPath}`);
216
+ }
217
+ if (!rootStat.value.isDirectory()) {
218
+ throw new Error(`rootPath must be a directory: ${rootPath}`);
219
+ }
220
+ const governancePath = path.join(rootPath, governanceDirName);
221
+ const directories = [];
222
+ const requiredDirectories = [governancePath, path.join(governancePath, "designs"), path.join(governancePath, "reports"), path.join(governancePath, "hooks")];
223
+ for (const dirPath of requiredDirectories) {
224
+ const exists = await pathExists(dirPath);
225
+ await fs.mkdir(dirPath, { recursive: true });
226
+ directories.push({ path: dirPath, action: exists ? "skipped" : "created" });
227
+ }
228
+ const markerPath = path.join(governancePath, PROJECT_MARKER);
229
+ const files = await Promise.all([
230
+ writeTextFile(markerPath, "", force),
231
+ writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
232
+ writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
233
+ writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
234
+ ]);
235
+ return {
236
+ rootPath,
237
+ governanceDir: governancePath,
238
+ markerPath,
239
+ directories,
240
+ files,
241
+ };
242
+ }
137
243
  export function registerProjectTools(server) {
244
+ server.registerTool("projectInit", {
245
+ title: "Project Init",
246
+ description: "Initialize Projitive governance directory structure manually (default .projitive)",
247
+ inputSchema: {
248
+ rootPath: z.string().optional(),
249
+ governanceDir: z.string().optional(),
250
+ force: z.boolean().optional(),
251
+ },
252
+ }, async ({ rootPath, governanceDir, force }) => {
253
+ const initialized = await initializeProjectStructure(rootPath, governanceDir, force ?? false);
254
+ const filesByAction = {
255
+ created: initialized.files.filter((item) => item.action === "created"),
256
+ updated: initialized.files.filter((item) => item.action === "updated"),
257
+ skipped: initialized.files.filter((item) => item.action === "skipped"),
258
+ };
259
+ const markdown = renderToolResponseMarkdown({
260
+ toolName: "projectInit",
261
+ sections: [
262
+ summarySection([
263
+ `- rootPath: ${initialized.rootPath}`,
264
+ `- governanceDir: ${initialized.governanceDir}`,
265
+ `- markerPath: ${initialized.markerPath}`,
266
+ `- force: ${force === true ? "true" : "false"}`,
267
+ ]),
268
+ evidenceSection([
269
+ `- createdFiles: ${filesByAction.created.length}`,
270
+ `- updatedFiles: ${filesByAction.updated.length}`,
271
+ `- skippedFiles: ${filesByAction.skipped.length}`,
272
+ "- directories:",
273
+ ...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
274
+ "- files:",
275
+ ...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
276
+ ]),
277
+ guidanceSection([
278
+ "- If files were skipped and you want to overwrite templates, rerun with force=true.",
279
+ "- Continue with projectContext and taskList for execution.",
280
+ ]),
281
+ lintSection([
282
+ "- After init, fill owner/roadmapRefs/links in tasks.md before marking DONE.",
283
+ "- Keep task source-of-truth inside marker block only.",
284
+ ]),
285
+ nextCallSection(`projectContext(projectPath=\"${initialized.governanceDir}\")`),
286
+ ],
287
+ });
288
+ return asText(markdown);
289
+ });
138
290
  server.registerTool("projectScan", {
139
291
  title: "Project Scan",
140
292
  description: "Scan filesystem and discover project governance roots marked by .projitive",
@@ -146,27 +298,30 @@ export function registerProjectTools(server) {
146
298
  const root = resolveScanRoot(rootPath);
147
299
  const depth = resolveScanDepth(maxDepth);
148
300
  const projects = await discoverProjects(root, depth);
149
- const markdown = [
150
- "# projectScan",
151
- "",
152
- "## Summary",
153
- `- rootPath: ${root}`,
154
- `- maxDepth: ${depth}`,
155
- `- discoveredCount: ${projects.length}`,
156
- "",
157
- "## Evidence",
158
- "- projects:",
159
- ...(projects.length > 0 ? projects.map((project, index) => `${index + 1}. ${project}`) : ["- (none)"]),
160
- "",
161
- "## Agent Guidance",
162
- "- Use one discovered project path and call `projectLocate` to lock governance root.",
163
- "- Then call `projectContext` to inspect current governance state.",
164
- "",
165
- "## Next Call",
166
- ...(projects.length > 0
167
- ? [`- projectLocate(inputPath=\"${projects[0]}\")`]
168
- : ["- (none)"]),
169
- ].join("\n");
301
+ const markdown = renderToolResponseMarkdown({
302
+ toolName: "projectScan",
303
+ sections: [
304
+ summarySection([
305
+ `- rootPath: ${root}`,
306
+ `- maxDepth: ${depth}`,
307
+ `- discoveredCount: ${projects.length}`,
308
+ ]),
309
+ evidenceSection([
310
+ "- projects:",
311
+ ...projects.map((project, index) => `${index + 1}. ${project}`),
312
+ ]),
313
+ guidanceSection([
314
+ "- Use one discovered project path and call `projectLocate` to lock governance root.",
315
+ "- Then call `projectContext` to inspect current governance state.",
316
+ ]),
317
+ lintSection(projects.length === 0
318
+ ? ["- No governance root discovered. Add `.projitive` marker and baseline artifacts before execution."]
319
+ : ["- Run `projectContext` on a discovered project to receive module-level lint suggestions."]),
320
+ nextCallSection(projects[0]
321
+ ? `projectLocate(inputPath=\"${projects[0]}\")`
322
+ : undefined),
323
+ ],
324
+ });
170
325
  return asText(markdown);
171
326
  });
172
327
  server.registerTool("projectNext", {
@@ -192,7 +347,7 @@ export function registerProjectTools(server) {
192
347
  governanceDir,
193
348
  tasksPath: snapshot.tasksPath,
194
349
  tasksExists: snapshot.exists,
195
- total: snapshot.tasks.length,
350
+ lintSuggestions: snapshot.lintSuggestions,
196
351
  inProgress,
197
352
  todo,
198
353
  blocked,
@@ -211,32 +366,31 @@ export function registerProjectTools(server) {
211
366
  return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
212
367
  })
213
368
  .slice(0, limit ?? 10);
214
- const markdown = [
215
- "# projectNext",
216
- "",
217
- "## Summary",
218
- `- rootPath: ${root}`,
219
- `- maxDepth: ${depth}`,
220
- `- matchedProjects: ${projects.length}`,
221
- `- actionableProjects: ${ranked.length}`,
222
- `- limit: ${limit ?? 10}`,
223
- "",
224
- "## Evidence",
225
- "- rankedProjects:",
226
- ...(ranked.length > 0
227
- ? ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt} | tasksPath=${item.tasksPath}${item.tasksExists ? "" : " (missing)"}`)
228
- : ["- (none)"]),
229
- "",
230
- "## Agent Guidance",
231
- "- Pick top 1 project and call `projectContext` with its governanceDir.",
232
- "- Then call `taskList` and `taskContext` to continue execution.",
233
- "- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
234
- "",
235
- "## Next Call",
236
- ...(ranked.length > 0
237
- ? [`- projectContext(projectPath=\"${ranked[0].governanceDir}\")`]
238
- : ["- (none)"]),
239
- ].join("\n");
369
+ const markdown = renderToolResponseMarkdown({
370
+ toolName: "projectNext",
371
+ sections: [
372
+ summarySection([
373
+ `- rootPath: ${root}`,
374
+ `- maxDepth: ${depth}`,
375
+ `- matchedProjects: ${projects.length}`,
376
+ `- actionableProjects: ${ranked.length}`,
377
+ `- limit: ${limit ?? 10}`,
378
+ ]),
379
+ evidenceSection([
380
+ "- rankedProjects:",
381
+ ...ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt} | tasksPath=${item.tasksPath}${item.tasksExists ? "" : " (missing)"}`),
382
+ ]),
383
+ guidanceSection([
384
+ "- Pick top 1 project and call `projectContext` with its governanceDir.",
385
+ "- Then call `taskList` and `taskContext` to continue execution.",
386
+ "- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
387
+ ]),
388
+ lintSection(ranked[0]?.lintSuggestions ?? []),
389
+ nextCallSection(ranked[0]
390
+ ? `projectContext(projectPath=\"${ranked[0].governanceDir}\")`
391
+ : undefined),
392
+ ],
393
+ });
240
394
  return asText(markdown);
241
395
  });
242
396
  server.registerTool("projectLocate", {
@@ -249,20 +403,19 @@ export function registerProjectTools(server) {
249
403
  const resolvedFrom = normalizePath(inputPath);
250
404
  const governanceDir = await resolveGovernanceDir(resolvedFrom);
251
405
  const markerPath = path.join(governanceDir, ".projitive");
252
- const markdown = [
253
- "# projectLocate",
254
- "",
255
- "## Summary",
256
- `- resolvedFrom: ${resolvedFrom}`,
257
- `- governanceDir: ${governanceDir}`,
258
- `- markerPath: ${markerPath}`,
259
- "",
260
- "## Agent Guidance",
261
- "- Call `projectContext` with this governanceDir to get task and roadmap summaries.",
262
- "",
263
- "## Next Call",
264
- `- projectContext(projectPath=\"${governanceDir}\")`,
265
- ].join("\n");
406
+ const markdown = renderToolResponseMarkdown({
407
+ toolName: "projectLocate",
408
+ sections: [
409
+ summarySection([
410
+ `- resolvedFrom: ${resolvedFrom}`,
411
+ `- governanceDir: ${governanceDir}`,
412
+ `- markerPath: ${markerPath}`,
413
+ ]),
414
+ guidanceSection(["- Call `projectContext` with this governanceDir to get task and roadmap summaries."]),
415
+ lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
416
+ nextCallSection(`projectContext(projectPath=\"${governanceDir}\")`),
417
+ ],
418
+ });
266
419
  return asText(markdown);
267
420
  });
268
421
  server.registerTool("projectContext", {
@@ -274,8 +427,9 @@ export function registerProjectTools(server) {
274
427
  }, async ({ projectPath }) => {
275
428
  const governanceDir = await resolveGovernanceDir(projectPath);
276
429
  const artifacts = await discoverGovernanceArtifacts(governanceDir);
277
- const { tasksPath, tasks } = await loadTasks(governanceDir);
430
+ const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
278
431
  const roadmapIds = await readRoadmapIds(governanceDir);
432
+ const lintSuggestions = collectTaskLintSuggestions(tasks, tasksMarkdown);
279
433
  const taskSummary = {
280
434
  total: tasks.length,
281
435
  TODO: tasks.filter((task) => task.status === "TODO").length,
@@ -283,32 +437,33 @@ export function registerProjectTools(server) {
283
437
  BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
284
438
  DONE: tasks.filter((task) => task.status === "DONE").length,
285
439
  };
286
- const markdown = [
287
- "# projectContext",
288
- "",
289
- "## Summary",
290
- `- governanceDir: ${governanceDir}`,
291
- `- tasksFile: ${tasksPath}`,
292
- `- roadmapIds: ${roadmapIds.length}`,
293
- "",
294
- "## Evidence",
295
- "### Task Summary",
296
- `- total: ${taskSummary.total}`,
297
- `- TODO: ${taskSummary.TODO}`,
298
- `- IN_PROGRESS: ${taskSummary.IN_PROGRESS}`,
299
- `- BLOCKED: ${taskSummary.BLOCKED}`,
300
- `- DONE: ${taskSummary.DONE}`,
301
- "",
302
- "### Artifacts",
303
- renderArtifactsMarkdown(artifacts),
304
- "",
305
- "## Agent Guidance",
306
- "- Start from `taskList` to choose a target task.",
307
- "- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.",
308
- "",
309
- "## Next Call",
310
- `- taskList(projectPath=\"${governanceDir}\")`,
311
- ].join("\n");
440
+ const markdown = renderToolResponseMarkdown({
441
+ toolName: "projectContext",
442
+ sections: [
443
+ summarySection([
444
+ `- governanceDir: ${governanceDir}`,
445
+ `- tasksFile: ${tasksPath}`,
446
+ `- roadmapIds: ${roadmapIds.length}`,
447
+ ]),
448
+ evidenceSection([
449
+ "### Task Summary",
450
+ `- total: ${taskSummary.total}`,
451
+ `- TODO: ${taskSummary.TODO}`,
452
+ `- IN_PROGRESS: ${taskSummary.IN_PROGRESS}`,
453
+ `- BLOCKED: ${taskSummary.BLOCKED}`,
454
+ `- DONE: ${taskSummary.DONE}`,
455
+ "",
456
+ "### Artifacts",
457
+ renderArtifactsMarkdown(artifacts),
458
+ ]),
459
+ guidanceSection([
460
+ "- Start from `taskList` to choose a target task.",
461
+ "- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.",
462
+ ]),
463
+ lintSection(lintSuggestions),
464
+ nextCallSection(`taskList(projectPath=\"${governanceDir}\")`),
465
+ ],
466
+ });
312
467
  return asText(markdown);
313
468
  });
314
469
  }
@@ -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 } from "vitest";
5
- import { discoverProjects, hasProjectMarker, resolveGovernanceDir } from "./projitive.js";
5
+ import { discoverProjects, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir } from "./projitive.js";
6
6
  const tempPaths = [];
7
7
  async function createTempDir() {
8
8
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
@@ -43,4 +43,32 @@ describe("projitive module", () => {
43
43
  expect(projects).toContain(p1);
44
44
  expect(projects).toContain(p2);
45
45
  });
46
+ it("initializes governance structure under default .projitive directory", async () => {
47
+ const root = await createTempDir();
48
+ const initialized = await initializeProjectStructure(root);
49
+ expect(initialized.governanceDir).toBe(path.join(root, ".projitive"));
50
+ const expectedPaths = [
51
+ path.join(root, ".projitive", ".projitive"),
52
+ path.join(root, ".projitive", "README.md"),
53
+ path.join(root, ".projitive", "roadmap.md"),
54
+ path.join(root, ".projitive", "tasks.md"),
55
+ path.join(root, ".projitive", "designs"),
56
+ path.join(root, ".projitive", "reports"),
57
+ path.join(root, ".projitive", "hooks"),
58
+ ];
59
+ await Promise.all(expectedPaths.map(async (targetPath) => {
60
+ await expect(fs.access(targetPath)).resolves.toBeUndefined();
61
+ }));
62
+ });
63
+ it("overwrites template files when force is enabled", async () => {
64
+ const root = await createTempDir();
65
+ const governanceDir = path.join(root, ".projitive");
66
+ const readmePath = path.join(governanceDir, "README.md");
67
+ await initializeProjectStructure(root);
68
+ await fs.writeFile(readmePath, "custom-content", "utf-8");
69
+ const initialized = await initializeProjectStructure(root, ".projitive", true);
70
+ const readmeContent = await fs.readFile(readmePath, "utf-8");
71
+ expect(readmeContent).toContain("Projitive Governance Workspace");
72
+ expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe("updated");
73
+ });
46
74
  });
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ const MODULE_FILES = [
5
+ "tasks.ts",
6
+ "roadmap.ts",
7
+ "projitive.ts",
8
+ ];
9
+ const INVALID_LITERAL_PATTERN = /["'`]\s*-\s+(#{1,6}\s|>\s*|```)/g;
10
+ describe("rendering input guard", () => {
11
+ it("does not contain accidental bullet-prefixed markdown literals in module outputs", async () => {
12
+ const sourceDir = path.resolve(import.meta.dirname);
13
+ for (const file of MODULE_FILES) {
14
+ const filePath = path.join(sourceDir, file);
15
+ const content = await fs.readFile(filePath, "utf-8");
16
+ const matches = content.match(INVALID_LITERAL_PATTERN) ?? [];
17
+ expect(matches, `invalid literals in ${filePath}`).toHaveLength(0);
18
+ }
19
+ });
20
+ });