@mcoda/core 0.1.34 → 0.1.36
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/AgentsApi.d.ts +4 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +4 -1
- package/dist/prompts/PdrPrompts.js +1 -1
- package/dist/services/docs/DocsService.d.ts +37 -0
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +537 -2
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -1
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +13 -2
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.d.ts.map +1 -1
- package/dist/services/docs/review/gates/SdsNoUnresolvedItemsGate.js +12 -1
- package/dist/services/planning/CreateTasksService.d.ts +57 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +2491 -291
- package/dist/services/planning/SdsCoverageModel.d.ts +27 -0
- package/dist/services/planning/SdsCoverageModel.d.ts.map +1 -0
- package/dist/services/planning/SdsCoverageModel.js +138 -0
- package/dist/services/planning/SdsPreflightService.d.ts +2 -0
- package/dist/services/planning/SdsPreflightService.d.ts.map +1 -1
- package/dist/services/planning/SdsPreflightService.js +131 -37
- package/dist/services/planning/SdsStructureSignals.d.ts +24 -0
- package/dist/services/planning/SdsStructureSignals.d.ts.map +1 -0
- package/dist/services/planning/SdsStructureSignals.js +402 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts +17 -0
- package/dist/services/planning/TaskSufficiencyService.d.ts.map +1 -1
- package/dist/services/planning/TaskSufficiencyService.js +409 -278
- package/package.json +6 -6
|
@@ -166,6 +166,35 @@ const slugify = (value) => value
|
|
|
166
166
|
.replace(/^-+|-+$/g, "")
|
|
167
167
|
.replace(/-{2,}/g, "-") || "draft";
|
|
168
168
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
169
|
+
const extractJsonObject = (raw) => {
|
|
170
|
+
if (!raw)
|
|
171
|
+
return undefined;
|
|
172
|
+
const fenced = raw.match(/```json([\s\S]*?)```/i);
|
|
173
|
+
const candidate = fenced ? fenced[1] : raw;
|
|
174
|
+
const start = candidate.indexOf("{");
|
|
175
|
+
const end = candidate.lastIndexOf("}");
|
|
176
|
+
if (start === -1 || end === -1 || end < start)
|
|
177
|
+
return undefined;
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(candidate.slice(start, end + 1));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const extractMarkdownFromOutput = (raw) => {
|
|
186
|
+
const trimmed = raw.trim();
|
|
187
|
+
if (!trimmed)
|
|
188
|
+
return "";
|
|
189
|
+
const fenced = [...trimmed.matchAll(/```(?:markdown|md)?\s*([\s\S]*?)```/gi)];
|
|
190
|
+
if (fenced.length === 0)
|
|
191
|
+
return trimmed;
|
|
192
|
+
const best = fenced
|
|
193
|
+
.map((match) => (match[1] ?? "").trim())
|
|
194
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
195
|
+
return best || trimmed;
|
|
196
|
+
};
|
|
197
|
+
const nowIso = () => new Date().toISOString();
|
|
169
198
|
const SDS_CONTEXT_TOKEN_BUDGET = 8000;
|
|
170
199
|
const extractBullets = (content, limit = 20) => {
|
|
171
200
|
return content
|
|
@@ -1168,11 +1197,13 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
|
|
|
1168
1197
|
structured = ensureSectionContent(structured, section, fallbackFor(section));
|
|
1169
1198
|
}
|
|
1170
1199
|
if ((context.openapi?.length ?? 0) === 0) {
|
|
1171
|
-
const interfaceTitle = sections.find((section) => /interface|
|
|
1200
|
+
const interfaceTitle = sections.find((section) => /(?:^|[\s,&/()-])(interface|interfaces|api|apis)(?:$|[\s,&/()-])/i.test(section)) ??
|
|
1201
|
+
sections.find((section) => /contract/i.test(section)) ??
|
|
1202
|
+
"Interfaces & Contracts";
|
|
1172
1203
|
const extracted = extractSection(structured, interfaceTitle);
|
|
1173
1204
|
if (extracted) {
|
|
1174
1205
|
const scrubbed = stripInventedEndpoints(cleanBody(extracted.body ?? ""));
|
|
1175
|
-
const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as
|
|
1206
|
+
const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as explicit contracts, assumptions, and open questions without inventing endpoint paths.";
|
|
1176
1207
|
let body = scrubbed.length > 0 && !/endpoint/i.test(scrubbed) ? scrubbed : cleanBody(openApiFallback);
|
|
1177
1208
|
if (!/openapi/i.test(body)) {
|
|
1178
1209
|
body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
|
|
@@ -1610,6 +1641,272 @@ export class DocsService {
|
|
|
1610
1641
|
const slug = slugify(projectKey ?? "sds");
|
|
1611
1642
|
return path.join(this.workspace.mcodaDir, "docs", "sds", `${slug}.md`);
|
|
1612
1643
|
}
|
|
1644
|
+
normalizeMaxIterations(value) {
|
|
1645
|
+
if (!Number.isFinite(value))
|
|
1646
|
+
return 100;
|
|
1647
|
+
const floored = Math.floor(value);
|
|
1648
|
+
if (floored < 1)
|
|
1649
|
+
return 1;
|
|
1650
|
+
if (floored > 100)
|
|
1651
|
+
return 100;
|
|
1652
|
+
return floored;
|
|
1653
|
+
}
|
|
1654
|
+
async listSdsSuggestionPathCandidates(projectKey) {
|
|
1655
|
+
const projectSlug = slugify(projectKey ?? "sds");
|
|
1656
|
+
const prioritized = [
|
|
1657
|
+
path.join(this.workspace.workspaceRoot, "docs", "sds", "sds.md"),
|
|
1658
|
+
path.join(this.workspace.workspaceRoot, "docs", "sds.md"),
|
|
1659
|
+
];
|
|
1660
|
+
const dynamicDir = path.join(this.workspace.workspaceRoot, "docs", "sds");
|
|
1661
|
+
const dynamicCandidates = [];
|
|
1662
|
+
try {
|
|
1663
|
+
const entries = await fs.readdir(dynamicDir, { withFileTypes: true });
|
|
1664
|
+
const markdown = entries
|
|
1665
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
|
|
1666
|
+
.map((entry) => path.join(dynamicDir, entry.name));
|
|
1667
|
+
const statRows = await Promise.all(markdown.map(async (candidatePath) => ({
|
|
1668
|
+
candidatePath,
|
|
1669
|
+
stat: await fs.stat(candidatePath),
|
|
1670
|
+
})));
|
|
1671
|
+
statRows
|
|
1672
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
|
|
1673
|
+
.forEach((row) => dynamicCandidates.push(row.candidatePath));
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
// Directory may not exist.
|
|
1677
|
+
}
|
|
1678
|
+
const fallback = [
|
|
1679
|
+
path.join(this.workspace.mcodaDir, "docs", "sds", `${projectSlug}.md`),
|
|
1680
|
+
path.join(this.workspace.mcodaDir, "docs", "sds", "sds.md"),
|
|
1681
|
+
];
|
|
1682
|
+
return Array.from(new Set([...prioritized, ...dynamicCandidates, ...fallback]));
|
|
1683
|
+
}
|
|
1684
|
+
async resolveSdsSuggestionsPath(projectKey, explicitSdsPath) {
|
|
1685
|
+
if (explicitSdsPath) {
|
|
1686
|
+
const resolved = path.isAbsolute(explicitSdsPath)
|
|
1687
|
+
? explicitSdsPath
|
|
1688
|
+
: path.resolve(this.workspace.workspaceRoot, explicitSdsPath);
|
|
1689
|
+
try {
|
|
1690
|
+
await fs.access(resolved);
|
|
1691
|
+
}
|
|
1692
|
+
catch {
|
|
1693
|
+
throw new Error(`Unable to locate SDS file at --sds-path: ${resolved}`);
|
|
1694
|
+
}
|
|
1695
|
+
return { path: resolved, attempted: [resolved] };
|
|
1696
|
+
}
|
|
1697
|
+
const attempted = await this.listSdsSuggestionPathCandidates(projectKey);
|
|
1698
|
+
for (const candidate of attempted) {
|
|
1699
|
+
try {
|
|
1700
|
+
const stat = await fs.stat(candidate);
|
|
1701
|
+
if (stat.isFile()) {
|
|
1702
|
+
return { path: candidate, attempted };
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
// keep trying
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
throw new Error(`Unable to locate an SDS file. Checked: ${attempted.join(", ")}. ` +
|
|
1710
|
+
`Pass --sds-path <FILE> to specify the SDS explicitly.`);
|
|
1711
|
+
}
|
|
1712
|
+
async ensureSuggestionsDir() {
|
|
1713
|
+
const suggestionsDir = path.join(this.workspace.workspaceRoot, "docs", "suggestions");
|
|
1714
|
+
await fs.mkdir(suggestionsDir, { recursive: true });
|
|
1715
|
+
return suggestionsDir;
|
|
1716
|
+
}
|
|
1717
|
+
async nextSuggestionsFilePath(suggestionsDir) {
|
|
1718
|
+
const entries = await fs.readdir(suggestionsDir).catch(() => []);
|
|
1719
|
+
const numbers = entries
|
|
1720
|
+
.map((entry) => /^sds_suggestions(\d+)\.md$/i.exec(entry))
|
|
1721
|
+
.filter((match) => Boolean(match))
|
|
1722
|
+
.map((match) => Number.parseInt(match[1], 10))
|
|
1723
|
+
.filter((value) => Number.isFinite(value));
|
|
1724
|
+
const next = (numbers.length > 0 ? Math.max(...numbers) : 0) + 1;
|
|
1725
|
+
return path.join(suggestionsDir, `sds_suggestions${next}.md`);
|
|
1726
|
+
}
|
|
1727
|
+
rankSdsSuggestionCandidates(candidates, required, preferred) {
|
|
1728
|
+
const requiredCaps = Array.from(new Set(required));
|
|
1729
|
+
const preferredCaps = Array.from(new Set(preferred));
|
|
1730
|
+
const scored = candidates
|
|
1731
|
+
.filter((candidate) => candidate.healthStatus !== "unreachable")
|
|
1732
|
+
.map((candidate) => {
|
|
1733
|
+
const caps = Array.from(new Set(candidate.capabilities ?? []));
|
|
1734
|
+
const requiredMatches = requiredCaps.filter((cap) => caps.includes(cap)).length;
|
|
1735
|
+
const preferredMatches = preferredCaps.filter((cap) => caps.includes(cap)).length;
|
|
1736
|
+
const hasRequired = requiredCaps.length === 0 || requiredMatches === requiredCaps.length;
|
|
1737
|
+
const rating = Number(candidate.agent.rating ?? 0);
|
|
1738
|
+
const reasoning = Number(candidate.agent.reasoningRating ?? rating);
|
|
1739
|
+
const cost = Number.isFinite(candidate.agent.costPerMillion)
|
|
1740
|
+
? Number(candidate.agent.costPerMillion)
|
|
1741
|
+
: Number.POSITIVE_INFINITY;
|
|
1742
|
+
return { candidate, requiredMatches, preferredMatches, hasRequired, rating, reasoning, cost };
|
|
1743
|
+
});
|
|
1744
|
+
scored.sort((a, b) => {
|
|
1745
|
+
if (a.hasRequired !== b.hasRequired)
|
|
1746
|
+
return a.hasRequired ? -1 : 1;
|
|
1747
|
+
if (a.requiredMatches !== b.requiredMatches)
|
|
1748
|
+
return b.requiredMatches - a.requiredMatches;
|
|
1749
|
+
if (a.preferredMatches !== b.preferredMatches)
|
|
1750
|
+
return b.preferredMatches - a.preferredMatches;
|
|
1751
|
+
if (a.rating !== b.rating)
|
|
1752
|
+
return b.rating - a.rating;
|
|
1753
|
+
if (a.reasoning !== b.reasoning)
|
|
1754
|
+
return b.reasoning - a.reasoning;
|
|
1755
|
+
if (a.cost !== b.cost)
|
|
1756
|
+
return a.cost - b.cost;
|
|
1757
|
+
return (a.candidate.agent.slug ?? a.candidate.agent.id).localeCompare(b.candidate.agent.slug ?? b.candidate.agent.id);
|
|
1758
|
+
});
|
|
1759
|
+
return scored.map((row) => row.candidate);
|
|
1760
|
+
}
|
|
1761
|
+
async selectSdsSuggestionAgents(input) {
|
|
1762
|
+
const required = ["doc_generation", "docdex_query"];
|
|
1763
|
+
const preferred = ["sds_writing", "spec_generation", "code_review", "multiple_draft_generation"];
|
|
1764
|
+
const warnings = [];
|
|
1765
|
+
const candidates = this.rankSdsSuggestionCandidates(await this.collectDocgenCandidates(), required, preferred);
|
|
1766
|
+
if (candidates.length === 0) {
|
|
1767
|
+
throw new Error("No healthy agents are available for SDS suggestions.");
|
|
1768
|
+
}
|
|
1769
|
+
const findByName = async (name) => {
|
|
1770
|
+
try {
|
|
1771
|
+
return await this.agentService.resolveAgent(name);
|
|
1772
|
+
}
|
|
1773
|
+
catch {
|
|
1774
|
+
throw new Error(`Unable to resolve agent: ${name}`);
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
const rankedIds = new Set(candidates.map((candidate) => candidate.agent.id));
|
|
1778
|
+
const rankedSlugs = new Set(candidates.map((candidate) => candidate.agent.slug).filter(Boolean));
|
|
1779
|
+
const isRankedCandidate = (agent) => rankedIds.has(agent.id) || (typeof agent.slug === "string" && rankedSlugs.has(agent.slug));
|
|
1780
|
+
const reviewer = input.reviewAgentName
|
|
1781
|
+
? await findByName(input.reviewAgentName)
|
|
1782
|
+
: candidates[0].agent;
|
|
1783
|
+
const fixer = input.fixAgentName
|
|
1784
|
+
? await findByName(input.fixAgentName)
|
|
1785
|
+
: candidates.find((candidate) => candidate.agent.id !== reviewer.id)?.agent ?? reviewer;
|
|
1786
|
+
if (input.reviewAgentName && !isRankedCandidate(reviewer)) {
|
|
1787
|
+
warnings.push(`Review agent override ${reviewer.slug ?? reviewer.id} is not in the healthy ranked candidate set; proceeding due explicit override.`);
|
|
1788
|
+
}
|
|
1789
|
+
if (input.fixAgentName && !isRankedCandidate(fixer)) {
|
|
1790
|
+
warnings.push(`Fix agent override ${fixer.slug ?? fixer.id} is not in the healthy ranked candidate set; proceeding due explicit override.`);
|
|
1791
|
+
}
|
|
1792
|
+
if (reviewer.id === fixer.id) {
|
|
1793
|
+
warnings.push(`Reviewer and fixer resolved to the same agent (${reviewer.slug ?? reviewer.id}); continuing with single-agent loop.`);
|
|
1794
|
+
}
|
|
1795
|
+
return { reviewer, fixer, warnings };
|
|
1796
|
+
}
|
|
1797
|
+
buildSdsSuggestionsReviewPrompt(input) {
|
|
1798
|
+
const contextBlocks = input.context.blocks
|
|
1799
|
+
.slice(0, 5)
|
|
1800
|
+
.map((block) => `- ${block.summary}`)
|
|
1801
|
+
.join("\n");
|
|
1802
|
+
return [
|
|
1803
|
+
"You are the SDS reviewer agent.",
|
|
1804
|
+
"Review the SDS against surrounding project documentation and produce strict remediation guidance.",
|
|
1805
|
+
"Focus on:",
|
|
1806
|
+
"- open questions and optimum answers aligned to the rest of documentation,",
|
|
1807
|
+
"- inconsistencies,",
|
|
1808
|
+
"- issues/risks,",
|
|
1809
|
+
"- possible enhancements.",
|
|
1810
|
+
"",
|
|
1811
|
+
"Return output in two parts:",
|
|
1812
|
+
"1) A JSON object inside a ```json code block with schema:",
|
|
1813
|
+
'{ "result": "PASS|FAIL", "issueCount": number, "summary": "short summary" }',
|
|
1814
|
+
"2) Markdown sections:",
|
|
1815
|
+
"## Open Questions and Optimum Answers",
|
|
1816
|
+
"## Inconsistencies",
|
|
1817
|
+
"## Issues",
|
|
1818
|
+
"## Enhancements",
|
|
1819
|
+
"## Suggested Fixes",
|
|
1820
|
+
"",
|
|
1821
|
+
"Set result=PASS and issueCount=0 only when there are no remaining issues.",
|
|
1822
|
+
"",
|
|
1823
|
+
`Iteration: ${input.iteration}`,
|
|
1824
|
+
`Project: ${input.projectKey ?? "n/a"}`,
|
|
1825
|
+
`SDS Path: ${input.sdsPath}`,
|
|
1826
|
+
"",
|
|
1827
|
+
`Context Summary: ${input.context.summary}`,
|
|
1828
|
+
contextBlocks ? `Context Blocks:\n${contextBlocks}` : "Context Blocks: none",
|
|
1829
|
+
"",
|
|
1830
|
+
"SDS Content:",
|
|
1831
|
+
input.sdsContent,
|
|
1832
|
+
].join("\n");
|
|
1833
|
+
}
|
|
1834
|
+
parseSdsSuggestionsReviewResult(raw) {
|
|
1835
|
+
const parsed = extractJsonObject(raw);
|
|
1836
|
+
const parsedResult = String(parsed?.result ?? parsed?.status ?? parsed?.outcome ?? "")
|
|
1837
|
+
.trim()
|
|
1838
|
+
.toUpperCase();
|
|
1839
|
+
const parsedIssueCount = Number(parsed?.issueCount ?? parsed?.issues ?? parsed?.issue_count ?? NaN);
|
|
1840
|
+
const summary = String(parsed?.summary ?? "").trim();
|
|
1841
|
+
const normalized = raw.trim();
|
|
1842
|
+
const hasNoIssuesSignal = /\bRESULT\s*:\s*PASS\b/i.test(normalized) ||
|
|
1843
|
+
/\bno (remaining )?(issues|inconsistencies|open questions)\b/i.test(normalized) ||
|
|
1844
|
+
/\ball clear\b/i.test(normalized);
|
|
1845
|
+
const preliminaryResult = parsedResult === "PASS"
|
|
1846
|
+
? "PASS"
|
|
1847
|
+
: parsedResult === "FAIL"
|
|
1848
|
+
? "FAIL"
|
|
1849
|
+
: hasNoIssuesSignal
|
|
1850
|
+
? "PASS"
|
|
1851
|
+
: "FAIL";
|
|
1852
|
+
const issueCount = Number.isFinite(parsedIssueCount)
|
|
1853
|
+
? Math.max(0, Math.floor(parsedIssueCount))
|
|
1854
|
+
: preliminaryResult === "PASS"
|
|
1855
|
+
? 0
|
|
1856
|
+
: 1;
|
|
1857
|
+
const result = preliminaryResult === "PASS" && issueCount === 0 ? "PASS" : "FAIL";
|
|
1858
|
+
return {
|
|
1859
|
+
result,
|
|
1860
|
+
issueCount,
|
|
1861
|
+
summary: summary || (result === "PASS" ? "No issues found." : "Issues detected."),
|
|
1862
|
+
markdown: normalized,
|
|
1863
|
+
raw,
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
buildSdsSuggestionsFixPrompt(input) {
|
|
1867
|
+
return [
|
|
1868
|
+
"You are the SDS fixer agent.",
|
|
1869
|
+
"Apply all valid review suggestions to the SDS.",
|
|
1870
|
+
"Integrate optimum answers for open questions and resolve inconsistencies/issues.",
|
|
1871
|
+
"Preserve valid existing sections and improve structure where needed.",
|
|
1872
|
+
"Return ONLY the full revised SDS markdown (no prose wrapper, no JSON).",
|
|
1873
|
+
"",
|
|
1874
|
+
`Project: ${input.projectKey ?? "n/a"}`,
|
|
1875
|
+
`SDS Path: ${input.sdsPath}`,
|
|
1876
|
+
"",
|
|
1877
|
+
"Review Suggestions:",
|
|
1878
|
+
input.suggestions,
|
|
1879
|
+
"",
|
|
1880
|
+
"Current SDS:",
|
|
1881
|
+
input.currentSds,
|
|
1882
|
+
].join("\n");
|
|
1883
|
+
}
|
|
1884
|
+
renderSdsSuggestionArtifact(input) {
|
|
1885
|
+
return [
|
|
1886
|
+
`# SDS Suggestions ${input.iteration}`,
|
|
1887
|
+
"",
|
|
1888
|
+
`- Timestamp: ${input.timestamp}`,
|
|
1889
|
+
`- Iteration: ${input.iteration}`,
|
|
1890
|
+
`- Reviewer Agent: ${input.reviewer.slug ?? input.reviewer.id}`,
|
|
1891
|
+
`- Fixer Agent: ${input.fixer.slug ?? input.fixer.id}`,
|
|
1892
|
+
`- SDS Path: ${input.sdsPath}`,
|
|
1893
|
+
`- Result: ${input.review.result}`,
|
|
1894
|
+
`- Issue Count: ${input.review.issueCount}`,
|
|
1895
|
+
`- Job ID: ${input.jobId}`,
|
|
1896
|
+
`- Command Run ID: ${input.commandRunId}`,
|
|
1897
|
+
"",
|
|
1898
|
+
"## Review Summary",
|
|
1899
|
+
input.review.summary,
|
|
1900
|
+
"",
|
|
1901
|
+
"## Reviewer Output",
|
|
1902
|
+
input.review.markdown || "(empty reviewer output)",
|
|
1903
|
+
"",
|
|
1904
|
+
"## Fix Application",
|
|
1905
|
+
`- Applied: ${input.fixApplied ? "yes" : "no"}`,
|
|
1906
|
+
`- Summary: ${input.fixSummary}`,
|
|
1907
|
+
"",
|
|
1908
|
+
].join("\n");
|
|
1909
|
+
}
|
|
1613
1910
|
async loadSdsTemplate(templateName) {
|
|
1614
1911
|
const names = templateName
|
|
1615
1912
|
? [templateName.replace(/\.md$/i, "")]
|
|
@@ -3885,4 +4182,242 @@ export class DocsService {
|
|
|
3885
4182
|
throw error;
|
|
3886
4183
|
}
|
|
3887
4184
|
}
|
|
4185
|
+
async generateSdsSuggestions(options) {
|
|
4186
|
+
const warnings = [];
|
|
4187
|
+
await this.checkSdsDocdexProfile(warnings);
|
|
4188
|
+
const assembler = new DocContextAssembler(this.docdex, this.workspace);
|
|
4189
|
+
const stream = options.agentStream === true;
|
|
4190
|
+
const maxIterations = this.normalizeMaxIterations(options.maxIterations);
|
|
4191
|
+
const commandRun = await this.jobService.startCommandRun("docs-sds-suggestions", options.projectKey);
|
|
4192
|
+
const job = await this.jobService.startJob("sds_suggestions", commandRun.id, options.projectKey, {
|
|
4193
|
+
commandName: commandRun.commandName,
|
|
4194
|
+
payload: {
|
|
4195
|
+
projectKey: options.projectKey,
|
|
4196
|
+
sdsPath: options.sdsPath,
|
|
4197
|
+
reviewAgentName: options.reviewAgentName,
|
|
4198
|
+
fixAgentName: options.fixAgentName,
|
|
4199
|
+
maxIterations,
|
|
4200
|
+
},
|
|
4201
|
+
});
|
|
4202
|
+
try {
|
|
4203
|
+
const context = await assembler.buildSdsContext({ projectKey: options.projectKey });
|
|
4204
|
+
warnings.push(...context.warnings);
|
|
4205
|
+
const pathResolution = await this.resolveSdsSuggestionsPath(options.projectKey, options.sdsPath);
|
|
4206
|
+
const sdsPath = pathResolution.path;
|
|
4207
|
+
let sdsContent = await fs.readFile(sdsPath, "utf8");
|
|
4208
|
+
const suggestionsDir = await this.ensureSuggestionsDir();
|
|
4209
|
+
const agentSelection = await this.selectSdsSuggestionAgents({
|
|
4210
|
+
reviewAgentName: options.reviewAgentName,
|
|
4211
|
+
fixAgentName: options.fixAgentName,
|
|
4212
|
+
});
|
|
4213
|
+
warnings.push(...agentSelection.warnings);
|
|
4214
|
+
const reviewer = agentSelection.reviewer;
|
|
4215
|
+
const fixer = agentSelection.fixer;
|
|
4216
|
+
const suggestionFiles = [];
|
|
4217
|
+
let iterations = 0;
|
|
4218
|
+
let finalStatus = "max_iterations_reached";
|
|
4219
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
4220
|
+
stage: "sds_loaded",
|
|
4221
|
+
timestamp: nowIso(),
|
|
4222
|
+
details: {
|
|
4223
|
+
sdsPath,
|
|
4224
|
+
attemptedPaths: pathResolution.attempted,
|
|
4225
|
+
maxIterations,
|
|
4226
|
+
},
|
|
4227
|
+
});
|
|
4228
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
4229
|
+
stage: "agents_selected",
|
|
4230
|
+
timestamp: nowIso(),
|
|
4231
|
+
details: {
|
|
4232
|
+
reviewerAgent: reviewer.slug ?? reviewer.id,
|
|
4233
|
+
fixerAgent: fixer.slug ?? fixer.id,
|
|
4234
|
+
},
|
|
4235
|
+
});
|
|
4236
|
+
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
4237
|
+
iterations = iteration;
|
|
4238
|
+
const reviewPrompt = this.buildSdsSuggestionsReviewPrompt({
|
|
4239
|
+
iteration,
|
|
4240
|
+
projectKey: options.projectKey,
|
|
4241
|
+
sdsPath,
|
|
4242
|
+
sdsContent,
|
|
4243
|
+
context,
|
|
4244
|
+
});
|
|
4245
|
+
const reviewInvoke = await this.invokeAgent(reviewer, reviewPrompt, stream, job.id, options.onToken);
|
|
4246
|
+
const reviewResult = this.parseSdsSuggestionsReviewResult(reviewInvoke.output);
|
|
4247
|
+
const suggestionPath = await this.nextSuggestionsFilePath(suggestionsDir);
|
|
4248
|
+
let fixApplied = false;
|
|
4249
|
+
let fixSummary = "Fix pending.";
|
|
4250
|
+
const preliminaryArtifact = this.renderSdsSuggestionArtifact({
|
|
4251
|
+
timestamp: nowIso(),
|
|
4252
|
+
iteration,
|
|
4253
|
+
reviewer,
|
|
4254
|
+
fixer,
|
|
4255
|
+
sdsPath,
|
|
4256
|
+
review: reviewResult,
|
|
4257
|
+
fixApplied,
|
|
4258
|
+
fixSummary,
|
|
4259
|
+
jobId: job.id,
|
|
4260
|
+
commandRunId: commandRun.id,
|
|
4261
|
+
});
|
|
4262
|
+
await fs.writeFile(suggestionPath, preliminaryArtifact, "utf8");
|
|
4263
|
+
suggestionFiles.push(suggestionPath);
|
|
4264
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
4265
|
+
stage: "iteration_reviewed",
|
|
4266
|
+
timestamp: nowIso(),
|
|
4267
|
+
details: {
|
|
4268
|
+
iteration,
|
|
4269
|
+
result: reviewResult.result,
|
|
4270
|
+
issueCount: reviewResult.issueCount,
|
|
4271
|
+
suggestionPath,
|
|
4272
|
+
},
|
|
4273
|
+
});
|
|
4274
|
+
if (reviewResult.result === "PASS" && reviewResult.issueCount === 0) {
|
|
4275
|
+
finalStatus = "pass";
|
|
4276
|
+
fixSummary = "Reviewer found no issues.";
|
|
4277
|
+
const artifact = this.renderSdsSuggestionArtifact({
|
|
4278
|
+
timestamp: nowIso(),
|
|
4279
|
+
iteration,
|
|
4280
|
+
reviewer,
|
|
4281
|
+
fixer,
|
|
4282
|
+
sdsPath,
|
|
4283
|
+
review: reviewResult,
|
|
4284
|
+
fixApplied,
|
|
4285
|
+
fixSummary,
|
|
4286
|
+
jobId: job.id,
|
|
4287
|
+
commandRunId: commandRun.id,
|
|
4288
|
+
});
|
|
4289
|
+
await fs.writeFile(suggestionPath, artifact, "utf8");
|
|
4290
|
+
break;
|
|
4291
|
+
}
|
|
4292
|
+
if (options.dryRun) {
|
|
4293
|
+
finalStatus = "dry_run";
|
|
4294
|
+
fixSummary = "Dry-run enabled; no SDS changes applied.";
|
|
4295
|
+
const artifact = this.renderSdsSuggestionArtifact({
|
|
4296
|
+
timestamp: nowIso(),
|
|
4297
|
+
iteration,
|
|
4298
|
+
reviewer,
|
|
4299
|
+
fixer,
|
|
4300
|
+
sdsPath,
|
|
4301
|
+
review: reviewResult,
|
|
4302
|
+
fixApplied,
|
|
4303
|
+
fixSummary,
|
|
4304
|
+
jobId: job.id,
|
|
4305
|
+
commandRunId: commandRun.id,
|
|
4306
|
+
});
|
|
4307
|
+
await fs.writeFile(suggestionPath, artifact, "utf8");
|
|
4308
|
+
break;
|
|
4309
|
+
}
|
|
4310
|
+
const suggestionsArtifact = await fs.readFile(suggestionPath, "utf8");
|
|
4311
|
+
const fixPrompt = this.buildSdsSuggestionsFixPrompt({
|
|
4312
|
+
projectKey: options.projectKey,
|
|
4313
|
+
sdsPath,
|
|
4314
|
+
currentSds: sdsContent,
|
|
4315
|
+
suggestions: suggestionsArtifact,
|
|
4316
|
+
});
|
|
4317
|
+
const fixInvoke = await this.invokeAgent(fixer, fixPrompt, stream, job.id, options.onToken);
|
|
4318
|
+
const candidateSds = extractMarkdownFromOutput(fixInvoke.output).trim();
|
|
4319
|
+
if (candidateSds.length < 80) {
|
|
4320
|
+
fixSummary = "Fixer output was too short; previous SDS kept.";
|
|
4321
|
+
warnings.push(`Iteration ${iteration}: fixer output too short; no SDS write performed.`);
|
|
4322
|
+
}
|
|
4323
|
+
else if (candidateSds === sdsContent.trim()) {
|
|
4324
|
+
fixSummary = "Fixer returned unchanged SDS content.";
|
|
4325
|
+
}
|
|
4326
|
+
else {
|
|
4327
|
+
await fs.writeFile(sdsPath, candidateSds, "utf8");
|
|
4328
|
+
sdsContent = candidateSds;
|
|
4329
|
+
fixApplied = true;
|
|
4330
|
+
fixSummary = "SDS updated from fixer output.";
|
|
4331
|
+
if (context.docdexAvailable) {
|
|
4332
|
+
try {
|
|
4333
|
+
await this.registerSds(sdsPath, sdsContent, options.projectKey);
|
|
4334
|
+
}
|
|
4335
|
+
catch (error) {
|
|
4336
|
+
warnings.push(`Iteration ${iteration}: docdex register skipped: ${error.message}`);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
const artifact = this.renderSdsSuggestionArtifact({
|
|
4341
|
+
timestamp: nowIso(),
|
|
4342
|
+
iteration,
|
|
4343
|
+
reviewer,
|
|
4344
|
+
fixer,
|
|
4345
|
+
sdsPath,
|
|
4346
|
+
review: reviewResult,
|
|
4347
|
+
fixApplied,
|
|
4348
|
+
fixSummary,
|
|
4349
|
+
jobId: job.id,
|
|
4350
|
+
commandRunId: commandRun.id,
|
|
4351
|
+
});
|
|
4352
|
+
await fs.writeFile(suggestionPath, artifact, "utf8");
|
|
4353
|
+
await this.jobService.writeCheckpoint(job.id, {
|
|
4354
|
+
stage: "iteration_fixed",
|
|
4355
|
+
timestamp: nowIso(),
|
|
4356
|
+
details: {
|
|
4357
|
+
iteration,
|
|
4358
|
+
result: reviewResult.result,
|
|
4359
|
+
issueCount: reviewResult.issueCount,
|
|
4360
|
+
fixApplied,
|
|
4361
|
+
suggestionPath,
|
|
4362
|
+
},
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
if (finalStatus === "max_iterations_reached") {
|
|
4366
|
+
warnings.push(`SDS suggestions stopped at max iterations (${maxIterations}) before reviewer returned PASS.`);
|
|
4367
|
+
}
|
|
4368
|
+
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
4369
|
+
payload: {
|
|
4370
|
+
sdsPath,
|
|
4371
|
+
suggestionsDir,
|
|
4372
|
+
suggestionFiles,
|
|
4373
|
+
reviewerAgentId: reviewer.id,
|
|
4374
|
+
fixerAgentId: fixer.id,
|
|
4375
|
+
iterations,
|
|
4376
|
+
finalStatus,
|
|
4377
|
+
},
|
|
4378
|
+
});
|
|
4379
|
+
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
4380
|
+
if (options.rateAgents) {
|
|
4381
|
+
try {
|
|
4382
|
+
const ratingService = await this.ensureRatingService();
|
|
4383
|
+
await ratingService.rate({
|
|
4384
|
+
workspace: this.workspace,
|
|
4385
|
+
agentId: reviewer.id,
|
|
4386
|
+
commandName: "docs-sds-suggestions-review",
|
|
4387
|
+
jobId: job.id,
|
|
4388
|
+
commandRunId: commandRun.id,
|
|
4389
|
+
});
|
|
4390
|
+
if (fixer.id !== reviewer.id) {
|
|
4391
|
+
await ratingService.rate({
|
|
4392
|
+
workspace: this.workspace,
|
|
4393
|
+
agentId: fixer.id,
|
|
4394
|
+
commandName: "docs-sds-suggestions-fix",
|
|
4395
|
+
jobId: job.id,
|
|
4396
|
+
commandRunId: commandRun.id,
|
|
4397
|
+
});
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
catch (error) {
|
|
4401
|
+
warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
return {
|
|
4405
|
+
jobId: job.id,
|
|
4406
|
+
commandRunId: commandRun.id,
|
|
4407
|
+
sdsPath,
|
|
4408
|
+
suggestionsDir,
|
|
4409
|
+
suggestionFiles,
|
|
4410
|
+
reviewerAgentId: reviewer.id,
|
|
4411
|
+
fixerAgentId: fixer.id,
|
|
4412
|
+
iterations,
|
|
4413
|
+
finalStatus,
|
|
4414
|
+
warnings,
|
|
4415
|
+
};
|
|
4416
|
+
}
|
|
4417
|
+
catch (error) {
|
|
4418
|
+
await this.jobService.updateJobStatus(job.id, "failed", { errorSummary: error.message });
|
|
4419
|
+
await this.jobService.finishCommandRun(commandRun.id, "failed", error.message);
|
|
4420
|
+
throw error;
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
3888
4423
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenQuestionsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/OpenQuestionsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAA+B,MAAM,mBAAmB,CAAC;AAElF,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,uBAAuB,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"OpenQuestionsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/OpenQuestionsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAA+B,MAAM,mBAAmB,CAAC;AAElF,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,uBAAuB,CAAC;CACpC;AAmMD,eAAO,MAAM,oBAAoB,GAC/B,OAAO,sBAAsB,KAC5B,OAAO,CAAC,gBAAgB,CA+C1B,CAAC"}
|
|
@@ -32,7 +32,9 @@ const IMPLICIT_PATTERNS = [
|
|
|
32
32
|
const isFenceLine = (line) => /^```|^~~~/.test(line.trim());
|
|
33
33
|
const isExampleHeading = (heading) => /example|sample/i.test(heading);
|
|
34
34
|
const isOpenQuestionsHeading = (heading) => /open (questions?|issues?|items?)|unresolved questions?/i.test(heading);
|
|
35
|
-
const
|
|
35
|
+
const MANAGED_PREFLIGHT_START = "<!-- mcoda:sds-preflight:start -->";
|
|
36
|
+
const MANAGED_PREFLIGHT_END = "<!-- mcoda:sds-preflight:end -->";
|
|
37
|
+
const RESOLVED_HINTS = [/^[-*+\d.)\s]*resolved[:\]]/i, /^[-*+\d.)\s]*decision:/i, /\[resolved\]/i];
|
|
36
38
|
const isResolvedLine = (line) => RESOLVED_HINTS.some((pattern) => pattern.test(line.trim()));
|
|
37
39
|
const normalizeQuestion = (text) => text
|
|
38
40
|
.toLowerCase()
|
|
@@ -105,6 +107,7 @@ const extractQuestions = async (record) => {
|
|
|
105
107
|
const lines = content.split(/\r?\n/);
|
|
106
108
|
const questions = [];
|
|
107
109
|
let inFence = false;
|
|
110
|
+
let inManagedPreflight = false;
|
|
108
111
|
let allowSection = false;
|
|
109
112
|
let inOpenSection = false;
|
|
110
113
|
let currentHeading;
|
|
@@ -113,6 +116,14 @@ const extractQuestions = async (record) => {
|
|
|
113
116
|
const trimmed = line.trim();
|
|
114
117
|
if (!trimmed)
|
|
115
118
|
continue;
|
|
119
|
+
if (trimmed === MANAGED_PREFLIGHT_START) {
|
|
120
|
+
inManagedPreflight = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (trimmed === MANAGED_PREFLIGHT_END) {
|
|
124
|
+
inManagedPreflight = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
116
127
|
if (isFenceLine(trimmed)) {
|
|
117
128
|
inFence = !inFence;
|
|
118
129
|
continue;
|
|
@@ -124,7 +135,7 @@ const extractQuestions = async (record) => {
|
|
|
124
135
|
inOpenSection = currentHeading ? isOpenQuestionsHeading(currentHeading) : false;
|
|
125
136
|
continue;
|
|
126
137
|
}
|
|
127
|
-
if (inFence || allowSection)
|
|
138
|
+
if (inFence || inManagedPreflight || allowSection)
|
|
128
139
|
continue;
|
|
129
140
|
if (isResolvedLine(trimmed))
|
|
130
141
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SdsNoUnresolvedItemsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/SdsNoUnresolvedItemsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAElE,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,uBAAuB,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"SdsNoUnresolvedItemsGate.d.ts","sourceRoot":"","sources":["../../../../../src/services/docs/review/gates/SdsNoUnresolvedItemsGate.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAe,MAAM,mBAAmB,CAAC;AAElE,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,uBAAuB,CAAC;CACpC;AA8CD,eAAO,MAAM,2BAA2B,GACtC,OAAO,6BAA6B,KACnC,OAAO,CAAC,gBAAgB,CAgG1B,CAAC"}
|
|
@@ -11,6 +11,8 @@ const UNRESOLVED_PATTERNS = [
|
|
|
11
11
|
const OPEN_QUESTIONS_HEADING = /open questions?/i;
|
|
12
12
|
const RESOLVED_LINE = /^[-*+\d.)\s]*resolved:/i;
|
|
13
13
|
const NO_OPEN_ITEMS_LINE = /no unresolved questions remain|no open questions remain/i;
|
|
14
|
+
const MANAGED_PREFLIGHT_START = "<!-- mcoda:sds-preflight:start -->";
|
|
15
|
+
const MANAGED_PREFLIGHT_END = "<!-- mcoda:sds-preflight:end -->";
|
|
14
16
|
const isFenceLine = (line) => /^```|^~~~/.test(line.trim());
|
|
15
17
|
const buildIssue = (input) => ({
|
|
16
18
|
id: input.id,
|
|
@@ -46,17 +48,26 @@ export const runSdsNoUnresolvedItemsGate = async (input) => {
|
|
|
46
48
|
const content = await fs.readFile(sds.path, "utf8");
|
|
47
49
|
const lines = content.split(/\r?\n/);
|
|
48
50
|
let inFence = false;
|
|
51
|
+
let inManagedPreflight = false;
|
|
49
52
|
let inOpenQuestions = false;
|
|
50
53
|
for (let i = 0; i < lines.length; i += 1) {
|
|
51
54
|
const line = lines[i] ?? "";
|
|
52
55
|
const trimmed = line.trim();
|
|
53
56
|
if (!trimmed)
|
|
54
57
|
continue;
|
|
58
|
+
if (trimmed === MANAGED_PREFLIGHT_START) {
|
|
59
|
+
inManagedPreflight = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (trimmed === MANAGED_PREFLIGHT_END) {
|
|
63
|
+
inManagedPreflight = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
55
66
|
if (isFenceLine(trimmed)) {
|
|
56
67
|
inFence = !inFence;
|
|
57
68
|
continue;
|
|
58
69
|
}
|
|
59
|
-
if (inFence)
|
|
70
|
+
if (inFence || inManagedPreflight)
|
|
60
71
|
continue;
|
|
61
72
|
const heading = trimmed.match(/^#{1,6}\s+(.*)$/);
|
|
62
73
|
if (heading) {
|