@mcoda/core 0.1.8 → 0.1.11
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/CHANGELOG.md +3 -0
- package/README.md +2 -2
- package/dist/api/AgentsApi.d.ts +9 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +201 -6
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +6 -0
- package/dist/api/TasksApi.d.ts.map +1 -1
- package/dist/api/TasksApi.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +9 -1
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +9 -0
- package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
- package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingFormula.js +45 -0
- package/dist/services/agents/AgentRatingService.d.ts +60 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingService.js +363 -0
- package/dist/services/agents/GatewayAgentService.d.ts +11 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +525 -84
- package/dist/services/agents/GatewayHandoff.d.ts +11 -0
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
- package/dist/services/agents/GatewayHandoff.js +141 -0
- 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 +17 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +538 -79
- 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 +70 -3
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1930 -89
- 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 +200 -0
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
- package/dist/services/execution/GatewayTrioService.js +2492 -0
- 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 +2 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +9 -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 +27 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +354 -7
- package/dist/services/execution/QaTasksService.d.ts +59 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +3347 -318
- 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 +32 -1
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +4667 -722
- 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 +51 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +953 -106
- package/dist/services/planning/CreateTasksService.d.ts +21 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +569 -31
- package/dist/services/planning/RefineTasksService.d.ts +9 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +409 -59
- package/dist/services/review/CodeReviewService.d.ts +18 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +1309 -167
- 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 +17 -0
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
- package/dist/services/shared/ProjectGuidance.js +78 -0
- 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/tasks/TaskCommentFormatter.d.ts +20 -0
- package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
- package/dist/services/tasks/TaskCommentFormatter.js +54 -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 +26 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +206 -32
- package/package.json +6 -5
|
@@ -1,15 +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 {
|
|
4
|
+
import { DocsScaffolder } from "@mcoda/generators";
|
|
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";
|
|
40
|
+
import { AgentRatingService, selectBestAgentForCapabilities, } from "../agents/AgentRatingService.js";
|
|
41
|
+
import { DocAlignmentPatcher } from "./alignment/DocAlignmentPatcher.js";
|
|
42
|
+
import { ToolDenylist } from "../system/ToolDenylist.js";
|
|
10
43
|
const ensureDir = async (targetPath) => {
|
|
11
44
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
12
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
|
+
]);
|
|
13
79
|
const readPromptIfExists = async (workspace, relative) => {
|
|
14
80
|
const candidate = path.join(workspace.mcodaDir, relative);
|
|
15
81
|
try {
|
|
@@ -22,6 +88,7 @@ const readPromptIfExists = async (workspace, relative) => {
|
|
|
22
88
|
const PDR_REQUIRED_HEADINGS = [
|
|
23
89
|
["Introduction"],
|
|
24
90
|
["Scope"],
|
|
91
|
+
["Technology Stack", "Tech Stack"],
|
|
25
92
|
["Requirements", "Requirements & Constraints"],
|
|
26
93
|
["Architecture", "Architecture Overview"],
|
|
27
94
|
["Interfaces", "Interfaces / APIs"],
|
|
@@ -88,6 +155,43 @@ const extractBullets = (content, limit = 20) => {
|
|
|
88
155
|
.filter((line) => line.length > 0)
|
|
89
156
|
.slice(0, limit);
|
|
90
157
|
};
|
|
158
|
+
const DEFAULT_TECH_STACK_FALLBACK = [
|
|
159
|
+
"- Frontend: React + TypeScript",
|
|
160
|
+
"- Backend/services: TypeScript (Node.js)",
|
|
161
|
+
"- Database: MySQL",
|
|
162
|
+
"- Cache/queues: Redis",
|
|
163
|
+
"- Scripting/ops: Bash",
|
|
164
|
+
"- Override defaults if the RFP specifies a different stack.",
|
|
165
|
+
].join("\n");
|
|
166
|
+
const ML_TECH_STACK_FALLBACK = [
|
|
167
|
+
"- Language: Python",
|
|
168
|
+
"- ML stack: PyTorch or TensorFlow (pick based on model requirements)",
|
|
169
|
+
"- Services/API: Python web framework (FastAPI/Flask) as needed",
|
|
170
|
+
"- Database: MySQL",
|
|
171
|
+
"- Cache/queues: Redis",
|
|
172
|
+
"- Scripting/ops: Bash",
|
|
173
|
+
"- Override defaults if the RFP specifies a different stack.",
|
|
174
|
+
].join("\n");
|
|
175
|
+
const contextIndicatesMlStack = (context) => {
|
|
176
|
+
const sources = [context.rfp?.content, ...context.related.map((doc) => doc.content ?? "")].filter(Boolean);
|
|
177
|
+
if (!sources.length)
|
|
178
|
+
return false;
|
|
179
|
+
const text = sources.join("\n").toLowerCase();
|
|
180
|
+
const patterns = [
|
|
181
|
+
/\bmachine learning\b/,
|
|
182
|
+
/\bdeep learning\b/,
|
|
183
|
+
/\bneural\b/,
|
|
184
|
+
/\bmodel training\b/,
|
|
185
|
+
/\bmodel inference\b/,
|
|
186
|
+
/\bml\b/,
|
|
187
|
+
/\bllm\b/,
|
|
188
|
+
/\bembeddings?\b/,
|
|
189
|
+
];
|
|
190
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
191
|
+
};
|
|
192
|
+
const resolveTechStackFallback = (context) => {
|
|
193
|
+
return contextIndicatesMlStack(context) ? ML_TECH_STACK_FALLBACK : DEFAULT_TECH_STACK_FALLBACK;
|
|
194
|
+
};
|
|
91
195
|
class DocContextAssembler {
|
|
92
196
|
constructor(docdex, workspace) {
|
|
93
197
|
this.docdex = docdex;
|
|
@@ -100,9 +204,28 @@ class DocContextAssembler {
|
|
|
100
204
|
}
|
|
101
205
|
async findLatestLocalDoc(docType) {
|
|
102
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
|
+
}
|
|
103
226
|
const dirs = [
|
|
104
|
-
path.join(this.workspace.mcodaDir, "docs",
|
|
105
|
-
path.join(this.workspace.workspaceRoot, "docs",
|
|
227
|
+
path.join(this.workspace.mcodaDir, "docs", lower),
|
|
228
|
+
path.join(this.workspace.workspaceRoot, "docs", lower),
|
|
106
229
|
];
|
|
107
230
|
for (const dir of dirs) {
|
|
108
231
|
try {
|
|
@@ -237,7 +360,7 @@ class DocContextAssembler {
|
|
|
237
360
|
existingSds = [localSds];
|
|
238
361
|
}
|
|
239
362
|
if (!pdrs.length && !rfp) {
|
|
240
|
-
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).`);
|
|
241
364
|
}
|
|
242
365
|
const blocks = [];
|
|
243
366
|
if (rfp)
|
|
@@ -330,10 +453,22 @@ class DocContextAssembler {
|
|
|
330
453
|
}
|
|
331
454
|
let related = [];
|
|
332
455
|
if (docdexAvailable) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
456
|
+
try {
|
|
457
|
+
related = await this.docdex.search({ projectKey: input.projectKey, docType: "PDR", profile: "rfp_default" });
|
|
458
|
+
const sds = await this.docdex.search({ projectKey: input.projectKey, docType: "SDS", profile: "rfp_default" });
|
|
459
|
+
openapi = await this.docdex.search({
|
|
460
|
+
projectKey: input.projectKey,
|
|
461
|
+
docType: "OPENAPI",
|
|
462
|
+
profile: "rfp_default",
|
|
463
|
+
});
|
|
464
|
+
related = [...related, ...sds];
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
docdexAvailable = false;
|
|
468
|
+
related = [];
|
|
469
|
+
openapi = [];
|
|
470
|
+
warnings.push(`Docdex unavailable; continuing without related docs (${error.message ?? "unknown error"}).`);
|
|
471
|
+
}
|
|
337
472
|
}
|
|
338
473
|
const summaryParts = [
|
|
339
474
|
`RFP: ${this.summarize(rfp)}`,
|
|
@@ -383,7 +518,7 @@ const buildRunPrompt = (context, projectKey, prompts, runbook) => {
|
|
|
383
518
|
docdexNote,
|
|
384
519
|
[
|
|
385
520
|
"Return markdown with exactly these sections as H2 headings, one time each:",
|
|
386
|
-
"Introduction, Scope, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria",
|
|
521
|
+
"Introduction, Scope, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria",
|
|
387
522
|
"Do not use bold headings; use `##` headings only. Do not repeat sections.",
|
|
388
523
|
].join("\n"),
|
|
389
524
|
runbookPrompt,
|
|
@@ -421,12 +556,14 @@ const buildSdsRunPrompt = (context, projectKey, prompts, runbook, template) => {
|
|
|
421
556
|
const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
|
|
422
557
|
const canonicalTitles = PDR_REQUIRED_HEADINGS.map((variants) => variants[0]);
|
|
423
558
|
const normalized = normalizeHeadingsToH2(draft, canonicalTitles);
|
|
559
|
+
const techStackFallback = resolveTechStackFallback(context);
|
|
424
560
|
const required = [
|
|
425
561
|
{ title: "Introduction", fallback: `This PDR summarizes project ${projectKey ?? "N/A"} based on ${rfpSource}.` },
|
|
426
562
|
{
|
|
427
563
|
title: "Scope",
|
|
428
564
|
fallback: "In-scope: todo CRUD (title required; optional description, due date, priority), status toggle, filters/sort/search, bulk complete/delete, keyboard shortcuts, responsive UI, offline/localStorage. Out-of-scope: multi-user/auth/sync/backends, notifications/reminders, team features, heavy UI kits.",
|
|
429
565
|
},
|
|
566
|
+
{ title: "Technology Stack", fallback: techStackFallback },
|
|
430
567
|
{
|
|
431
568
|
title: "Requirements & Constraints",
|
|
432
569
|
fallback: context.bullets.map((b) => `- ${b}`).join("\n") ||
|
|
@@ -444,7 +581,20 @@ const ensureStructuredDraft = (draft, projectKey, context, rfpSource) => {
|
|
|
444
581
|
for (const section of required) {
|
|
445
582
|
const best = getBestSectionBody(normalized, section.title);
|
|
446
583
|
const cleaned = cleanBody(best ?? "");
|
|
447
|
-
|
|
584
|
+
let body = cleaned && cleaned.length > 0 ? cleaned : cleanBody(section.fallback);
|
|
585
|
+
if (section.title === "Interfaces / APIs" && (context.openapi?.length ?? 0) === 0) {
|
|
586
|
+
const scrubbed = stripInventedEndpoints(body);
|
|
587
|
+
const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (authentication/identity, data access, integrations, eventing, analytics/observability).";
|
|
588
|
+
if (!scrubbed || scrubbed.length === 0 || /endpoint/i.test(scrubbed)) {
|
|
589
|
+
body = cleanBody(openApiFallback);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
body = scrubbed;
|
|
593
|
+
}
|
|
594
|
+
if (!/openapi/i.test(body)) {
|
|
595
|
+
body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
448
598
|
parts.push(`## ${section.title}`);
|
|
449
599
|
parts.push(body);
|
|
450
600
|
}
|
|
@@ -458,7 +608,7 @@ const tidyPdrDraft = async (draft, agent, invoke) => {
|
|
|
458
608
|
draft,
|
|
459
609
|
"",
|
|
460
610
|
"Requirements:",
|
|
461
|
-
"- Keep exactly one instance of each H2 section: Introduction, Scope, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria, Source RFP.",
|
|
611
|
+
"- Keep exactly one instance of each H2 section: Introduction, Scope, Technology Stack, Requirements & Constraints, Architecture Overview, Interfaces / APIs, Non-Functional Requirements, Risks & Mitigations, Open Questions, Acceptance Criteria, Source RFP.",
|
|
462
612
|
"- Remove duplicate sections, bold headings posing as sections, placeholder sentences, and repeated bullet blocks. If the same idea appears twice, keep the richer/longer version and drop the restatement.",
|
|
463
613
|
"- Do not add new sections or reorder the required outline.",
|
|
464
614
|
"- Keep content concise and aligned to the headings. Do not alter semantics.",
|
|
@@ -468,6 +618,13 @@ const tidyPdrDraft = async (draft, agent, invoke) => {
|
|
|
468
618
|
return output.trim();
|
|
469
619
|
};
|
|
470
620
|
const PDR_ENRICHMENT_SECTIONS = [
|
|
621
|
+
{
|
|
622
|
+
title: "Technology Stack",
|
|
623
|
+
guidance: [
|
|
624
|
+
"List frontend, backend/services, databases, caches/queues, infra/runtime, and scripting/tooling choices.",
|
|
625
|
+
"If the RFP omits stack details, state the default stack (TypeScript/React/MySQL/Redis/Bash) or a Python ML stack when neural/ML workloads are explicit.",
|
|
626
|
+
],
|
|
627
|
+
},
|
|
471
628
|
{
|
|
472
629
|
title: "Architecture Overview",
|
|
473
630
|
guidance: [
|
|
@@ -588,6 +745,19 @@ const ensureSdsStructuredDraft = (draft, projectKey, context, template) => {
|
|
|
588
745
|
for (const section of sections) {
|
|
589
746
|
structured = ensureSectionContent(structured, section, fallbackFor(section));
|
|
590
747
|
}
|
|
748
|
+
if ((context.openapi?.length ?? 0) === 0) {
|
|
749
|
+
const interfaceTitle = sections.find((section) => /interface|contract/i.test(section)) ?? "Interfaces & Contracts";
|
|
750
|
+
const extracted = extractSection(structured, interfaceTitle);
|
|
751
|
+
if (extracted) {
|
|
752
|
+
const scrubbed = stripInventedEndpoints(cleanBody(extracted.body ?? ""));
|
|
753
|
+
const openApiFallback = "No OpenAPI excerpts available. Capture interface needs as open questions (auth/identity, restaurant suggestions, voting cycles, results/analytics).";
|
|
754
|
+
let body = scrubbed.length > 0 && !/endpoint/i.test(scrubbed) ? scrubbed : cleanBody(openApiFallback);
|
|
755
|
+
if (!/openapi/i.test(body)) {
|
|
756
|
+
body = `${body}\n- No OpenAPI excerpts available; keep endpoints as open questions.`;
|
|
757
|
+
}
|
|
758
|
+
structured = replaceSection(structured, interfaceTitle, body);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
591
761
|
return structured;
|
|
592
762
|
};
|
|
593
763
|
const getSdsSections = (template) => {
|
|
@@ -847,6 +1017,7 @@ const PLACEHOLDER_PATTERNS = [
|
|
|
847
1017
|
/^[-*+.]?\s*Outstanding questions/i,
|
|
848
1018
|
/^[-*+.]?\s*Performance, reliability, compliance/i,
|
|
849
1019
|
/^[-*+.]?\s*Enumerate risks from the RFP/i,
|
|
1020
|
+
/^I (will|am going to|plan to|am)\s+(read|review|analy[sz]e|gather|start|begin|look|scan)\b/i,
|
|
850
1021
|
];
|
|
851
1022
|
const cleanBody = (body) => {
|
|
852
1023
|
const requiredTitles = PDR_REQUIRED_HEADINGS.flat().map((t) => t.toLowerCase());
|
|
@@ -887,6 +1058,14 @@ const cleanBody = (body) => {
|
|
|
887
1058
|
}
|
|
888
1059
|
return deduped.join("\n").trim();
|
|
889
1060
|
};
|
|
1061
|
+
const stripInventedEndpoints = (body) => {
|
|
1062
|
+
const lines = body.split(/\r?\n/);
|
|
1063
|
+
const filtered = lines.filter((line) => {
|
|
1064
|
+
const normalized = line.replace(/[`*_]/g, "");
|
|
1065
|
+
return !/(^|\s)\b(GET|POST|PUT|PATCH|DELETE)\b[^\n]*\//i.test(normalized);
|
|
1066
|
+
});
|
|
1067
|
+
return filtered.join("\n").trim();
|
|
1068
|
+
};
|
|
890
1069
|
const extractSection = (draft, title) => {
|
|
891
1070
|
const regex = new RegExp(`(^#{1,6}\\s+${title}\\b)([\\s\\S]*?)(?=^#{1,6}\\s+|(?![\\s\\S]))`, "im");
|
|
892
1071
|
const match = draft.match(regex);
|
|
@@ -910,7 +1089,7 @@ const getBestSectionBody = (draft, title) => {
|
|
|
910
1089
|
};
|
|
911
1090
|
const replaceSection = (draft, title, newBody) => {
|
|
912
1091
|
const normalizedBody = cleanBody(newBody);
|
|
913
|
-
const regex = new RegExp(`(^#{1,6}\\s+${title}\\b)([\\s\\S]*?)(?=^#{1,6}\\s
|
|
1092
|
+
const regex = new RegExp(`(^#{1,6}\\s+${title}\\b)([\\s\\S]*?)(?=^#{1,6}\\s+|(?![\\s\\S]))`, "im");
|
|
914
1093
|
if (regex.test(draft)) {
|
|
915
1094
|
return draft.replace(regex, `$1\n\n${normalizedBody}\n\n`);
|
|
916
1095
|
}
|
|
@@ -978,33 +1157,42 @@ const readGitBranch = async (workspaceRoot) => {
|
|
|
978
1157
|
export class DocsService {
|
|
979
1158
|
constructor(workspace, deps) {
|
|
980
1159
|
this.workspace = workspace;
|
|
981
|
-
|
|
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 });
|
|
982
1162
|
this.jobService = deps?.jobService ?? new JobService(workspace, undefined, { noTelemetry: deps?.noTelemetry });
|
|
983
1163
|
this.repo = deps.repo;
|
|
984
1164
|
this.agentService = deps.agentService;
|
|
985
1165
|
this.routingService = deps.routingService;
|
|
1166
|
+
this.workspaceRepo = deps.workspaceRepo;
|
|
1167
|
+
this.ratingService = deps.ratingService;
|
|
986
1168
|
}
|
|
987
1169
|
static async create(workspace, options = {}) {
|
|
988
1170
|
const repo = await GlobalRepository.create();
|
|
989
1171
|
const agentService = new AgentService(repo);
|
|
990
1172
|
const routingService = await RoutingService.create();
|
|
1173
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
991
1174
|
const docdex = new DocdexClient({
|
|
992
1175
|
workspaceRoot: workspace.workspaceRoot,
|
|
993
1176
|
baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
|
|
1177
|
+
repoId: docdexRepoId,
|
|
994
1178
|
});
|
|
995
1179
|
const jobService = new JobService(workspace, undefined, { noTelemetry: options.noTelemetry });
|
|
996
1180
|
return new DocsService(workspace, { repo, agentService, routingService, docdex, jobService, noTelemetry: options.noTelemetry });
|
|
997
1181
|
}
|
|
998
1182
|
async close() {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1183
|
+
const swallow = async (fn) => {
|
|
1184
|
+
try {
|
|
1185
|
+
if (fn)
|
|
1186
|
+
await fn();
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
// Best-effort close; ignore errors (including "database is closed").
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
await swallow(this.agentService.close?.bind(this.agentService));
|
|
1193
|
+
await swallow(this.repo.close?.bind(this.repo));
|
|
1194
|
+
await swallow(this.jobService.close?.bind(this.jobService));
|
|
1195
|
+
await swallow(this.workspaceRepo?.close?.bind(this.workspaceRepo));
|
|
1008
1196
|
}
|
|
1009
1197
|
defaultPdrOutputPath(projectKey, rfpPath) {
|
|
1010
1198
|
const slug = slugify(projectKey ?? (rfpPath ? path.basename(rfpPath, path.extname(rfpPath)) : "pdr"));
|
|
@@ -1036,14 +1224,1391 @@ export class DocsService {
|
|
|
1036
1224
|
}
|
|
1037
1225
|
return { name: templateName ?? "default", content: DEFAULT_SDS_TEMPLATE };
|
|
1038
1226
|
}
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
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
|
+
},
|
|
1412
|
+
});
|
|
1413
|
+
return selected;
|
|
1414
|
+
}
|
|
1415
|
+
async ensureRatingService() {
|
|
1416
|
+
if (this.ratingService)
|
|
1417
|
+
return this.ratingService;
|
|
1418
|
+
if (process.env.MCODA_DISABLE_DB === "1") {
|
|
1419
|
+
throw new Error("Workspace DB disabled; agent rating requires DB access.");
|
|
1420
|
+
}
|
|
1421
|
+
if (!this.workspaceRepo) {
|
|
1422
|
+
this.workspaceRepo = await WorkspaceRepository.create(this.workspace.workspaceRoot);
|
|
1423
|
+
}
|
|
1424
|
+
this.ratingService = new AgentRatingService(this.workspace, {
|
|
1425
|
+
workspaceRepo: this.workspaceRepo,
|
|
1426
|
+
globalRepo: this.repo,
|
|
1427
|
+
agentService: this.agentService,
|
|
1428
|
+
routingService: this.routingService,
|
|
1429
|
+
});
|
|
1430
|
+
return this.ratingService;
|
|
1431
|
+
}
|
|
1432
|
+
createRunContext(input) {
|
|
1433
|
+
return {
|
|
1434
|
+
version: 1,
|
|
1435
|
+
commandName: input.commandName,
|
|
1436
|
+
commandRunId: input.commandRunId,
|
|
1437
|
+
jobId: input.jobId,
|
|
1042
1438
|
workspace: this.workspace,
|
|
1043
|
-
|
|
1044
|
-
|
|
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(),
|
|
1045
2007
|
});
|
|
1046
|
-
|
|
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);
|
|
1047
2612
|
}
|
|
1048
2613
|
async writePdrFile(outPath, content) {
|
|
1049
2614
|
await ensureDir(outPath);
|
|
@@ -1067,16 +2632,18 @@ export class DocsService {
|
|
|
1067
2632
|
await ensureDir(outPath);
|
|
1068
2633
|
await fs.writeFile(outPath, content, "utf8");
|
|
1069
2634
|
}
|
|
1070
|
-
async
|
|
2635
|
+
async checkSdsDocdexProfile(warnings) {
|
|
1071
2636
|
const base = this.workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL;
|
|
1072
2637
|
if (base)
|
|
1073
2638
|
return;
|
|
1074
|
-
const localStore = path.join(this.workspace.
|
|
2639
|
+
const localStore = path.join(this.workspace.mcodaDir, "docdex", "documents.json");
|
|
1075
2640
|
try {
|
|
1076
2641
|
await fs.access(localStore);
|
|
2642
|
+
return;
|
|
1077
2643
|
}
|
|
1078
2644
|
catch {
|
|
1079
|
-
|
|
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).");
|
|
1080
2647
|
}
|
|
1081
2648
|
}
|
|
1082
2649
|
async registerSds(outPath, content, projectKey) {
|
|
@@ -1163,24 +2730,95 @@ export class DocsService {
|
|
|
1163
2730
|
workspaceId: this.workspace.workspaceId,
|
|
1164
2731
|
commandName: "docs-pdr-generate",
|
|
1165
2732
|
jobId: job.id,
|
|
2733
|
+
commandRunId: commandRun.id,
|
|
1166
2734
|
action: "docdex_context",
|
|
1167
2735
|
promptTokens: 0,
|
|
1168
2736
|
completionTokens: 0,
|
|
1169
2737
|
metadata: { docdexAvailable: context.docdexAvailable },
|
|
1170
2738
|
});
|
|
1171
|
-
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
|
+
});
|
|
1172
2810
|
const prompts = await this.agentService.getPrompts(agent.id);
|
|
1173
2811
|
const runbook = (await readPromptIfExists(this.workspace, path.join("prompts", "commands", "pdr-generate.md"))) ||
|
|
1174
2812
|
DEFAULT_PDR_RUNBOOK_PROMPT;
|
|
1175
2813
|
let draft = "";
|
|
2814
|
+
let agentUsed = false;
|
|
1176
2815
|
let agentMetadata;
|
|
1177
2816
|
let adapter = agent.adapter;
|
|
1178
|
-
const stream = options.agentStream ?? true;
|
|
1179
|
-
const skipValidation = process.env.MCODA_SKIP_PDR_VALIDATION === "1";
|
|
1180
2817
|
let lastInvoke;
|
|
1181
2818
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1182
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.`);
|
|
1183
2820
|
const invoke = async (input) => {
|
|
2821
|
+
agentUsed = true;
|
|
1184
2822
|
const { output: out, adapter: usedAdapter, metadata } = await this.invokeAgent(agent, input, stream, job.id, options.onToken);
|
|
1185
2823
|
adapter = usedAdapter;
|
|
1186
2824
|
agentMetadata = metadata;
|
|
@@ -1195,12 +2833,18 @@ export class DocsService {
|
|
|
1195
2833
|
workspaceId: this.workspace.workspaceId,
|
|
1196
2834
|
commandName: "docs-pdr-generate",
|
|
1197
2835
|
jobId: job.id,
|
|
2836
|
+
commandRunId: commandRun.id,
|
|
1198
2837
|
agentId: agent.id,
|
|
1199
2838
|
modelName: agent.defaultModel,
|
|
1200
2839
|
action: attempt === 0 ? "draft_pdr" : "draft_pdr_retry",
|
|
1201
2840
|
promptTokens: estimateTokens(prompt),
|
|
1202
2841
|
completionTokens: estimateTokens(agentOutput),
|
|
1203
|
-
metadata: {
|
|
2842
|
+
metadata: {
|
|
2843
|
+
adapter,
|
|
2844
|
+
docdexAvailable: context.docdexAvailable,
|
|
2845
|
+
attempt: attempt + 1,
|
|
2846
|
+
phase: attempt === 0 ? "draft_pdr" : "draft_pdr_retry",
|
|
2847
|
+
},
|
|
1204
2848
|
});
|
|
1205
2849
|
if (valid) {
|
|
1206
2850
|
draft = structured;
|
|
@@ -1218,11 +2862,14 @@ export class DocsService {
|
|
|
1218
2862
|
if (!draft) {
|
|
1219
2863
|
throw new Error("PDR draft generation failed; no valid draft produced.");
|
|
1220
2864
|
}
|
|
1221
|
-
if (
|
|
2865
|
+
if (fastMode) {
|
|
2866
|
+
context.warnings.push("Fast mode enabled; skipping PDR enrichment and tidy passes.");
|
|
2867
|
+
}
|
|
2868
|
+
else if (lastInvoke) {
|
|
1222
2869
|
draft = await enrichPdrDraft(draft, agent, context, options.projectKey, lastInvoke);
|
|
1223
2870
|
draft = ensureStructuredDraft(draft, options.projectKey, context, context.rfp.path ?? context.rfp.id ?? "RFP");
|
|
1224
2871
|
}
|
|
1225
|
-
if (lastInvoke) {
|
|
2872
|
+
if (!fastMode && lastInvoke) {
|
|
1226
2873
|
try {
|
|
1227
2874
|
const tidiedRaw = await tidyPdrDraft(draft, agent, lastInvoke);
|
|
1228
2875
|
const tidied = ensureStructuredDraft(tidiedRaw, options.projectKey, context, context.rfp.path ?? context.rfp.id ?? "RFP");
|
|
@@ -1236,17 +2883,21 @@ export class DocsService {
|
|
|
1236
2883
|
context.warnings.push(`Tidy pass skipped: ${error.message ?? "unknown error"}`);
|
|
1237
2884
|
}
|
|
1238
2885
|
}
|
|
1239
|
-
const outputPath = options.outPath ?? this.defaultPdrOutputPath(options.projectKey, context.rfp.path);
|
|
1240
2886
|
if (!options.dryRun) {
|
|
1241
|
-
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`);
|
|
1242
2888
|
await ensureDir(firstDraftPath);
|
|
1243
2889
|
await fs.writeFile(firstDraftPath, draft, "utf8");
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
draft = iterativeDraft;
|
|
2890
|
+
if (fastMode) {
|
|
2891
|
+
context.warnings.push("Fast mode enabled; skipping iterative PDR refinement.");
|
|
1247
2892
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
2893
|
+
else {
|
|
2894
|
+
try {
|
|
2895
|
+
const iterativeDraft = await buildIterativePdr(options.projectKey, context, draft, iterativeOutputPath, lastInvoke ?? (async (input) => this.invokeAgent(agent, input, stream, job.id, options.onToken)));
|
|
2896
|
+
draft = iterativeDraft;
|
|
2897
|
+
}
|
|
2898
|
+
catch (error) {
|
|
2899
|
+
context.warnings.push(`Iterative PDR refinement failed; keeping first draft. ${String(error)}`);
|
|
2900
|
+
}
|
|
1250
2901
|
}
|
|
1251
2902
|
}
|
|
1252
2903
|
await this.jobService.writeCheckpoint(job.id, {
|
|
@@ -1257,23 +2908,29 @@ export class DocsService {
|
|
|
1257
2908
|
let docdexId;
|
|
1258
2909
|
let segments;
|
|
1259
2910
|
let mirrorStatus = "skipped";
|
|
2911
|
+
let reviewReportPath;
|
|
1260
2912
|
if (options.dryRun) {
|
|
1261
2913
|
context.warnings.push("Dry run enabled; PDR was not written to disk or registered in docdex.");
|
|
1262
2914
|
}
|
|
1263
2915
|
if (!options.dryRun) {
|
|
1264
|
-
await this.writePdrFile(outputPath, draft);
|
|
2916
|
+
await this.writePdrFile(runContext.outputPath, draft);
|
|
1265
2917
|
if (context.docdexAvailable) {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
2918
|
+
try {
|
|
2919
|
+
const registered = await this.registerPdr(runContext.outputPath, draft, options.projectKey);
|
|
2920
|
+
docdexId = registered.id;
|
|
2921
|
+
segments = (registered.segments ?? []).map((s) => s.id);
|
|
2922
|
+
await fs.writeFile(`${runContext.outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
|
|
2923
|
+
}
|
|
2924
|
+
catch (error) {
|
|
2925
|
+
context.warnings.push(`Docdex registration skipped: ${error.message}`);
|
|
2926
|
+
}
|
|
1270
2927
|
}
|
|
1271
2928
|
const publicDocsDir = path.join(this.workspace.workspaceRoot, "docs", "pdr");
|
|
1272
2929
|
const shouldMirror = this.workspace.config?.mirrorDocs !== false;
|
|
1273
2930
|
if (shouldMirror) {
|
|
1274
2931
|
try {
|
|
1275
2932
|
await ensureDir(path.join(publicDocsDir, "placeholder"));
|
|
1276
|
-
const mirrorPath = path.join(publicDocsDir, path.basename(outputPath));
|
|
2933
|
+
const mirrorPath = path.join(publicDocsDir, path.basename(runContext.outputPath));
|
|
1277
2934
|
await ensureDir(mirrorPath);
|
|
1278
2935
|
await fs.writeFile(mirrorPath, draft, "utf8");
|
|
1279
2936
|
mirrorStatus = "mirrored";
|
|
@@ -1283,18 +2940,54 @@ export class DocsService {
|
|
|
1283
2940
|
mirrorStatus = "failed";
|
|
1284
2941
|
}
|
|
1285
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;
|
|
1286
2958
|
}
|
|
1287
2959
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
1288
|
-
payload: {
|
|
2960
|
+
payload: {
|
|
2961
|
+
outputPath: runContext.outputPath,
|
|
2962
|
+
docdexId,
|
|
2963
|
+
segments,
|
|
2964
|
+
mirrorStatus,
|
|
2965
|
+
...(reviewReportPath ? { reviewReportPath } : {}),
|
|
2966
|
+
},
|
|
1289
2967
|
});
|
|
1290
2968
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
2969
|
+
if (options.rateAgents && agentUsed) {
|
|
2970
|
+
try {
|
|
2971
|
+
const ratingService = await this.ensureRatingService();
|
|
2972
|
+
await ratingService.rate({
|
|
2973
|
+
workspace: this.workspace,
|
|
2974
|
+
agentId: agent.id,
|
|
2975
|
+
commandName: "docs-pdr-generate",
|
|
2976
|
+
jobId: job.id,
|
|
2977
|
+
commandRunId: commandRun.id,
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
catch (error) {
|
|
2981
|
+
context.warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
1291
2984
|
return {
|
|
1292
2985
|
jobId: job.id,
|
|
1293
2986
|
commandRunId: commandRun.id,
|
|
1294
|
-
outputPath,
|
|
2987
|
+
outputPath: runContext.outputPath,
|
|
1295
2988
|
draft,
|
|
1296
2989
|
docdexId,
|
|
1297
|
-
warnings:
|
|
2990
|
+
warnings: runContext.warnings,
|
|
1298
2991
|
};
|
|
1299
2992
|
}
|
|
1300
2993
|
catch (error) {
|
|
@@ -1363,9 +3056,9 @@ export class DocsService {
|
|
|
1363
3056
|
}
|
|
1364
3057
|
}
|
|
1365
3058
|
async generateSds(options) {
|
|
1366
|
-
await this.assertSdsDocdexProfile();
|
|
1367
|
-
const assembler = new DocContextAssembler(this.docdex, this.workspace);
|
|
1368
3059
|
const warnings = [];
|
|
3060
|
+
await this.checkSdsDocdexProfile(warnings);
|
|
3061
|
+
const assembler = new DocContextAssembler(this.docdex, this.workspace);
|
|
1369
3062
|
const commandRun = await this.jobService.startCommandRun("docs-sds-generate", options.projectKey);
|
|
1370
3063
|
let job;
|
|
1371
3064
|
let resumeDraft;
|
|
@@ -1423,6 +3116,7 @@ export class DocsService {
|
|
|
1423
3116
|
workspaceId: this.workspace.workspaceId,
|
|
1424
3117
|
commandName: "docs-sds-generate",
|
|
1425
3118
|
jobId: job.id,
|
|
3119
|
+
commandRunId: commandRun.id,
|
|
1426
3120
|
action: "docdex_context",
|
|
1427
3121
|
promptTokens: 0,
|
|
1428
3122
|
completionTokens: 0,
|
|
@@ -1444,7 +3138,76 @@ export class DocsService {
|
|
|
1444
3138
|
}
|
|
1445
3139
|
}
|
|
1446
3140
|
}
|
|
1447
|
-
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
|
+
});
|
|
1448
3211
|
const prompts = await this.agentService.getPrompts(agent.id);
|
|
1449
3212
|
const template = await this.loadSdsTemplate(options.templateName);
|
|
1450
3213
|
const sdsSections = getSdsSections(template.content);
|
|
@@ -1452,11 +3215,11 @@ export class DocsService {
|
|
|
1452
3215
|
(await readPromptIfExists(this.workspace, path.join("prompts", "sds", "generate.md"))) ||
|
|
1453
3216
|
DEFAULT_SDS_RUNBOOK_PROMPT;
|
|
1454
3217
|
let draft = resumeDraft ?? "";
|
|
3218
|
+
let agentUsed = false;
|
|
1455
3219
|
let agentMetadata;
|
|
1456
3220
|
let adapter = agent.adapter;
|
|
1457
|
-
const stream = options.agentStream ?? true;
|
|
1458
|
-
const skipValidation = process.env.MCODA_SKIP_SDS_VALIDATION === "1";
|
|
1459
3221
|
const invoke = async (input) => {
|
|
3222
|
+
agentUsed = true;
|
|
1460
3223
|
const { output: out, adapter: usedAdapter, metadata } = await this.invokeAgent(agent, input, stream, job.id, options.onToken);
|
|
1461
3224
|
adapter = usedAdapter;
|
|
1462
3225
|
if (metadata)
|
|
@@ -1476,6 +3239,7 @@ export class DocsService {
|
|
|
1476
3239
|
workspaceId: this.workspace.workspaceId,
|
|
1477
3240
|
commandName: "docs-sds-generate",
|
|
1478
3241
|
jobId: job.id,
|
|
3242
|
+
commandRunId: commandRun.id,
|
|
1479
3243
|
agentId: agent.id,
|
|
1480
3244
|
modelName: agent.defaultModel,
|
|
1481
3245
|
action: attempt === 0 ? "draft_sds" : "draft_sds_retry",
|
|
@@ -1486,7 +3250,8 @@ export class DocsService {
|
|
|
1486
3250
|
provider: adapter,
|
|
1487
3251
|
docdexAvailable: context.docdexAvailable,
|
|
1488
3252
|
template: template.name,
|
|
1489
|
-
attempt,
|
|
3253
|
+
attempt: attempt + 1,
|
|
3254
|
+
phase: attempt === 0 ? "draft_sds" : "draft_sds_retry",
|
|
1490
3255
|
},
|
|
1491
3256
|
});
|
|
1492
3257
|
if (valid)
|
|
@@ -1508,6 +3273,7 @@ export class DocsService {
|
|
|
1508
3273
|
workspaceId: this.workspace.workspaceId,
|
|
1509
3274
|
commandName: "docs-sds-generate",
|
|
1510
3275
|
jobId: job.id,
|
|
3276
|
+
commandRunId: commandRun.id,
|
|
1511
3277
|
action: "draft_sds_resume",
|
|
1512
3278
|
promptTokens: 0,
|
|
1513
3279
|
completionTokens: estimateTokens(draft),
|
|
@@ -1531,42 +3297,60 @@ export class DocsService {
|
|
|
1531
3297
|
workspaceId: this.workspace.workspaceId,
|
|
1532
3298
|
commandName: "docs-sds-generate",
|
|
1533
3299
|
jobId: job.id,
|
|
3300
|
+
commandRunId: commandRun.id,
|
|
1534
3301
|
agentId: agent.id,
|
|
1535
3302
|
modelName: agent.defaultModel,
|
|
1536
3303
|
action: "draft_sds_resume_regenerate",
|
|
1537
3304
|
promptTokens: estimateTokens(prompt),
|
|
1538
3305
|
completionTokens: estimateTokens(agentOutput),
|
|
1539
|
-
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
|
+
},
|
|
1540
3314
|
});
|
|
1541
3315
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
|
|
1545
|
-
if (!skipValidation && !(validateSdsDraft(draft) && headingHasContent(draft, "Architecture"))) {
|
|
1546
|
-
warnings.push("Enriched SDS draft failed validation; using structured fallback.");
|
|
1547
|
-
draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
|
|
3316
|
+
if (fastMode) {
|
|
3317
|
+
warnings.push("Fast mode enabled; skipping SDS enrichment and tidy passes.");
|
|
1548
3318
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
3319
|
+
else {
|
|
3320
|
+
// Enrich each section sequentially after a valid base draft exists.
|
|
3321
|
+
draft = await enrichSdsDraft(draft, sdsSections, agent, context, options.projectKey, invoke);
|
|
3322
|
+
draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
|
|
3323
|
+
if (!skipValidation && !(validateSdsDraft(draft) && headingHasContent(draft, "Architecture"))) {
|
|
3324
|
+
warnings.push("Enriched SDS draft failed validation; using structured fallback.");
|
|
3325
|
+
draft = ensureSdsStructuredDraft(draft, options.projectKey, context, template.content);
|
|
3326
|
+
}
|
|
3327
|
+
try {
|
|
3328
|
+
const tidiedRaw = await tidySdsDraft(draft, sdsSections, agent, invoke);
|
|
3329
|
+
const tidied = ensureSdsStructuredDraft(tidiedRaw, options.projectKey, context, template.content);
|
|
3330
|
+
if (skipValidation || (validateSdsDraft(tidied) && headingHasContent(tidied, "Architecture"))) {
|
|
3331
|
+
draft = tidied;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
catch (error) {
|
|
3335
|
+
warnings.push(`SDS tidy pass skipped: ${error.message ?? "unknown error"}`);
|
|
1554
3336
|
}
|
|
1555
|
-
}
|
|
1556
|
-
catch (error) {
|
|
1557
|
-
warnings.push(`SDS tidy pass skipped: ${error.message ?? "unknown error"}`);
|
|
1558
3337
|
}
|
|
1559
3338
|
await fs.mkdir(path.dirname(draftPath), { recursive: true });
|
|
1560
3339
|
await fs.writeFile(draftPath, draft, "utf8");
|
|
1561
|
-
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`);
|
|
1562
3341
|
await ensureDir(firstDraftPath);
|
|
1563
3342
|
await fs.writeFile(firstDraftPath, draft, "utf8");
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
draft = iterativeDraft;
|
|
3343
|
+
if (fastMode) {
|
|
3344
|
+
warnings.push("Fast mode enabled; skipping iterative SDS refinement.");
|
|
1567
3345
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
3346
|
+
else {
|
|
3347
|
+
try {
|
|
3348
|
+
const iterativeDraft = await buildIterativeSds(options.projectKey, context, draft, sdsSections, iterativeOutputPath, invoke);
|
|
3349
|
+
draft = iterativeDraft;
|
|
3350
|
+
}
|
|
3351
|
+
catch (error) {
|
|
3352
|
+
warnings.push(`Iterative SDS refinement failed; keeping first draft. ${String(error)}`);
|
|
3353
|
+
}
|
|
1570
3354
|
}
|
|
1571
3355
|
await this.jobService.writeCheckpoint(job.id, {
|
|
1572
3356
|
stage: "draft_completed",
|
|
@@ -1576,23 +3360,29 @@ export class DocsService {
|
|
|
1576
3360
|
let docdexId;
|
|
1577
3361
|
let segments;
|
|
1578
3362
|
let mirrorStatus = "skipped";
|
|
3363
|
+
let reviewReportPath;
|
|
1579
3364
|
if (options.dryRun) {
|
|
1580
3365
|
warnings.push("Dry run enabled; SDS was not written to disk or registered in docdex.");
|
|
1581
3366
|
}
|
|
1582
3367
|
if (!options.dryRun) {
|
|
1583
|
-
await this.writeSdsFile(outputPath, draft);
|
|
3368
|
+
await this.writeSdsFile(runContext.outputPath, draft);
|
|
1584
3369
|
if (context.docdexAvailable) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
3370
|
+
try {
|
|
3371
|
+
const registered = await this.registerSds(runContext.outputPath, draft, options.projectKey);
|
|
3372
|
+
docdexId = registered.id;
|
|
3373
|
+
segments = (registered.segments ?? []).map((s) => s.id);
|
|
3374
|
+
await fs.writeFile(`${runContext.outputPath}.meta.json`, JSON.stringify({ docdexId, segments, projectKey: options.projectKey }, null, 2), "utf8");
|
|
3375
|
+
}
|
|
3376
|
+
catch (error) {
|
|
3377
|
+
warnings.push(`Docdex registration skipped: ${error.message}`);
|
|
3378
|
+
}
|
|
1589
3379
|
}
|
|
1590
3380
|
const publicDocsDir = path.join(this.workspace.workspaceRoot, "docs", "sds");
|
|
1591
3381
|
const shouldMirror = this.workspace.config?.mirrorDocs !== false;
|
|
1592
3382
|
if (shouldMirror) {
|
|
1593
3383
|
try {
|
|
1594
3384
|
await ensureDir(path.join(publicDocsDir, "placeholder"));
|
|
1595
|
-
const mirrorPath = path.join(publicDocsDir, path.basename(outputPath));
|
|
3385
|
+
const mirrorPath = path.join(publicDocsDir, path.basename(runContext.outputPath));
|
|
1596
3386
|
await ensureDir(mirrorPath);
|
|
1597
3387
|
await fs.writeFile(mirrorPath, draft, "utf8");
|
|
1598
3388
|
mirrorStatus = "mirrored";
|
|
@@ -1601,25 +3391,76 @@ export class DocsService {
|
|
|
1601
3391
|
mirrorStatus = "failed";
|
|
1602
3392
|
}
|
|
1603
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;
|
|
1604
3429
|
}
|
|
1605
3430
|
await this.jobService.updateJobStatus(job.id, "completed", {
|
|
1606
3431
|
payload: {
|
|
1607
|
-
outputPath,
|
|
3432
|
+
outputPath: runContext.outputPath,
|
|
1608
3433
|
docdexId,
|
|
1609
3434
|
segments,
|
|
1610
3435
|
template: template.name,
|
|
1611
3436
|
mirrorStatus,
|
|
1612
3437
|
agentMetadata,
|
|
3438
|
+
...(reviewReportPath ? { reviewReportPath } : {}),
|
|
1613
3439
|
},
|
|
1614
3440
|
});
|
|
1615
3441
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
3442
|
+
if (options.rateAgents && agentUsed) {
|
|
3443
|
+
try {
|
|
3444
|
+
const ratingService = await this.ensureRatingService();
|
|
3445
|
+
await ratingService.rate({
|
|
3446
|
+
workspace: this.workspace,
|
|
3447
|
+
agentId: agent.id,
|
|
3448
|
+
commandName: "docs-sds-generate",
|
|
3449
|
+
jobId: job.id,
|
|
3450
|
+
commandRunId: commandRun.id,
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
catch (error) {
|
|
3454
|
+
warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
1616
3457
|
return {
|
|
1617
3458
|
jobId: job.id,
|
|
1618
3459
|
commandRunId: commandRun.id,
|
|
1619
|
-
outputPath,
|
|
3460
|
+
outputPath: runContext.outputPath,
|
|
1620
3461
|
draft,
|
|
1621
3462
|
docdexId,
|
|
1622
|
-
warnings,
|
|
3463
|
+
warnings: runContext.warnings,
|
|
1623
3464
|
};
|
|
1624
3465
|
}
|
|
1625
3466
|
catch (error) {
|