@mcoda/core 0.1.8 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -0
- package/README.md +2 -2
- package/dist/api/AgentsApi.d.ts +9 -1
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +201 -6
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +6 -0
- package/dist/api/TasksApi.d.ts.map +1 -1
- package/dist/api/TasksApi.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +9 -1
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +9 -0
- package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
- package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingFormula.js +45 -0
- package/dist/services/agents/AgentRatingService.d.ts +60 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
- package/dist/services/agents/AgentRatingService.js +363 -0
- package/dist/services/agents/GatewayAgentService.d.ts +11 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +525 -84
- package/dist/services/agents/GatewayHandoff.d.ts +11 -0
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
- package/dist/services/agents/GatewayHandoff.js +141 -0
- package/dist/services/agents/RoutingService.d.ts +1 -0
- package/dist/services/agents/RoutingService.d.ts.map +1 -1
- package/dist/services/agents/RoutingService.js +4 -4
- package/dist/services/backlog/BacklogService.d.ts +23 -0
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +62 -7
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
- package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +17 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +538 -79
- package/dist/services/docs/DocInventory.d.ts +11 -0
- package/dist/services/docs/DocInventory.d.ts.map +1 -0
- package/dist/services/docs/DocInventory.js +230 -0
- package/dist/services/docs/DocgenRunContext.d.ts +59 -0
- package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
- package/dist/services/docs/DocgenRunContext.js +4 -0
- package/dist/services/docs/DocsService.d.ts +70 -3
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1930 -89
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
- package/dist/services/docs/patch/DocPatchEngine.js +331 -0
- package/dist/services/docs/review/Glossary.d.ts +16 -0
- package/dist/services/docs/review/Glossary.d.ts.map +1 -0
- package/dist/services/docs/review/Glossary.js +47 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportSchema.js +47 -0
- package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
- package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewTypes.js +94 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
- package/dist/services/docs/review/glossary.json +47 -0
- package/dist/services/estimate/EstimateService.d.ts +2 -0
- package/dist/services/estimate/EstimateService.d.ts.map +1 -1
- package/dist/services/estimate/EstimateService.js +66 -18
- package/dist/services/estimate/VelocityService.d.ts +4 -0
- package/dist/services/estimate/VelocityService.d.ts.map +1 -1
- package/dist/services/estimate/VelocityService.js +179 -36
- package/dist/services/estimate/types.d.ts +1 -0
- package/dist/services/estimate/types.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.d.ts +200 -0
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
- package/dist/services/execution/GatewayTrioService.js +2492 -0
- package/dist/services/execution/QaApiRunner.d.ts +30 -0
- package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
- package/dist/services/execution/QaApiRunner.js +881 -0
- package/dist/services/execution/QaFollowupService.d.ts +2 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +9 -2
- package/dist/services/execution/QaPlanValidator.d.ts +10 -0
- package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
- package/dist/services/execution/QaPlanValidator.js +128 -0
- package/dist/services/execution/QaProfileService.d.ts +27 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +354 -7
- package/dist/services/execution/QaTasksService.d.ts +59 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +3347 -318
- package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
- package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
- package/dist/services/execution/QaTestCommandBuilder.js +495 -0
- package/dist/services/execution/TaskSelectionService.d.ts +4 -2
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +144 -28
- package/dist/services/execution/TaskStateService.d.ts +19 -6
- package/dist/services/execution/TaskStateService.d.ts.map +1 -1
- package/dist/services/execution/TaskStateService.js +128 -13
- package/dist/services/execution/WorkOnTasksService.d.ts +32 -1
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +4667 -722
- package/dist/services/jobs/JobInsightsService.d.ts +4 -0
- package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
- package/dist/services/jobs/JobInsightsService.js +51 -5
- package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
- package/dist/services/jobs/JobResumeService.js +23 -10
- package/dist/services/jobs/JobService.d.ts +56 -4
- package/dist/services/jobs/JobService.d.ts.map +1 -1
- package/dist/services/jobs/JobService.js +232 -1
- package/dist/services/openapi/OpenApiService.d.ts +51 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +953 -106
- package/dist/services/planning/CreateTasksService.d.ts +21 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +569 -31
- package/dist/services/planning/RefineTasksService.d.ts +9 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +409 -59
- package/dist/services/review/CodeReviewService.d.ts +18 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +1309 -167
- package/dist/services/review/ReviewNormalizer.d.ts +9 -0
- package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
- package/dist/services/review/ReviewNormalizer.js +147 -0
- package/dist/services/shared/AuthErrors.d.ts +3 -0
- package/dist/services/shared/AuthErrors.d.ts.map +1 -0
- package/dist/services/shared/AuthErrors.js +17 -0
- package/dist/services/shared/DocdexGuidance.d.ts +7 -0
- package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
- package/dist/services/shared/DocdexGuidance.js +12 -0
- package/dist/services/shared/ProjectGuidance.d.ts +17 -0
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
- package/dist/services/shared/ProjectGuidance.js +78 -0
- package/dist/services/system/ToolDenylist.d.ts +13 -0
- package/dist/services/system/ToolDenylist.d.ts.map +1 -0
- package/dist/services/system/ToolDenylist.js +85 -0
- package/dist/services/tasks/TaskCommentFormatter.d.ts +20 -0
- package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
- package/dist/services/tasks/TaskCommentFormatter.js +54 -0
- package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/dist/services/telemetry/TelemetryService.js +39 -7
- package/dist/workspace/WorkspaceManager.d.ts +26 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +206 -32
- package/package.json +6 -5
|
@@ -4,15 +4,267 @@ import YAML from "yaml";
|
|
|
4
4
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
5
5
|
import { AgentService } from "@mcoda/agents";
|
|
6
6
|
import { DocdexClient } from "@mcoda/integrations";
|
|
7
|
-
import { GlobalRepository } from "@mcoda/db";
|
|
7
|
+
import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
|
|
8
8
|
import { JobService } from "../jobs/JobService.js";
|
|
9
9
|
import { RoutingService } from "../agents/RoutingService.js";
|
|
10
|
+
import { AgentRatingService } from "../agents/AgentRatingService.js";
|
|
11
|
+
import { buildDocInventory } from "../docs/DocInventory.js";
|
|
10
12
|
const OPENAPI_TAGS = [
|
|
11
13
|
// For project-specific specs, tags come from context; this is only a fallback.
|
|
12
14
|
];
|
|
13
15
|
const OPENAPI_VERSION = "3.1.0";
|
|
16
|
+
const PRIMARY_OPENAPI_FILENAME = "mcoda.yaml";
|
|
17
|
+
const ADMIN_OPENAPI_FILENAME = "mcoda-admin.yaml";
|
|
14
18
|
const CONTEXT_TOKEN_BUDGET = 8000;
|
|
19
|
+
const OPENAPI_TIMEOUT_ENV = "MCODA_OPENAPI_TIMEOUT_SECONDS";
|
|
20
|
+
const OPENAPI_HEARTBEAT_INTERVAL_MS = 15000;
|
|
21
|
+
const OPENAPI_PRIMARY_DRAFT = "openapi-primary-draft.yaml";
|
|
22
|
+
const OPENAPI_ADMIN_DRAFT = "openapi-admin-draft.yaml";
|
|
15
23
|
const estimateTokens = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
24
|
+
const parseTimeoutSeconds = (value) => {
|
|
25
|
+
if (!value)
|
|
26
|
+
return undefined;
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
return parsed * 1000;
|
|
31
|
+
};
|
|
32
|
+
const formatIterationLabel = (iteration) => {
|
|
33
|
+
if (!iteration || !Number.isFinite(iteration.current))
|
|
34
|
+
return undefined;
|
|
35
|
+
const current = iteration.current;
|
|
36
|
+
const max = iteration.max;
|
|
37
|
+
if (Number.isFinite(max) && max > 0)
|
|
38
|
+
return `${current}/${max}`;
|
|
39
|
+
return `${current}`;
|
|
40
|
+
};
|
|
41
|
+
const compactPayload = (payload) => Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
|
|
42
|
+
export class OpenApiJobError extends Error {
|
|
43
|
+
constructor(code, message, jobId) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.jobId = jobId;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const normalizeOpenApiPath = (value) => {
|
|
50
|
+
if (!value)
|
|
51
|
+
return "/";
|
|
52
|
+
const trimmed = value.trim();
|
|
53
|
+
const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
54
|
+
const withoutTrailing = withLeading.length > 1 ? withLeading.replace(/\/+$/, "") : withLeading;
|
|
55
|
+
const segments = withoutTrailing
|
|
56
|
+
.split("/")
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map((segment) => {
|
|
59
|
+
if (segment.startsWith("{") && segment.endsWith("}"))
|
|
60
|
+
return "{param}";
|
|
61
|
+
return segment;
|
|
62
|
+
});
|
|
63
|
+
return segments.length ? `/${segments.join("/")}` : "/";
|
|
64
|
+
};
|
|
65
|
+
export const extractOpenApiPaths = (raw) => {
|
|
66
|
+
const errors = [];
|
|
67
|
+
if (!raw || !raw.trim()) {
|
|
68
|
+
return { paths: [], errors: ["OpenAPI spec is empty."] };
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = YAML.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return { paths: [], errors: [`OpenAPI parse failed: ${error.message}`] };
|
|
76
|
+
}
|
|
77
|
+
if (!parsed || typeof parsed !== "object") {
|
|
78
|
+
return { paths: [], errors: ["OpenAPI spec is not a YAML object."] };
|
|
79
|
+
}
|
|
80
|
+
const paths = parsed.paths;
|
|
81
|
+
if (!paths || typeof paths !== "object") {
|
|
82
|
+
return { paths: [], errors: ["OpenAPI spec missing paths section."] };
|
|
83
|
+
}
|
|
84
|
+
return { paths: Object.keys(paths).filter(Boolean), errors };
|
|
85
|
+
};
|
|
86
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
87
|
+
export const findOpenApiPathLine = (raw, target) => {
|
|
88
|
+
if (!raw || !target)
|
|
89
|
+
return undefined;
|
|
90
|
+
const lines = raw.split(/\r?\n/);
|
|
91
|
+
const escaped = escapeRegExp(target);
|
|
92
|
+
const pattern = new RegExp(`^\\s*['"]?${escaped}['"]?\\s*:`);
|
|
93
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
94
|
+
if (pattern.test(lines[i] ?? ""))
|
|
95
|
+
return i + 1;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
const ADMIN_PATH_PATTERN = /\/admin(?:\/|\b)/i;
|
|
100
|
+
const ADMIN_CONTEXT_PATTERN = /\badmin\b.*\b(api|endpoint|console|dashboard|portal|interface|panel|ui)\b/i;
|
|
101
|
+
export const findAdminSurfaceMentions = (raw) => {
|
|
102
|
+
if (!raw || !raw.trim())
|
|
103
|
+
return [];
|
|
104
|
+
const lines = raw.split(/\r?\n/);
|
|
105
|
+
const mentions = [];
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
let inFence = false;
|
|
108
|
+
let currentHeading;
|
|
109
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
110
|
+
const line = lines[i] ?? "";
|
|
111
|
+
const trimmed = line.trim();
|
|
112
|
+
if (!trimmed)
|
|
113
|
+
continue;
|
|
114
|
+
if (/^```|^~~~/.test(trimmed)) {
|
|
115
|
+
inFence = !inFence;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (inFence)
|
|
119
|
+
continue;
|
|
120
|
+
const headingMatch = trimmed.match(/^#{1,6}\s+(.*)$/);
|
|
121
|
+
if (headingMatch) {
|
|
122
|
+
currentHeading = headingMatch[1]?.trim() || undefined;
|
|
123
|
+
if (currentHeading && /\badmin\b/i.test(currentHeading)) {
|
|
124
|
+
if (!seen.has(i + 1)) {
|
|
125
|
+
mentions.push({ line: i + 1, excerpt: currentHeading, heading: currentHeading });
|
|
126
|
+
seen.add(i + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ADMIN_PATH_PATTERN.test(trimmed) || ADMIN_CONTEXT_PATTERN.test(trimmed)) {
|
|
132
|
+
if (!seen.has(i + 1)) {
|
|
133
|
+
mentions.push({ line: i + 1, excerpt: trimmed, heading: currentHeading });
|
|
134
|
+
seen.add(i + 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return mentions;
|
|
139
|
+
};
|
|
140
|
+
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
141
|
+
const OPERATION_ID_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
142
|
+
const isPlainObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
143
|
+
const operationUsesJsonSchema = (operation) => {
|
|
144
|
+
const contentBlocks = [];
|
|
145
|
+
const requestContent = operation.requestBody?.content;
|
|
146
|
+
if (isPlainObject(requestContent))
|
|
147
|
+
contentBlocks.push(requestContent);
|
|
148
|
+
const responses = operation.responses;
|
|
149
|
+
if (isPlainObject(responses)) {
|
|
150
|
+
for (const response of Object.values(responses)) {
|
|
151
|
+
const responseContent = response?.content;
|
|
152
|
+
if (isPlainObject(responseContent))
|
|
153
|
+
contentBlocks.push(responseContent);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const content of contentBlocks) {
|
|
157
|
+
for (const [contentType, media] of Object.entries(content)) {
|
|
158
|
+
if (!contentType.toLowerCase().includes("json"))
|
|
159
|
+
continue;
|
|
160
|
+
if (media?.schema)
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
export const validateOpenApiSchema = (doc) => {
|
|
167
|
+
const errors = [];
|
|
168
|
+
if (!isPlainObject(doc)) {
|
|
169
|
+
errors.push("OpenAPI spec is not an object.");
|
|
170
|
+
return errors;
|
|
171
|
+
}
|
|
172
|
+
const version = doc.openapi;
|
|
173
|
+
if (!version) {
|
|
174
|
+
errors.push("Missing openapi version.");
|
|
175
|
+
}
|
|
176
|
+
else if (typeof version !== "string" || !version.startsWith("3.")) {
|
|
177
|
+
errors.push(`Invalid openapi version: ${String(version)}.`);
|
|
178
|
+
}
|
|
179
|
+
const info = doc.info;
|
|
180
|
+
if (!isPlainObject(info)) {
|
|
181
|
+
errors.push("Missing info section.");
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
if (!info.title)
|
|
185
|
+
errors.push("Missing info.title.");
|
|
186
|
+
if (!info.version)
|
|
187
|
+
errors.push("Missing info.version.");
|
|
188
|
+
}
|
|
189
|
+
const paths = doc.paths;
|
|
190
|
+
if (!isPlainObject(paths)) {
|
|
191
|
+
errors.push("Missing paths section.");
|
|
192
|
+
}
|
|
193
|
+
else if (Object.keys(paths).length === 0) {
|
|
194
|
+
errors.push("paths section is empty.");
|
|
195
|
+
}
|
|
196
|
+
const operationIds = new Map();
|
|
197
|
+
let hasOperations = false;
|
|
198
|
+
let hasJsonSchemaUsage = false;
|
|
199
|
+
if (isPlainObject(paths)) {
|
|
200
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
201
|
+
if (!isPlainObject(pathItem)) {
|
|
202
|
+
errors.push(`Path item for ${pathKey} must be an object.`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const methods = HTTP_METHODS.filter((method) => method in pathItem);
|
|
206
|
+
if (methods.length === 0) {
|
|
207
|
+
errors.push(`Path ${pathKey} has no operations.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const method of methods) {
|
|
211
|
+
const operation = pathItem[method];
|
|
212
|
+
if (!isPlainObject(operation)) {
|
|
213
|
+
errors.push(`Operation ${method.toUpperCase()} ${pathKey} must be an object.`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
hasOperations = true;
|
|
217
|
+
if (operationUsesJsonSchema(operation)) {
|
|
218
|
+
hasJsonSchemaUsage = true;
|
|
219
|
+
}
|
|
220
|
+
const operationId = operation.operationId;
|
|
221
|
+
if (!operationId || typeof operationId !== "string") {
|
|
222
|
+
errors.push(`Missing operationId for ${method.toUpperCase()} ${pathKey}.`);
|
|
223
|
+
}
|
|
224
|
+
else if (/\s/.test(operationId) || !OPERATION_ID_PATTERN.test(operationId)) {
|
|
225
|
+
errors.push(`Invalid operationId "${operationId}" for ${method.toUpperCase()} ${pathKey}.`);
|
|
226
|
+
}
|
|
227
|
+
else if (operationIds.has(operationId)) {
|
|
228
|
+
errors.push(`Duplicate operationId "${operationId}" detected.`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
operationIds.set(operationId, `${method.toUpperCase()} ${pathKey}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const components = doc.components;
|
|
237
|
+
const schemas = isPlainObject(components) ? components.schemas : undefined;
|
|
238
|
+
const schemaCount = isPlainObject(schemas) ? Object.keys(schemas).length : 0;
|
|
239
|
+
const hasSchemaRefs = typeof doc === "object" && JSON.stringify(doc).includes("#/components/schemas/");
|
|
240
|
+
if ((hasJsonSchemaUsage || hasSchemaRefs || hasOperations) && schemaCount === 0) {
|
|
241
|
+
errors.push("Missing components.schemas for JSON payloads.");
|
|
242
|
+
}
|
|
243
|
+
return errors;
|
|
244
|
+
};
|
|
245
|
+
export const validateOpenApiSchemaContent = (raw) => {
|
|
246
|
+
if (!raw || !raw.trim()) {
|
|
247
|
+
return { errors: ["OpenAPI spec is empty."] };
|
|
248
|
+
}
|
|
249
|
+
let parsed;
|
|
250
|
+
try {
|
|
251
|
+
parsed = YAML.parse(raw);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(raw);
|
|
256
|
+
}
|
|
257
|
+
catch (jsonError) {
|
|
258
|
+
return {
|
|
259
|
+
errors: [
|
|
260
|
+
`OpenAPI parse failed: ${error.message ?? String(error)}`,
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const errors = validateOpenApiSchema(parsed);
|
|
266
|
+
return { doc: parsed, errors };
|
|
267
|
+
};
|
|
16
268
|
const fileExists = async (candidate) => {
|
|
17
269
|
try {
|
|
18
270
|
await fs.access(candidate);
|
|
@@ -47,9 +299,12 @@ class OpenapiContextAssembler {
|
|
|
47
299
|
}
|
|
48
300
|
async findLatestLocalDoc(docType) {
|
|
49
301
|
const candidates = [];
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
302
|
+
const docDirs = [
|
|
303
|
+
path.join(this.workspace.mcodaDir, "docs"),
|
|
304
|
+
path.join(this.workspace.workspaceRoot, "docs"),
|
|
305
|
+
];
|
|
306
|
+
for (const dir of docDirs) {
|
|
307
|
+
const target = path.join(dir, docType.toLowerCase());
|
|
53
308
|
try {
|
|
54
309
|
const entries = await fs.readdir(target);
|
|
55
310
|
for (const entry of entries.filter((e) => e.endsWith(".md"))) {
|
|
@@ -203,27 +458,42 @@ class OpenapiContextAssembler {
|
|
|
203
458
|
export class OpenApiService {
|
|
204
459
|
constructor(workspace, deps) {
|
|
205
460
|
this.workspace = workspace;
|
|
206
|
-
|
|
461
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
462
|
+
this.docdex = deps?.docdex ?? new DocdexClient({ workspaceRoot: workspace.workspaceRoot, repoId: docdexRepoId });
|
|
207
463
|
this.jobService = deps?.jobService ?? new JobService(workspace, undefined, { noTelemetry: deps?.noTelemetry });
|
|
208
464
|
this.agentService = deps.agentService;
|
|
209
465
|
this.routingService = deps.routingService;
|
|
466
|
+
this.repo = deps.repo;
|
|
467
|
+
this.workspaceRepo = deps.workspaceRepo;
|
|
468
|
+
this.ratingService = deps.ratingService;
|
|
210
469
|
}
|
|
211
470
|
static async create(workspace, options = {}) {
|
|
212
471
|
const repo = await GlobalRepository.create();
|
|
213
472
|
const agentService = new AgentService(repo);
|
|
214
473
|
const routingService = await RoutingService.create();
|
|
474
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
215
475
|
const docdex = new DocdexClient({
|
|
216
476
|
workspaceRoot: workspace.workspaceRoot,
|
|
217
477
|
baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
|
|
478
|
+
repoId: docdexRepoId,
|
|
218
479
|
});
|
|
219
480
|
const jobService = new JobService(workspace, undefined, { noTelemetry: options.noTelemetry });
|
|
220
|
-
return new OpenApiService(workspace, { agentService, routingService, docdex, jobService, noTelemetry: options.noTelemetry });
|
|
481
|
+
return new OpenApiService(workspace, { agentService, routingService, docdex, jobService, repo, noTelemetry: options.noTelemetry });
|
|
221
482
|
}
|
|
222
483
|
async close() {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
484
|
+
const swallow = async (fn) => {
|
|
485
|
+
try {
|
|
486
|
+
if (fn)
|
|
487
|
+
await fn();
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// Best-effort close; ignore errors (including "database is closed").
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
await swallow(this.agentService.close?.bind(this.agentService));
|
|
494
|
+
await swallow(this.jobService.close?.bind(this.jobService));
|
|
495
|
+
await swallow(this.repo?.close?.bind(this.repo));
|
|
496
|
+
await swallow(this.workspaceRepo?.close?.bind(this.workspaceRepo));
|
|
227
497
|
}
|
|
228
498
|
async resolveAgent(agentName) {
|
|
229
499
|
const resolved = await this.routingService.resolveAgentForCommand({
|
|
@@ -233,6 +503,26 @@ export class OpenApiService {
|
|
|
233
503
|
});
|
|
234
504
|
return resolved.agent;
|
|
235
505
|
}
|
|
506
|
+
async ensureRatingService() {
|
|
507
|
+
if (this.ratingService)
|
|
508
|
+
return this.ratingService;
|
|
509
|
+
if (process.env.MCODA_DISABLE_DB === "1") {
|
|
510
|
+
throw new Error("Workspace DB disabled; agent rating requires DB access.");
|
|
511
|
+
}
|
|
512
|
+
if (!this.workspaceRepo) {
|
|
513
|
+
this.workspaceRepo = await WorkspaceRepository.create(this.workspace.workspaceRoot);
|
|
514
|
+
}
|
|
515
|
+
if (!this.repo) {
|
|
516
|
+
this.repo = await GlobalRepository.create();
|
|
517
|
+
}
|
|
518
|
+
this.ratingService = new AgentRatingService(this.workspace, {
|
|
519
|
+
workspaceRepo: this.workspaceRepo,
|
|
520
|
+
globalRepo: this.repo,
|
|
521
|
+
agentService: this.agentService,
|
|
522
|
+
routingService: this.routingService,
|
|
523
|
+
});
|
|
524
|
+
return this.ratingService;
|
|
525
|
+
}
|
|
236
526
|
async invokeAgent(agent, prompt, stream, jobId, onToken) {
|
|
237
527
|
if (stream) {
|
|
238
528
|
try {
|
|
@@ -262,27 +552,18 @@ export class OpenApiService {
|
|
|
262
552
|
}
|
|
263
553
|
sanitizeOutput(raw) {
|
|
264
554
|
const trimmed = raw.trim();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
555
|
+
let body = trimmed;
|
|
556
|
+
if (body.startsWith("```")) {
|
|
557
|
+
body = body.replace(/^```[a-zA-Z]*\s*/m, "").replace(/```$/, "");
|
|
268
558
|
}
|
|
269
|
-
|
|
559
|
+
const openapiIndex = body.search(/^openapi:\s*\d/m);
|
|
560
|
+
if (openapiIndex > 0) {
|
|
561
|
+
body = body.slice(openapiIndex);
|
|
562
|
+
}
|
|
563
|
+
return body.trim();
|
|
270
564
|
}
|
|
271
565
|
validateSpec(doc) {
|
|
272
|
-
|
|
273
|
-
if (!doc || typeof doc !== "object") {
|
|
274
|
-
errors.push("Spec is not a YAML object.");
|
|
275
|
-
return errors;
|
|
276
|
-
}
|
|
277
|
-
if (!doc.openapi)
|
|
278
|
-
errors.push("Missing openapi version");
|
|
279
|
-
if (!doc.info?.title)
|
|
280
|
-
errors.push("Missing info.title");
|
|
281
|
-
if (!doc.info?.version)
|
|
282
|
-
errors.push("Missing info.version");
|
|
283
|
-
if (!doc.paths)
|
|
284
|
-
errors.push("paths section is required (can be empty if no HTTP API)");
|
|
285
|
-
return errors;
|
|
566
|
+
return validateOpenApiSchema(doc);
|
|
286
567
|
}
|
|
287
568
|
async runOpenapiValidator(doc) {
|
|
288
569
|
try {
|
|
@@ -297,19 +578,27 @@ export class OpenApiService {
|
|
|
297
578
|
return [error.message];
|
|
298
579
|
}
|
|
299
580
|
}
|
|
300
|
-
buildPrompt(context, cliVersion, retryReasons) {
|
|
581
|
+
buildPrompt(context, cliVersion, retryReasons, variant = "primary") {
|
|
301
582
|
const contextBlocks = context.blocks
|
|
302
583
|
.map((block) => `### ${block.label}\n${block.content}`)
|
|
303
584
|
.join("\n\n");
|
|
304
585
|
const retryNote = retryReasons?.length
|
|
305
586
|
? `\nPrevious attempt issues:\n${retryReasons.map((r) => `- ${r}`).join("\n")}\nFix them in this draft.\n`
|
|
306
587
|
: "";
|
|
588
|
+
const adminNote = variant === "admin"
|
|
589
|
+
? [
|
|
590
|
+
"This is the ADMIN OpenAPI spec. Include only administrative/control-plane endpoints.",
|
|
591
|
+
"Focus on admin consoles, moderation, user management, and internal admin workflows described in docs.",
|
|
592
|
+
"Prefer /admin or /internal/admin style prefixes; omit public/customer-facing APIs.",
|
|
593
|
+
].join("\n")
|
|
594
|
+
: "";
|
|
307
595
|
return [
|
|
308
596
|
"You are generating an OpenAPI 3.1 YAML for THIS workspace/project using only the provided PDR/SDS/RFP context.",
|
|
309
597
|
"Derive resources, schemas, and HTTP endpoints directly from the product requirements (e.g., todos CRUD, filters, search, bulk actions).",
|
|
310
598
|
"If the documents describe a frontend-only/localStorage app, design a minimal REST API that could back those features (e.g., /todos, /todos/{id}, bulk operations, search/filter params) instead of returning an empty spec.",
|
|
311
599
|
"Prefer concise tags derived from domain resources (e.g., Todos). Avoid generic mcoda/system endpoints unless explicitly described in the context.",
|
|
312
600
|
`Use OpenAPI version ${OPENAPI_VERSION}, set info.title to the project name from context (fallback \"mcoda API\"), and info.version ${cliVersion}.`,
|
|
601
|
+
adminNote,
|
|
313
602
|
"Return only valid YAML (no Markdown fences, no commentary).",
|
|
314
603
|
retryNote,
|
|
315
604
|
"Scope rules:",
|
|
@@ -332,7 +621,7 @@ export class OpenApiService {
|
|
|
332
621
|
await fs.copyFile(target, backup);
|
|
333
622
|
return backup;
|
|
334
623
|
}
|
|
335
|
-
async registerOpenapi(outPath, content) {
|
|
624
|
+
async registerOpenapi(outPath, content, variant = "primary") {
|
|
336
625
|
const branch = this.workspace.config?.branch ?? (await readGitBranch(this.workspace.workspaceRoot));
|
|
337
626
|
return this.docdex.registerDocument({
|
|
338
627
|
docType: "OPENAPI",
|
|
@@ -343,6 +632,7 @@ export class OpenApiService {
|
|
|
343
632
|
branch,
|
|
344
633
|
status: "canonical",
|
|
345
634
|
projectKey: this.workspace.config?.projectKey,
|
|
635
|
+
variant,
|
|
346
636
|
},
|
|
347
637
|
});
|
|
348
638
|
}
|
|
@@ -352,53 +642,454 @@ export class OpenApiService {
|
|
|
352
642
|
const issues = this.validateSpec(parsed);
|
|
353
643
|
const validatorIssues = await this.runOpenapiValidator(parsed);
|
|
354
644
|
issues.push(...validatorIssues);
|
|
355
|
-
return { spec: content, issues };
|
|
645
|
+
return { spec: content, issues, doc: parsed };
|
|
646
|
+
}
|
|
647
|
+
async collectAdminMentions() {
|
|
648
|
+
const warnings = [];
|
|
649
|
+
let inventory;
|
|
650
|
+
try {
|
|
651
|
+
inventory = await buildDocInventory({ workspace: this.workspace });
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
warnings.push(`Doc inventory build failed: ${error.message ?? String(error)}`);
|
|
655
|
+
}
|
|
656
|
+
const records = [inventory?.pdr, inventory?.sds].filter((record) => Boolean(record));
|
|
657
|
+
if (records.length === 0) {
|
|
658
|
+
return { required: false, mentions: [], warnings };
|
|
659
|
+
}
|
|
660
|
+
const mentions = [];
|
|
661
|
+
for (const record of records) {
|
|
662
|
+
try {
|
|
663
|
+
const content = await fs.readFile(record.path, "utf8");
|
|
664
|
+
const found = findAdminSurfaceMentions(content);
|
|
665
|
+
for (const mention of found) {
|
|
666
|
+
mentions.push({ record, mention });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
warnings.push(`Unable to read doc ${record.path}: ${error.message ?? String(error)}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return { required: mentions.length > 0, mentions, warnings };
|
|
674
|
+
}
|
|
675
|
+
openapiDraftPath(jobId, variant) {
|
|
676
|
+
const filename = variant === "admin" ? OPENAPI_ADMIN_DRAFT : OPENAPI_PRIMARY_DRAFT;
|
|
677
|
+
return path.join(this.workspace.mcodaDir, "jobs", jobId, filename);
|
|
678
|
+
}
|
|
679
|
+
async readOpenapiDraft(jobId, variant, draftPathOverride) {
|
|
680
|
+
const draftPath = draftPathOverride ?? this.openapiDraftPath(jobId, variant);
|
|
681
|
+
try {
|
|
682
|
+
return await fs.readFile(draftPath, "utf8");
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
async writeOpenapiDraft(jobId, variant, content) {
|
|
689
|
+
const draftPath = this.openapiDraftPath(jobId, variant);
|
|
690
|
+
await fs.mkdir(path.dirname(draftPath), { recursive: true });
|
|
691
|
+
await fs.writeFile(draftPath, content, "utf8");
|
|
692
|
+
}
|
|
693
|
+
async isJobCancelled(jobId) {
|
|
694
|
+
const job = await this.jobService.getJob(jobId);
|
|
695
|
+
const state = job?.jobState ?? job?.state;
|
|
696
|
+
return state === "cancelled";
|
|
697
|
+
}
|
|
698
|
+
async updateOpenapiJobStatus(jobId, state, options = {}) {
|
|
699
|
+
const iterationLabel = formatIterationLabel(options.iteration);
|
|
700
|
+
const detail = options.jobStateDetail ??
|
|
701
|
+
[
|
|
702
|
+
options.stage ? `openapi:${options.stage}` : undefined,
|
|
703
|
+
options.variant ? `variant:${options.variant}` : undefined,
|
|
704
|
+
iterationLabel ? `iter:${iterationLabel}` : undefined,
|
|
705
|
+
]
|
|
706
|
+
.filter(Boolean)
|
|
707
|
+
.join(" ");
|
|
708
|
+
const payload = compactPayload({
|
|
709
|
+
...options.payload,
|
|
710
|
+
openapi_stage: options.stage,
|
|
711
|
+
openapi_variant: options.variant,
|
|
712
|
+
openapi_iteration_current: options.iteration?.current,
|
|
713
|
+
openapi_iteration_max: options.iteration?.max,
|
|
714
|
+
openapi_iteration_label: iterationLabel,
|
|
715
|
+
});
|
|
716
|
+
const totalItems = options.totalUnits;
|
|
717
|
+
const processedItems = options.completedUnits;
|
|
718
|
+
await this.jobService.updateJobStatus(jobId, state, {
|
|
719
|
+
job_state_detail: detail || undefined,
|
|
720
|
+
totalUnits: options.totalUnits,
|
|
721
|
+
completedUnits: options.completedUnits,
|
|
722
|
+
totalItems,
|
|
723
|
+
processedItems,
|
|
724
|
+
payload,
|
|
725
|
+
errorSummary: options.errorSummary,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async writeOpenapiCheckpoint(jobId, stage, details) {
|
|
729
|
+
await this.jobService.writeCheckpoint(jobId, {
|
|
730
|
+
stage,
|
|
731
|
+
timestamp: new Date().toISOString(),
|
|
732
|
+
details,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
startOpenapiHeartbeat(jobId, getStatus) {
|
|
736
|
+
let stopped = false;
|
|
737
|
+
let error;
|
|
738
|
+
const interval = setInterval(() => {
|
|
739
|
+
void (async () => {
|
|
740
|
+
if (stopped || error)
|
|
741
|
+
return;
|
|
742
|
+
if (await this.isJobCancelled(jobId)) {
|
|
743
|
+
error = new OpenApiJobError("cancelled", `OpenAPI job ${jobId} was cancelled.`, jobId);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const status = getStatus();
|
|
747
|
+
if (!status.stage)
|
|
748
|
+
return;
|
|
749
|
+
await this.updateOpenapiJobStatus(jobId, "running", {
|
|
750
|
+
stage: status.stage,
|
|
751
|
+
variant: status.variant,
|
|
752
|
+
iteration: status.iteration,
|
|
753
|
+
totalUnits: status.totalUnits,
|
|
754
|
+
completedUnits: status.completedUnits,
|
|
755
|
+
payload: status.payload,
|
|
756
|
+
});
|
|
757
|
+
})().catch(() => {
|
|
758
|
+
// best-effort heartbeat
|
|
759
|
+
});
|
|
760
|
+
}, OPENAPI_HEARTBEAT_INTERVAL_MS);
|
|
761
|
+
return {
|
|
762
|
+
stop: () => {
|
|
763
|
+
stopped = true;
|
|
764
|
+
clearInterval(interval);
|
|
765
|
+
},
|
|
766
|
+
getError: () => error,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async tryResumeOpenapi(resumeJobId, warnings) {
|
|
770
|
+
const manifest = await this.jobService.readManifest(resumeJobId);
|
|
771
|
+
if (!manifest) {
|
|
772
|
+
warnings.push(`No resume data found for job ${resumeJobId}; starting a new OpenAPI job.`);
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
const manifestType = manifest.type ?? manifest.job_type ?? manifest.jobType;
|
|
776
|
+
if (manifestType && manifestType !== "openapi_change") {
|
|
777
|
+
throw new Error(`Job ${resumeJobId} is type ${manifestType}, not openapi_change. Use a matching job id or rerun without --resume.`);
|
|
778
|
+
}
|
|
779
|
+
const status = manifest.status ?? manifest.state ?? manifest.jobState;
|
|
780
|
+
if (status === "running" || status === "queued" || status === "checkpointing") {
|
|
781
|
+
throw new Error(`Job ${resumeJobId} is still running; use "mcoda job watch --id ${resumeJobId}" to monitor.`);
|
|
782
|
+
}
|
|
783
|
+
const payload = manifest.payload ?? {};
|
|
784
|
+
const outputPath = payload.outputPath ??
|
|
785
|
+
payload.openapi_primary_output_path ??
|
|
786
|
+
payload.openapi_output_path ??
|
|
787
|
+
payload.output_path;
|
|
788
|
+
const adminOutputPath = payload.adminOutputPath ??
|
|
789
|
+
payload.openapi_admin_output_path ??
|
|
790
|
+
payload.admin_output_path;
|
|
791
|
+
const primaryDraftPath = payload.openapi_primary_draft_path ?? this.openapiDraftPath(resumeJobId, "primary");
|
|
792
|
+
const adminDraftPath = payload.openapi_admin_draft_path ?? this.openapiDraftPath(resumeJobId, "admin");
|
|
793
|
+
let primaryDraft;
|
|
794
|
+
let adminDraft;
|
|
795
|
+
let spec;
|
|
796
|
+
let adminSpec;
|
|
797
|
+
primaryDraft = await this.readOpenapiDraft(resumeJobId, "primary", primaryDraftPath);
|
|
798
|
+
adminDraft = await this.readOpenapiDraft(resumeJobId, "admin", adminDraftPath);
|
|
799
|
+
if (outputPath && (await fileExists(outputPath))) {
|
|
800
|
+
try {
|
|
801
|
+
spec = await fs.readFile(outputPath, "utf8");
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// ignore output read failures
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (adminOutputPath && (await fileExists(adminOutputPath))) {
|
|
808
|
+
try {
|
|
809
|
+
adminSpec = await fs.readFile(adminOutputPath, "utf8");
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
// ignore output read failures
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const docdexId = payload.docdexId ?? payload.openapi_docdex_id ?? payload.docdex_id;
|
|
816
|
+
const adminDocdexId = payload.adminDocdexId ?? payload.openapi_admin_docdex_id ?? payload.admin_docdex_id;
|
|
817
|
+
const lastStage = payload.openapi_stage ?? manifest.lastCheckpoint ?? manifest.last_checkpoint;
|
|
818
|
+
const jobId = manifest.id ?? manifest.job_id ?? resumeJobId;
|
|
819
|
+
const outputReady = Boolean(spec || primaryDraft);
|
|
820
|
+
if (status === "completed" || status === "succeeded") {
|
|
821
|
+
if (outputReady) {
|
|
822
|
+
warnings.push(`Resume requested; returning completed OpenAPI from job ${resumeJobId}.`);
|
|
823
|
+
return {
|
|
824
|
+
job: { ...manifest, id: jobId },
|
|
825
|
+
completed: true,
|
|
826
|
+
outputPath,
|
|
827
|
+
adminOutputPath,
|
|
828
|
+
spec: spec ?? primaryDraft,
|
|
829
|
+
adminSpec: adminSpec ?? adminDraft,
|
|
830
|
+
primaryDraft,
|
|
831
|
+
adminDraft,
|
|
832
|
+
docdexId,
|
|
833
|
+
adminDocdexId,
|
|
834
|
+
lastStage,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
warnings.push(`Resume requested for job ${resumeJobId}, but output is missing; restarting generation.`);
|
|
838
|
+
}
|
|
839
|
+
if (primaryDraft) {
|
|
840
|
+
warnings.push(`Resuming OpenAPI primary draft from job ${resumeJobId}.`);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
warnings.push(`Resume requested for ${resumeJobId}; regenerating OpenAPI primary draft.`);
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
job: { ...manifest, id: jobId },
|
|
847
|
+
completed: false,
|
|
848
|
+
outputPath,
|
|
849
|
+
adminOutputPath,
|
|
850
|
+
spec,
|
|
851
|
+
adminSpec,
|
|
852
|
+
primaryDraft,
|
|
853
|
+
adminDraft,
|
|
854
|
+
docdexId,
|
|
855
|
+
adminDocdexId,
|
|
856
|
+
lastStage,
|
|
857
|
+
};
|
|
356
858
|
}
|
|
357
859
|
async generateFromDocs(options) {
|
|
860
|
+
const warnings = [];
|
|
358
861
|
const commandRun = await this.jobService.startCommandRun("openapi-from-docs", options.projectKey);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
862
|
+
let job;
|
|
863
|
+
let resumePrimaryDraft;
|
|
864
|
+
let resumeAdminDraft;
|
|
865
|
+
let resumeDocdexId;
|
|
866
|
+
let resumeAdminDocdexId;
|
|
867
|
+
let resumeOutputPath;
|
|
868
|
+
let resumeAdminOutputPath;
|
|
869
|
+
let resumeSpec;
|
|
870
|
+
let resumeAdminSpec;
|
|
871
|
+
if (options.resumeJobId) {
|
|
872
|
+
const resumed = await this.tryResumeOpenapi(options.resumeJobId, warnings);
|
|
873
|
+
if (resumed) {
|
|
874
|
+
job = resumed.job;
|
|
875
|
+
if (resumed.completed) {
|
|
876
|
+
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
877
|
+
return {
|
|
878
|
+
jobId: job.id,
|
|
879
|
+
commandRunId: commandRun.id,
|
|
880
|
+
outputPath: resumed.outputPath,
|
|
881
|
+
spec: resumed.spec ?? "",
|
|
882
|
+
adminOutputPath: resumed.adminOutputPath,
|
|
883
|
+
adminSpec: resumed.adminSpec,
|
|
884
|
+
docdexId: resumed.docdexId,
|
|
885
|
+
adminDocdexId: resumed.adminDocdexId,
|
|
886
|
+
warnings,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
await this.updateOpenapiJobStatus(job.id, "running", {
|
|
890
|
+
stage: "resuming",
|
|
891
|
+
iteration: options.iteration,
|
|
892
|
+
payload: compactPayload({ resumedBy: commandRun.id }),
|
|
893
|
+
});
|
|
894
|
+
await this.writeOpenapiCheckpoint(job.id, "resume_started", { resumedBy: commandRun.id });
|
|
895
|
+
resumePrimaryDraft = resumed.primaryDraft;
|
|
896
|
+
resumeAdminDraft = resumed.adminDraft;
|
|
897
|
+
resumeDocdexId = resumed.docdexId;
|
|
898
|
+
resumeAdminDocdexId = resumed.adminDocdexId;
|
|
899
|
+
resumeOutputPath = resumed.outputPath;
|
|
900
|
+
resumeAdminOutputPath = resumed.adminOutputPath;
|
|
901
|
+
resumeSpec = resumed.spec;
|
|
902
|
+
resumeAdminSpec = resumed.adminSpec;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (!job) {
|
|
906
|
+
job = await this.jobService.startJob("openapi_change", commandRun.id, options.projectKey, {
|
|
907
|
+
commandName: commandRun.commandName,
|
|
908
|
+
payload: {
|
|
909
|
+
workspaceRoot: this.workspace.workspaceRoot,
|
|
910
|
+
projectKey: options.projectKey,
|
|
911
|
+
resumeSupported: true,
|
|
912
|
+
cliVersion: options.cliVersion,
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
const timeoutMs = options.timeoutMs ?? parseTimeoutSeconds(process.env[OPENAPI_TIMEOUT_ENV]);
|
|
917
|
+
const timeoutAt = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
918
|
+
const iteration = options.iteration;
|
|
919
|
+
const iterationLabel = formatIterationLabel(iteration);
|
|
920
|
+
const openapiDir = await this.ensureOpenapiDir();
|
|
921
|
+
const outputPath = resumeOutputPath ?? path.join(openapiDir, PRIMARY_OPENAPI_FILENAME);
|
|
922
|
+
const adminOutputPath = resumeAdminOutputPath ?? path.join(openapiDir, ADMIN_OPENAPI_FILENAME);
|
|
923
|
+
const primaryDraftPath = this.openapiDraftPath(job.id, "primary");
|
|
924
|
+
const adminDraftPath = this.openapiDraftPath(job.id, "admin");
|
|
925
|
+
const basePayload = compactPayload({
|
|
926
|
+
workspaceRoot: this.workspace.workspaceRoot,
|
|
927
|
+
projectKey: options.projectKey,
|
|
928
|
+
resumeSupported: true,
|
|
929
|
+
cliVersion: options.cliVersion,
|
|
930
|
+
openapi_timeout_ms: timeoutMs,
|
|
931
|
+
openapi_primary_output_path: outputPath,
|
|
932
|
+
openapi_admin_output_path: adminOutputPath,
|
|
933
|
+
openapi_primary_draft_path: primaryDraftPath,
|
|
934
|
+
openapi_admin_draft_path: adminDraftPath,
|
|
935
|
+
openapi_iteration_current: iteration?.current,
|
|
936
|
+
openapi_iteration_max: iteration?.max,
|
|
937
|
+
openapi_iteration_label: iterationLabel,
|
|
362
938
|
});
|
|
363
|
-
const
|
|
939
|
+
const buildPayload = (payload) => compactPayload({ ...basePayload, ...payload });
|
|
940
|
+
let totalUnits = 1;
|
|
941
|
+
let completedUnits = 0;
|
|
942
|
+
const perVariantUnits = options.validateOnly ? 1 : 2;
|
|
943
|
+
let currentStage = "starting";
|
|
944
|
+
let currentVariant;
|
|
945
|
+
const setStage = async (stage, variant, payload) => {
|
|
946
|
+
currentStage = stage;
|
|
947
|
+
currentVariant = variant;
|
|
948
|
+
await this.updateOpenapiJobStatus(job.id, "running", {
|
|
949
|
+
stage,
|
|
950
|
+
variant,
|
|
951
|
+
iteration,
|
|
952
|
+
totalUnits,
|
|
953
|
+
completedUnits,
|
|
954
|
+
payload: buildPayload(payload),
|
|
955
|
+
});
|
|
956
|
+
};
|
|
957
|
+
const completeStage = async (stage, variant, payload) => {
|
|
958
|
+
completedUnits += 1;
|
|
959
|
+
await this.updateOpenapiJobStatus(job.id, "running", {
|
|
960
|
+
stage,
|
|
961
|
+
variant,
|
|
962
|
+
iteration,
|
|
963
|
+
totalUnits,
|
|
964
|
+
completedUnits,
|
|
965
|
+
payload: buildPayload(payload),
|
|
966
|
+
});
|
|
967
|
+
};
|
|
968
|
+
const runWithTimeout = async (label, fn) => {
|
|
969
|
+
void label;
|
|
970
|
+
if (await this.isJobCancelled(job.id)) {
|
|
971
|
+
throw new OpenApiJobError("cancelled", `OpenAPI job ${job.id} was cancelled.`, job.id);
|
|
972
|
+
}
|
|
973
|
+
if (!timeoutAt)
|
|
974
|
+
return fn();
|
|
975
|
+
const remaining = timeoutAt - Date.now();
|
|
976
|
+
const timeoutSeconds = Math.max(1, Math.round((timeoutMs ?? 0) / 1000));
|
|
977
|
+
if (remaining <= 0) {
|
|
978
|
+
throw new OpenApiJobError("timeout", `OpenAPI job ${job.id} timed out after ${timeoutSeconds}s.`, job.id);
|
|
979
|
+
}
|
|
980
|
+
let timer;
|
|
981
|
+
try {
|
|
982
|
+
return await Promise.race([
|
|
983
|
+
fn(),
|
|
984
|
+
new Promise((_, reject) => {
|
|
985
|
+
timer = setTimeout(() => {
|
|
986
|
+
reject(new OpenApiJobError("timeout", `OpenAPI job ${job.id} timed out after ${timeoutSeconds}s.`, job.id));
|
|
987
|
+
}, remaining);
|
|
988
|
+
}),
|
|
989
|
+
]);
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
if (timer)
|
|
993
|
+
clearTimeout(timer);
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
const heartbeat = this.startOpenapiHeartbeat(job.id, () => ({
|
|
997
|
+
stage: currentStage,
|
|
998
|
+
variant: currentVariant,
|
|
999
|
+
iteration,
|
|
1000
|
+
totalUnits,
|
|
1001
|
+
completedUnits,
|
|
1002
|
+
payload: buildPayload({ openapi_last_heartbeat_at: new Date().toISOString() }),
|
|
1003
|
+
}));
|
|
1004
|
+
const assertHeartbeat = () => {
|
|
1005
|
+
const error = heartbeat.getError();
|
|
1006
|
+
if (error)
|
|
1007
|
+
throw error;
|
|
1008
|
+
};
|
|
364
1009
|
try {
|
|
1010
|
+
await setStage("context");
|
|
365
1011
|
const projectKey = options.projectKey ?? this.workspace.config?.projectKey;
|
|
366
1012
|
const assembler = new OpenapiContextAssembler(this.docdex, this.workspace, projectKey);
|
|
367
|
-
const context = await assembler.build();
|
|
1013
|
+
const context = await runWithTimeout("context", () => assembler.build());
|
|
368
1014
|
warnings.push(...context.warnings);
|
|
369
|
-
await this.
|
|
370
|
-
|
|
371
|
-
timestamp: new Date().toISOString(),
|
|
372
|
-
details: { docdexAvailable: context.docdexAvailable },
|
|
1015
|
+
await this.writeOpenapiCheckpoint(job.id, "context_built", {
|
|
1016
|
+
docdexAvailable: context.docdexAvailable,
|
|
373
1017
|
});
|
|
374
1018
|
await this.jobService.recordTokenUsage({
|
|
375
1019
|
timestamp: new Date().toISOString(),
|
|
376
1020
|
workspaceId: this.workspace.workspaceId,
|
|
377
1021
|
commandName: "openapi-from-docs",
|
|
378
1022
|
jobId: job.id,
|
|
1023
|
+
commandRunId: commandRun.id,
|
|
379
1024
|
action: "docdex_context",
|
|
380
1025
|
promptTokens: 0,
|
|
381
1026
|
completionTokens: 0,
|
|
382
1027
|
metadata: { docdexAvailable: context.docdexAvailable },
|
|
383
1028
|
});
|
|
384
|
-
|
|
385
|
-
const
|
|
1029
|
+
await completeStage("context", undefined, { docdexAvailable: context.docdexAvailable });
|
|
1030
|
+
const adminCheck = await runWithTimeout("admin_check", () => this.collectAdminMentions());
|
|
1031
|
+
warnings.push(...adminCheck.warnings);
|
|
1032
|
+
const adminRequired = adminCheck.required;
|
|
1033
|
+
totalUnits = 1 + perVariantUnits * (adminRequired ? 2 : 1);
|
|
1034
|
+
await this.updateOpenapiJobStatus(job.id, "running", {
|
|
1035
|
+
stage: currentStage,
|
|
1036
|
+
variant: currentVariant,
|
|
1037
|
+
iteration,
|
|
1038
|
+
totalUnits,
|
|
1039
|
+
completedUnits,
|
|
1040
|
+
payload: buildPayload({ openapi_admin_required: adminRequired }),
|
|
1041
|
+
});
|
|
1042
|
+
assertHeartbeat();
|
|
386
1043
|
if (options.validateOnly) {
|
|
1044
|
+
await setStage("validate", "primary");
|
|
387
1045
|
if (!(await fileExists(outputPath))) {
|
|
388
1046
|
throw new Error(`Cannot validate missing spec: ${outputPath}`);
|
|
389
1047
|
}
|
|
390
|
-
const
|
|
1048
|
+
const primaryResult = await runWithTimeout("validate_primary", () => this.validateExistingSpec(outputPath));
|
|
1049
|
+
const issues = primaryResult.issues.map((issue) => `Primary spec: ${issue}`);
|
|
1050
|
+
let adminSpec;
|
|
1051
|
+
let adminResult;
|
|
1052
|
+
const adminExists = await fileExists(adminOutputPath);
|
|
1053
|
+
if (adminRequired && !adminExists) {
|
|
1054
|
+
throw new Error(`Admin spec required but missing: ${adminOutputPath}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (adminExists) {
|
|
1057
|
+
adminResult = await runWithTimeout("validate_admin", () => this.validateExistingSpec(adminOutputPath));
|
|
1058
|
+
adminSpec = adminResult.spec;
|
|
1059
|
+
issues.push(...adminResult.issues.map((issue) => `Admin spec (${adminOutputPath}): ${issue}`));
|
|
1060
|
+
}
|
|
1061
|
+
if (adminResult?.doc && primaryResult.doc) {
|
|
1062
|
+
const primaryVersion = primaryResult.doc?.info?.version;
|
|
1063
|
+
const adminVersion = adminResult.doc?.info?.version;
|
|
1064
|
+
if (primaryVersion && adminVersion && primaryVersion !== adminVersion) {
|
|
1065
|
+
issues.push(`Admin spec info.version (${adminVersion}) does not match primary spec (${primaryVersion}).`);
|
|
1066
|
+
}
|
|
1067
|
+
const primaryOpenapi = primaryResult.doc?.openapi;
|
|
1068
|
+
const adminOpenapi = adminResult.doc?.openapi;
|
|
1069
|
+
if (primaryOpenapi && adminOpenapi && primaryOpenapi !== adminOpenapi) {
|
|
1070
|
+
issues.push(`Admin spec openapi version (${adminOpenapi}) does not match primary spec (${primaryOpenapi}).`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
391
1073
|
const validationNote = issues.length ? `Validation issues:\n${issues.join("\n")}` : "Validation passed.";
|
|
392
1074
|
await this.jobService.appendLog(job.id, `${validationNote}\n`);
|
|
1075
|
+
await completeStage("validate", "primary", { validation: validationNote });
|
|
1076
|
+
if (adminExists) {
|
|
1077
|
+
await completeStage("validate", "admin", { validation: validationNote });
|
|
1078
|
+
}
|
|
393
1079
|
const jobState = issues.length ? "failed" : "completed";
|
|
394
1080
|
const commandState = issues.length ? "failed" : "succeeded";
|
|
395
|
-
await this.jobService.updateJobStatus(job.id, jobState, {
|
|
1081
|
+
await this.jobService.updateJobStatus(job.id, jobState, {
|
|
1082
|
+
errorSummary: issues.length ? issues.join("; ") : undefined,
|
|
1083
|
+
payload: buildPayload({ validation: validationNote, openapi_admin_required: adminRequired }),
|
|
1084
|
+
});
|
|
396
1085
|
await this.jobService.finishCommandRun(commandRun.id, commandState, issues.join("; "));
|
|
397
1086
|
return {
|
|
398
1087
|
jobId: job.id,
|
|
399
1088
|
commandRunId: commandRun.id,
|
|
400
1089
|
outputPath,
|
|
401
|
-
spec,
|
|
1090
|
+
spec: primaryResult.spec,
|
|
1091
|
+
adminOutputPath: adminExists ? adminOutputPath : undefined,
|
|
1092
|
+
adminSpec,
|
|
402
1093
|
warnings,
|
|
403
1094
|
};
|
|
404
1095
|
}
|
|
@@ -407,97 +1098,253 @@ export class OpenApiService {
|
|
|
407
1098
|
}
|
|
408
1099
|
const agent = await this.resolveAgent(options.agentName);
|
|
409
1100
|
const stream = options.agentStream ?? true;
|
|
410
|
-
let
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
agentId: agent.id,
|
|
438
|
-
modelName: agent.defaultModel,
|
|
439
|
-
action: attempt === 0 ? "draft_openapi" : "draft_openapi_retry",
|
|
440
|
-
promptTokens: estimateTokens(prompt),
|
|
441
|
-
completionTokens: estimateTokens(output),
|
|
442
|
-
metadata: { adapter, provider: adapter, attempt },
|
|
443
|
-
});
|
|
444
|
-
if (errors.length === 0) {
|
|
445
|
-
specYaml = YAML.stringify(parsed);
|
|
446
|
-
break;
|
|
1101
|
+
let agentUsed = false;
|
|
1102
|
+
const generateVariant = async (variant, resumeDraft) => {
|
|
1103
|
+
const fallbackTitle = variant === "admin"
|
|
1104
|
+
? `${projectKey ?? "mcoda"} Admin API`
|
|
1105
|
+
: projectKey ?? "mcoda API";
|
|
1106
|
+
if (resumeDraft) {
|
|
1107
|
+
try {
|
|
1108
|
+
const parsed = YAML.parse(resumeDraft);
|
|
1109
|
+
if (!parsed.info)
|
|
1110
|
+
parsed.info = {};
|
|
1111
|
+
parsed.info.title = parsed.info.title ?? fallbackTitle;
|
|
1112
|
+
parsed.info.version = options.cliVersion;
|
|
1113
|
+
parsed.openapi = OPENAPI_VERSION;
|
|
1114
|
+
const errors = this.validateSpec(parsed);
|
|
1115
|
+
const validatorErrors = await runWithTimeout("validate_resume", () => this.runOpenapiValidator(parsed));
|
|
1116
|
+
errors.push(...validatorErrors);
|
|
1117
|
+
if (errors.length === 0) {
|
|
1118
|
+
const specYaml = YAML.stringify(parsed);
|
|
1119
|
+
await this.writeOpenapiDraft(job.id, variant, specYaml);
|
|
1120
|
+
await this.writeOpenapiCheckpoint(job.id, `draft_${variant}_completed`, {
|
|
1121
|
+
variant,
|
|
1122
|
+
draftPath: variant === "admin" ? adminDraftPath : primaryDraftPath,
|
|
1123
|
+
resumed: true,
|
|
1124
|
+
});
|
|
1125
|
+
return { specYaml, parsed, adapter: "resume" };
|
|
1126
|
+
}
|
|
1127
|
+
warnings.push(`Saved ${variant} draft invalid; regenerating: ${errors.join("; ")}`);
|
|
447
1128
|
}
|
|
448
|
-
|
|
449
|
-
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
warnings.push(`Saved ${variant} draft could not be parsed; regenerating: ${error.message ?? String(error)}`);
|
|
450
1131
|
}
|
|
451
|
-
lastErrors = errors;
|
|
452
1132
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1133
|
+
let specYaml = "";
|
|
1134
|
+
let parsed;
|
|
1135
|
+
let adapter = agent.adapter;
|
|
1136
|
+
let agentMetadata;
|
|
1137
|
+
let lastErrors;
|
|
1138
|
+
await setStage("draft", variant);
|
|
1139
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1140
|
+
const prompt = this.buildPrompt(context, options.cliVersion, lastErrors, variant);
|
|
1141
|
+
agentUsed = true;
|
|
1142
|
+
const { output, adapter: usedAdapter, metadata } = await runWithTimeout("draft_agent", async () => this.invokeAgent(agent, prompt, stream, job.id, options.onToken));
|
|
1143
|
+
adapter = usedAdapter;
|
|
1144
|
+
agentMetadata = metadata;
|
|
1145
|
+
specYaml = this.sanitizeOutput(output);
|
|
1146
|
+
try {
|
|
1147
|
+
parsed = YAML.parse(specYaml);
|
|
1148
|
+
if (!parsed.info)
|
|
1149
|
+
parsed.info = {};
|
|
1150
|
+
parsed.info.title = parsed.info.title ?? fallbackTitle;
|
|
1151
|
+
parsed.info.version = options.cliVersion;
|
|
1152
|
+
parsed.openapi = OPENAPI_VERSION;
|
|
1153
|
+
const errors = this.validateSpec(parsed);
|
|
1154
|
+
const validatorErrors = await runWithTimeout("validate_generated", () => this.runOpenapiValidator(parsed));
|
|
1155
|
+
errors.push(...validatorErrors);
|
|
1156
|
+
const action = variant === "admin"
|
|
1157
|
+
? attempt === 0
|
|
1158
|
+
? "draft_openapi_admin"
|
|
1159
|
+
: "draft_openapi_admin_retry"
|
|
1160
|
+
: attempt === 0
|
|
1161
|
+
? "draft_openapi"
|
|
1162
|
+
: "draft_openapi_retry";
|
|
1163
|
+
await this.jobService.recordTokenUsage({
|
|
1164
|
+
timestamp: new Date().toISOString(),
|
|
1165
|
+
workspaceId: this.workspace.workspaceId,
|
|
1166
|
+
commandName: "openapi-from-docs",
|
|
1167
|
+
jobId: job.id,
|
|
1168
|
+
commandRunId: commandRun.id,
|
|
1169
|
+
agentId: agent.id,
|
|
1170
|
+
modelName: agent.defaultModel,
|
|
1171
|
+
action,
|
|
1172
|
+
promptTokens: estimateTokens(prompt),
|
|
1173
|
+
completionTokens: estimateTokens(output),
|
|
1174
|
+
metadata: {
|
|
1175
|
+
adapter,
|
|
1176
|
+
provider: adapter,
|
|
1177
|
+
attempt: attempt + 1,
|
|
1178
|
+
phase: action,
|
|
1179
|
+
variant,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
if (errors.length === 0) {
|
|
1183
|
+
specYaml = YAML.stringify(parsed);
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
if (attempt === 1) {
|
|
1187
|
+
throw new Error(`Generated ${variant} spec failed validation: ${errors.join("; ")}`);
|
|
1188
|
+
}
|
|
1189
|
+
lastErrors = errors;
|
|
456
1190
|
}
|
|
457
|
-
|
|
1191
|
+
catch (error) {
|
|
1192
|
+
if (attempt === 1) {
|
|
1193
|
+
throw new Error(error.message || `Failed to parse generated ${variant} YAML`);
|
|
1194
|
+
}
|
|
1195
|
+
lastErrors = [error.message ?? "Invalid YAML"];
|
|
1196
|
+
}
|
|
1197
|
+
assertHeartbeat();
|
|
458
1198
|
}
|
|
1199
|
+
await this.writeOpenapiDraft(job.id, variant, specYaml);
|
|
1200
|
+
await this.writeOpenapiCheckpoint(job.id, `draft_${variant}_completed`, {
|
|
1201
|
+
variant,
|
|
1202
|
+
draftPath: variant === "admin" ? adminDraftPath : primaryDraftPath,
|
|
1203
|
+
});
|
|
1204
|
+
return { specYaml, parsed, adapter, agentMetadata };
|
|
1205
|
+
};
|
|
1206
|
+
const primarySpec = await generateVariant("primary", resumePrimaryDraft ?? resumeSpec);
|
|
1207
|
+
await completeStage("draft", "primary", { openapi_variant: "primary" });
|
|
1208
|
+
const adminSpec = adminRequired
|
|
1209
|
+
? await generateVariant("admin", resumeAdminDraft ?? resumeAdminSpec)
|
|
1210
|
+
: undefined;
|
|
1211
|
+
if (adminSpec) {
|
|
1212
|
+
await completeStage("draft", "admin", { openapi_variant: "admin" });
|
|
459
1213
|
}
|
|
460
1214
|
let backup;
|
|
1215
|
+
let adminBackup;
|
|
1216
|
+
let docdexId = resumeDocdexId;
|
|
1217
|
+
let adminDocdexId = resumeAdminDocdexId;
|
|
1218
|
+
await setStage("write", "primary");
|
|
461
1219
|
if (!options.dryRun) {
|
|
462
1220
|
backup = await this.backupIfNeeded(outputPath);
|
|
463
|
-
await fs.writeFile(outputPath, specYaml, "utf8");
|
|
1221
|
+
await runWithTimeout("write_primary", async () => fs.writeFile(outputPath, primarySpec.specYaml, "utf8"));
|
|
1222
|
+
if (context.docdexAvailable) {
|
|
1223
|
+
try {
|
|
1224
|
+
const registered = await runWithTimeout("docdex_primary", async () => this.registerOpenapi(outputPath, primarySpec.specYaml, "primary"));
|
|
1225
|
+
docdexId = registered.id;
|
|
1226
|
+
}
|
|
1227
|
+
catch (error) {
|
|
1228
|
+
warnings.push(`Docdex registration skipped: ${error.message}`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
464
1231
|
}
|
|
465
1232
|
else {
|
|
466
1233
|
warnings.push("Dry run enabled; spec not written to disk.");
|
|
467
1234
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1235
|
+
await completeStage("write", "primary", {
|
|
1236
|
+
outputPath,
|
|
1237
|
+
backupPath: backup,
|
|
1238
|
+
docdexId,
|
|
1239
|
+
adapter: primarySpec.adapter,
|
|
1240
|
+
adminAdapter: adminSpec?.adapter,
|
|
1241
|
+
agentMetadata: primarySpec.agentMetadata,
|
|
1242
|
+
adminAgentMetadata: adminSpec?.agentMetadata,
|
|
1243
|
+
openapi_admin_required: adminRequired,
|
|
1244
|
+
});
|
|
1245
|
+
if (adminSpec) {
|
|
1246
|
+
await setStage("write", "admin");
|
|
1247
|
+
if (!options.dryRun) {
|
|
1248
|
+
adminBackup = await this.backupIfNeeded(adminOutputPath);
|
|
1249
|
+
await runWithTimeout("write_admin", async () => fs.writeFile(adminOutputPath, adminSpec.specYaml, "utf8"));
|
|
1250
|
+
if (context.docdexAvailable) {
|
|
1251
|
+
try {
|
|
1252
|
+
const registered = await runWithTimeout("docdex_admin", async () => this.registerOpenapi(adminOutputPath, adminSpec.specYaml, "admin"));
|
|
1253
|
+
adminDocdexId = registered.id;
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
warnings.push(`Admin Docdex registration skipped: ${error.message}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
476
1259
|
}
|
|
1260
|
+
await completeStage("write", "admin", {
|
|
1261
|
+
adminOutputPath,
|
|
1262
|
+
adminBackupPath: adminBackup,
|
|
1263
|
+
adminDocdexId,
|
|
1264
|
+
openapi_admin_required: adminRequired,
|
|
1265
|
+
});
|
|
477
1266
|
}
|
|
478
|
-
await this.
|
|
479
|
-
|
|
1267
|
+
await this.updateOpenapiJobStatus(job.id, "completed", {
|
|
1268
|
+
stage: "complete",
|
|
1269
|
+
iteration,
|
|
1270
|
+
totalUnits,
|
|
1271
|
+
completedUnits: totalUnits,
|
|
1272
|
+
payload: buildPayload({
|
|
480
1273
|
outputPath,
|
|
481
1274
|
backupPath: backup,
|
|
1275
|
+
adminOutputPath: adminSpec ? adminOutputPath : undefined,
|
|
1276
|
+
adminBackupPath: adminBackup,
|
|
482
1277
|
docdexId,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1278
|
+
adminDocdexId,
|
|
1279
|
+
adapter: primarySpec.adapter,
|
|
1280
|
+
adminAdapter: adminSpec?.adapter,
|
|
1281
|
+
agentMetadata: primarySpec.agentMetadata,
|
|
1282
|
+
adminAgentMetadata: adminSpec?.agentMetadata,
|
|
1283
|
+
openapi_admin_required: adminRequired,
|
|
1284
|
+
}),
|
|
486
1285
|
});
|
|
487
1286
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
1287
|
+
if (options.rateAgents && agentUsed) {
|
|
1288
|
+
try {
|
|
1289
|
+
const ratingService = await this.ensureRatingService();
|
|
1290
|
+
await ratingService.rate({
|
|
1291
|
+
workspace: this.workspace,
|
|
1292
|
+
agentId: agent.id,
|
|
1293
|
+
commandName: "openapi-from-docs",
|
|
1294
|
+
jobId: job.id,
|
|
1295
|
+
commandRunId: commandRun.id,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
warnings.push(`Agent rating failed: ${error.message ?? String(error)}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
488
1302
|
return {
|
|
489
1303
|
jobId: job.id,
|
|
490
1304
|
commandRunId: commandRun.id,
|
|
491
1305
|
outputPath: options.dryRun ? undefined : outputPath,
|
|
492
|
-
spec: specYaml,
|
|
1306
|
+
spec: primarySpec.specYaml,
|
|
1307
|
+
adminOutputPath: options.dryRun ? undefined : adminSpec ? adminOutputPath : undefined,
|
|
1308
|
+
adminSpec: adminSpec?.specYaml,
|
|
493
1309
|
docdexId,
|
|
1310
|
+
adminDocdexId,
|
|
494
1311
|
warnings,
|
|
495
1312
|
};
|
|
496
1313
|
}
|
|
497
1314
|
catch (error) {
|
|
498
|
-
|
|
499
|
-
|
|
1315
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1316
|
+
const isOpenapiError = error instanceof OpenApiJobError;
|
|
1317
|
+
if (job) {
|
|
1318
|
+
if (isOpenapiError && error.code === "cancelled") {
|
|
1319
|
+
await this.updateOpenapiJobStatus(job.id, "cancelled", {
|
|
1320
|
+
stage: "cancelled",
|
|
1321
|
+
iteration: options.iteration,
|
|
1322
|
+
totalUnits,
|
|
1323
|
+
completedUnits,
|
|
1324
|
+
payload: buildPayload({ openapi_admin_required: undefined }),
|
|
1325
|
+
errorSummary: message,
|
|
1326
|
+
});
|
|
1327
|
+
await this.jobService.finishCommandRun(commandRun.id, "cancelled", message);
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
await this.updateOpenapiJobStatus(job.id, "failed", {
|
|
1331
|
+
stage: isOpenapiError && error.code === "timeout" ? "timeout" : "failed",
|
|
1332
|
+
iteration: options.iteration,
|
|
1333
|
+
totalUnits,
|
|
1334
|
+
completedUnits,
|
|
1335
|
+
payload: buildPayload({ openapi_admin_required: undefined }),
|
|
1336
|
+
errorSummary: message,
|
|
1337
|
+
});
|
|
1338
|
+
await this.jobService.finishCommandRun(commandRun.id, "failed", message);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
await this.jobService.finishCommandRun(commandRun.id, "failed", message);
|
|
1343
|
+
}
|
|
500
1344
|
throw error;
|
|
501
1345
|
}
|
|
1346
|
+
finally {
|
|
1347
|
+
heartbeat.stop();
|
|
1348
|
+
}
|
|
502
1349
|
}
|
|
503
1350
|
}
|