@mcoda/core 0.1.19 → 0.1.21
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/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +3 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +22 -8
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +53 -34
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +3 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +9 -0
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +251 -35
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +487 -71
- package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrFolderTreeGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrFolderTreeGate.js +151 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrNoUnresolvedItemsGate.js +109 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrTechStackRationaleGate.js +128 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsFolderTreeGate.js +153 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +109 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsTechStackRationaleGate.js +128 -0
- package/dist/services/execution/QaTasksService.d.ts +6 -0
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +278 -95
- package/dist/services/execution/TaskSelectionService.d.ts +3 -0
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +33 -0
- package/dist/services/execution/WorkOnTasksService.d.ts +5 -1
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +178 -34
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +43 -4
- package/dist/services/planning/CreateTasksService.d.ts +12 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +585 -48
- package/dist/services/planning/RefineTasksService.d.ts +1 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +88 -2
- package/dist/services/review/CodeReviewService.d.ts +6 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +260 -41
- package/dist/services/shared/ProjectGuidance.d.ts +18 -2
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +535 -34
- package/package.json +6 -6
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { PathHelper } from "@mcoda/shared";
|
|
@@ -5,48 +6,226 @@ const QA_DOC_PATTERN = /(^|[\\/])(qa|e2e)([-_/]|$)/i;
|
|
|
5
6
|
const MCODA_DOC_PATTERN = /(^|[\\/])\.mcoda([\\/]|$)/i;
|
|
6
7
|
const SDS_DOC_PATTERN = /(^|[\\/])docs[\\/]+sds([\\/]|\.|$)/i;
|
|
7
8
|
const FRONTMATTER_BLOCK = /^---[\s\S]*?\n---/;
|
|
9
|
+
const SDS_NAME_PATTERN = /(^|\/)(sds(?:[-_. ][a-z0-9]+)?|software[-_ ]design(?:[-_ ](?:spec|specification|outline|doc))?|design[-_ ]spec(?:ification)?)(\/|[-_.]|$)/i;
|
|
10
|
+
const SDS_PATH_HINT_PATTERN = /(^|\/)(docs\/sds|sds\/|software[-_ ]design|design[-_ ]spec|requirements|prd|pdr|rfp|architecture|solution[-_ ]design)/i;
|
|
11
|
+
const REQUIRED_GUIDANCE_SECTIONS = [
|
|
12
|
+
"Product Context",
|
|
13
|
+
"Architecture Notes",
|
|
14
|
+
"Coding Constraints",
|
|
15
|
+
"Testing Policy",
|
|
16
|
+
"Operational Notes",
|
|
17
|
+
];
|
|
18
|
+
const PLACEHOLDER_PATTERNS = [
|
|
19
|
+
/\bdescribe the product domain\b/i,
|
|
20
|
+
/\blist important modules\b/i,
|
|
21
|
+
/\bdocument non-negotiable implementation rules\b/i,
|
|
22
|
+
/\bdefine required test levels\b/i,
|
|
23
|
+
/\badd deployment\/runtime constraints\b/i,
|
|
24
|
+
];
|
|
25
|
+
const FRONTMATTER_TRUE_VALUES = new Set(["true", "1", "yes", "on"]);
|
|
26
|
+
const TREE_LIKE_PATTERN = /[├└│]|^\s*[./A-Za-z0-9_-]+\/\s*$/m;
|
|
27
|
+
const MARKDOWN_FILE_EXTENSIONS = new Set([".md", ".mdx", ".markdown"]);
|
|
28
|
+
const DIR_SCAN_EXCLUDES = new Set([
|
|
29
|
+
".git",
|
|
30
|
+
".svn",
|
|
31
|
+
".hg",
|
|
32
|
+
".mcoda",
|
|
33
|
+
"node_modules",
|
|
34
|
+
"vendor",
|
|
35
|
+
"dist",
|
|
36
|
+
"build",
|
|
37
|
+
"out",
|
|
38
|
+
"target",
|
|
39
|
+
".next",
|
|
40
|
+
".nuxt",
|
|
41
|
+
".venv",
|
|
42
|
+
"venv",
|
|
43
|
+
"coverage",
|
|
44
|
+
]);
|
|
8
45
|
const DEFAULT_PROJECT_GUIDANCE_TEMPLATE = [
|
|
9
46
|
"# Project Guidance",
|
|
10
47
|
"",
|
|
11
|
-
"This file is loaded by mcoda agents before task execution.",
|
|
12
|
-
"
|
|
48
|
+
"This file is loaded by mcoda agents before task execution/review/QA.",
|
|
49
|
+
"SDS is the source of truth. Keep this guidance concrete and implementation-oriented.",
|
|
13
50
|
"",
|
|
14
51
|
"## Product Context",
|
|
15
|
-
"-
|
|
52
|
+
"- Align scope and implementation decisions with the latest SDS.",
|
|
53
|
+
"- Prefer product behavior changes over test-only deltas for implementation tasks.",
|
|
16
54
|
"",
|
|
17
55
|
"## Architecture Notes",
|
|
18
|
-
"-
|
|
56
|
+
"- Respect existing module/service boundaries and data contracts.",
|
|
57
|
+
"- Keep new interfaces and schemas backward-compatible unless SDS explicitly changes them.",
|
|
19
58
|
"",
|
|
20
59
|
"## Coding Constraints",
|
|
21
|
-
"-
|
|
60
|
+
"- Avoid hardcoded environment-specific values (ports, hosts, secrets, file paths).",
|
|
61
|
+
"- Reuse existing code patterns and adapters before adding new abstractions.",
|
|
22
62
|
"",
|
|
23
63
|
"## Testing Policy",
|
|
24
|
-
"-
|
|
64
|
+
"- Run targeted tests for touched behavior, then broader suites when required.",
|
|
65
|
+
"- Ensure implementation changes and tests evolve together.",
|
|
25
66
|
"",
|
|
26
67
|
"## Operational Notes",
|
|
27
|
-
"-
|
|
68
|
+
"- Preserve observability and runtime configuration conventions already used by the project.",
|
|
69
|
+
"- Document notable rollout risks and mitigation in task summaries.",
|
|
28
70
|
"",
|
|
29
71
|
].join("\n");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
72
|
+
const normalizePathSeparators = (value) => value.replace(/\\/g, "/");
|
|
73
|
+
const dedupe = (values) => {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const output = [];
|
|
76
|
+
for (const value of values) {
|
|
77
|
+
const normalized = normalizePathSeparators(value);
|
|
78
|
+
if (seen.has(normalized))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(normalized);
|
|
81
|
+
output.push(value);
|
|
82
|
+
}
|
|
83
|
+
return output;
|
|
34
84
|
};
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
85
|
+
const normalizeProjectKey = (value) => {
|
|
86
|
+
const trimmed = (value ?? "").trim();
|
|
87
|
+
if (!trimmed)
|
|
88
|
+
return undefined;
|
|
89
|
+
const normalized = trimmed
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
92
|
+
.replace(/^-+|-+$/g, "");
|
|
93
|
+
return normalized || undefined;
|
|
40
94
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
95
|
+
const sha256 = (content) => createHash("sha256").update(content).digest("hex");
|
|
96
|
+
const readTextFile = async (targetPath) => {
|
|
97
|
+
try {
|
|
98
|
+
return await fs.readFile(targetPath, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const trimSection = (value, maxLines = 10) => {
|
|
105
|
+
const lines = value
|
|
106
|
+
.split(/\r?\n/)
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
if (lines.length === 0)
|
|
110
|
+
return "";
|
|
111
|
+
return lines.slice(0, maxLines).join("\n");
|
|
112
|
+
};
|
|
113
|
+
const toBullets = (value, maxItems = 6) => {
|
|
114
|
+
const lines = value
|
|
115
|
+
.split(/\r?\n/)
|
|
116
|
+
.map((line) => line.trim())
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.filter((line) => !line.startsWith("```"));
|
|
119
|
+
const bullets = [];
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
let normalized = line.replace(/^[-*]\s+/, "").trim();
|
|
122
|
+
normalized = normalized.replace(/^\d+\.\s+/, "").trim();
|
|
123
|
+
if (!normalized)
|
|
124
|
+
continue;
|
|
125
|
+
if (normalized.length > 180)
|
|
126
|
+
normalized = `${normalized.slice(0, 177)}...`;
|
|
127
|
+
bullets.push(`- ${normalized}`);
|
|
128
|
+
if (bullets.length >= maxItems)
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
return bullets;
|
|
132
|
+
};
|
|
133
|
+
const extractFencedTreeBlock = (content) => {
|
|
134
|
+
const fenceRegex = /```(?:text|md|markdown|tree)?\s*\n([\s\S]*?)\n```/gi;
|
|
135
|
+
for (const match of content.matchAll(fenceRegex)) {
|
|
136
|
+
const block = (match[1] ?? "").trim();
|
|
137
|
+
if (!block)
|
|
138
|
+
continue;
|
|
139
|
+
if (!TREE_LIKE_PATTERN.test(block))
|
|
140
|
+
continue;
|
|
141
|
+
const normalized = block.split(/\r?\n/).slice(0, 40).join("\n").trim();
|
|
142
|
+
if (normalized)
|
|
143
|
+
return normalized;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
const parseHeadingSections = (content) => {
|
|
148
|
+
const lines = content.split(/\r?\n/);
|
|
149
|
+
const sections = [];
|
|
150
|
+
let currentHeading = "";
|
|
151
|
+
let currentBody = [];
|
|
152
|
+
const flush = () => {
|
|
153
|
+
if (!currentHeading)
|
|
154
|
+
return;
|
|
155
|
+
sections.push({ heading: currentHeading, body: currentBody.join("\n").trim() });
|
|
156
|
+
};
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
const match = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*$/);
|
|
159
|
+
if (match) {
|
|
160
|
+
flush();
|
|
161
|
+
currentHeading = match[1]?.trim() ?? "";
|
|
162
|
+
currentBody = [];
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (currentHeading)
|
|
166
|
+
currentBody.push(line);
|
|
167
|
+
}
|
|
168
|
+
flush();
|
|
169
|
+
return sections;
|
|
170
|
+
};
|
|
171
|
+
const findSectionByHeading = (sections, patterns) => {
|
|
172
|
+
for (const section of sections) {
|
|
173
|
+
const heading = section.heading.trim();
|
|
174
|
+
if (!heading)
|
|
175
|
+
continue;
|
|
176
|
+
if (patterns.some((pattern) => pattern.test(heading))) {
|
|
177
|
+
const body = trimSection(section.body, 14);
|
|
178
|
+
if (body)
|
|
179
|
+
return body;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
};
|
|
184
|
+
const extractIntroParagraph = (content) => {
|
|
185
|
+
const stripped = content
|
|
186
|
+
.replace(FRONTMATTER_BLOCK, "")
|
|
187
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
188
|
+
.split(/\r?\n/)
|
|
189
|
+
.map((line) => line.trim())
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
const first = stripped.find((line) => !line.startsWith("#"));
|
|
192
|
+
if (!first)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (first.length <= 200)
|
|
195
|
+
return first;
|
|
196
|
+
return `${first.slice(0, 197)}...`;
|
|
197
|
+
};
|
|
198
|
+
const buildGuidanceFrontmatter = (metadata) => {
|
|
199
|
+
const lines = ["---", "mcoda_guidance: true"];
|
|
200
|
+
if (metadata.projectKey)
|
|
201
|
+
lines.push(`project_key: ${metadata.projectKey}`);
|
|
202
|
+
if (metadata.sdsSource)
|
|
203
|
+
lines.push(`sds_source: ${metadata.sdsSource}`);
|
|
204
|
+
if (metadata.sdsSha256)
|
|
205
|
+
lines.push(`sds_sha256: ${metadata.sdsSha256}`);
|
|
206
|
+
if (metadata.generatedAt)
|
|
207
|
+
lines.push(`generated_at: ${metadata.generatedAt}`);
|
|
208
|
+
lines.push("---");
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
};
|
|
211
|
+
const parseFrontmatterPairs = (frontmatter) => {
|
|
212
|
+
const lines = frontmatter
|
|
213
|
+
.split(/\r?\n/)
|
|
214
|
+
.slice(1, -1)
|
|
215
|
+
.map((line) => line.trim())
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
const pairs = {};
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const idx = line.indexOf(":");
|
|
220
|
+
if (idx <= 0)
|
|
221
|
+
continue;
|
|
222
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
223
|
+
const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
224
|
+
if (!key)
|
|
225
|
+
continue;
|
|
226
|
+
pairs[key] = value;
|
|
227
|
+
}
|
|
228
|
+
return pairs;
|
|
50
229
|
};
|
|
51
230
|
const extractFrontmatter = (content) => {
|
|
52
231
|
if (!content)
|
|
@@ -57,6 +236,25 @@ const extractFrontmatter = (content) => {
|
|
|
57
236
|
const match = trimmed.match(FRONTMATTER_BLOCK);
|
|
58
237
|
return match ? match[0] : undefined;
|
|
59
238
|
};
|
|
239
|
+
const parseGuidanceFrontmatter = (content) => {
|
|
240
|
+
if (!content)
|
|
241
|
+
return undefined;
|
|
242
|
+
const frontmatter = extractFrontmatter(content);
|
|
243
|
+
if (!frontmatter)
|
|
244
|
+
return undefined;
|
|
245
|
+
const pairs = parseFrontmatterPairs(frontmatter);
|
|
246
|
+
const markerValue = (pairs.mcoda_guidance ?? "").toLowerCase();
|
|
247
|
+
const mcodaGuidance = FRONTMATTER_TRUE_VALUES.has(markerValue);
|
|
248
|
+
if (!mcodaGuidance)
|
|
249
|
+
return undefined;
|
|
250
|
+
return {
|
|
251
|
+
mcodaGuidance,
|
|
252
|
+
projectKey: pairs.project_key || undefined,
|
|
253
|
+
sdsSource: pairs.sds_source || undefined,
|
|
254
|
+
sdsSha256: pairs.sds_sha256 || undefined,
|
|
255
|
+
generatedAt: pairs.generated_at || undefined,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
60
258
|
const hasSdsFrontmatter = (content) => {
|
|
61
259
|
if (!content)
|
|
62
260
|
return false;
|
|
@@ -71,6 +269,260 @@ const hasSdsFrontmatter = (content) => {
|
|
|
71
269
|
return true;
|
|
72
270
|
return false;
|
|
73
271
|
};
|
|
272
|
+
const listMarkdownFiles = async (root, maxDepth = 5) => {
|
|
273
|
+
const output = [];
|
|
274
|
+
const walk = async (current, depth) => {
|
|
275
|
+
if (depth > maxDepth)
|
|
276
|
+
return;
|
|
277
|
+
let entries = [];
|
|
278
|
+
try {
|
|
279
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
const entryPath = path.join(current, entry.name);
|
|
286
|
+
const relative = normalizePathSeparators(path.relative(root, entryPath));
|
|
287
|
+
if (!relative)
|
|
288
|
+
continue;
|
|
289
|
+
if (entry.isDirectory()) {
|
|
290
|
+
if (DIR_SCAN_EXCLUDES.has(entry.name))
|
|
291
|
+
continue;
|
|
292
|
+
await walk(entryPath, depth + 1);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!entry.isFile())
|
|
296
|
+
continue;
|
|
297
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
298
|
+
if (!MARKDOWN_FILE_EXTENSIONS.has(ext))
|
|
299
|
+
continue;
|
|
300
|
+
output.push(relative);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
await walk(root, 0);
|
|
304
|
+
return output;
|
|
305
|
+
};
|
|
306
|
+
const defaultSdsCandidates = (workspaceRoot, projectKey) => {
|
|
307
|
+
const slug = normalizeProjectKey(projectKey);
|
|
308
|
+
const candidates = [
|
|
309
|
+
"docs/sds.md",
|
|
310
|
+
"docs/sds/sds.md",
|
|
311
|
+
"docs/sds/index.md",
|
|
312
|
+
"docs/software-design-specification.md",
|
|
313
|
+
"sds.md",
|
|
314
|
+
];
|
|
315
|
+
if (slug) {
|
|
316
|
+
candidates.unshift(`docs/sds/${slug}.md`);
|
|
317
|
+
candidates.unshift(`docs/${slug}-sds.md`);
|
|
318
|
+
}
|
|
319
|
+
return candidates.map((entry) => normalizePathSeparators(path.join(workspaceRoot, entry)));
|
|
320
|
+
};
|
|
321
|
+
const scoreSdsPath = (relativePath, projectKey) => {
|
|
322
|
+
const normalized = normalizePathSeparators(relativePath).toLowerCase();
|
|
323
|
+
const fileName = path.basename(normalized);
|
|
324
|
+
const project = normalizeProjectKey(projectKey);
|
|
325
|
+
let score = 0;
|
|
326
|
+
if (normalized === "docs/sds.md")
|
|
327
|
+
score += 120;
|
|
328
|
+
if (normalized === "docs/sds/sds.md")
|
|
329
|
+
score += 110;
|
|
330
|
+
if (normalized.startsWith("docs/sds/"))
|
|
331
|
+
score += 90;
|
|
332
|
+
if (SDS_NAME_PATTERN.test(fileName))
|
|
333
|
+
score += 40;
|
|
334
|
+
if (SDS_PATH_HINT_PATTERN.test(normalized))
|
|
335
|
+
score += 25;
|
|
336
|
+
if (project && normalized.includes(project))
|
|
337
|
+
score += 20;
|
|
338
|
+
return score;
|
|
339
|
+
};
|
|
340
|
+
const hasSdsContentSignals = (content, relativePath) => {
|
|
341
|
+
if (SDS_DOC_PATTERN.test(relativePath))
|
|
342
|
+
return true;
|
|
343
|
+
if (hasSdsFrontmatter(content))
|
|
344
|
+
return true;
|
|
345
|
+
if (/^\s*#\s*(software\s+design\s+specification|sds)\b/im.test(content))
|
|
346
|
+
return true;
|
|
347
|
+
if (/\bnon-functional requirements\b/i.test(content))
|
|
348
|
+
return true;
|
|
349
|
+
if (/\bfolder tree\b/i.test(content))
|
|
350
|
+
return true;
|
|
351
|
+
return false;
|
|
352
|
+
};
|
|
353
|
+
const findSdsContext = async (workspaceRoot, projectKey) => {
|
|
354
|
+
const preferred = defaultSdsCandidates(workspaceRoot, projectKey);
|
|
355
|
+
const docsRoot = path.join(workspaceRoot, "docs");
|
|
356
|
+
const markdownFiles = await listMarkdownFiles(docsRoot, 6).catch(() => []);
|
|
357
|
+
const fuzzyAbsolute = markdownFiles
|
|
358
|
+
.filter((file) => SDS_NAME_PATTERN.test(file) || SDS_PATH_HINT_PATTERN.test(file))
|
|
359
|
+
.map((file) => path.join(docsRoot, file));
|
|
360
|
+
const candidates = dedupe([...preferred, ...fuzzyAbsolute]);
|
|
361
|
+
let best;
|
|
362
|
+
for (const absolutePath of candidates) {
|
|
363
|
+
const content = await readTextFile(absolutePath);
|
|
364
|
+
if (!content)
|
|
365
|
+
continue;
|
|
366
|
+
const relativePath = normalizePathSeparators(path.relative(workspaceRoot, absolutePath));
|
|
367
|
+
let score = scoreSdsPath(relativePath, projectKey);
|
|
368
|
+
if (hasSdsContentSignals(content, relativePath))
|
|
369
|
+
score += 50;
|
|
370
|
+
if (score <= 0)
|
|
371
|
+
continue;
|
|
372
|
+
const context = {
|
|
373
|
+
absolutePath,
|
|
374
|
+
relativePath,
|
|
375
|
+
content,
|
|
376
|
+
hash: sha256(content),
|
|
377
|
+
};
|
|
378
|
+
if (!best || score > best.score) {
|
|
379
|
+
best = { context, score };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return best?.context;
|
|
383
|
+
};
|
|
384
|
+
const buildSdsDerivedTemplate = (sds, projectKey) => {
|
|
385
|
+
const sections = parseHeadingSections(sds.content);
|
|
386
|
+
const intro = extractIntroParagraph(sds.content);
|
|
387
|
+
const productContext = findSectionByHeading(sections, [/overview/i, /introduction/i, /scope/i, /goals?/i, /product/i]) ?? intro ?? "";
|
|
388
|
+
const architecture = findSectionByHeading(sections, [/architecture/i, /services?/i, /components?/i, /modules?/i]);
|
|
389
|
+
const constraints = findSectionByHeading(sections, [/constraints?/i, /security/i, /compliance/i, /standards?/i]);
|
|
390
|
+
const testing = findSectionByHeading(sections, [/testing/i, /\bqa\b/i, /verification/i, /validation/i]);
|
|
391
|
+
const operations = findSectionByHeading(sections, [/operations?/i, /deployment/i, /runbook/i, /observability/i, /monitoring/i]);
|
|
392
|
+
const folderSection = findSectionByHeading(sections, [/folder tree/i, /repo structure/i, /project structure/i]);
|
|
393
|
+
const treeBlock = extractFencedTreeBlock(folderSection ?? sds.content);
|
|
394
|
+
const generatedAt = new Date().toISOString();
|
|
395
|
+
const frontmatter = buildGuidanceFrontmatter({
|
|
396
|
+
projectKey: normalizeProjectKey(projectKey),
|
|
397
|
+
sdsSource: sds.relativePath,
|
|
398
|
+
sdsSha256: sds.hash,
|
|
399
|
+
generatedAt,
|
|
400
|
+
});
|
|
401
|
+
const productBullets = toBullets(productContext || `SDS source: ${sds.relativePath}`);
|
|
402
|
+
const architectureBullets = toBullets(architecture || "Follow the module and service boundaries specified in the SDS architecture section.");
|
|
403
|
+
const constraintsBullets = toBullets(constraints ||
|
|
404
|
+
"Do not add undocumented dependencies. Keep interfaces, schemas, and naming aligned with SDS and OpenAPI artifacts.");
|
|
405
|
+
const testingBullets = toBullets(testing ||
|
|
406
|
+
"For every implementation change, update tests that cover behavior and regression risk, then run the smallest relevant suite before broad test runs.");
|
|
407
|
+
const operationsBullets = toBullets(operations ||
|
|
408
|
+
"Keep runtime configuration environment-driven, avoid hardcoded ports/hosts, and preserve existing logging/telemetry conventions.");
|
|
409
|
+
const lines = [
|
|
410
|
+
frontmatter,
|
|
411
|
+
"",
|
|
412
|
+
"# Project Guidance",
|
|
413
|
+
"",
|
|
414
|
+
`This guidance is generated from SDS source \`${sds.relativePath}\` and is intended to keep implementation/review/QA aligned.`,
|
|
415
|
+
"",
|
|
416
|
+
"## Product Context",
|
|
417
|
+
...(productBullets.length > 0 ? productBullets : [`- SDS source: ${sds.relativePath}`]),
|
|
418
|
+
"",
|
|
419
|
+
"## Architecture Notes",
|
|
420
|
+
...(architectureBullets.length > 0
|
|
421
|
+
? architectureBullets
|
|
422
|
+
: ["- Follow SDS-defined service boundaries and data contracts."]),
|
|
423
|
+
"",
|
|
424
|
+
"## Coding Constraints",
|
|
425
|
+
...(constraintsBullets.length > 0
|
|
426
|
+
? constraintsBullets
|
|
427
|
+
: ["- Match SDS/OpenAPI contracts and avoid introducing undocumented behavior."]),
|
|
428
|
+
"",
|
|
429
|
+
"## Testing Policy",
|
|
430
|
+
...(testingBullets.length > 0 ? testingBullets : ["- Run targeted tests first, then broader suites where relevant."]),
|
|
431
|
+
"",
|
|
432
|
+
"## Operational Notes",
|
|
433
|
+
...(operationsBullets.length > 0
|
|
434
|
+
? operationsBullets
|
|
435
|
+
: ["- Keep runtime settings configurable and align with existing deployment/observability practices."]),
|
|
436
|
+
"",
|
|
437
|
+
"## Folder Tree Baseline",
|
|
438
|
+
treeBlock ? "```text" : `- Refer to folder tree defined in \`${sds.relativePath}\`.`,
|
|
439
|
+
...(treeBlock ? [treeBlock, "```"] : []),
|
|
440
|
+
"",
|
|
441
|
+
];
|
|
442
|
+
return lines.join("\n");
|
|
443
|
+
};
|
|
444
|
+
const validateGuidanceContent = (content) => {
|
|
445
|
+
const warnings = [];
|
|
446
|
+
for (const section of REQUIRED_GUIDANCE_SECTIONS) {
|
|
447
|
+
const matcher = new RegExp(`^##\\s+${section.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*$`, "im");
|
|
448
|
+
if (!matcher.test(content)) {
|
|
449
|
+
warnings.push(`missing_section:${section}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const placeholder of PLACEHOLDER_PATTERNS) {
|
|
453
|
+
if (placeholder.test(content)) {
|
|
454
|
+
warnings.push("placeholder_text_detected");
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return warnings;
|
|
459
|
+
};
|
|
460
|
+
const detectGuidanceStaleness = async (workspaceRoot, content) => {
|
|
461
|
+
const metadata = parseGuidanceFrontmatter(content);
|
|
462
|
+
if (!metadata) {
|
|
463
|
+
return { stale: false, warnings: [] };
|
|
464
|
+
}
|
|
465
|
+
const warnings = [];
|
|
466
|
+
if (!metadata.sdsSource || !metadata.sdsSha256) {
|
|
467
|
+
warnings.push("guidance_missing_sds_metadata");
|
|
468
|
+
return { stale: false, warnings, metadata };
|
|
469
|
+
}
|
|
470
|
+
const absoluteSource = path.isAbsolute(metadata.sdsSource)
|
|
471
|
+
? metadata.sdsSource
|
|
472
|
+
: path.join(workspaceRoot, metadata.sdsSource);
|
|
473
|
+
const sourceContent = await readTextFile(absoluteSource);
|
|
474
|
+
if (!sourceContent) {
|
|
475
|
+
warnings.push(`guidance_sds_source_missing:${metadata.sdsSource}`);
|
|
476
|
+
return { stale: true, warnings, metadata };
|
|
477
|
+
}
|
|
478
|
+
const currentHash = sha256(sourceContent);
|
|
479
|
+
if (currentHash !== metadata.sdsSha256) {
|
|
480
|
+
warnings.push(`guidance_stale_sds_hash:${metadata.sdsSource}`);
|
|
481
|
+
return { stale: true, warnings, metadata };
|
|
482
|
+
}
|
|
483
|
+
return { stale: false, warnings, metadata };
|
|
484
|
+
};
|
|
485
|
+
const resolveGuidanceTemplate = async (workspaceRoot, options) => {
|
|
486
|
+
if (options.template && options.template.trim()) {
|
|
487
|
+
return { template: options.template.trim(), source: "custom" };
|
|
488
|
+
}
|
|
489
|
+
const sds = await findSdsContext(workspaceRoot, options.projectKey);
|
|
490
|
+
if (sds) {
|
|
491
|
+
return { template: buildSdsDerivedTemplate(sds, options.projectKey), source: "sds", sds };
|
|
492
|
+
}
|
|
493
|
+
return { template: getDefaultProjectGuidanceTemplate().trim(), source: "default" };
|
|
494
|
+
};
|
|
495
|
+
export const getDefaultProjectGuidanceTemplate = () => DEFAULT_PROJECT_GUIDANCE_TEMPLATE;
|
|
496
|
+
export const resolveWorkspaceProjectGuidancePath = (workspaceRoot, mcodaDir, projectKey) => {
|
|
497
|
+
const resolvedMcodaDir = mcodaDir ?? PathHelper.getWorkspaceDir(workspaceRoot);
|
|
498
|
+
const normalizedProject = normalizeProjectKey(projectKey);
|
|
499
|
+
if (normalizedProject) {
|
|
500
|
+
return path.join(resolvedMcodaDir, "docs", "projects", normalizedProject, "project-guidance.md");
|
|
501
|
+
}
|
|
502
|
+
return path.join(resolvedMcodaDir, "docs", "project-guidance.md");
|
|
503
|
+
};
|
|
504
|
+
const guidanceCandidates = (workspaceRoot, mcodaDir, projectKey) => {
|
|
505
|
+
const normalizedProject = normalizeProjectKey(projectKey);
|
|
506
|
+
const repoProjectPath = normalizedProject
|
|
507
|
+
? path.join(workspaceRoot, "docs", "projects", normalizedProject, "project-guidance.md")
|
|
508
|
+
: undefined;
|
|
509
|
+
return dedupe([
|
|
510
|
+
resolveWorkspaceProjectGuidancePath(workspaceRoot, mcodaDir, normalizedProject),
|
|
511
|
+
resolveWorkspaceProjectGuidancePath(workspaceRoot, mcodaDir),
|
|
512
|
+
repoProjectPath,
|
|
513
|
+
path.join(workspaceRoot, "docs", "project-guidance.md"),
|
|
514
|
+
].filter((entry) => Boolean(entry)));
|
|
515
|
+
};
|
|
516
|
+
export const isDocContextExcluded = (value, allowQaDocs = false) => {
|
|
517
|
+
if (!value)
|
|
518
|
+
return false;
|
|
519
|
+
const normalized = value.replace(/\\/g, "/");
|
|
520
|
+
if (MCODA_DOC_PATTERN.test(normalized))
|
|
521
|
+
return true;
|
|
522
|
+
if (!allowQaDocs && QA_DOC_PATTERN.test(normalized))
|
|
523
|
+
return true;
|
|
524
|
+
return false;
|
|
525
|
+
};
|
|
74
526
|
export const normalizeDocType = (params) => {
|
|
75
527
|
const raw = (params.docType ?? "DOC").trim();
|
|
76
528
|
const normalizedType = raw ? raw.toUpperCase() : "DOC";
|
|
@@ -87,25 +539,43 @@ export const normalizeDocType = (params) => {
|
|
|
87
539
|
const reason = [inSdsPath ? null : "path_not_sds", frontmatter ? null : "frontmatter_missing"].filter(Boolean).join("|");
|
|
88
540
|
return { docType: "DOC", downgraded: true, reason: reason || "not_sds" };
|
|
89
541
|
};
|
|
90
|
-
export const loadProjectGuidance = async (workspaceRoot, mcodaDir) => {
|
|
91
|
-
|
|
542
|
+
export const loadProjectGuidance = async (workspaceRoot, mcodaDir, options = {}) => {
|
|
543
|
+
const candidates = guidanceCandidates(workspaceRoot, mcodaDir, options.projectKey);
|
|
544
|
+
for (const candidate of candidates) {
|
|
92
545
|
try {
|
|
93
546
|
const content = (await fs.readFile(candidate, "utf8")).trim();
|
|
94
547
|
if (!content)
|
|
95
548
|
continue;
|
|
96
|
-
|
|
549
|
+
const validationWarnings = validateGuidanceContent(content);
|
|
550
|
+
const staleness = await detectGuidanceStaleness(workspaceRoot, content);
|
|
551
|
+
const metadata = staleness.metadata;
|
|
552
|
+
const warnings = [...validationWarnings, ...staleness.warnings];
|
|
553
|
+
return {
|
|
554
|
+
content,
|
|
555
|
+
source: candidate,
|
|
556
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
557
|
+
stale: staleness.stale,
|
|
558
|
+
projectKey: metadata?.projectKey,
|
|
559
|
+
sdsSource: metadata?.sdsSource,
|
|
560
|
+
sdsSha256: metadata?.sdsSha256,
|
|
561
|
+
generatedAt: metadata?.generatedAt,
|
|
562
|
+
};
|
|
97
563
|
}
|
|
98
564
|
catch {
|
|
99
565
|
// ignore missing file
|
|
100
566
|
}
|
|
101
567
|
}
|
|
102
|
-
console.warn(`[project-guidance] no project guidance found; searched: ${
|
|
568
|
+
console.warn(`[project-guidance] no project guidance found; searched: ${candidates.join(", ")}`);
|
|
103
569
|
return null;
|
|
104
570
|
};
|
|
105
571
|
export const ensureProjectGuidance = async (workspaceRoot, options = {}) => {
|
|
106
|
-
const
|
|
572
|
+
const normalizedProject = normalizeProjectKey(options.projectKey);
|
|
573
|
+
const targetPath = resolveWorkspaceProjectGuidancePath(workspaceRoot, options.mcodaDir, normalizedProject);
|
|
107
574
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
108
575
|
const force = Boolean(options.force);
|
|
576
|
+
const resolvedTemplate = await resolveGuidanceTemplate(workspaceRoot, options);
|
|
577
|
+
const template = resolvedTemplate.template.trim();
|
|
578
|
+
const payload = template.length > 0 ? `${template}\n` : "";
|
|
109
579
|
let existed = false;
|
|
110
580
|
try {
|
|
111
581
|
await fs.access(targetPath);
|
|
@@ -118,15 +588,46 @@ export const ensureProjectGuidance = async (workspaceRoot, options = {}) => {
|
|
|
118
588
|
try {
|
|
119
589
|
const existing = (await fs.readFile(targetPath, "utf8")).trim();
|
|
120
590
|
if (existing.length > 0) {
|
|
121
|
-
|
|
591
|
+
const validationWarnings = validateGuidanceContent(existing);
|
|
592
|
+
const staleness = await detectGuidanceStaleness(workspaceRoot, existing);
|
|
593
|
+
const warnings = [...validationWarnings, ...staleness.warnings];
|
|
594
|
+
const metadata = staleness.metadata;
|
|
595
|
+
if (staleness.stale && metadata?.mcodaGuidance) {
|
|
596
|
+
await fs.writeFile(targetPath, payload, "utf8");
|
|
597
|
+
return {
|
|
598
|
+
path: targetPath,
|
|
599
|
+
status: "overwritten",
|
|
600
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
601
|
+
stale: true,
|
|
602
|
+
source: resolvedTemplate.source,
|
|
603
|
+
projectKey: normalizedProject,
|
|
604
|
+
sdsSource: resolvedTemplate.sds?.relativePath,
|
|
605
|
+
sdsSha256: resolvedTemplate.sds?.hash,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
path: targetPath,
|
|
610
|
+
status: "existing",
|
|
611
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
612
|
+
stale: staleness.stale,
|
|
613
|
+
source: metadata?.mcodaGuidance ? "sds" : "custom",
|
|
614
|
+
projectKey: metadata?.projectKey,
|
|
615
|
+
sdsSource: metadata?.sdsSource,
|
|
616
|
+
sdsSha256: metadata?.sdsSha256,
|
|
617
|
+
};
|
|
122
618
|
}
|
|
123
619
|
}
|
|
124
620
|
catch {
|
|
125
621
|
// fall through and write template
|
|
126
622
|
}
|
|
127
623
|
}
|
|
128
|
-
const template = (options.template ?? getDefaultProjectGuidanceTemplate()).trim();
|
|
129
|
-
const payload = template.length > 0 ? `${template}\n` : "";
|
|
130
624
|
await fs.writeFile(targetPath, payload, "utf8");
|
|
131
|
-
return {
|
|
625
|
+
return {
|
|
626
|
+
path: targetPath,
|
|
627
|
+
status: existed ? "overwritten" : "created",
|
|
628
|
+
source: resolvedTemplate.source,
|
|
629
|
+
projectKey: normalizedProject,
|
|
630
|
+
sdsSource: resolvedTemplate.sds?.relativePath,
|
|
631
|
+
sdsSha256: resolvedTemplate.sds?.hash,
|
|
632
|
+
};
|
|
132
633
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcoda/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Core services and APIs for the mcoda CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@apidevtools/swagger-parser": "^10.1.0",
|
|
34
34
|
"yaml": "^2.4.2",
|
|
35
|
-
"@mcoda/shared": "0.1.
|
|
36
|
-
"@mcoda/db": "0.1.
|
|
37
|
-
"@mcoda/agents": "0.1.
|
|
38
|
-
"@mcoda/generators": "0.1.
|
|
39
|
-
"@mcoda/integrations": "0.1.
|
|
35
|
+
"@mcoda/shared": "0.1.21",
|
|
36
|
+
"@mcoda/db": "0.1.21",
|
|
37
|
+
"@mcoda/agents": "0.1.21",
|
|
38
|
+
"@mcoda/generators": "0.1.21",
|
|
39
|
+
"@mcoda/integrations": "0.1.21"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|