@mcoda/core 0.1.9 → 0.1.12

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