@mcoda/core 0.1.9 → 0.1.12
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 +2 -2
- package/dist/api/AgentsApi.d.ts +1 -0
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +136 -11
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +6 -0
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +7 -0
- package/dist/services/agents/AgentRatingService.d.ts +19 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -1
- package/dist/services/agents/AgentRatingService.js +66 -2
- package/dist/services/agents/GatewayAgentService.d.ts +8 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +462 -65
- package/dist/services/agents/GatewayHandoff.d.ts +5 -1
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -1
- package/dist/services/agents/GatewayHandoff.js +65 -32
- package/dist/services/agents/RoutingService.d.ts +1 -0
- package/dist/services/agents/RoutingService.d.ts.map +1 -1
- package/dist/services/agents/RoutingService.js +4 -4
- package/dist/services/backlog/BacklogService.d.ts +23 -0
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +62 -7
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
- package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +16 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +529 -73
- package/dist/services/docs/DocInventory.d.ts +11 -0
- package/dist/services/docs/DocInventory.d.ts.map +1 -0
- package/dist/services/docs/DocInventory.js +230 -0
- package/dist/services/docs/DocgenRunContext.d.ts +59 -0
- package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
- package/dist/services/docs/DocgenRunContext.js +4 -0
- package/dist/services/docs/DocsService.d.ts +59 -2
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1701 -48
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
- package/dist/services/docs/patch/DocPatchEngine.js +331 -0
- package/dist/services/docs/review/Glossary.d.ts +16 -0
- package/dist/services/docs/review/Glossary.d.ts.map +1 -0
- package/dist/services/docs/review/Glossary.js +47 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportSchema.js +47 -0
- package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
- package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewTypes.js +94 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
- package/dist/services/docs/review/glossary.json +47 -0
- package/dist/services/estimate/EstimateService.d.ts +2 -0
- package/dist/services/estimate/EstimateService.d.ts.map +1 -1
- package/dist/services/estimate/EstimateService.js +66 -18
- package/dist/services/estimate/VelocityService.d.ts +4 -0
- package/dist/services/estimate/VelocityService.d.ts.map +1 -1
- package/dist/services/estimate/VelocityService.js +179 -36
- package/dist/services/estimate/types.d.ts +1 -0
- package/dist/services/estimate/types.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.d.ts +71 -4
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.js +1695 -328
- package/dist/services/execution/QaApiRunner.d.ts +30 -0
- package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
- package/dist/services/execution/QaApiRunner.js +881 -0
- package/dist/services/execution/QaFollowupService.d.ts +1 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +8 -2
- package/dist/services/execution/QaPlanValidator.d.ts +10 -0
- package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
- package/dist/services/execution/QaPlanValidator.js +128 -0
- package/dist/services/execution/QaProfileService.d.ts +21 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +214 -29
- package/dist/services/execution/QaTasksService.d.ts +41 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +2851 -500
- package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
- package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
- package/dist/services/execution/QaTestCommandBuilder.js +495 -0
- package/dist/services/execution/TaskSelectionService.d.ts +4 -2
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +144 -28
- package/dist/services/execution/TaskStateService.d.ts +19 -6
- package/dist/services/execution/TaskStateService.d.ts.map +1 -1
- package/dist/services/execution/TaskStateService.js +128 -13
- package/dist/services/execution/WorkOnTasksService.d.ts +19 -2
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +3913 -1225
- package/dist/services/jobs/JobInsightsService.d.ts +4 -0
- package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
- package/dist/services/jobs/JobInsightsService.js +51 -5
- package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
- package/dist/services/jobs/JobResumeService.js +23 -10
- package/dist/services/jobs/JobService.d.ts +56 -4
- package/dist/services/jobs/JobService.d.ts.map +1 -1
- package/dist/services/jobs/JobService.js +232 -1
- package/dist/services/openapi/OpenApiService.d.ts +41 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +889 -98
- package/dist/services/planning/CreateTasksService.d.ts +15 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +311 -6
- package/dist/services/planning/RefineTasksService.d.ts +4 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +225 -24
- package/dist/services/review/CodeReviewService.d.ts +4 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +778 -232
- package/dist/services/review/ReviewNormalizer.d.ts +9 -0
- package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
- package/dist/services/review/ReviewNormalizer.js +147 -0
- package/dist/services/shared/AuthErrors.d.ts +3 -0
- package/dist/services/shared/AuthErrors.d.ts.map +1 -0
- package/dist/services/shared/AuthErrors.js +17 -0
- package/dist/services/shared/DocdexGuidance.d.ts +7 -0
- package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
- package/dist/services/shared/DocdexGuidance.js +12 -0
- package/dist/services/shared/ProjectGuidance.d.ts +12 -1
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +64 -7
- package/dist/services/system/ToolDenylist.d.ts +13 -0
- package/dist/services/system/ToolDenylist.d.ts.map +1 -0
- package/dist/services/system/ToolDenylist.js +85 -0
- package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/dist/services/telemetry/TelemetryService.js +39 -7
- package/dist/workspace/WorkspaceManager.d.ts +22 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +203 -32
- package/package.json +6 -5
|
@@ -1,16 +1,81 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import { AgentService } from "@mcoda/agents";
|
|
4
|
+
import { DocsScaffolder } from "@mcoda/generators";
|
|
4
5
|
import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
|
|
5
6
|
import { DocdexClient } from "@mcoda/integrations";
|
|
6
7
|
import { DEFAULT_PDR_CHARACTER_PROMPT, DEFAULT_PDR_JOB_PROMPT, DEFAULT_PDR_RUNBOOK_PROMPT, } from "../../prompts/PdrPrompts.js";
|
|
7
8
|
import { DEFAULT_SDS_CHARACTER_PROMPT, DEFAULT_SDS_JOB_PROMPT, DEFAULT_SDS_RUNBOOK_PROMPT, DEFAULT_SDS_TEMPLATE, } from "../../prompts/SdsPrompts.js";
|
|
9
|
+
import { createEmptyArtifacts, } from "./DocgenRunContext.js";
|
|
10
|
+
import { buildDocInventory } from "./DocInventory.js";
|
|
11
|
+
import { DocPatchEngine, } from "./patch/DocPatchEngine.js";
|
|
12
|
+
import { runApiPathConsistencyGate } from "./review/gates/ApiPathConsistencyGate.js";
|
|
13
|
+
import { runOpenApiCoverageGate } from "./review/gates/OpenApiCoverageGate.js";
|
|
14
|
+
import { runBuildReadyCompletenessGate } from "./review/gates/BuildReadyCompletenessGate.js";
|
|
15
|
+
import { runDeploymentBlueprintGate } from "./review/gates/DeploymentBlueprintGate.js";
|
|
16
|
+
import { runPlaceholderArtifactGate } from "./review/gates/PlaceholderArtifactGate.js";
|
|
17
|
+
import { runSqlSyntaxGate } from "./review/gates/SqlSyntaxGate.js";
|
|
18
|
+
import { runSqlRequiredTablesGate } from "./review/gates/SqlRequiredTablesGate.js";
|
|
19
|
+
import { runTerminologyNormalizationGate } from "./review/gates/TerminologyNormalizationGate.js";
|
|
20
|
+
import { runOpenQuestionsGate } from "./review/gates/OpenQuestionsGate.js";
|
|
21
|
+
import { runNoMaybesGate } from "./review/gates/NoMaybesGate.js";
|
|
22
|
+
import { runRfpConsentGate } from "./review/gates/RfpConsentGate.js";
|
|
23
|
+
import { runRfpDefinitionGate } from "./review/gates/RfpDefinitionGate.js";
|
|
24
|
+
import { runPdrInterfacesGate } from "./review/gates/PdrInterfacesGate.js";
|
|
25
|
+
import { runPdrOwnershipGate } from "./review/gates/PdrOwnershipGate.js";
|
|
26
|
+
import { runPdrOpenQuestionsGate } from "./review/gates/PdrOpenQuestionsGate.js";
|
|
27
|
+
import { runSdsDecisionsGate } from "./review/gates/SdsDecisionsGate.js";
|
|
28
|
+
import { runSdsPolicyTelemetryGate } from "./review/gates/SdsPolicyTelemetryGate.js";
|
|
29
|
+
import { runSdsOpsGate } from "./review/gates/SdsOpsGate.js";
|
|
30
|
+
import { runSdsAdaptersGate } from "./review/gates/SdsAdaptersGate.js";
|
|
31
|
+
import { runAdminOpenApiSpecGate } from "./review/gates/AdminOpenApiSpecGate.js";
|
|
32
|
+
import { runOpenApiSchemaSanityGate } from "./review/gates/OpenApiSchemaSanityGate.js";
|
|
33
|
+
import { renderReviewReport } from "./review/ReviewReportRenderer.js";
|
|
34
|
+
import { serializeReviewReport, } from "./review/ReviewReportSchema.js";
|
|
35
|
+
import { aggregateReviewOutcome, } from "./review/ReviewTypes.js";
|
|
36
|
+
import { findAdminSurfaceMentions, validateOpenApiSchemaContent } from "../openapi/OpenApiService.js";
|
|
8
37
|
import { JobService } from "../jobs/JobService.js";
|
|
38
|
+
import { cleanupWorkspaceStateDirs, resolveDocgenStatePath, } from "../../workspace/WorkspaceManager.js";
|
|
9
39
|
import { RoutingService } from "../agents/RoutingService.js";
|
|
10
|
-
import { AgentRatingService } from "../agents/AgentRatingService.js";
|
|
40
|
+
import { AgentRatingService, selectBestAgentForCapabilities, } from "../agents/AgentRatingService.js";
|
|
41
|
+
import { DocAlignmentPatcher } from "./alignment/DocAlignmentPatcher.js";
|
|
42
|
+
import { ToolDenylist } from "../system/ToolDenylist.js";
|
|
11
43
|
const ensureDir = async (targetPath) => {
|
|
12
44
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
13
45
|
};
|
|
46
|
+
const parseDelimitedList = (value) => {
|
|
47
|
+
if (!value)
|
|
48
|
+
return [];
|
|
49
|
+
return value
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((entry) => entry.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
};
|
|
54
|
+
const ALWAYS_BLOCKING_GATES = new Set([
|
|
55
|
+
"gate-placeholder-artifacts",
|
|
56
|
+
"gate-no-maybes",
|
|
57
|
+
"gate-pdr-open-questions-quality",
|
|
58
|
+
]);
|
|
59
|
+
const BUILD_READY_ONLY_GATES = new Set([
|
|
60
|
+
"gate-api-path-consistency",
|
|
61
|
+
"gate-openapi-schema-sanity",
|
|
62
|
+
"gate-openapi-coverage",
|
|
63
|
+
"gate-sql-syntax",
|
|
64
|
+
"gate-sql-required-tables",
|
|
65
|
+
"gate-admin-openapi-spec",
|
|
66
|
+
"gate-terminology-normalization",
|
|
67
|
+
"gate-open-questions",
|
|
68
|
+
"gate-rfp-consent-contradictions",
|
|
69
|
+
"gate-rfp-definition-coverage",
|
|
70
|
+
"gate-pdr-interfaces-pipeline",
|
|
71
|
+
"gate-pdr-ownership-consent-flow",
|
|
72
|
+
"gate-sds-explicit-decisions",
|
|
73
|
+
"gate-sds-policy-telemetry-metering",
|
|
74
|
+
"gate-sds-ops-observability-testing",
|
|
75
|
+
"gate-sds-external-adapters",
|
|
76
|
+
"gate-deployment-blueprint-validator",
|
|
77
|
+
"gate-build-ready-completeness",
|
|
78
|
+
]);
|
|
14
79
|
const readPromptIfExists = async (workspace, relative) => {
|
|
15
80
|
const candidate = path.join(workspace.mcodaDir, relative);
|
|
16
81
|
try {
|
|
@@ -139,9 +204,28 @@ class DocContextAssembler {
|
|
|
139
204
|
}
|
|
140
205
|
async findLatestLocalDoc(docType) {
|
|
141
206
|
const candidates = [];
|
|
207
|
+
const lower = docType.toLowerCase();
|
|
208
|
+
const explicitFiles = [
|
|
209
|
+
path.join(this.workspace.mcodaDir, "docs", `${lower}.md`),
|
|
210
|
+
path.join(this.workspace.workspaceRoot, "docs", `${lower}.md`),
|
|
211
|
+
];
|
|
212
|
+
const addCandidate = async (candidatePath) => {
|
|
213
|
+
try {
|
|
214
|
+
const stat = await fs.stat(candidatePath);
|
|
215
|
+
if (stat.isFile()) {
|
|
216
|
+
candidates.push({ path: candidatePath, mtime: stat.mtimeMs });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// ignore
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
for (const filePath of explicitFiles) {
|
|
224
|
+
await addCandidate(filePath);
|
|
225
|
+
}
|
|
142
226
|
const dirs = [
|
|
143
|
-
path.join(this.workspace.mcodaDir, "docs",
|
|
144
|
-
path.join(this.workspace.workspaceRoot, "docs",
|
|
227
|
+
path.join(this.workspace.mcodaDir, "docs", lower),
|
|
228
|
+
path.join(this.workspace.workspaceRoot, "docs", lower),
|
|
145
229
|
];
|
|
146
230
|
for (const dir of dirs) {
|
|
147
231
|
try {
|
|
@@ -276,7 +360,7 @@ class DocContextAssembler {
|
|
|
276
360
|
existingSds = [localSds];
|
|
277
361
|
}
|
|
278
362
|
if (!pdrs.length && !rfp) {
|
|
279
|
-
throw new Error(
|
|
363
|
+
throw new Error(`No PDR or RFP content could be resolved. Ensure docdex is reachable with an sds_default profile or add local docs under ${path.join(this.workspace.mcodaDir, "docs", "pdr")} and docs/rfp (or docs/rfp.md).`);
|
|
280
364
|
}
|
|
281
365
|
const blocks = [];
|
|
282
366
|
if (rfp)
|
|
@@ -500,7 +584,7 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
|
|
|
500
584
|
let body = cleaned && cleaned.length > 0 ? cleaned : cleanBody(section.fallback);
|
|
501
585
|
if (section.title === "Interfaces / APIs" && (context.openapi?.length ?? 0) === 0) {
|
|
502
586
|
const scrubbed = stripInventedEndpoints(body);
|
|
503
|
-
const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (
|
|
587
|
+
const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (authentication/identity, data access, integrations, eventing, analytics/observability).";
|
|
504
588
|
if (!scrubbed || scrubbed.length === 0 || /endpoint/i.test(scrubbed)) {
|
|
505
589
|
body = cleanBody(openApiFallback);
|
|
506
590
|
}
|
|
@@ -1073,7 +1157,8 @@ const readGitBranch = async (workspaceRoot) => {
|
|
|
1073
1157
|
export class DocsService {
|
|
1074
1158
|
constructor(workspace, deps) {
|
|
1075
1159
|
this.workspace = workspace;
|
|
1076
|
-
|
|
1160
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
1161
|
+
this.docdex = deps?.docdex ?? new DocdexClient({ workspaceRoot: workspace.workspaceRoot, repoId: docdexRepoId });
|
|
1077
1162
|
this.jobService = deps?.jobService ?? new JobService(workspace, undefined, { noTelemetry: deps?.noTelemetry });
|
|
1078
1163
|
this.repo = deps.repo;
|
|
1079
1164
|
this.agentService = deps.agentService;
|
|
@@ -1085,9 +1170,11 @@ export class DocsService {
|
|
|
1085
1170
|
const repo = await GlobalRepository.create();
|
|
1086
1171
|
const agentService = new AgentService(repo);
|
|
1087
1172
|
const routingService = await RoutingService.create();
|
|
1173
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
1088
1174
|
const docdex = new DocdexClient({
|
|
1089
1175
|
workspaceRoot: workspace.workspaceRoot,
|
|
1090
1176
|
baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
|
|
1177
|
+
repoId: docdexRepoId,
|
|
1091
1178
|
});
|
|
1092
1179
|
const jobService = new JobService(workspace, undefined, { noTelemetry: options.noTelemetry });
|
|
1093
1180
|
return new DocsService(workspace, { repo, agentService, routingService, docdex, jobService, noTelemetry: options.noTelemetry });
|
|
@@ -1137,14 +1224,193 @@ export class DocsService {
|
|
|
1137
1224
|
}
|
|
1138
1225
|
return { name: templateName ?? "default", content: DEFAULT_SDS_TEMPLATE };
|
|
1139
1226
|
}
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1227
|
+
buildDocgenCapabilityProfile(input) {
|
|
1228
|
+
const required = ["doc_generation", "docdex_query"];
|
|
1229
|
+
const preferred = [];
|
|
1230
|
+
if (input.commandName === "docs-pdr-generate") {
|
|
1231
|
+
preferred.push("rfp_creation", "spec_generation");
|
|
1232
|
+
}
|
|
1233
|
+
if (input.commandName === "docs-sds-generate") {
|
|
1234
|
+
preferred.push("sds_writing", "spec_generation");
|
|
1235
|
+
}
|
|
1236
|
+
if (input.iterationEnabled) {
|
|
1237
|
+
preferred.push("multiple_draft_generation", "quick_bug_patching", "code_review");
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
required: Array.from(new Set(required)),
|
|
1241
|
+
preferred: Array.from(new Set(preferred)),
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
async collectDocgenCandidates(resolved) {
|
|
1245
|
+
const candidates = new Map();
|
|
1246
|
+
let agents = [];
|
|
1247
|
+
try {
|
|
1248
|
+
agents = await this.repo.listAgents();
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
agents = [];
|
|
1252
|
+
}
|
|
1253
|
+
let healthRows = [];
|
|
1254
|
+
try {
|
|
1255
|
+
healthRows = await this.repo.listAgentHealthSummary();
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
healthRows = [];
|
|
1259
|
+
}
|
|
1260
|
+
const healthById = new Map(healthRows.map((row) => [row.agentId, row.status]));
|
|
1261
|
+
for (const agent of agents) {
|
|
1262
|
+
let caps = [];
|
|
1263
|
+
try {
|
|
1264
|
+
caps = await this.repo.getAgentCapabilities(agent.id);
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
caps = agent.capabilities ?? [];
|
|
1268
|
+
}
|
|
1269
|
+
candidates.set(agent.id, {
|
|
1270
|
+
agent,
|
|
1271
|
+
capabilities: caps,
|
|
1272
|
+
healthStatus: healthById.get(agent.id),
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (resolved) {
|
|
1276
|
+
const existing = candidates.get(resolved.agent.id);
|
|
1277
|
+
if (!existing || existing.capabilities.length === 0) {
|
|
1278
|
+
candidates.set(resolved.agent.id, {
|
|
1279
|
+
agent: resolved.agent,
|
|
1280
|
+
capabilities: resolved.capabilities ?? [],
|
|
1281
|
+
healthStatus: resolved.healthStatus,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return Array.from(candidates.values());
|
|
1286
|
+
}
|
|
1287
|
+
async selectDocgenAgent(input) {
|
|
1288
|
+
const commandName = input.commandAliases[input.commandAliases.length - 1] ?? input.commandName;
|
|
1289
|
+
const profile = this.buildDocgenCapabilityProfile({
|
|
1290
|
+
commandName: input.commandName,
|
|
1291
|
+
iterationEnabled: input.iterationEnabled,
|
|
1292
|
+
});
|
|
1293
|
+
let resolved;
|
|
1294
|
+
let resolveError;
|
|
1295
|
+
try {
|
|
1296
|
+
resolved = await this.routingService.resolveAgentForCommand({
|
|
1297
|
+
workspace: this.workspace,
|
|
1298
|
+
commandName,
|
|
1299
|
+
overrideAgentSlug: input.agentName,
|
|
1300
|
+
requiredCapabilities: profile.required,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
resolveError = error instanceof Error ? error : new Error(String(error));
|
|
1305
|
+
resolved = undefined;
|
|
1306
|
+
}
|
|
1307
|
+
if (!resolved) {
|
|
1308
|
+
const candidates = await this.collectDocgenCandidates();
|
|
1309
|
+
const selection = selectBestAgentForCapabilities({
|
|
1310
|
+
candidates,
|
|
1311
|
+
required: profile.required,
|
|
1312
|
+
preferred: profile.preferred,
|
|
1313
|
+
});
|
|
1314
|
+
if (!selection) {
|
|
1315
|
+
throw resolveError ?? new Error("No agents available for doc generation.");
|
|
1316
|
+
}
|
|
1317
|
+
const selected = selection.agent;
|
|
1318
|
+
const selectedLabel = selected.slug ?? selected.id;
|
|
1319
|
+
const preferredLabel = input.agentName ?? "routing-default";
|
|
1320
|
+
const warn = (message) => {
|
|
1321
|
+
if (!input.warnings.includes(message)) {
|
|
1322
|
+
input.warnings.push(message);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
warn(`Docgen preflight selected fallback agent ${selectedLabel} (preferred ${preferredLabel}).`);
|
|
1326
|
+
if (selection.missingRequired.length > 0) {
|
|
1327
|
+
warn(`Docgen preflight selected agent ${selectedLabel} missing required capabilities: ${selection.missingRequired.join(", ")}.`);
|
|
1328
|
+
}
|
|
1329
|
+
const logLines = [
|
|
1330
|
+
`[docgen preflight] command=${input.commandName} routing=${commandName}`,
|
|
1331
|
+
`[docgen preflight] preferred=${preferredLabel} selected=${selectedLabel} fallback=yes`,
|
|
1332
|
+
`[docgen preflight] required=${profile.required.join(", ") || "none"}`,
|
|
1333
|
+
`[docgen preflight] preferred_caps=${profile.preferred.join(", ") || "none"}`,
|
|
1334
|
+
`[docgen preflight] missing_required=${selection.missingRequired.join(", ") || "none"}`,
|
|
1335
|
+
`[docgen preflight] missing_preferred=${selection.missingPreferred.join(", ") || "none"}`,
|
|
1336
|
+
`[docgen preflight] reason=${selection.reason}`,
|
|
1337
|
+
resolveError ? `[docgen preflight] routing_error=${resolveError.message}` : undefined,
|
|
1338
|
+
].filter(Boolean);
|
|
1339
|
+
await this.jobService.appendLog(input.jobId, `${logLines.join("\n")}\n`);
|
|
1340
|
+
await this.jobService.writeCheckpoint(input.jobId, {
|
|
1341
|
+
stage: "agent_preflight",
|
|
1342
|
+
timestamp: new Date().toISOString(),
|
|
1343
|
+
details: {
|
|
1344
|
+
commandName: input.commandName,
|
|
1345
|
+
routingCommand: commandName,
|
|
1346
|
+
preferredAgent: preferredLabel,
|
|
1347
|
+
selectedAgent: selectedLabel,
|
|
1348
|
+
fallbackUsed: true,
|
|
1349
|
+
requiredCapabilities: profile.required,
|
|
1350
|
+
preferredCapabilities: profile.preferred,
|
|
1351
|
+
missingRequired: selection.missingRequired,
|
|
1352
|
+
missingPreferred: selection.missingPreferred,
|
|
1353
|
+
reason: selection.reason,
|
|
1354
|
+
routingError: resolveError?.message,
|
|
1355
|
+
},
|
|
1356
|
+
});
|
|
1357
|
+
return selected;
|
|
1358
|
+
}
|
|
1359
|
+
const resolvedMissing = profile.required.filter((cap) => !(resolved.capabilities ?? []).includes(cap));
|
|
1360
|
+
const needsFallback = resolvedMissing.length > 0 || resolved.healthStatus === "unreachable";
|
|
1361
|
+
const candidates = needsFallback ? await this.collectDocgenCandidates(resolved) : [];
|
|
1362
|
+
const selection = needsFallback
|
|
1363
|
+
? selectBestAgentForCapabilities({
|
|
1364
|
+
candidates,
|
|
1365
|
+
required: profile.required,
|
|
1366
|
+
preferred: profile.preferred,
|
|
1367
|
+
})
|
|
1368
|
+
: undefined;
|
|
1369
|
+
const selected = selection?.agent ?? resolved.agent;
|
|
1370
|
+
const missingRequired = selection?.missingRequired ?? resolvedMissing;
|
|
1371
|
+
const missingPreferred = selection?.missingPreferred ??
|
|
1372
|
+
profile.preferred.filter((cap) => !(resolved.capabilities ?? []).includes(cap));
|
|
1373
|
+
const fallbackUsed = selected.id !== resolved.agent.id;
|
|
1374
|
+
const preferredLabel = resolved.agent.slug ?? resolved.agent.id;
|
|
1375
|
+
const selectedLabel = selected.slug ?? selected.id;
|
|
1376
|
+
const warn = (message) => {
|
|
1377
|
+
if (!input.warnings.includes(message)) {
|
|
1378
|
+
input.warnings.push(message);
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
if (fallbackUsed) {
|
|
1382
|
+
warn(`Docgen preflight selected fallback agent ${selectedLabel} (preferred ${preferredLabel}).`);
|
|
1383
|
+
}
|
|
1384
|
+
if (missingRequired.length > 0) {
|
|
1385
|
+
warn(`Docgen preflight selected agent ${selectedLabel} missing required capabilities: ${missingRequired.join(", ")}.`);
|
|
1386
|
+
}
|
|
1387
|
+
const logLines = [
|
|
1388
|
+
`[docgen preflight] command=${input.commandName} routing=${commandName}`,
|
|
1389
|
+
`[docgen preflight] preferred=${preferredLabel} selected=${selectedLabel} fallback=${fallbackUsed ? "yes" : "no"}`,
|
|
1390
|
+
`[docgen preflight] required=${profile.required.join(", ") || "none"}`,
|
|
1391
|
+
`[docgen preflight] preferred_caps=${profile.preferred.join(", ") || "none"}`,
|
|
1392
|
+
`[docgen preflight] missing_required=${missingRequired.join(", ") || "none"}`,
|
|
1393
|
+
`[docgen preflight] missing_preferred=${missingPreferred.join(", ") || "none"}`,
|
|
1394
|
+
`[docgen preflight] reason=${selection?.reason ?? "routing default"}`,
|
|
1395
|
+
];
|
|
1396
|
+
await this.jobService.appendLog(input.jobId, `${logLines.join("\n")}\n`);
|
|
1397
|
+
await this.jobService.writeCheckpoint(input.jobId, {
|
|
1398
|
+
stage: "agent_preflight",
|
|
1399
|
+
timestamp: new Date().toISOString(),
|
|
1400
|
+
details: {
|
|
1401
|
+
commandName: input.commandName,
|
|
1402
|
+
routingCommand: commandName,
|
|
1403
|
+
preferredAgent: preferredLabel,
|
|
1404
|
+
selectedAgent: selectedLabel,
|
|
1405
|
+
fallbackUsed,
|
|
1406
|
+
requiredCapabilities: profile.required,
|
|
1407
|
+
preferredCapabilities: profile.preferred,
|
|
1408
|
+
missingRequired,
|
|
1409
|
+
missingPreferred,
|
|
1410
|
+
reason: selection?.reason,
|
|
1411
|
+
},
|
|
1146
1412
|
});
|
|
1147
|
-
return
|
|
1413
|
+
return selected;
|
|
1148
1414
|
}
|
|
1149
1415
|
async ensureRatingService() {
|
|
1150
1416
|
if (this.ratingService)
|
|
@@ -1163,6 +1429,1187 @@ export class DocsService {
|
|
|
1163
1429
|
});
|
|
1164
1430
|
return this.ratingService;
|
|
1165
1431
|
}
|
|
1432
|
+
createRunContext(input) {
|
|
1433
|
+
return {
|
|
1434
|
+
version: 1,
|
|
1435
|
+
commandName: input.commandName,
|
|
1436
|
+
commandRunId: input.commandRunId,
|
|
1437
|
+
jobId: input.jobId,
|
|
1438
|
+
workspace: this.workspace,
|
|
1439
|
+
projectKey: input.projectKey,
|
|
1440
|
+
rfpId: input.rfpId,
|
|
1441
|
+
rfpPath: input.rfpPath,
|
|
1442
|
+
templateName: input.templateName,
|
|
1443
|
+
outputPath: input.outputPath,
|
|
1444
|
+
createdAt: new Date().toISOString(),
|
|
1445
|
+
flags: input.flags,
|
|
1446
|
+
iteration: { current: 0, max: 0 },
|
|
1447
|
+
artifacts: createEmptyArtifacts(),
|
|
1448
|
+
warnings: input.warnings,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
async recordDocgenStage(runContext, input) {
|
|
1452
|
+
const iteration = runContext.iteration.max > 0 && runContext.iteration.current > 0
|
|
1453
|
+
? { current: runContext.iteration.current, max: runContext.iteration.max, phase: input.phase }
|
|
1454
|
+
: undefined;
|
|
1455
|
+
await this.jobService.recordJobProgress(runContext.jobId, {
|
|
1456
|
+
stage: input.stage,
|
|
1457
|
+
message: input.message,
|
|
1458
|
+
iteration,
|
|
1459
|
+
details: input.details,
|
|
1460
|
+
totalItems: input.totalItems,
|
|
1461
|
+
processedItems: input.processedItems,
|
|
1462
|
+
heartbeat: input.heartbeat,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
async enforceToolDenylist(input) {
|
|
1466
|
+
const denylist = await ToolDenylist.load({
|
|
1467
|
+
mcodaDir: this.workspace.mcodaDir,
|
|
1468
|
+
env: process.env,
|
|
1469
|
+
});
|
|
1470
|
+
const identifiers = [input.agent.slug, input.agent.adapter, input.agent.id].filter((value) => Boolean(value));
|
|
1471
|
+
const matched = denylist.findMatch(identifiers);
|
|
1472
|
+
if (!matched)
|
|
1473
|
+
return;
|
|
1474
|
+
const message = denylist.formatViolation(matched);
|
|
1475
|
+
const artifact = input.runContext.commandName === "docs-pdr-generate" ? "pdr" : "sds";
|
|
1476
|
+
const issue = {
|
|
1477
|
+
id: `gate-tool-denylist-${matched}`,
|
|
1478
|
+
gateId: "gate-tool-denylist",
|
|
1479
|
+
severity: "blocker",
|
|
1480
|
+
category: "compliance",
|
|
1481
|
+
artifact,
|
|
1482
|
+
message,
|
|
1483
|
+
remediation: "Select a non-deprecated agent or update the tool denylist configuration.",
|
|
1484
|
+
location: {
|
|
1485
|
+
kind: "heading",
|
|
1486
|
+
heading: "Tooling Preflight",
|
|
1487
|
+
path: input.runContext.outputPath,
|
|
1488
|
+
},
|
|
1489
|
+
metadata: {
|
|
1490
|
+
matchedTool: matched,
|
|
1491
|
+
identifiers,
|
|
1492
|
+
agentId: input.agent.id,
|
|
1493
|
+
agentSlug: input.agent.slug,
|
|
1494
|
+
agentAdapter: input.agent.adapter,
|
|
1495
|
+
denylist: denylist.list(),
|
|
1496
|
+
},
|
|
1497
|
+
};
|
|
1498
|
+
const gateResult = {
|
|
1499
|
+
gateId: "gate-tool-denylist",
|
|
1500
|
+
gateName: "Tool Denylist",
|
|
1501
|
+
status: "fail",
|
|
1502
|
+
issues: [issue],
|
|
1503
|
+
notes: [message],
|
|
1504
|
+
metadata: {
|
|
1505
|
+
matchedTool: matched,
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
if (input.runContext.iteration.max === 0) {
|
|
1509
|
+
input.runContext.iteration.max = 1;
|
|
1510
|
+
}
|
|
1511
|
+
const report = this.buildReviewReport({
|
|
1512
|
+
runContext: input.runContext,
|
|
1513
|
+
gateResults: [gateResult],
|
|
1514
|
+
remainingOpenItems: [issue],
|
|
1515
|
+
fixesApplied: [],
|
|
1516
|
+
iterationStatus: "completed",
|
|
1517
|
+
});
|
|
1518
|
+
await this.persistReviewReport(input.runContext, "review-final", report);
|
|
1519
|
+
await this.jobService.appendLog(input.runContext.jobId, `${message}\n`);
|
|
1520
|
+
await this.jobService.writeCheckpoint(input.runContext.jobId, {
|
|
1521
|
+
stage: "tool_denylist_blocked",
|
|
1522
|
+
timestamp: new Date().toISOString(),
|
|
1523
|
+
details: {
|
|
1524
|
+
matchedTool: matched,
|
|
1525
|
+
identifiers,
|
|
1526
|
+
denylist: denylist.list(),
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
throw new Error(message);
|
|
1530
|
+
}
|
|
1531
|
+
async applyDocPatches(runContext, patches, options) {
|
|
1532
|
+
const engine = new DocPatchEngine();
|
|
1533
|
+
return engine.apply({
|
|
1534
|
+
runContext,
|
|
1535
|
+
patches,
|
|
1536
|
+
dryRun: options?.dryRun ?? runContext.flags.dryRun,
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
resolveMaxIterations() {
|
|
1540
|
+
const raw = process.env.MCODA_DOCS_MAX_ITERATIONS;
|
|
1541
|
+
if (!raw)
|
|
1542
|
+
return 2;
|
|
1543
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1544
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
1545
|
+
return 2;
|
|
1546
|
+
return Math.max(1, parsed);
|
|
1547
|
+
}
|
|
1548
|
+
isIterationEnabled(runContext) {
|
|
1549
|
+
if (runContext.flags.dryRun)
|
|
1550
|
+
return false;
|
|
1551
|
+
if (runContext.flags.iterate)
|
|
1552
|
+
return true;
|
|
1553
|
+
if (runContext.flags.fast)
|
|
1554
|
+
return false;
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
shouldBlockGate(gateId, runContext) {
|
|
1558
|
+
if (ALWAYS_BLOCKING_GATES.has(gateId))
|
|
1559
|
+
return true;
|
|
1560
|
+
const resolveOpenQuestions = runContext.flags.resolveOpenQuestions ||
|
|
1561
|
+
process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
1562
|
+
if (gateId === "gate-open-questions" && resolveOpenQuestions) {
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
if (BUILD_READY_ONLY_GATES.has(gateId))
|
|
1566
|
+
return runContext.flags.buildReady;
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
appendUniqueWarnings(runContext, values) {
|
|
1570
|
+
for (const value of values) {
|
|
1571
|
+
if (!value)
|
|
1572
|
+
continue;
|
|
1573
|
+
if (runContext.warnings.includes(value))
|
|
1574
|
+
continue;
|
|
1575
|
+
runContext.warnings.push(value);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
summarizeIssues(issues) {
|
|
1579
|
+
const summary = issues
|
|
1580
|
+
.slice(0, 3)
|
|
1581
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
1582
|
+
.join(" ");
|
|
1583
|
+
const suffix = issues.length > 3 ? ` (+${issues.length - 3} more)` : "";
|
|
1584
|
+
return `${summary}${suffix}`;
|
|
1585
|
+
}
|
|
1586
|
+
appendGateWarnings(runContext, gate, blocking) {
|
|
1587
|
+
if (gate.notes?.length) {
|
|
1588
|
+
this.appendUniqueWarnings(runContext, gate.notes);
|
|
1589
|
+
}
|
|
1590
|
+
if (blocking)
|
|
1591
|
+
return;
|
|
1592
|
+
if (gate.issues.length === 0)
|
|
1593
|
+
return;
|
|
1594
|
+
const summary = this.summarizeIssues(gate.issues);
|
|
1595
|
+
this.appendUniqueWarnings(runContext, [
|
|
1596
|
+
`${gate.gateName} issues (${gate.issues.length}). ${summary}`,
|
|
1597
|
+
]);
|
|
1598
|
+
}
|
|
1599
|
+
async runReviewGates(runContext, phase = "review") {
|
|
1600
|
+
const gateResults = [];
|
|
1601
|
+
const blockingIssues = [];
|
|
1602
|
+
const phaseLabel = phase === "recheck" ? "Re-check" : "Review";
|
|
1603
|
+
const runGate = async (gateId, gateName, runner) => {
|
|
1604
|
+
await this.recordDocgenStage(runContext, {
|
|
1605
|
+
stage: `${phase}:${gateId}`,
|
|
1606
|
+
message: `${phaseLabel}: ${gateName}`,
|
|
1607
|
+
phase,
|
|
1608
|
+
heartbeat: true,
|
|
1609
|
+
details: { gateId, gateName },
|
|
1610
|
+
});
|
|
1611
|
+
return await runner();
|
|
1612
|
+
};
|
|
1613
|
+
const addResult = (result) => {
|
|
1614
|
+
const blocking = this.shouldBlockGate(result.gateId, runContext);
|
|
1615
|
+
if (result.status === "fail" && blocking) {
|
|
1616
|
+
blockingIssues.push(...result.issues);
|
|
1617
|
+
}
|
|
1618
|
+
const normalized = result.status === "fail" && !blocking ? { ...result, status: "warn" } : result;
|
|
1619
|
+
this.appendGateWarnings(runContext, normalized, blocking);
|
|
1620
|
+
gateResults.push(normalized);
|
|
1621
|
+
};
|
|
1622
|
+
const stateWarnings = (runContext.stateWarnings ?? []).filter((warning) => typeof warning === "string" && warning.trim().length > 0);
|
|
1623
|
+
const stateArtifact = runContext.commandName === "docs-pdr-generate" ? "pdr" : "sds";
|
|
1624
|
+
const stateIssues = stateWarnings.map((warning, index) => ({
|
|
1625
|
+
id: `gate-state-dir-cleanup-${index + 1}`,
|
|
1626
|
+
gateId: "gate-state-dir-cleanup",
|
|
1627
|
+
severity: "info",
|
|
1628
|
+
category: "compliance",
|
|
1629
|
+
artifact: stateArtifact,
|
|
1630
|
+
message: warning,
|
|
1631
|
+
remediation: "Keep docgen intermediate state under .mcoda or OS temp directories and relocate legacy state directories into .mcoda.",
|
|
1632
|
+
location: { kind: "heading", heading: "State Directory Cleanup", path: runContext.outputPath },
|
|
1633
|
+
}));
|
|
1634
|
+
addResult({
|
|
1635
|
+
gateId: "gate-state-dir-cleanup",
|
|
1636
|
+
gateName: "State Directory Cleanup",
|
|
1637
|
+
status: stateIssues.length > 0 ? "warn" : "pass",
|
|
1638
|
+
issues: stateIssues,
|
|
1639
|
+
});
|
|
1640
|
+
if (runContext.flags.noPlaceholders || runContext.flags.buildReady) {
|
|
1641
|
+
const allowlist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_ALLOWLIST);
|
|
1642
|
+
const denylist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_DENYLIST);
|
|
1643
|
+
addResult(await runGate("gate-placeholder-artifacts", "Placeholder Artifacts", () => runPlaceholderArtifactGate({
|
|
1644
|
+
artifacts: runContext.artifacts,
|
|
1645
|
+
allowlist: allowlist.length > 0 ? allowlist : undefined,
|
|
1646
|
+
denylist: denylist.length > 0 ? denylist : undefined,
|
|
1647
|
+
})));
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
const skippedGate = {
|
|
1651
|
+
gateId: "gate-placeholder-artifacts",
|
|
1652
|
+
gateName: "Placeholder Artifacts",
|
|
1653
|
+
status: "skipped",
|
|
1654
|
+
issues: [],
|
|
1655
|
+
notes: ["Placeholder gate disabled (noPlaceholders/buildReady not set)."],
|
|
1656
|
+
};
|
|
1657
|
+
addResult(skippedGate);
|
|
1658
|
+
}
|
|
1659
|
+
addResult(await runGate("gate-api-path-consistency", "API Path Consistency", () => runApiPathConsistencyGate({ artifacts: runContext.artifacts })));
|
|
1660
|
+
addResult(await runGate("gate-openapi-schema-sanity", "OpenAPI Schema Sanity", () => runOpenApiSchemaSanityGate({ artifacts: runContext.artifacts })));
|
|
1661
|
+
addResult(await runGate("gate-openapi-coverage", "OpenAPI Coverage", () => runOpenApiCoverageGate({ artifacts: runContext.artifacts })));
|
|
1662
|
+
addResult(await runGate("gate-sql-syntax", "SQL Syntax", () => runSqlSyntaxGate({ artifacts: runContext.artifacts })));
|
|
1663
|
+
addResult(await runGate("gate-sql-required-tables", "SQL Required Tables", () => runSqlRequiredTablesGate({ artifacts: runContext.artifacts })));
|
|
1664
|
+
addResult(await runGate("gate-admin-openapi-spec", "Admin OpenAPI Spec", () => runAdminOpenApiSpecGate({ artifacts: runContext.artifacts })));
|
|
1665
|
+
addResult(await runGate("gate-terminology-normalization", "Terminology Normalization", () => runTerminologyNormalizationGate({ artifacts: runContext.artifacts })));
|
|
1666
|
+
addResult(await runGate("gate-open-questions", "Open Questions", () => runOpenQuestionsGate({ artifacts: runContext.artifacts })));
|
|
1667
|
+
const resolveOpenQuestions = runContext.flags.resolveOpenQuestions ||
|
|
1668
|
+
process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
1669
|
+
const noMaybesEnabled = runContext.flags.noMaybes ||
|
|
1670
|
+
process.env.MCODA_DOCS_NO_MAYBES === "1" ||
|
|
1671
|
+
resolveOpenQuestions;
|
|
1672
|
+
addResult(await runGate("gate-no-maybes", "No Maybes", () => runNoMaybesGate({
|
|
1673
|
+
artifacts: runContext.artifacts,
|
|
1674
|
+
enabled: noMaybesEnabled,
|
|
1675
|
+
})));
|
|
1676
|
+
addResult(await runGate("gate-rfp-consent", "RFP Consent", () => runRfpConsentGate({ rfpPath: runContext.rfpPath })));
|
|
1677
|
+
const definitionAllowlist = parseDelimitedList(process.env.MCODA_DOCS_RFP_DEFINITION_ALLOWLIST);
|
|
1678
|
+
addResult(await runGate("gate-rfp-definition", "RFP Definition Coverage", () => runRfpDefinitionGate({
|
|
1679
|
+
rfpPath: runContext.rfpPath,
|
|
1680
|
+
allowlist: definitionAllowlist.length > 0 ? definitionAllowlist : undefined,
|
|
1681
|
+
})));
|
|
1682
|
+
addResult(await runGate("gate-pdr-interfaces", "PDR Interfaces", () => runPdrInterfacesGate({ artifacts: runContext.artifacts })));
|
|
1683
|
+
addResult(await runGate("gate-pdr-ownership", "PDR Ownership", () => runPdrOwnershipGate({ artifacts: runContext.artifacts })));
|
|
1684
|
+
const openQuestionsEnabled = resolveOpenQuestions;
|
|
1685
|
+
addResult(await runGate("gate-pdr-open-questions", "PDR Open Questions", () => runPdrOpenQuestionsGate({
|
|
1686
|
+
artifacts: runContext.artifacts,
|
|
1687
|
+
enabled: openQuestionsEnabled,
|
|
1688
|
+
})));
|
|
1689
|
+
addResult(await runGate("gate-sds-explicit-decisions", "SDS Explicit Decisions", () => runSdsDecisionsGate({ artifacts: runContext.artifacts })));
|
|
1690
|
+
addResult(await runGate("gate-sds-policy-telemetry", "SDS Policy Telemetry", () => runSdsPolicyTelemetryGate({ artifacts: runContext.artifacts })));
|
|
1691
|
+
addResult(await runGate("gate-sds-ops-observability-testing", "SDS Ops/Observability/Testing", () => runSdsOpsGate({ artifacts: runContext.artifacts })));
|
|
1692
|
+
addResult(await runGate("gate-sds-external-adapters", "SDS External Adapters", () => runSdsAdaptersGate({ artifacts: runContext.artifacts })));
|
|
1693
|
+
addResult(await runGate("gate-deployment-blueprint", "Deployment Blueprint", () => runDeploymentBlueprintGate({
|
|
1694
|
+
artifacts: runContext.artifacts,
|
|
1695
|
+
buildReady: runContext.flags.buildReady,
|
|
1696
|
+
})));
|
|
1697
|
+
addResult(await runGate("gate-build-ready-completeness", "Build Ready Completeness", () => runBuildReadyCompletenessGate({
|
|
1698
|
+
artifacts: runContext.artifacts,
|
|
1699
|
+
buildReady: runContext.flags.buildReady,
|
|
1700
|
+
})));
|
|
1701
|
+
return { gateResults, blockingIssues };
|
|
1702
|
+
}
|
|
1703
|
+
buildPatchPlanFromIssues(inputIssues) {
|
|
1704
|
+
const patchesByPath = new Map();
|
|
1705
|
+
const fixes = [];
|
|
1706
|
+
const seenRanges = new Set();
|
|
1707
|
+
for (const issue of inputIssues) {
|
|
1708
|
+
if (issue.gateId !== "gate-placeholder-artifacts")
|
|
1709
|
+
continue;
|
|
1710
|
+
if (issue.location.kind !== "line_range")
|
|
1711
|
+
continue;
|
|
1712
|
+
const key = `${issue.location.path}:${issue.location.lineStart}-${issue.location.lineEnd}`;
|
|
1713
|
+
if (seenRanges.has(key))
|
|
1714
|
+
continue;
|
|
1715
|
+
seenRanges.add(key);
|
|
1716
|
+
const patch = patchesByPath.get(issue.location.path) ??
|
|
1717
|
+
{
|
|
1718
|
+
path: issue.location.path,
|
|
1719
|
+
operations: [],
|
|
1720
|
+
};
|
|
1721
|
+
patch.operations.push({
|
|
1722
|
+
type: "remove_block",
|
|
1723
|
+
location: issue.location,
|
|
1724
|
+
});
|
|
1725
|
+
patchesByPath.set(issue.location.path, patch);
|
|
1726
|
+
fixes.push({
|
|
1727
|
+
issueId: issue.id,
|
|
1728
|
+
summary: `Removed placeholder content in ${path.basename(issue.location.path)}`,
|
|
1729
|
+
appliedAt: new Date().toISOString(),
|
|
1730
|
+
metadata: {
|
|
1731
|
+
gateId: issue.gateId,
|
|
1732
|
+
path: issue.location.path,
|
|
1733
|
+
lineStart: issue.location.lineStart,
|
|
1734
|
+
lineEnd: issue.location.lineEnd,
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
return { patches: Array.from(patchesByPath.values()), fixes };
|
|
1739
|
+
}
|
|
1740
|
+
resolveQuestionDecision(question) {
|
|
1741
|
+
const trimmed = question.trim().replace(/\?+$/, "").trim();
|
|
1742
|
+
if (!trimmed)
|
|
1743
|
+
return undefined;
|
|
1744
|
+
const patterns = [
|
|
1745
|
+
{ pattern: /should we use\s+(.+)$/i, verb: "Use" },
|
|
1746
|
+
{ pattern: /^use\s+(.+)$/i, verb: "Use" },
|
|
1747
|
+
{ pattern: /^choose\s+(.+)$/i, verb: "Choose" },
|
|
1748
|
+
{ pattern: /^select\s+(.+)$/i, verb: "Select" },
|
|
1749
|
+
{ pattern: /decide on\s+(.+)$/i, verb: "Use" },
|
|
1750
|
+
];
|
|
1751
|
+
for (const entry of patterns) {
|
|
1752
|
+
const match = trimmed.match(entry.pattern);
|
|
1753
|
+
if (!match)
|
|
1754
|
+
continue;
|
|
1755
|
+
const choice = match[1]?.trim() ?? "";
|
|
1756
|
+
if (!choice)
|
|
1757
|
+
continue;
|
|
1758
|
+
if (/\bor\b|\/|either/i.test(choice))
|
|
1759
|
+
continue;
|
|
1760
|
+
const suffix = choice.endsWith(".") ? "" : ".";
|
|
1761
|
+
return `${entry.verb} ${choice}${suffix}`;
|
|
1762
|
+
}
|
|
1763
|
+
return undefined;
|
|
1764
|
+
}
|
|
1765
|
+
sanitizeIndecisiveLine(line, patternId) {
|
|
1766
|
+
const patterns = {
|
|
1767
|
+
maybe: /\bmaybe\b/i,
|
|
1768
|
+
optional: /\boptional\b/i,
|
|
1769
|
+
could: /\bcould\b/i,
|
|
1770
|
+
might: /\bmight\b/i,
|
|
1771
|
+
possibly: /\bpossibly\b/i,
|
|
1772
|
+
either: /\beither\b/i,
|
|
1773
|
+
tbd: /\btbd\b/i,
|
|
1774
|
+
};
|
|
1775
|
+
const pattern = patterns[patternId];
|
|
1776
|
+
if (!pattern)
|
|
1777
|
+
return undefined;
|
|
1778
|
+
const match = line.match(/^(\s*[-*+]\s+|\s*)(.*)$/);
|
|
1779
|
+
const prefix = match?.[1] ?? "";
|
|
1780
|
+
const body = match?.[2] ?? line;
|
|
1781
|
+
const cleaned = body
|
|
1782
|
+
.replace(pattern, "")
|
|
1783
|
+
.replace(/\s{2,}/g, " ")
|
|
1784
|
+
.replace(/\s+([,.;:])/g, "$1")
|
|
1785
|
+
.trim();
|
|
1786
|
+
if (!cleaned || cleaned === body.trim())
|
|
1787
|
+
return undefined;
|
|
1788
|
+
return `${prefix}${cleaned}`;
|
|
1789
|
+
}
|
|
1790
|
+
formatResolvedDecisions(decisions) {
|
|
1791
|
+
const lines = [];
|
|
1792
|
+
for (const decision of decisions) {
|
|
1793
|
+
const metadata = (decision.metadata ?? {});
|
|
1794
|
+
const question = typeof metadata.question === "string" && metadata.question.trim().length > 0
|
|
1795
|
+
? metadata.question.trim()
|
|
1796
|
+
: undefined;
|
|
1797
|
+
const suffix = question ? ` (question: ${question})` : "";
|
|
1798
|
+
lines.push(`- ${decision.summary}${suffix}`);
|
|
1799
|
+
}
|
|
1800
|
+
return lines.join("\n");
|
|
1801
|
+
}
|
|
1802
|
+
async resolveOpenQuestions(runContext, gateResults) {
|
|
1803
|
+
const enabled = runContext.flags.resolveOpenQuestions ||
|
|
1804
|
+
process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
1805
|
+
if (!enabled)
|
|
1806
|
+
return { decisions: [], warnings: [] };
|
|
1807
|
+
const openQuestions = gateResults.find((gate) => gate.gateId === "gate-open-questions");
|
|
1808
|
+
if (!openQuestions || openQuestions.issues.length === 0) {
|
|
1809
|
+
return { decisions: [], warnings: [] };
|
|
1810
|
+
}
|
|
1811
|
+
const decisions = [];
|
|
1812
|
+
const warnings = [];
|
|
1813
|
+
const decisionTargets = new Map();
|
|
1814
|
+
const lineReplacements = new Map();
|
|
1815
|
+
const replacedLines = new Set();
|
|
1816
|
+
for (const issue of openQuestions.issues) {
|
|
1817
|
+
if (issue.location.kind !== "line_range")
|
|
1818
|
+
continue;
|
|
1819
|
+
const metadata = (issue.metadata ?? {});
|
|
1820
|
+
const question = typeof metadata.question === "string" && metadata.question.trim().length > 0
|
|
1821
|
+
? metadata.question.trim()
|
|
1822
|
+
: undefined;
|
|
1823
|
+
const normalized = typeof metadata.normalized === "string" && metadata.normalized.trim().length > 0
|
|
1824
|
+
? metadata.normalized.trim()
|
|
1825
|
+
: undefined;
|
|
1826
|
+
const required = metadata.required === true;
|
|
1827
|
+
if (!question)
|
|
1828
|
+
continue;
|
|
1829
|
+
const decisionSummary = this.resolveQuestionDecision(question);
|
|
1830
|
+
if (!decisionSummary) {
|
|
1831
|
+
if (required) {
|
|
1832
|
+
warnings.push(`Open question unresolved: ${question}`);
|
|
1833
|
+
}
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
const fallbackId = question
|
|
1837
|
+
.toLowerCase()
|
|
1838
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1839
|
+
.replace(/^-+|-+$/g, "");
|
|
1840
|
+
const decisionIdBase = normalized ?? (fallbackId || issue.id);
|
|
1841
|
+
const decisionId = `decision-${decisionIdBase}`;
|
|
1842
|
+
const decision = {
|
|
1843
|
+
id: decisionId,
|
|
1844
|
+
summary: decisionSummary,
|
|
1845
|
+
rationale: `Resolved from open question: "${question}"`,
|
|
1846
|
+
decidedAt: new Date().toISOString(),
|
|
1847
|
+
relatedIssueIds: [issue.id],
|
|
1848
|
+
metadata: {
|
|
1849
|
+
question,
|
|
1850
|
+
normalized,
|
|
1851
|
+
target: metadata.target,
|
|
1852
|
+
},
|
|
1853
|
+
};
|
|
1854
|
+
decisions.push(decision);
|
|
1855
|
+
const target = typeof metadata.target === "string" && metadata.target.trim().length > 0
|
|
1856
|
+
? metadata.target.trim()
|
|
1857
|
+
: issue.artifact;
|
|
1858
|
+
let targetPath;
|
|
1859
|
+
if (target === "pdr" || issue.artifact === "pdr") {
|
|
1860
|
+
targetPath = runContext.artifacts.pdr?.path;
|
|
1861
|
+
}
|
|
1862
|
+
else if (target === "sds" || issue.artifact === "sds") {
|
|
1863
|
+
targetPath = runContext.artifacts.sds?.path;
|
|
1864
|
+
}
|
|
1865
|
+
else {
|
|
1866
|
+
targetPath = runContext.artifacts.sds?.path ?? runContext.artifacts.pdr?.path;
|
|
1867
|
+
}
|
|
1868
|
+
if (!targetPath) {
|
|
1869
|
+
warnings.push(`Resolved decision could not be inserted (missing target doc): ${question}`);
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
const existing = decisionTargets.get(targetPath);
|
|
1873
|
+
if (existing) {
|
|
1874
|
+
existing.push(decision);
|
|
1875
|
+
}
|
|
1876
|
+
else {
|
|
1877
|
+
decisionTargets.set(targetPath, [decision]);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const lineIndex = issue.location.lineStart - 1;
|
|
1881
|
+
const key = `${issue.location.path}:${lineIndex}`;
|
|
1882
|
+
replacedLines.add(key);
|
|
1883
|
+
if (!lineReplacements.has(issue.location.path)) {
|
|
1884
|
+
lineReplacements.set(issue.location.path, new Map());
|
|
1885
|
+
}
|
|
1886
|
+
lineReplacements.get(issue.location.path)?.set(lineIndex, `Resolved: ${decision.summary}`);
|
|
1887
|
+
}
|
|
1888
|
+
const noMaybesGate = gateResults.find((gate) => gate.gateId === "gate-no-maybes");
|
|
1889
|
+
if (noMaybesGate && noMaybesGate.issues.length > 0) {
|
|
1890
|
+
const contentCache = new Map();
|
|
1891
|
+
const loadContent = async (filePath) => {
|
|
1892
|
+
if (contentCache.has(filePath))
|
|
1893
|
+
return contentCache.get(filePath);
|
|
1894
|
+
try {
|
|
1895
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1896
|
+
contentCache.set(filePath, content);
|
|
1897
|
+
return content;
|
|
1898
|
+
}
|
|
1899
|
+
catch (error) {
|
|
1900
|
+
warnings.push(`Unable to read ${filePath} for indecisive cleanup: ${error.message ?? String(error)}`);
|
|
1901
|
+
return undefined;
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
for (const issue of noMaybesGate.issues) {
|
|
1905
|
+
if (issue.location.kind !== "line_range")
|
|
1906
|
+
continue;
|
|
1907
|
+
const lineIndex = issue.location.lineStart - 1;
|
|
1908
|
+
const key = `${issue.location.path}:${lineIndex}`;
|
|
1909
|
+
if (replacedLines.has(key))
|
|
1910
|
+
continue;
|
|
1911
|
+
const content = await loadContent(issue.location.path);
|
|
1912
|
+
if (!content)
|
|
1913
|
+
continue;
|
|
1914
|
+
const lines = content.split(/\r?\n/);
|
|
1915
|
+
if (lineIndex < 0 || lineIndex >= lines.length)
|
|
1916
|
+
continue;
|
|
1917
|
+
const line = lines[lineIndex] ?? "";
|
|
1918
|
+
const metadata = (issue.metadata ?? {});
|
|
1919
|
+
const patternId = typeof metadata.patternId === "string" ? metadata.patternId : "";
|
|
1920
|
+
const sanitized = this.sanitizeIndecisiveLine(line, patternId);
|
|
1921
|
+
if (!sanitized)
|
|
1922
|
+
continue;
|
|
1923
|
+
if (!lineReplacements.has(issue.location.path)) {
|
|
1924
|
+
lineReplacements.set(issue.location.path, new Map());
|
|
1925
|
+
}
|
|
1926
|
+
lineReplacements.get(issue.location.path)?.set(lineIndex, sanitized);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (lineReplacements.size === 0 && decisionTargets.size === 0) {
|
|
1930
|
+
return { decisions, warnings };
|
|
1931
|
+
}
|
|
1932
|
+
const patches = [];
|
|
1933
|
+
for (const [filePath, replacements] of lineReplacements.entries()) {
|
|
1934
|
+
const operations = [];
|
|
1935
|
+
const sorted = Array.from(replacements.entries()).sort((a, b) => a[0] - b[0]);
|
|
1936
|
+
for (const [lineIndex, content] of sorted) {
|
|
1937
|
+
const lineNumber = lineIndex + 1;
|
|
1938
|
+
operations.push({
|
|
1939
|
+
type: "replace_section",
|
|
1940
|
+
location: {
|
|
1941
|
+
kind: "line_range",
|
|
1942
|
+
path: filePath,
|
|
1943
|
+
lineStart: lineNumber,
|
|
1944
|
+
lineEnd: lineNumber,
|
|
1945
|
+
},
|
|
1946
|
+
content,
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
const decisionSet = decisionTargets.get(filePath);
|
|
1950
|
+
if (decisionSet && decisionSet.length > 0) {
|
|
1951
|
+
operations.push({
|
|
1952
|
+
type: "insert_section",
|
|
1953
|
+
heading: "Resolved Decisions",
|
|
1954
|
+
content: this.formatResolvedDecisions(decisionSet),
|
|
1955
|
+
position: "append",
|
|
1956
|
+
headingLevel: 2,
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
if (operations.length > 0) {
|
|
1960
|
+
patches.push({ path: filePath, operations });
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
for (const [filePath, decisionSet] of decisionTargets.entries()) {
|
|
1964
|
+
if (lineReplacements.has(filePath))
|
|
1965
|
+
continue;
|
|
1966
|
+
if (!decisionSet.length)
|
|
1967
|
+
continue;
|
|
1968
|
+
patches.push({
|
|
1969
|
+
path: filePath,
|
|
1970
|
+
operations: [
|
|
1971
|
+
{
|
|
1972
|
+
type: "insert_section",
|
|
1973
|
+
heading: "Resolved Decisions",
|
|
1974
|
+
content: this.formatResolvedDecisions(decisionSet),
|
|
1975
|
+
position: "append",
|
|
1976
|
+
headingLevel: 2,
|
|
1977
|
+
},
|
|
1978
|
+
],
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
if (patches.length > 0) {
|
|
1982
|
+
const patchResult = await this.applyDocPatches(runContext, patches);
|
|
1983
|
+
if (patchResult.warnings.length > 0) {
|
|
1984
|
+
warnings.push(...patchResult.warnings);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return { decisions, warnings };
|
|
1988
|
+
}
|
|
1989
|
+
reviewReportDir(runContext) {
|
|
1990
|
+
return path.join(this.workspace.mcodaDir, "jobs", runContext.jobId, "review");
|
|
1991
|
+
}
|
|
1992
|
+
reviewReportPaths(runContext, label) {
|
|
1993
|
+
const reportDir = this.reviewReportDir(runContext);
|
|
1994
|
+
const jobDir = path.join(this.workspace.mcodaDir, "jobs", runContext.jobId);
|
|
1995
|
+
const jsonPath = path.join(reportDir, `${label}.json`);
|
|
1996
|
+
const markdownPath = path.join(reportDir, `${label}.md`);
|
|
1997
|
+
const markdownRelative = path.relative(jobDir, markdownPath) || path.basename(markdownPath);
|
|
1998
|
+
return { jsonPath, markdownPath, markdownRelative };
|
|
1999
|
+
}
|
|
2000
|
+
buildReviewReport(input) {
|
|
2001
|
+
const outcome = aggregateReviewOutcome({
|
|
2002
|
+
gateResults: input.gateResults,
|
|
2003
|
+
remainingOpenItems: input.remainingOpenItems,
|
|
2004
|
+
fixesApplied: input.fixesApplied,
|
|
2005
|
+
decisions: input.decisions ?? [],
|
|
2006
|
+
generatedAt: new Date().toISOString(),
|
|
2007
|
+
});
|
|
2008
|
+
const iterationReports = input.iterationReports && input.iterationReports.length > 0
|
|
2009
|
+
? input.iterationReports
|
|
2010
|
+
: undefined;
|
|
2011
|
+
return {
|
|
2012
|
+
version: 1,
|
|
2013
|
+
generatedAt: outcome.generatedAt,
|
|
2014
|
+
iteration: {
|
|
2015
|
+
current: input.runContext.iteration.current,
|
|
2016
|
+
max: input.runContext.iteration.max,
|
|
2017
|
+
status: input.iterationStatus,
|
|
2018
|
+
},
|
|
2019
|
+
status: outcome.summary.status,
|
|
2020
|
+
summary: outcome.summary,
|
|
2021
|
+
gateResults: outcome.gateResults,
|
|
2022
|
+
issues: outcome.issues,
|
|
2023
|
+
remainingOpenItems: outcome.remainingOpenItems,
|
|
2024
|
+
fixesApplied: outcome.fixesApplied,
|
|
2025
|
+
decisions: outcome.decisions,
|
|
2026
|
+
deltas: input.deltas ?? [],
|
|
2027
|
+
metadata: {
|
|
2028
|
+
commandName: input.runContext.commandName,
|
|
2029
|
+
commandRunId: input.runContext.commandRunId,
|
|
2030
|
+
jobId: input.runContext.jobId,
|
|
2031
|
+
projectKey: input.runContext.projectKey,
|
|
2032
|
+
iterationReports,
|
|
2033
|
+
},
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
async persistReviewReport(runContext, label, report) {
|
|
2037
|
+
const paths = this.reviewReportPaths(runContext, label);
|
|
2038
|
+
await ensureDir(paths.jsonPath);
|
|
2039
|
+
await fs.writeFile(paths.jsonPath, serializeReviewReport(report), "utf8");
|
|
2040
|
+
await fs.writeFile(paths.markdownPath, renderReviewReport(report), "utf8");
|
|
2041
|
+
return { markdownRelative: paths.markdownRelative };
|
|
2042
|
+
}
|
|
2043
|
+
async runIterationLoop(runContext) {
|
|
2044
|
+
const iterationEnabled = this.isIterationEnabled(runContext);
|
|
2045
|
+
const maxIterations = runContext.iteration.max > 0
|
|
2046
|
+
? runContext.iteration.max
|
|
2047
|
+
: iterationEnabled
|
|
2048
|
+
? this.resolveMaxIterations()
|
|
2049
|
+
: 1;
|
|
2050
|
+
runContext.iteration.max = maxIterations;
|
|
2051
|
+
const fixesApplied = [];
|
|
2052
|
+
const alignmentDeltas = [];
|
|
2053
|
+
const decisions = [];
|
|
2054
|
+
const iterationReports = [];
|
|
2055
|
+
let lastGateResults = [];
|
|
2056
|
+
let lastBlockingIssues = [];
|
|
2057
|
+
const alignmentPatcher = new DocAlignmentPatcher();
|
|
2058
|
+
let finalReportRelative;
|
|
2059
|
+
const persistIterationReport = async (status) => {
|
|
2060
|
+
const report = this.buildReviewReport({
|
|
2061
|
+
runContext,
|
|
2062
|
+
gateResults: lastGateResults,
|
|
2063
|
+
remainingOpenItems: lastBlockingIssues,
|
|
2064
|
+
fixesApplied,
|
|
2065
|
+
deltas: alignmentDeltas,
|
|
2066
|
+
decisions,
|
|
2067
|
+
iterationStatus: status,
|
|
2068
|
+
});
|
|
2069
|
+
const { markdownRelative } = await this.persistReviewReport(runContext, `review-iteration-${runContext.iteration.current}`, report);
|
|
2070
|
+
iterationReports.push(markdownRelative);
|
|
2071
|
+
};
|
|
2072
|
+
const persistFinalReport = async (status) => {
|
|
2073
|
+
const report = this.buildReviewReport({
|
|
2074
|
+
runContext,
|
|
2075
|
+
gateResults: lastGateResults,
|
|
2076
|
+
remainingOpenItems: lastBlockingIssues,
|
|
2077
|
+
fixesApplied,
|
|
2078
|
+
deltas: alignmentDeltas,
|
|
2079
|
+
decisions,
|
|
2080
|
+
iterationStatus: status,
|
|
2081
|
+
iterationReports,
|
|
2082
|
+
});
|
|
2083
|
+
const { markdownRelative } = await this.persistReviewReport(runContext, "review-final", report);
|
|
2084
|
+
finalReportRelative = markdownRelative;
|
|
2085
|
+
};
|
|
2086
|
+
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
2087
|
+
runContext.iteration.current = iteration;
|
|
2088
|
+
await this.jobService.recordIterationProgress(runContext.jobId, {
|
|
2089
|
+
current: iteration,
|
|
2090
|
+
max: maxIterations,
|
|
2091
|
+
phase: "review",
|
|
2092
|
+
});
|
|
2093
|
+
const review = await this.runReviewGates(runContext, "review");
|
|
2094
|
+
lastGateResults = review.gateResults;
|
|
2095
|
+
lastBlockingIssues = review.blockingIssues;
|
|
2096
|
+
if (review.blockingIssues.length === 0) {
|
|
2097
|
+
await persistIterationReport("completed");
|
|
2098
|
+
await persistFinalReport("completed");
|
|
2099
|
+
return { gateResults: review.gateResults, fixesApplied, reviewReportPath: finalReportRelative };
|
|
2100
|
+
}
|
|
2101
|
+
if (!iterationEnabled) {
|
|
2102
|
+
await persistIterationReport("max_iterations");
|
|
2103
|
+
await persistFinalReport("max_iterations");
|
|
2104
|
+
const summary = this.summarizeIssues(review.blockingIssues);
|
|
2105
|
+
throw new Error(`Doc generation review failed. ${summary}`);
|
|
2106
|
+
}
|
|
2107
|
+
const questionResolution = await this.resolveOpenQuestions(runContext, review.gateResults);
|
|
2108
|
+
if (questionResolution.decisions.length > 0) {
|
|
2109
|
+
decisions.push(...questionResolution.decisions);
|
|
2110
|
+
}
|
|
2111
|
+
if (questionResolution.warnings.length > 0) {
|
|
2112
|
+
this.appendUniqueWarnings(runContext, questionResolution.warnings);
|
|
2113
|
+
}
|
|
2114
|
+
if (runContext.flags.crossAlign) {
|
|
2115
|
+
const alignmentResult = await alignmentPatcher.apply({
|
|
2116
|
+
runContext,
|
|
2117
|
+
gateResults: review.gateResults,
|
|
2118
|
+
});
|
|
2119
|
+
if (alignmentResult.warnings.length > 0) {
|
|
2120
|
+
this.appendUniqueWarnings(runContext, alignmentResult.warnings);
|
|
2121
|
+
}
|
|
2122
|
+
if (alignmentResult.deltas.length > 0) {
|
|
2123
|
+
alignmentDeltas.push(...alignmentResult.deltas);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
const patchPlan = this.buildPatchPlanFromIssues(review.blockingIssues);
|
|
2127
|
+
await this.jobService.recordIterationProgress(runContext.jobId, {
|
|
2128
|
+
current: iteration,
|
|
2129
|
+
max: maxIterations,
|
|
2130
|
+
phase: "patch",
|
|
2131
|
+
details: { patches: patchPlan.patches.length, fixes: patchPlan.fixes.length },
|
|
2132
|
+
});
|
|
2133
|
+
if (patchPlan.patches.length > 0) {
|
|
2134
|
+
fixesApplied.push(...patchPlan.fixes);
|
|
2135
|
+
const patchResult = await this.applyDocPatches(runContext, patchPlan.patches);
|
|
2136
|
+
if (patchResult.warnings.length > 0) {
|
|
2137
|
+
this.appendUniqueWarnings(runContext, patchResult.warnings);
|
|
2138
|
+
}
|
|
2139
|
+
await this.jobService.recordIterationProgress(runContext.jobId, {
|
|
2140
|
+
current: iteration,
|
|
2141
|
+
max: maxIterations,
|
|
2142
|
+
phase: "recheck",
|
|
2143
|
+
});
|
|
2144
|
+
const recheck = await this.runReviewGates(runContext, "recheck");
|
|
2145
|
+
lastGateResults = recheck.gateResults;
|
|
2146
|
+
lastBlockingIssues = recheck.blockingIssues;
|
|
2147
|
+
if (recheck.blockingIssues.length === 0) {
|
|
2148
|
+
await persistIterationReport("completed");
|
|
2149
|
+
await persistFinalReport("completed");
|
|
2150
|
+
return { gateResults: recheck.gateResults, fixesApplied, reviewReportPath: finalReportRelative };
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const iterationStatus = lastBlockingIssues.length === 0
|
|
2154
|
+
? "completed"
|
|
2155
|
+
: iteration === maxIterations
|
|
2156
|
+
? "max_iterations"
|
|
2157
|
+
: "in_progress";
|
|
2158
|
+
await persistIterationReport(iterationStatus);
|
|
2159
|
+
}
|
|
2160
|
+
const summary = this.summarizeIssues(lastBlockingIssues);
|
|
2161
|
+
await persistFinalReport("max_iterations");
|
|
2162
|
+
throw new Error(`Doc generation review failed after ${maxIterations} iteration(s). ${summary}`);
|
|
2163
|
+
}
|
|
2164
|
+
async enforcePlaceholderArtifacts(runContext) {
|
|
2165
|
+
if (!runContext.flags.noPlaceholders)
|
|
2166
|
+
return;
|
|
2167
|
+
const allowlist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_ALLOWLIST);
|
|
2168
|
+
const denylist = parseDelimitedList(process.env.MCODA_DOCS_PLACEHOLDER_DENYLIST);
|
|
2169
|
+
const result = await runPlaceholderArtifactGate({
|
|
2170
|
+
artifacts: runContext.artifacts,
|
|
2171
|
+
allowlist: allowlist.length > 0 ? allowlist : undefined,
|
|
2172
|
+
denylist: denylist.length > 0 ? denylist : undefined,
|
|
2173
|
+
});
|
|
2174
|
+
if (result.notes?.length) {
|
|
2175
|
+
runContext.warnings.push(...result.notes);
|
|
2176
|
+
}
|
|
2177
|
+
if (result.status !== "fail")
|
|
2178
|
+
return;
|
|
2179
|
+
const summary = result.issues
|
|
2180
|
+
.slice(0, 3)
|
|
2181
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2182
|
+
.join(" ");
|
|
2183
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2184
|
+
throw new Error(`Placeholder artifacts detected (${result.issues.length}). ${summary}${suffix}`);
|
|
2185
|
+
}
|
|
2186
|
+
async enforceApiPathConsistency(runContext) {
|
|
2187
|
+
const result = await runApiPathConsistencyGate({ artifacts: runContext.artifacts });
|
|
2188
|
+
if (result.notes?.length) {
|
|
2189
|
+
runContext.warnings.push(...result.notes);
|
|
2190
|
+
}
|
|
2191
|
+
if (result.status !== "fail")
|
|
2192
|
+
return;
|
|
2193
|
+
const summary = result.issues
|
|
2194
|
+
.slice(0, 3)
|
|
2195
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2196
|
+
.join(" ");
|
|
2197
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2198
|
+
const message = `API path consistency check failed (${result.issues.length}). ${summary}${suffix}`;
|
|
2199
|
+
if (runContext.flags.buildReady) {
|
|
2200
|
+
throw new Error(message);
|
|
2201
|
+
}
|
|
2202
|
+
runContext.warnings.push(message);
|
|
2203
|
+
}
|
|
2204
|
+
async enforceOpenApiSchemaSanity(runContext) {
|
|
2205
|
+
if (!runContext.artifacts.openapi?.length)
|
|
2206
|
+
return;
|
|
2207
|
+
const issues = [];
|
|
2208
|
+
for (const record of runContext.artifacts.openapi) {
|
|
2209
|
+
try {
|
|
2210
|
+
const content = await fs.readFile(record.path, "utf8");
|
|
2211
|
+
const result = validateOpenApiSchemaContent(content);
|
|
2212
|
+
if (result.errors.length > 0) {
|
|
2213
|
+
issues.push(...result.errors.map((error) => `${record.path}: ${error}`));
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
catch (error) {
|
|
2217
|
+
runContext.warnings.push(`Unable to read OpenAPI spec ${record.path}: ${error.message ?? String(error)}`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (issues.length === 0)
|
|
2221
|
+
return;
|
|
2222
|
+
const summary = issues.slice(0, 3).join(" ");
|
|
2223
|
+
const suffix = issues.length > 3 ? ` (+${issues.length - 3} more)` : "";
|
|
2224
|
+
const message = `OpenAPI schema sanity issues (${issues.length}). ${summary}${suffix}`;
|
|
2225
|
+
if (runContext.flags.buildReady) {
|
|
2226
|
+
throw new Error(message);
|
|
2227
|
+
}
|
|
2228
|
+
runContext.warnings.push(message);
|
|
2229
|
+
}
|
|
2230
|
+
async enforceOpenApiCoverage(runContext) {
|
|
2231
|
+
const result = await runOpenApiCoverageGate({ artifacts: runContext.artifacts });
|
|
2232
|
+
if (result.notes?.length) {
|
|
2233
|
+
runContext.warnings.push(...result.notes);
|
|
2234
|
+
}
|
|
2235
|
+
if (result.status !== "fail")
|
|
2236
|
+
return;
|
|
2237
|
+
const summary = result.issues
|
|
2238
|
+
.slice(0, 3)
|
|
2239
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2240
|
+
.join(" ");
|
|
2241
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2242
|
+
const message = `OpenAPI endpoint coverage check failed (${result.issues.length}). ${summary}${suffix}`;
|
|
2243
|
+
if (runContext.flags.buildReady) {
|
|
2244
|
+
throw new Error(message);
|
|
2245
|
+
}
|
|
2246
|
+
runContext.warnings.push(message);
|
|
2247
|
+
}
|
|
2248
|
+
async enforceSqlSyntax(runContext) {
|
|
2249
|
+
const result = await runSqlSyntaxGate({ artifacts: runContext.artifacts });
|
|
2250
|
+
if (result.notes?.length) {
|
|
2251
|
+
runContext.warnings.push(...result.notes);
|
|
2252
|
+
}
|
|
2253
|
+
if (result.status !== "fail")
|
|
2254
|
+
return;
|
|
2255
|
+
const summary = result.issues
|
|
2256
|
+
.slice(0, 3)
|
|
2257
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2258
|
+
.join(" ");
|
|
2259
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2260
|
+
const message = `SQL syntax validation failed (${result.issues.length}). ${summary}${suffix}`;
|
|
2261
|
+
if (runContext.flags.buildReady) {
|
|
2262
|
+
throw new Error(message);
|
|
2263
|
+
}
|
|
2264
|
+
runContext.warnings.push(message);
|
|
2265
|
+
}
|
|
2266
|
+
async enforceSqlRequiredTables(runContext) {
|
|
2267
|
+
const result = await runSqlRequiredTablesGate({ artifacts: runContext.artifacts });
|
|
2268
|
+
if (result.notes?.length) {
|
|
2269
|
+
runContext.warnings.push(...result.notes);
|
|
2270
|
+
}
|
|
2271
|
+
if (result.status !== "fail")
|
|
2272
|
+
return;
|
|
2273
|
+
const summary = result.issues
|
|
2274
|
+
.slice(0, 3)
|
|
2275
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2276
|
+
.join(" ");
|
|
2277
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2278
|
+
const message = `SQL required tables check failed (${result.issues.length}). ${summary}${suffix}`;
|
|
2279
|
+
if (runContext.flags.buildReady) {
|
|
2280
|
+
throw new Error(message);
|
|
2281
|
+
}
|
|
2282
|
+
runContext.warnings.push(message);
|
|
2283
|
+
}
|
|
2284
|
+
async generateDeploymentBlueprint(runContext, sdsContent, projectKey) {
|
|
2285
|
+
if (runContext.flags.dryRun) {
|
|
2286
|
+
runContext.warnings.push("Dry run enabled; deployment blueprint generation skipped.");
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2289
|
+
const scaffolder = new DocsScaffolder();
|
|
2290
|
+
const openapiRecords = runContext.artifacts.openapi ?? [];
|
|
2291
|
+
const primaryOpenApi = openapiRecords.find((record) => record.variant !== "admin") ?? openapiRecords[0];
|
|
2292
|
+
let openapiContent;
|
|
2293
|
+
if (primaryOpenApi) {
|
|
2294
|
+
try {
|
|
2295
|
+
openapiContent = await fs.readFile(primaryOpenApi.path, "utf8");
|
|
2296
|
+
}
|
|
2297
|
+
catch (error) {
|
|
2298
|
+
runContext.warnings.push(`Unable to read OpenAPI spec ${primaryOpenApi.path} for deployment blueprint: ${error.message ?? String(error)}`);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
try {
|
|
2302
|
+
await scaffolder.generateDeploymentBlueprintFiles({
|
|
2303
|
+
sdsContent,
|
|
2304
|
+
openapiContent,
|
|
2305
|
+
outputDir: path.join(this.workspace.workspaceRoot, "deploy"),
|
|
2306
|
+
serviceName: projectKey ?? "app",
|
|
2307
|
+
});
|
|
2308
|
+
return true;
|
|
2309
|
+
}
|
|
2310
|
+
catch (error) {
|
|
2311
|
+
runContext.warnings.push(`Deployment blueprint generation failed: ${error.message ?? String(error)}`);
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
async enforceAdminOpenApiSpec(runContext) {
|
|
2316
|
+
const docRecords = [runContext.artifacts.pdr, runContext.artifacts.sds].filter((record) => Boolean(record));
|
|
2317
|
+
if (docRecords.length === 0)
|
|
2318
|
+
return;
|
|
2319
|
+
const mentions = [];
|
|
2320
|
+
for (const record of docRecords) {
|
|
2321
|
+
try {
|
|
2322
|
+
const content = await fs.readFile(record.path, "utf8");
|
|
2323
|
+
const found = findAdminSurfaceMentions(content);
|
|
2324
|
+
for (const mention of found) {
|
|
2325
|
+
mentions.push({
|
|
2326
|
+
record,
|
|
2327
|
+
line: mention.line,
|
|
2328
|
+
excerpt: mention.heading ? `${mention.heading}: ${mention.excerpt}` : mention.excerpt,
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
catch (error) {
|
|
2333
|
+
runContext.warnings.push(`Unable to scan ${record.path} for admin surface mentions: ${error.message ?? String(error)}`);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
if (mentions.length === 0)
|
|
2337
|
+
return;
|
|
2338
|
+
const openapiRecords = runContext.artifacts.openapi ?? [];
|
|
2339
|
+
const hasAdminSpec = openapiRecords.some((record) => record.variant === "admin" || /admin/i.test(path.basename(record.path)));
|
|
2340
|
+
if (hasAdminSpec)
|
|
2341
|
+
return;
|
|
2342
|
+
const summary = mentions
|
|
2343
|
+
.slice(0, 2)
|
|
2344
|
+
.map((entry) => `${path.basename(entry.record.path)}:${entry.line} ${entry.excerpt}`)
|
|
2345
|
+
.join(" | ");
|
|
2346
|
+
const suffix = mentions.length > 2 ? ` (+${mentions.length - 2} more)` : "";
|
|
2347
|
+
const message = `Admin OpenAPI spec required (admin surfaces referenced): ${summary}${suffix}`;
|
|
2348
|
+
if (runContext.flags.buildReady) {
|
|
2349
|
+
throw new Error(message);
|
|
2350
|
+
}
|
|
2351
|
+
runContext.warnings.push(message);
|
|
2352
|
+
}
|
|
2353
|
+
async enforceTerminologyNormalization(runContext) {
|
|
2354
|
+
const result = await runTerminologyNormalizationGate({ artifacts: runContext.artifacts });
|
|
2355
|
+
if (result.notes?.length) {
|
|
2356
|
+
runContext.warnings.push(...result.notes);
|
|
2357
|
+
}
|
|
2358
|
+
if (result.status === "pass")
|
|
2359
|
+
return;
|
|
2360
|
+
const summary = result.issues
|
|
2361
|
+
.slice(0, 3)
|
|
2362
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2363
|
+
.join(" ");
|
|
2364
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2365
|
+
const message = `Terminology normalization findings (${result.issues.length}). ${summary}${suffix}`;
|
|
2366
|
+
if (result.status === "fail" && runContext.flags.buildReady) {
|
|
2367
|
+
throw new Error(message);
|
|
2368
|
+
}
|
|
2369
|
+
runContext.warnings.push(message);
|
|
2370
|
+
}
|
|
2371
|
+
async enforceOpenQuestions(runContext) {
|
|
2372
|
+
const result = await runOpenQuestionsGate({ artifacts: runContext.artifacts });
|
|
2373
|
+
if (result.notes?.length) {
|
|
2374
|
+
runContext.warnings.push(...result.notes);
|
|
2375
|
+
}
|
|
2376
|
+
if (result.status === "pass")
|
|
2377
|
+
return;
|
|
2378
|
+
const summary = result.issues
|
|
2379
|
+
.slice(0, 3)
|
|
2380
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2381
|
+
.join(" ");
|
|
2382
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2383
|
+
const message = `Open questions detected (${result.issues.length}). ${summary}${suffix}`;
|
|
2384
|
+
if (result.status === "fail" && runContext.flags.buildReady) {
|
|
2385
|
+
throw new Error(message);
|
|
2386
|
+
}
|
|
2387
|
+
runContext.warnings.push(message);
|
|
2388
|
+
}
|
|
2389
|
+
async enforceNoMaybes(runContext) {
|
|
2390
|
+
const enabled = runContext.flags.noMaybes ||
|
|
2391
|
+
runContext.flags.resolveOpenQuestions ||
|
|
2392
|
+
process.env.MCODA_DOCS_NO_MAYBES === "1" ||
|
|
2393
|
+
process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
2394
|
+
const result = await runNoMaybesGate({ artifacts: runContext.artifacts, enabled });
|
|
2395
|
+
if (result.notes?.length) {
|
|
2396
|
+
runContext.warnings.push(...result.notes);
|
|
2397
|
+
}
|
|
2398
|
+
if (result.status !== "fail")
|
|
2399
|
+
return;
|
|
2400
|
+
const summary = result.issues
|
|
2401
|
+
.slice(0, 3)
|
|
2402
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2403
|
+
.join(" ");
|
|
2404
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2405
|
+
throw new Error(`Indecisive language detected (${result.issues.length}). ${summary}${suffix}`);
|
|
2406
|
+
}
|
|
2407
|
+
async enforceRfpConsent(runContext) {
|
|
2408
|
+
const result = await runRfpConsentGate({ rfpPath: runContext.rfpPath });
|
|
2409
|
+
if (result.notes?.length) {
|
|
2410
|
+
runContext.warnings.push(...result.notes);
|
|
2411
|
+
}
|
|
2412
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2413
|
+
return;
|
|
2414
|
+
const summary = result.issues
|
|
2415
|
+
.slice(0, 3)
|
|
2416
|
+
.map((issue) => `${issue.message}`)
|
|
2417
|
+
.join(" ");
|
|
2418
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2419
|
+
const message = `RFP consent contradictions detected (${result.issues.length}). ${summary}${suffix}`;
|
|
2420
|
+
if (runContext.flags.buildReady) {
|
|
2421
|
+
throw new Error(message);
|
|
2422
|
+
}
|
|
2423
|
+
runContext.warnings.push(message);
|
|
2424
|
+
}
|
|
2425
|
+
async enforceRfpDefinitionCoverage(runContext) {
|
|
2426
|
+
const allowlist = parseDelimitedList(process.env.MCODA_DOCS_RFP_DEFINITION_ALLOWLIST);
|
|
2427
|
+
const result = await runRfpDefinitionGate({
|
|
2428
|
+
rfpPath: runContext.rfpPath,
|
|
2429
|
+
allowlist: allowlist.length > 0 ? allowlist : undefined,
|
|
2430
|
+
});
|
|
2431
|
+
if (result.notes?.length) {
|
|
2432
|
+
runContext.warnings.push(...result.notes);
|
|
2433
|
+
}
|
|
2434
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2435
|
+
return;
|
|
2436
|
+
const summary = result.issues
|
|
2437
|
+
.slice(0, 3)
|
|
2438
|
+
.map((issue) => issue.message)
|
|
2439
|
+
.join(" ");
|
|
2440
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2441
|
+
const message = `RFP definition coverage issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2442
|
+
if (runContext.flags.buildReady) {
|
|
2443
|
+
throw new Error(message);
|
|
2444
|
+
}
|
|
2445
|
+
runContext.warnings.push(message);
|
|
2446
|
+
}
|
|
2447
|
+
async enforcePdrInterfaces(runContext) {
|
|
2448
|
+
const result = await runPdrInterfacesGate({ artifacts: runContext.artifacts });
|
|
2449
|
+
if (result.notes?.length) {
|
|
2450
|
+
runContext.warnings.push(...result.notes);
|
|
2451
|
+
}
|
|
2452
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2453
|
+
return;
|
|
2454
|
+
const summary = result.issues
|
|
2455
|
+
.slice(0, 3)
|
|
2456
|
+
.map((issue) => issue.message)
|
|
2457
|
+
.join(" ");
|
|
2458
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2459
|
+
const message = `PDR interface/pipeline issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2460
|
+
if (runContext.flags.buildReady) {
|
|
2461
|
+
throw new Error(message);
|
|
2462
|
+
}
|
|
2463
|
+
runContext.warnings.push(message);
|
|
2464
|
+
}
|
|
2465
|
+
async enforcePdrOwnership(runContext) {
|
|
2466
|
+
const result = await runPdrOwnershipGate({ artifacts: runContext.artifacts });
|
|
2467
|
+
if (result.notes?.length) {
|
|
2468
|
+
runContext.warnings.push(...result.notes);
|
|
2469
|
+
}
|
|
2470
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2471
|
+
return;
|
|
2472
|
+
const summary = result.issues
|
|
2473
|
+
.slice(0, 3)
|
|
2474
|
+
.map((issue) => issue.message)
|
|
2475
|
+
.join(" ");
|
|
2476
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2477
|
+
const message = `PDR ownership/consent flow issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2478
|
+
if (runContext.flags.buildReady) {
|
|
2479
|
+
throw new Error(message);
|
|
2480
|
+
}
|
|
2481
|
+
runContext.warnings.push(message);
|
|
2482
|
+
}
|
|
2483
|
+
async enforcePdrOpenQuestionsQuality(runContext) {
|
|
2484
|
+
const enabled = process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
2485
|
+
const result = await runPdrOpenQuestionsGate({ artifacts: runContext.artifacts, enabled });
|
|
2486
|
+
if (result.notes?.length) {
|
|
2487
|
+
runContext.warnings.push(...result.notes);
|
|
2488
|
+
}
|
|
2489
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2490
|
+
return;
|
|
2491
|
+
const summary = result.issues
|
|
2492
|
+
.slice(0, 3)
|
|
2493
|
+
.map((issue) => issue.message)
|
|
2494
|
+
.join(" ");
|
|
2495
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2496
|
+
const message = `PDR open question quality issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2497
|
+
throw new Error(message);
|
|
2498
|
+
}
|
|
2499
|
+
async enforceSdsExplicitDecisions(runContext) {
|
|
2500
|
+
const result = await runSdsDecisionsGate({ artifacts: runContext.artifacts });
|
|
2501
|
+
if (result.notes?.length) {
|
|
2502
|
+
runContext.warnings.push(...result.notes);
|
|
2503
|
+
}
|
|
2504
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2505
|
+
return;
|
|
2506
|
+
const summary = result.issues
|
|
2507
|
+
.slice(0, 3)
|
|
2508
|
+
.map((issue) => issue.message)
|
|
2509
|
+
.join(" ");
|
|
2510
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2511
|
+
const message = `SDS explicit decision issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2512
|
+
if (runContext.flags.buildReady) {
|
|
2513
|
+
throw new Error(message);
|
|
2514
|
+
}
|
|
2515
|
+
runContext.warnings.push(message);
|
|
2516
|
+
}
|
|
2517
|
+
async enforceSdsPolicyTelemetry(runContext) {
|
|
2518
|
+
const result = await runSdsPolicyTelemetryGate({ artifacts: runContext.artifacts });
|
|
2519
|
+
if (result.notes?.length) {
|
|
2520
|
+
runContext.warnings.push(...result.notes);
|
|
2521
|
+
}
|
|
2522
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2523
|
+
return;
|
|
2524
|
+
const summary = result.issues
|
|
2525
|
+
.slice(0, 3)
|
|
2526
|
+
.map((issue) => issue.message)
|
|
2527
|
+
.join(" ");
|
|
2528
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2529
|
+
const message = `SDS policy/telemetry/metering issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2530
|
+
if (runContext.flags.buildReady) {
|
|
2531
|
+
throw new Error(message);
|
|
2532
|
+
}
|
|
2533
|
+
runContext.warnings.push(message);
|
|
2534
|
+
}
|
|
2535
|
+
async enforceSdsOpsObservabilityTesting(runContext) {
|
|
2536
|
+
const result = await runSdsOpsGate({ artifacts: runContext.artifacts });
|
|
2537
|
+
if (result.notes?.length) {
|
|
2538
|
+
runContext.warnings.push(...result.notes);
|
|
2539
|
+
}
|
|
2540
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2541
|
+
return;
|
|
2542
|
+
const summary = result.issues
|
|
2543
|
+
.slice(0, 3)
|
|
2544
|
+
.map((issue) => issue.message)
|
|
2545
|
+
.join(" ");
|
|
2546
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2547
|
+
const message = `SDS ops/observability/testing issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2548
|
+
if (runContext.flags.buildReady) {
|
|
2549
|
+
throw new Error(message);
|
|
2550
|
+
}
|
|
2551
|
+
runContext.warnings.push(message);
|
|
2552
|
+
}
|
|
2553
|
+
async enforceSdsExternalAdapters(runContext) {
|
|
2554
|
+
const result = await runSdsAdaptersGate({ artifacts: runContext.artifacts });
|
|
2555
|
+
if (result.notes?.length) {
|
|
2556
|
+
runContext.warnings.push(...result.notes);
|
|
2557
|
+
}
|
|
2558
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2559
|
+
return;
|
|
2560
|
+
const summary = result.issues
|
|
2561
|
+
.slice(0, 3)
|
|
2562
|
+
.map((issue) => issue.message)
|
|
2563
|
+
.join(" ");
|
|
2564
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2565
|
+
const message = `SDS external adapter issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2566
|
+
if (runContext.flags.buildReady) {
|
|
2567
|
+
throw new Error(message);
|
|
2568
|
+
}
|
|
2569
|
+
runContext.warnings.push(message);
|
|
2570
|
+
}
|
|
2571
|
+
async enforceDeploymentBlueprint(runContext) {
|
|
2572
|
+
const result = await runDeploymentBlueprintGate({
|
|
2573
|
+
artifacts: runContext.artifacts,
|
|
2574
|
+
buildReady: runContext.flags.buildReady,
|
|
2575
|
+
});
|
|
2576
|
+
if (result.notes?.length) {
|
|
2577
|
+
runContext.warnings.push(...result.notes);
|
|
2578
|
+
}
|
|
2579
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2580
|
+
return;
|
|
2581
|
+
const summary = result.issues
|
|
2582
|
+
.slice(0, 3)
|
|
2583
|
+
.map((issue) => `${issue.message}`)
|
|
2584
|
+
.join(" ");
|
|
2585
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2586
|
+
const message = `Deployment blueprint validation issues (${result.issues.length}). ${summary}${suffix}`;
|
|
2587
|
+
if (result.status === "fail") {
|
|
2588
|
+
throw new Error(message);
|
|
2589
|
+
}
|
|
2590
|
+
runContext.warnings.push(message);
|
|
2591
|
+
}
|
|
2592
|
+
async enforceBuildReadyCompleteness(runContext) {
|
|
2593
|
+
const result = await runBuildReadyCompletenessGate({
|
|
2594
|
+
artifacts: runContext.artifacts,
|
|
2595
|
+
buildReady: runContext.flags.buildReady,
|
|
2596
|
+
});
|
|
2597
|
+
if (result.notes?.length) {
|
|
2598
|
+
runContext.warnings.push(...result.notes);
|
|
2599
|
+
}
|
|
2600
|
+
if (result.status === "pass" || result.status === "skipped")
|
|
2601
|
+
return;
|
|
2602
|
+
const summary = result.issues
|
|
2603
|
+
.slice(0, 3)
|
|
2604
|
+
.map((issue) => `${issue.artifact}: ${issue.message}`)
|
|
2605
|
+
.join(" ");
|
|
2606
|
+
const suffix = result.issues.length > 3 ? ` (+${result.issues.length - 3} more)` : "";
|
|
2607
|
+
const message = `Build-ready completeness check failed (${result.issues.length}). ${summary}${suffix}`;
|
|
2608
|
+
if (result.status === "fail") {
|
|
2609
|
+
throw new Error(message);
|
|
2610
|
+
}
|
|
2611
|
+
runContext.warnings.push(message);
|
|
2612
|
+
}
|
|
1166
2613
|
async writePdrFile(outPath, content) {
|
|
1167
2614
|
await ensureDir(outPath);
|
|
1168
2615
|
await fs.writeFile(outPath, content, "utf8");
|
|
@@ -1185,16 +2632,18 @@ export class DocsService {
|
|
|
1185
2632
|
await ensureDir(outPath);
|
|
1186
2633
|
await fs.writeFile(outPath, content, "utf8");
|
|
1187
2634
|
}
|
|
1188
|
-
async
|
|
2635
|
+
async checkSdsDocdexProfile(warnings) {
|
|
1189
2636
|
const base = this.workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL;
|
|
1190
2637
|
if (base)
|
|
1191
2638
|
return;
|
|
1192
|
-
const localStore = path.join(this.workspace.
|
|
2639
|
+
const localStore = path.join(this.workspace.mcodaDir, "docdex", "documents.json");
|
|
1193
2640
|
try {
|
|
1194
2641
|
await fs.access(localStore);
|
|
2642
|
+
return;
|
|
1195
2643
|
}
|
|
1196
2644
|
catch {
|
|
1197
|
-
|
|
2645
|
+
// No docdex URL or local store; continue with local docs if present.
|
|
2646
|
+
warnings.push("Docdex is not configured for SDS retrieval; attempting local docs (no local docdex store found).");
|
|
1198
2647
|
}
|
|
1199
2648
|
}
|
|
1200
2649
|
async registerSds(outPath, content, projectKey) {
|
|
@@ -1287,7 +2736,77 @@ export class DocsService {
|
|
|
1287
2736
|
completionTokens: 0,
|
|
1288
2737
|
metadata: { docdexAvailable: context.docdexAvailable },
|
|
1289
2738
|
});
|
|
1290
|
-
const
|
|
2739
|
+
const stream = options.agentStream ?? true;
|
|
2740
|
+
const iterate = options.iterate === true;
|
|
2741
|
+
const fastMode = iterate ? false : options.fast === true || process.env.MCODA_DOCS_FAST === "1";
|
|
2742
|
+
const skipValidation = process.env.MCODA_SKIP_PDR_VALIDATION === "1";
|
|
2743
|
+
const buildReady = options.buildReady === true || process.env.MCODA_DOCS_BUILD_READY === "1";
|
|
2744
|
+
const resolveOpenQuestions = options.resolveOpenQuestions === true ||
|
|
2745
|
+
process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
2746
|
+
const noMaybes = options.noMaybes === true ||
|
|
2747
|
+
process.env.MCODA_DOCS_NO_MAYBES === "1" ||
|
|
2748
|
+
resolveOpenQuestions;
|
|
2749
|
+
const noPlaceholders = options.noPlaceholders === true ||
|
|
2750
|
+
process.env.MCODA_DOCS_NO_PLACEHOLDERS === "1" ||
|
|
2751
|
+
buildReady;
|
|
2752
|
+
const crossAlign = options.crossAlign !== false;
|
|
2753
|
+
const iterationEnabled = !options.dryRun && !fastMode;
|
|
2754
|
+
const maxIterations = iterationEnabled ? this.resolveMaxIterations() : 1;
|
|
2755
|
+
const agent = await this.selectDocgenAgent({
|
|
2756
|
+
agentName: options.agentName,
|
|
2757
|
+
commandName: "docs-pdr-generate",
|
|
2758
|
+
commandAliases: ["docs-pdr-generate", "docs:pdr:generate", "pdr"],
|
|
2759
|
+
jobId: job.id,
|
|
2760
|
+
warnings: context.warnings,
|
|
2761
|
+
iterationEnabled,
|
|
2762
|
+
});
|
|
2763
|
+
const outputPath = options.outPath ?? this.defaultPdrOutputPath(options.projectKey, context.rfp.path);
|
|
2764
|
+
const runContext = this.createRunContext({
|
|
2765
|
+
commandName: "docs-pdr-generate",
|
|
2766
|
+
commandRunId: commandRun.id,
|
|
2767
|
+
jobId: job.id,
|
|
2768
|
+
projectKey: options.projectKey,
|
|
2769
|
+
rfpId: options.rfpId,
|
|
2770
|
+
rfpPath: context.rfp.path ?? options.rfpPath,
|
|
2771
|
+
outputPath,
|
|
2772
|
+
flags: {
|
|
2773
|
+
dryRun: options.dryRun === true,
|
|
2774
|
+
fast: fastMode,
|
|
2775
|
+
iterate,
|
|
2776
|
+
json: options.json === true,
|
|
2777
|
+
stream,
|
|
2778
|
+
buildReady,
|
|
2779
|
+
noPlaceholders,
|
|
2780
|
+
resolveOpenQuestions,
|
|
2781
|
+
noMaybes,
|
|
2782
|
+
crossAlign,
|
|
2783
|
+
},
|
|
2784
|
+
warnings: context.warnings,
|
|
2785
|
+
});
|
|
2786
|
+
runContext.iteration.max = maxIterations;
|
|
2787
|
+
const stateCleanupWarnings = await cleanupWorkspaceStateDirs({
|
|
2788
|
+
workspaceRoot: this.workspace.workspaceRoot,
|
|
2789
|
+
mcodaDir: this.workspace.mcodaDir,
|
|
2790
|
+
});
|
|
2791
|
+
const { statePath: iterativeOutputPath, warnings: statePathWarnings } = resolveDocgenStatePath({
|
|
2792
|
+
outputPath: runContext.outputPath,
|
|
2793
|
+
mcodaDir: this.workspace.mcodaDir,
|
|
2794
|
+
jobId: runContext.jobId,
|
|
2795
|
+
commandName: runContext.commandName,
|
|
2796
|
+
});
|
|
2797
|
+
const stateWarnings = [...stateCleanupWarnings, ...statePathWarnings];
|
|
2798
|
+
if (stateWarnings.length > 0) {
|
|
2799
|
+
runContext.stateWarnings = stateWarnings;
|
|
2800
|
+
this.appendUniqueWarnings(runContext, stateWarnings);
|
|
2801
|
+
}
|
|
2802
|
+
runContext.artifacts.pdr = { kind: "pdr", path: runContext.outputPath, meta: {} };
|
|
2803
|
+
await this.enforceToolDenylist({ runContext, agent });
|
|
2804
|
+
await this.recordDocgenStage(runContext, {
|
|
2805
|
+
stage: "generation",
|
|
2806
|
+
message: "Generating PDR draft",
|
|
2807
|
+
totalItems: maxIterations,
|
|
2808
|
+
processedItems: 0,
|
|
2809
|
+
});
|
|
1291
2810
|
const prompts = await this.agentService.getPrompts(agent.id);
|
|
1292
2811
|
const runbook = (await readPromptIfExists(this.workspace, path.join("prompts", "commands", "pdr-generate.md"))) ||
|
|
1293
2812
|
DEFAULT_PDR_RUNBOOK_PROMPT;
|
|
@@ -1295,9 +2814,6 @@ export class DocsService {
|
|
|
1295
2814
|
let agentUsed = false;
|
|
1296
2815
|
let agentMetadata;
|
|
1297
2816
|
let adapter = agent.adapter;
|
|
1298
|
-
const stream = options.agentStream ?? true;
|
|
1299
|
-
const fastMode = options.fast === true || process.env.MCODA_DOCS_FAST === "1";
|
|
1300
|
-
const skipValidation = process.env.MCODA_SKIP_PDR_VALIDATION === "1";
|
|
1301
2817
|
let lastInvoke;
|
|
1302
2818
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1303
2819
|
const prompt = buildRunPrompt(context, options.projectKey, prompts, attempt === 0 ? runbook : `${runbook}\n\nRETRY: The previous attempt failed validation. Ensure all required sections are present and non-empty. Do not leave placeholders.`);
|
|
@@ -1323,7 +2839,12 @@ export class DocsService {
|
|
|
1323
2839
|
action: attempt === 0 ? "draft_pdr" : "draft_pdr_retry",
|
|
1324
2840
|
promptTokens: estimateTokens(prompt),
|
|
1325
2841
|
completionTokens: estimateTokens(agentOutput),
|
|
1326
|
-
metadata: {
|
|
2842
|
+
metadata: {
|
|
2843
|
+
adapter,
|
|
2844
|
+
docdexAvailable: context.docdexAvailable,
|
|
2845
|
+
attempt: attempt + 1,
|
|
2846
|
+
phase: attempt === 0 ? "draft_pdr" : "draft_pdr_retry",
|
|
2847
|
+
},
|
|
1327
2848
|
});
|
|
1328
2849
|
if (valid) {
|
|
1329
2850
|
draft = structured;
|
|
@@ -1362,9 +2883,8 @@ export class DocsService {
|
|
|
1362
2883
|
context.warnings.push(`Tidy pass skipped: ${error.message ?? "unknown error"}`);
|
|
1363
2884
|
}
|
|
1364
2885
|
}
|
|
1365
|
-
const outputPath = options.outPath ?? this.defaultPdrOutputPath(options.projectKey, context.rfp.path);
|
|
1366
2886
|
if (!options.dryRun) {
|
|
1367
|
-
const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "pdr", `${path.basename(outputPath, path.extname(outputPath))}-first-draft.md`);
|
|
2887
|
+
const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "pdr", `${path.basename(runContext.outputPath, path.extname(runContext.outputPath))}-first-draft.md`);
|
|
1368
2888
|
await ensureDir(firstDraftPath);
|
|
1369
2889
|
await fs.writeFile(firstDraftPath, draft, "utf8");
|
|
1370
2890
|
if (fastMode) {
|
|
@@ -1372,7 +2892,7 @@ export class DocsService {
|
|
|
1372
2892
|
}
|
|
1373
2893
|
else {
|
|
1374
2894
|
try {
|
|
1375
|
-
const iterativeDraft = await buildIterativePdr(options.projectKey, context, draft,
|
|
2895
|
+
const iterativeDraft = await buildIterativePdr(options.projectKey, context, draft, iterativeOutputPath, lastInvoke ?? (async (input) => this.invokeAgent(agent, input, stream, job.id, options.onToken)));
|
|
1376
2896
|
draft = iterativeDraft;
|
|
1377
2897
|
}
|
|
1378
2898
|
catch (error) {
|
|
@@ -1388,17 +2908,18 @@ export class DocsService {
|
|
|
1388
2908
|
let docdexId;
|
|
1389
2909
|
let segments;
|
|
1390
2910
|
let mirrorStatus = "skipped";
|
|
2911
|
+
let reviewReportPath;
|
|
1391
2912
|
if (options.dryRun) {
|
|
1392
2913
|
context.warnings.push("Dry run enabled; PDR was not written to disk or registered in docdex.");
|
|
1393
2914
|
}
|
|
1394
2915
|
if (!options.dryRun) {
|
|
1395
|
-
await this.writePdrFile(outputPath, draft);
|
|
2916
|
+
await this.writePdrFile(runContext.outputPath, draft);
|
|
1396
2917
|
if (context.docdexAvailable) {
|
|
1397
2918
|
try {
|
|
1398
|
-
const registered = await this.registerPdr(outputPath, draft, options.projectKey);
|
|
2919
|
+
const registered = await this.registerPdr(runContext.outputPath, draft, options.projectKey);
|
|
1399
2920
|
docdexId = registered.id;
|
|
1400
2921
|
segments = (registered.segments ?? []).map((s) => s.id);
|
|
1401
|
-
await fs.writeFile(`${outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
|
|
2922
|
+
await fs.writeFile(`${runContext.outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
|
|
1402
2923
|
}
|
|
1403
2924
|
catch (error) {
|
|
1404
2925
|
context.warnings.push(`Docdex registration skipped: ${error.message}`);
|
|
@@ -1409,7 +2930,7 @@ export class DocsService {
|
|
|
1409
2930
|
if (shouldMirror) {
|
|
1410
2931
|
try {
|
|
1411
2932
|
await ensureDir(path.join(publicDocsDir, "placeholder"));
|
|
1412
|
-
const mirrorPath = path.join(publicDocsDir, path.basename(outputPath));
|
|
2933
|
+
const mirrorPath = path.join(publicDocsDir, path.basename(runContext.outputPath));
|
|
1413
2934
|
await ensureDir(mirrorPath);
|
|
1414
2935
|
await fs.writeFile(mirrorPath, draft, "utf8");
|
|
1415
2936
|
mirrorStatus = "mirrored";
|
|
@@ -1419,9 +2940,30 @@ export class DocsService {
|
|
|
1419
2940
|
mirrorStatus = "failed";
|
|
1420
2941
|
}
|
|
1421
2942
|
}
|
|
2943
|
+
try {
|
|
2944
|
+
await this.recordDocgenStage(runContext, {
|
|
2945
|
+
stage: "inventory",
|
|
2946
|
+
message: "Building doc inventory",
|
|
2947
|
+
});
|
|
2948
|
+
runContext.artifacts = await buildDocInventory({
|
|
2949
|
+
workspace: this.workspace,
|
|
2950
|
+
preferred: { pdrPath: runContext.outputPath },
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
catch (error) {
|
|
2954
|
+
runContext.warnings.push(`Doc inventory build failed: ${error.message ?? String(error)}`);
|
|
2955
|
+
}
|
|
2956
|
+
const iterationResult = await this.runIterationLoop(runContext);
|
|
2957
|
+
reviewReportPath = iterationResult.reviewReportPath;
|
|
1422
2958
|
}
|
|
1423
2959
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
1424
|
-
payload: {
|
|
2960
|
+
payload: {
|
|
2961
|
+
outputPath: runContext.outputPath,
|
|
2962
|
+
docdexId,
|
|
2963
|
+
segments,
|
|
2964
|
+
mirrorStatus,
|
|
2965
|
+
...(reviewReportPath ? { reviewReportPath } : {}),
|
|
2966
|
+
},
|
|
1425
2967
|
});
|
|
1426
2968
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
1427
2969
|
if (options.rateAgents && agentUsed) {
|
|
@@ -1442,10 +2984,10 @@ export class DocsService {
|
|
|
1442
2984
|
return {
|
|
1443
2985
|
jobId: job.id,
|
|
1444
2986
|
commandRunId: commandRun.id,
|
|
1445
|
-
outputPath,
|
|
2987
|
+
outputPath: runContext.outputPath,
|
|
1446
2988
|
draft,
|
|
1447
2989
|
docdexId,
|
|
1448
|
-
warnings:
|
|
2990
|
+
warnings: runContext.warnings,
|
|
1449
2991
|
};
|
|
1450
2992
|
}
|
|
1451
2993
|
catch (error) {
|
|
@@ -1514,9 +3056,9 @@ export class DocsService {
|
|
|
1514
3056
|
}
|
|
1515
3057
|
}
|
|
1516
3058
|
async generateSds(options) {
|
|
1517
|
-
await this.assertSdsDocdexProfile();
|
|
1518
|
-
const assembler = new DocContextAssembler(this.docdex, this.workspace);
|
|
1519
3059
|
const warnings = [];
|
|
3060
|
+
await this.checkSdsDocdexProfile(warnings);
|
|
3061
|
+
const assembler = new DocContextAssembler(this.docdex, this.workspace);
|
|
1520
3062
|
const commandRun = await this.jobService.startCommandRun("docs-sds-generate", options.projectKey);
|
|
1521
3063
|
let job;
|
|
1522
3064
|
let resumeDraft;
|
|
@@ -1596,7 +3138,76 @@ export class DocsService {
|
|
|
1596
3138
|
}
|
|
1597
3139
|
}
|
|
1598
3140
|
}
|
|
1599
|
-
const
|
|
3141
|
+
const stream = options.agentStream ?? true;
|
|
3142
|
+
const iterate = options.iterate === true;
|
|
3143
|
+
const fastMode = iterate ? false : options.fast === true || process.env.MCODA_DOCS_FAST === "1";
|
|
3144
|
+
const skipValidation = process.env.MCODA_SKIP_SDS_VALIDATION === "1";
|
|
3145
|
+
const buildReady = options.buildReady === true || process.env.MCODA_DOCS_BUILD_READY === "1";
|
|
3146
|
+
const resolveOpenQuestions = options.resolveOpenQuestions === true ||
|
|
3147
|
+
process.env.MCODA_DOCS_RESOLVE_OPEN_QUESTIONS === "1";
|
|
3148
|
+
const noMaybes = options.noMaybes === true ||
|
|
3149
|
+
process.env.MCODA_DOCS_NO_MAYBES === "1" ||
|
|
3150
|
+
resolveOpenQuestions;
|
|
3151
|
+
const noPlaceholders = options.noPlaceholders === true ||
|
|
3152
|
+
process.env.MCODA_DOCS_NO_PLACEHOLDERS === "1" ||
|
|
3153
|
+
buildReady;
|
|
3154
|
+
const crossAlign = options.crossAlign !== false;
|
|
3155
|
+
const iterationEnabled = !options.dryRun && !fastMode;
|
|
3156
|
+
const maxIterations = iterationEnabled ? this.resolveMaxIterations() : 1;
|
|
3157
|
+
const agent = await this.selectDocgenAgent({
|
|
3158
|
+
agentName: options.agentName,
|
|
3159
|
+
commandName: "docs-sds-generate",
|
|
3160
|
+
commandAliases: ["docs-sds-generate", "docs:sds:generate", "sds"],
|
|
3161
|
+
jobId: job.id,
|
|
3162
|
+
warnings,
|
|
3163
|
+
iterationEnabled,
|
|
3164
|
+
});
|
|
3165
|
+
const runContext = this.createRunContext({
|
|
3166
|
+
commandName: "docs-sds-generate",
|
|
3167
|
+
commandRunId: commandRun.id,
|
|
3168
|
+
jobId: job.id,
|
|
3169
|
+
projectKey: options.projectKey,
|
|
3170
|
+
rfpPath: context.rfp?.path,
|
|
3171
|
+
templateName: options.templateName,
|
|
3172
|
+
outputPath,
|
|
3173
|
+
flags: {
|
|
3174
|
+
dryRun: options.dryRun === true,
|
|
3175
|
+
fast: fastMode,
|
|
3176
|
+
iterate,
|
|
3177
|
+
json: options.json === true,
|
|
3178
|
+
stream,
|
|
3179
|
+
buildReady,
|
|
3180
|
+
noPlaceholders,
|
|
3181
|
+
resolveOpenQuestions,
|
|
3182
|
+
noMaybes,
|
|
3183
|
+
crossAlign,
|
|
3184
|
+
},
|
|
3185
|
+
warnings,
|
|
3186
|
+
});
|
|
3187
|
+
runContext.iteration.max = maxIterations;
|
|
3188
|
+
const stateCleanupWarnings = await cleanupWorkspaceStateDirs({
|
|
3189
|
+
workspaceRoot: this.workspace.workspaceRoot,
|
|
3190
|
+
mcodaDir: this.workspace.mcodaDir,
|
|
3191
|
+
});
|
|
3192
|
+
const { statePath: iterativeOutputPath, warnings: statePathWarnings } = resolveDocgenStatePath({
|
|
3193
|
+
outputPath: runContext.outputPath,
|
|
3194
|
+
mcodaDir: this.workspace.mcodaDir,
|
|
3195
|
+
jobId: runContext.jobId,
|
|
3196
|
+
commandName: runContext.commandName,
|
|
3197
|
+
});
|
|
3198
|
+
const stateWarnings = [...stateCleanupWarnings, ...statePathWarnings];
|
|
3199
|
+
if (stateWarnings.length > 0) {
|
|
3200
|
+
runContext.stateWarnings = stateWarnings;
|
|
3201
|
+
this.appendUniqueWarnings(runContext, stateWarnings);
|
|
3202
|
+
}
|
|
3203
|
+
runContext.artifacts.sds = { kind: "sds", path: runContext.outputPath, meta: {} };
|
|
3204
|
+
await this.enforceToolDenylist({ runContext, agent });
|
|
3205
|
+
await this.recordDocgenStage(runContext, {
|
|
3206
|
+
stage: "generation",
|
|
3207
|
+
message: "Generating SDS draft",
|
|
3208
|
+
totalItems: maxIterations,
|
|
3209
|
+
processedItems: 0,
|
|
3210
|
+
});
|
|
1600
3211
|
const prompts = await this.agentService.getPrompts(agent.id);
|
|
1601
3212
|
const template = await this.loadSdsTemplate(options.templateName);
|
|
1602
3213
|
const sdsSections = getSdsSections(template.content);
|
|
@@ -1607,9 +3218,6 @@ export class DocsService {
|
|
|
1607
3218
|
let agentUsed = false;
|
|
1608
3219
|
let agentMetadata;
|
|
1609
3220
|
let adapter = agent.adapter;
|
|
1610
|
-
const stream = options.agentStream ?? true;
|
|
1611
|
-
const fastMode = options.fast === true || process.env.MCODA_DOCS_FAST === "1";
|
|
1612
|
-
const skipValidation = process.env.MCODA_SKIP_SDS_VALIDATION === "1";
|
|
1613
3221
|
const invoke = async (input) => {
|
|
1614
3222
|
agentUsed = true;
|
|
1615
3223
|
const { output: out, adapter: usedAdapter, metadata } = await this.invokeAgent(agent, input, stream, job.id, options.onToken);
|
|
@@ -1642,7 +3250,8 @@ export class DocsService {
|
|
|
1642
3250
|
provider: adapter,
|
|
1643
3251
|
docdexAvailable: context.docdexAvailable,
|
|
1644
3252
|
template: template.name,
|
|
1645
|
-
attempt,
|
|
3253
|
+
attempt: attempt + 1,
|
|
3254
|
+
phase: attempt === 0 ? "draft_sds" : "draft_sds_retry",
|
|
1646
3255
|
},
|
|
1647
3256
|
});
|
|
1648
3257
|
if (valid)
|
|
@@ -1694,7 +3303,14 @@ export class DocsService {
|
|
|
1694
3303
|
action: "draft_sds_resume_regenerate",
|
|
1695
3304
|
promptTokens: estimateTokens(prompt),
|
|
1696
3305
|
completionTokens: estimateTokens(agentOutput),
|
|
1697
|
-
metadata: {
|
|
3306
|
+
metadata: {
|
|
3307
|
+
adapter,
|
|
3308
|
+
provider: adapter,
|
|
3309
|
+
docdexAvailable: context.docdexAvailable,
|
|
3310
|
+
template: template.name,
|
|
3311
|
+
phase: "draft_sds_resume_regenerate",
|
|
3312
|
+
attempt: 1,
|
|
3313
|
+
},
|
|
1698
3314
|
});
|
|
1699
3315
|
}
|
|
1700
3316
|
if (fastMode) {
|
|
@@ -1721,7 +3337,7 @@ export class DocsService {
|
|
|
1721
3337
|
}
|
|
1722
3338
|
await fs.mkdir(path.dirname(draftPath), { recursive: true });
|
|
1723
3339
|
await fs.writeFile(draftPath, draft, "utf8");
|
|
1724
|
-
const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "sds", `${path.basename(outputPath, path.extname(outputPath))}-first-draft.md`);
|
|
3340
|
+
const firstDraftPath = path.join(this.workspace.mcodaDir, "docs", "sds", `${path.basename(runContext.outputPath, path.extname(runContext.outputPath))}-first-draft.md`);
|
|
1725
3341
|
await ensureDir(firstDraftPath);
|
|
1726
3342
|
await fs.writeFile(firstDraftPath, draft, "utf8");
|
|
1727
3343
|
if (fastMode) {
|
|
@@ -1729,7 +3345,7 @@ export class DocsService {
|
|
|
1729
3345
|
}
|
|
1730
3346
|
else {
|
|
1731
3347
|
try {
|
|
1732
|
-
const iterativeDraft = await buildIterativeSds(options.projectKey, context, draft, sdsSections,
|
|
3348
|
+
const iterativeDraft = await buildIterativeSds(options.projectKey, context, draft, sdsSections, iterativeOutputPath, invoke);
|
|
1733
3349
|
draft = iterativeDraft;
|
|
1734
3350
|
}
|
|
1735
3351
|
catch (error) {
|
|
@@ -1744,17 +3360,18 @@ export class DocsService {
|
|
|
1744
3360
|
let docdexId;
|
|
1745
3361
|
let segments;
|
|
1746
3362
|
let mirrorStatus = "skipped";
|
|
3363
|
+
let reviewReportPath;
|
|
1747
3364
|
if (options.dryRun) {
|
|
1748
3365
|
warnings.push("Dry run enabled; SDS was not written to disk or registered in docdex.");
|
|
1749
3366
|
}
|
|
1750
3367
|
if (!options.dryRun) {
|
|
1751
|
-
await this.writeSdsFile(outputPath, draft);
|
|
3368
|
+
await this.writeSdsFile(runContext.outputPath, draft);
|
|
1752
3369
|
if (context.docdexAvailable) {
|
|
1753
3370
|
try {
|
|
1754
|
-
const registered = await this.registerSds(outputPath, draft, options.projectKey);
|
|
3371
|
+
const registered = await this.registerSds(runContext.outputPath, draft, options.projectKey);
|
|
1755
3372
|
docdexId = registered.id;
|
|
1756
3373
|
segments = (registered.segments ?? []).map((s) => s.id);
|
|
1757
|
-
await fs.writeFile(`${outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
|
|
3374
|
+
await fs.writeFile(`${runContext.outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
|
|
1758
3375
|
}
|
|
1759
3376
|
catch (error) {
|
|
1760
3377
|
warnings.push(`Docdex registration skipped: ${error.message}`);
|
|
@@ -1765,7 +3382,7 @@ export class DocsService {
|
|
|
1765
3382
|
if (shouldMirror) {
|
|
1766
3383
|
try {
|
|
1767
3384
|
await ensureDir(path.join(publicDocsDir, "placeholder"));
|
|
1768
|
-
const mirrorPath = path.join(publicDocsDir, path.basename(outputPath));
|
|
3385
|
+
const mirrorPath = path.join(publicDocsDir, path.basename(runContext.outputPath));
|
|
1769
3386
|
await ensureDir(mirrorPath);
|
|
1770
3387
|
await fs.writeFile(mirrorPath, draft, "utf8");
|
|
1771
3388
|
mirrorStatus = "mirrored";
|
|
@@ -1774,15 +3391,51 @@ export class DocsService {
|
|
|
1774
3391
|
mirrorStatus = "failed";
|
|
1775
3392
|
}
|
|
1776
3393
|
}
|
|
3394
|
+
try {
|
|
3395
|
+
await this.recordDocgenStage(runContext, {
|
|
3396
|
+
stage: "inventory",
|
|
3397
|
+
message: "Building doc inventory",
|
|
3398
|
+
});
|
|
3399
|
+
runContext.artifacts = await buildDocInventory({
|
|
3400
|
+
workspace: this.workspace,
|
|
3401
|
+
preferred: { sdsPath: runContext.outputPath },
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
catch (error) {
|
|
3405
|
+
runContext.warnings.push(`Doc inventory build failed: ${error.message ?? String(error)}`);
|
|
3406
|
+
}
|
|
3407
|
+
await this.recordDocgenStage(runContext, {
|
|
3408
|
+
stage: "blueprint",
|
|
3409
|
+
message: "Generating deployment blueprint",
|
|
3410
|
+
});
|
|
3411
|
+
const blueprintGenerated = await this.generateDeploymentBlueprint(runContext, draft, options.projectKey);
|
|
3412
|
+
if (blueprintGenerated) {
|
|
3413
|
+
try {
|
|
3414
|
+
await this.recordDocgenStage(runContext, {
|
|
3415
|
+
stage: "inventory",
|
|
3416
|
+
message: "Rebuilding doc inventory",
|
|
3417
|
+
});
|
|
3418
|
+
runContext.artifacts = await buildDocInventory({
|
|
3419
|
+
workspace: this.workspace,
|
|
3420
|
+
preferred: { sdsPath: runContext.outputPath },
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
catch (error) {
|
|
3424
|
+
runContext.warnings.push(`Doc inventory rebuild after blueprint failed: ${error.message ?? String(error)}`);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
const iterationResult = await this.runIterationLoop(runContext);
|
|
3428
|
+
reviewReportPath = iterationResult.reviewReportPath;
|
|
1777
3429
|
}
|
|
1778
3430
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
1779
3431
|
payload: {
|
|
1780
|
-
outputPath,
|
|
3432
|
+
outputPath: runContext.outputPath,
|
|
1781
3433
|
docdexId,
|
|
1782
3434
|
segments,
|
|
1783
3435
|
template: template.name,
|
|
1784
3436
|
mirrorStatus,
|
|
1785
3437
|
agentMetadata,
|
|
3438
|
+
...(reviewReportPath ? { reviewReportPath } : {}),
|
|
1786
3439
|
},
|
|
1787
3440
|
});
|
|
1788
3441
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
@@ -1804,10 +3457,10 @@ export class DocsService {
|
|
|
1804
3457
|
return {
|
|
1805
3458
|
jobId: job.id,
|
|
1806
3459
|
commandRunId: commandRun.id,
|
|
1807
|
-
outputPath,
|
|
3460
|
+
outputPath: runContext.outputPath,
|
|
1808
3461
|
draft,
|
|
1809
3462
|
docdexId,
|
|
1810
|
-
warnings,
|
|
3463
|
+
warnings: runContext.warnings,
|
|
1811
3464
|
};
|
|
1812
3465
|
}
|
|
1813
3466
|
catch (error) {
|