@mcoda/core 0.1.9 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api/AgentsApi.d.ts +1 -0
- package/dist/api/AgentsApi.d.ts.map +1 -1
- package/dist/api/AgentsApi.js +136 -11
- package/dist/api/QaTasksApi.d.ts.map +1 -1
- package/dist/api/QaTasksApi.js +4 -0
- package/dist/prompts/PdrPrompts.d.ts.map +1 -1
- package/dist/prompts/PdrPrompts.js +6 -0
- package/dist/prompts/SdsPrompts.d.ts.map +1 -1
- package/dist/prompts/SdsPrompts.js +7 -0
- package/dist/services/agents/AgentRatingService.d.ts +19 -0
- package/dist/services/agents/AgentRatingService.d.ts.map +1 -1
- package/dist/services/agents/AgentRatingService.js +66 -2
- package/dist/services/agents/GatewayAgentService.d.ts +8 -0
- package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
- package/dist/services/agents/GatewayAgentService.js +462 -65
- package/dist/services/agents/GatewayHandoff.d.ts +5 -1
- package/dist/services/agents/GatewayHandoff.d.ts.map +1 -1
- package/dist/services/agents/GatewayHandoff.js +65 -32
- package/dist/services/agents/RoutingService.d.ts +1 -0
- package/dist/services/agents/RoutingService.d.ts.map +1 -1
- package/dist/services/agents/RoutingService.js +4 -4
- package/dist/services/backlog/BacklogService.d.ts +23 -0
- package/dist/services/backlog/BacklogService.d.ts.map +1 -1
- package/dist/services/backlog/BacklogService.js +62 -7
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
- package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
- package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
- package/dist/services/backlog/TaskOrderingService.d.ts +16 -4
- package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
- package/dist/services/backlog/TaskOrderingService.js +529 -73
- package/dist/services/docs/DocInventory.d.ts +11 -0
- package/dist/services/docs/DocInventory.d.ts.map +1 -0
- package/dist/services/docs/DocInventory.js +230 -0
- package/dist/services/docs/DocgenRunContext.d.ts +59 -0
- package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
- package/dist/services/docs/DocgenRunContext.js +4 -0
- package/dist/services/docs/DocsService.d.ts +59 -2
- package/dist/services/docs/DocsService.d.ts.map +1 -1
- package/dist/services/docs/DocsService.js +1701 -48
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
- package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
- package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
- package/dist/services/docs/patch/DocPatchEngine.js +331 -0
- package/dist/services/docs/review/Glossary.d.ts +16 -0
- package/dist/services/docs/review/Glossary.d.ts.map +1 -0
- package/dist/services/docs/review/Glossary.js +47 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
- package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
- package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewReportSchema.js +47 -0
- package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
- package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
- package/dist/services/docs/review/ReviewTypes.js +94 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
- package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
- package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
- package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
- package/dist/services/docs/review/glossary.json +47 -0
- package/dist/services/estimate/EstimateService.d.ts +2 -0
- package/dist/services/estimate/EstimateService.d.ts.map +1 -1
- package/dist/services/estimate/EstimateService.js +66 -18
- package/dist/services/estimate/VelocityService.d.ts +4 -0
- package/dist/services/estimate/VelocityService.d.ts.map +1 -1
- package/dist/services/estimate/VelocityService.js +179 -36
- package/dist/services/estimate/types.d.ts +1 -0
- package/dist/services/estimate/types.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.d.ts +71 -4
- package/dist/services/execution/GatewayTrioService.d.ts.map +1 -1
- package/dist/services/execution/GatewayTrioService.js +1695 -328
- package/dist/services/execution/QaApiRunner.d.ts +30 -0
- package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
- package/dist/services/execution/QaApiRunner.js +881 -0
- package/dist/services/execution/QaFollowupService.d.ts +1 -0
- package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
- package/dist/services/execution/QaFollowupService.js +8 -2
- package/dist/services/execution/QaPlanValidator.d.ts +10 -0
- package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
- package/dist/services/execution/QaPlanValidator.js +128 -0
- package/dist/services/execution/QaProfileService.d.ts +21 -1
- package/dist/services/execution/QaProfileService.d.ts.map +1 -1
- package/dist/services/execution/QaProfileService.js +214 -29
- package/dist/services/execution/QaTasksService.d.ts +41 -1
- package/dist/services/execution/QaTasksService.d.ts.map +1 -1
- package/dist/services/execution/QaTasksService.js +2851 -500
- package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
- package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
- package/dist/services/execution/QaTestCommandBuilder.js +495 -0
- package/dist/services/execution/TaskSelectionService.d.ts +4 -2
- package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
- package/dist/services/execution/TaskSelectionService.js +144 -28
- package/dist/services/execution/TaskStateService.d.ts +19 -6
- package/dist/services/execution/TaskStateService.d.ts.map +1 -1
- package/dist/services/execution/TaskStateService.js +128 -13
- package/dist/services/execution/WorkOnTasksService.d.ts +19 -2
- package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
- package/dist/services/execution/WorkOnTasksService.js +3913 -1225
- package/dist/services/jobs/JobInsightsService.d.ts +4 -0
- package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
- package/dist/services/jobs/JobInsightsService.js +51 -5
- package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
- package/dist/services/jobs/JobResumeService.js +23 -10
- package/dist/services/jobs/JobService.d.ts +56 -4
- package/dist/services/jobs/JobService.d.ts.map +1 -1
- package/dist/services/jobs/JobService.js +232 -1
- package/dist/services/openapi/OpenApiService.d.ts +41 -0
- package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
- package/dist/services/openapi/OpenApiService.js +889 -98
- package/dist/services/planning/CreateTasksService.d.ts +15 -0
- package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
- package/dist/services/planning/CreateTasksService.js +311 -6
- package/dist/services/planning/RefineTasksService.d.ts +4 -0
- package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
- package/dist/services/planning/RefineTasksService.js +225 -24
- package/dist/services/review/CodeReviewService.d.ts +4 -0
- package/dist/services/review/CodeReviewService.d.ts.map +1 -1
- package/dist/services/review/CodeReviewService.js +778 -232
- package/dist/services/review/ReviewNormalizer.d.ts +9 -0
- package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
- package/dist/services/review/ReviewNormalizer.js +147 -0
- package/dist/services/shared/AuthErrors.d.ts +3 -0
- package/dist/services/shared/AuthErrors.d.ts.map +1 -0
- package/dist/services/shared/AuthErrors.js +17 -0
- package/dist/services/shared/DocdexGuidance.d.ts +7 -0
- package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
- package/dist/services/shared/DocdexGuidance.js +12 -0
- package/dist/services/shared/ProjectGuidance.d.ts +12 -1
- package/dist/services/shared/ProjectGuidance.d.ts.map +1 -1
- package/dist/services/shared/ProjectGuidance.js +64 -7
- package/dist/services/system/ToolDenylist.d.ts +13 -0
- package/dist/services/system/ToolDenylist.d.ts.map +1 -0
- package/dist/services/system/ToolDenylist.js +85 -0
- package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
- package/dist/services/telemetry/TelemetryService.js +39 -7
- package/dist/workspace/WorkspaceManager.d.ts +22 -0
- package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
- package/dist/workspace/WorkspaceManager.js +203 -32
- package/package.json +6 -5
|
@@ -8,12 +8,263 @@ import { GlobalRepository, WorkspaceRepository } from "@mcoda/db";
|
|
|
8
8
|
import { JobService } from "../jobs/JobService.js";
|
|
9
9
|
import { RoutingService } from "../agents/RoutingService.js";
|
|
10
10
|
import { AgentRatingService } from "../agents/AgentRatingService.js";
|
|
11
|
+
import { buildDocInventory } from "../docs/DocInventory.js";
|
|
11
12
|
const OPENAPI_TAGS = [
|
|
12
13
|
// For project-specific specs, tags come from context; this is only a fallback.
|
|
13
14
|
];
|
|
14
15
|
const OPENAPI_VERSION = "3.1.0";
|
|
16
|
+
const PRIMARY_OPENAPI_FILENAME = "mcoda.yaml";
|
|
17
|
+
const ADMIN_OPENAPI_FILENAME = "mcoda-admin.yaml";
|
|
15
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";
|
|
16
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
|
+
};
|
|
17
268
|
const fileExists = async (candidate) => {
|
|
18
269
|
try {
|
|
19
270
|
await fs.access(candidate);
|
|
@@ -48,9 +299,12 @@ class OpenapiContextAssembler {
|
|
|
48
299
|
}
|
|
49
300
|
async findLatestLocalDoc(docType) {
|
|
50
301
|
const candidates = [];
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
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());
|
|
54
308
|
try {
|
|
55
309
|
const entries = await fs.readdir(target);
|
|
56
310
|
for (const entry of entries.filter((e) => e.endsWith(".md"))) {
|
|
@@ -204,7 +458,8 @@ class OpenapiContextAssembler {
|
|
|
204
458
|
export class OpenApiService {
|
|
205
459
|
constructor(workspace, deps) {
|
|
206
460
|
this.workspace = workspace;
|
|
207
|
-
|
|
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 });
|
|
208
463
|
this.jobService = deps?.jobService ?? new JobService(workspace, undefined, { noTelemetry: deps?.noTelemetry });
|
|
209
464
|
this.agentService = deps.agentService;
|
|
210
465
|
this.routingService = deps.routingService;
|
|
@@ -216,9 +471,11 @@ export class OpenApiService {
|
|
|
216
471
|
const repo = await GlobalRepository.create();
|
|
217
472
|
const agentService = new AgentService(repo);
|
|
218
473
|
const routingService = await RoutingService.create();
|
|
474
|
+
const docdexRepoId = workspace.config?.docdexRepoId ?? process.env.MCODA_DOCDEX_REPO_ID ?? process.env.DOCDEX_REPO_ID;
|
|
219
475
|
const docdex = new DocdexClient({
|
|
220
476
|
workspaceRoot: workspace.workspaceRoot,
|
|
221
477
|
baseUrl: workspace.config?.docdexUrl ?? process.env.MCODA_DOCDEX_URL,
|
|
478
|
+
repoId: docdexRepoId,
|
|
222
479
|
});
|
|
223
480
|
const jobService = new JobService(workspace, undefined, { noTelemetry: options.noTelemetry });
|
|
224
481
|
return new OpenApiService(workspace, { agentService, routingService, docdex, jobService, repo, noTelemetry: options.noTelemetry });
|
|
@@ -306,20 +563,7 @@ export class OpenApiService {
|
|
|
306
563
|
return body.trim();
|
|
307
564
|
}
|
|
308
565
|
validateSpec(doc) {
|
|
309
|
-
|
|
310
|
-
if (!doc || typeof doc !== "object") {
|
|
311
|
-
errors.push("Spec is not a YAML object.");
|
|
312
|
-
return errors;
|
|
313
|
-
}
|
|
314
|
-
if (!doc.openapi)
|
|
315
|
-
errors.push("Missing openapi version");
|
|
316
|
-
if (!doc.info?.title)
|
|
317
|
-
errors.push("Missing info.title");
|
|
318
|
-
if (!doc.info?.version)
|
|
319
|
-
errors.push("Missing info.version");
|
|
320
|
-
if (!doc.paths)
|
|
321
|
-
errors.push("paths section is required (can be empty if no HTTP API)");
|
|
322
|
-
return errors;
|
|
566
|
+
return validateOpenApiSchema(doc);
|
|
323
567
|
}
|
|
324
568
|
async runOpenapiValidator(doc) {
|
|
325
569
|
try {
|
|
@@ -334,19 +578,27 @@ export class OpenApiService {
|
|
|
334
578
|
return [error.message];
|
|
335
579
|
}
|
|
336
580
|
}
|
|
337
|
-
buildPrompt(context, cliVersion, retryReasons) {
|
|
581
|
+
buildPrompt(context, cliVersion, retryReasons, variant = "primary") {
|
|
338
582
|
const contextBlocks = context.blocks
|
|
339
583
|
.map((block) => `### ${block.label}\n${block.content}`)
|
|
340
584
|
.join("\n\n");
|
|
341
585
|
const retryNote = retryReasons?.length
|
|
342
586
|
? `\nPrevious attempt issues:\n${retryReasons.map((r) => `- ${r}`).join("\n")}\nFix them in this draft.\n`
|
|
343
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
|
+
: "";
|
|
344
595
|
return [
|
|
345
596
|
"You are generating an OpenAPI 3.1 YAML for THIS workspace/project using only the provided PDR/SDS/RFP context.",
|
|
346
597
|
"Derive resources, schemas, and HTTP endpoints directly from the product requirements (e.g., todos CRUD, filters, search, bulk actions).",
|
|
347
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.",
|
|
348
599
|
"Prefer concise tags derived from domain resources (e.g., Todos). Avoid generic mcoda/system endpoints unless explicitly described in the context.",
|
|
349
600
|
`Use OpenAPI version ${OPENAPI_VERSION}, set info.title to the project name from context (fallback \"mcoda API\"), and info.version ${cliVersion}.`,
|
|
601
|
+
adminNote,
|
|
350
602
|
"Return only valid YAML (no Markdown fences, no commentary).",
|
|
351
603
|
retryNote,
|
|
352
604
|
"Scope rules:",
|
|
@@ -369,7 +621,7 @@ export class OpenApiService {
|
|
|
369
621
|
await fs.copyFile(target, backup);
|
|
370
622
|
return backup;
|
|
371
623
|
}
|
|
372
|
-
async registerOpenapi(outPath, content) {
|
|
624
|
+
async registerOpenapi(outPath, content, variant = "primary") {
|
|
373
625
|
const branch = this.workspace.config?.branch ?? (await readGitBranch(this.workspace.workspaceRoot));
|
|
374
626
|
return this.docdex.registerDocument({
|
|
375
627
|
docType: "OPENAPI",
|
|
@@ -380,6 +632,7 @@ export class OpenApiService {
|
|
|
380
632
|
branch,
|
|
381
633
|
status: "canonical",
|
|
382
634
|
projectKey: this.workspace.config?.projectKey,
|
|
635
|
+
variant,
|
|
383
636
|
},
|
|
384
637
|
});
|
|
385
638
|
}
|
|
@@ -389,24 +642,378 @@ export class OpenApiService {
|
|
|
389
642
|
const issues = this.validateSpec(parsed);
|
|
390
643
|
const validatorIssues = await this.runOpenapiValidator(parsed);
|
|
391
644
|
issues.push(...validatorIssues);
|
|
392
|
-
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
|
+
};
|
|
393
858
|
}
|
|
394
859
|
async generateFromDocs(options) {
|
|
860
|
+
const warnings = [];
|
|
395
861
|
const commandRun = await this.jobService.startCommandRun("openapi-from-docs", options.projectKey);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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,
|
|
399
938
|
});
|
|
400
|
-
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
|
+
};
|
|
401
1009
|
try {
|
|
1010
|
+
await setStage("context");
|
|
402
1011
|
const projectKey = options.projectKey ?? this.workspace.config?.projectKey;
|
|
403
1012
|
const assembler = new OpenapiContextAssembler(this.docdex, this.workspace, projectKey);
|
|
404
|
-
const context = await assembler.build();
|
|
1013
|
+
const context = await runWithTimeout("context", () => assembler.build());
|
|
405
1014
|
warnings.push(...context.warnings);
|
|
406
|
-
await this.
|
|
407
|
-
|
|
408
|
-
timestamp: new Date().toISOString(),
|
|
409
|
-
details: { docdexAvailable: context.docdexAvailable },
|
|
1015
|
+
await this.writeOpenapiCheckpoint(job.id, "context_built", {
|
|
1016
|
+
docdexAvailable: context.docdexAvailable,
|
|
410
1017
|
});
|
|
411
1018
|
await this.jobService.recordTokenUsage({
|
|
412
1019
|
timestamp: new Date().toISOString(),
|
|
@@ -419,24 +1026,70 @@ export class OpenApiService {
|
|
|
419
1026
|
completionTokens: 0,
|
|
420
1027
|
metadata: { docdexAvailable: context.docdexAvailable },
|
|
421
1028
|
});
|
|
422
|
-
|
|
423
|
-
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();
|
|
424
1043
|
if (options.validateOnly) {
|
|
1044
|
+
await setStage("validate", "primary");
|
|
425
1045
|
if (!(await fileExists(outputPath))) {
|
|
426
1046
|
throw new Error(`Cannot validate missing spec: ${outputPath}`);
|
|
427
1047
|
}
|
|
428
|
-
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
|
+
}
|
|
429
1073
|
const validationNote = issues.length ? `Validation issues:\n${issues.join("\n")}` : "Validation passed.";
|
|
430
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
|
+
}
|
|
431
1079
|
const jobState = issues.length ? "failed" : "completed";
|
|
432
1080
|
const commandState = issues.length ? "failed" : "succeeded";
|
|
433
|
-
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
|
+
});
|
|
434
1085
|
await this.jobService.finishCommandRun(commandRun.id, commandState, issues.join("; "));
|
|
435
1086
|
return {
|
|
436
1087
|
jobId: job.id,
|
|
437
1088
|
commandRunId: commandRun.id,
|
|
438
1089
|
outputPath,
|
|
439
|
-
spec,
|
|
1090
|
+
spec: primaryResult.spec,
|
|
1091
|
+
adminOutputPath: adminExists ? adminOutputPath : undefined,
|
|
1092
|
+
adminSpec,
|
|
440
1093
|
warnings,
|
|
441
1094
|
};
|
|
442
1095
|
}
|
|
@@ -445,85 +1098,190 @@ export class OpenApiService {
|
|
|
445
1098
|
}
|
|
446
1099
|
const agent = await this.resolveAgent(options.agentName);
|
|
447
1100
|
const stream = options.agentStream ?? true;
|
|
448
|
-
let specYaml = "";
|
|
449
|
-
let parsed;
|
|
450
|
-
let adapter = agent.adapter;
|
|
451
|
-
let agentMetadata;
|
|
452
|
-
let lastErrors;
|
|
453
1101
|
let agentUsed = false;
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
parsed.info =
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
action: attempt === 0 ? "draft_openapi" : "draft_openapi_retry",
|
|
481
|
-
promptTokens: estimateTokens(prompt),
|
|
482
|
-
completionTokens: estimateTokens(output),
|
|
483
|
-
metadata: { adapter, provider: adapter, attempt },
|
|
484
|
-
});
|
|
485
|
-
if (errors.length === 0) {
|
|
486
|
-
specYaml = YAML.stringify(parsed);
|
|
487
|
-
break;
|
|
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("; ")}`);
|
|
488
1128
|
}
|
|
489
|
-
|
|
490
|
-
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
warnings.push(`Saved ${variant} draft could not be parsed; regenerating: ${error.message ?? String(error)}`);
|
|
491
1131
|
}
|
|
492
|
-
lastErrors = errors;
|
|
493
1132
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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;
|
|
1190
|
+
}
|
|
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"];
|
|
497
1196
|
}
|
|
498
|
-
|
|
1197
|
+
assertHeartbeat();
|
|
499
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" });
|
|
500
1213
|
}
|
|
501
1214
|
let backup;
|
|
1215
|
+
let adminBackup;
|
|
1216
|
+
let docdexId = resumeDocdexId;
|
|
1217
|
+
let adminDocdexId = resumeAdminDocdexId;
|
|
1218
|
+
await setStage("write", "primary");
|
|
502
1219
|
if (!options.dryRun) {
|
|
503
1220
|
backup = await this.backupIfNeeded(outputPath);
|
|
504
|
-
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
|
+
}
|
|
505
1231
|
}
|
|
506
1232
|
else {
|
|
507
1233
|
warnings.push("Dry run enabled; spec not written to disk.");
|
|
508
1234
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
}
|
|
517
1259
|
}
|
|
1260
|
+
await completeStage("write", "admin", {
|
|
1261
|
+
adminOutputPath,
|
|
1262
|
+
adminBackupPath: adminBackup,
|
|
1263
|
+
adminDocdexId,
|
|
1264
|
+
openapi_admin_required: adminRequired,
|
|
1265
|
+
});
|
|
518
1266
|
}
|
|
519
|
-
await this.
|
|
520
|
-
|
|
1267
|
+
await this.updateOpenapiJobStatus(job.id, "completed", {
|
|
1268
|
+
stage: "complete",
|
|
1269
|
+
iteration,
|
|
1270
|
+
totalUnits,
|
|
1271
|
+
completedUnits: totalUnits,
|
|
1272
|
+
payload: buildPayload({
|
|
521
1273
|
outputPath,
|
|
522
1274
|
backupPath: backup,
|
|
1275
|
+
adminOutputPath: adminSpec ? adminOutputPath : undefined,
|
|
1276
|
+
adminBackupPath: adminBackup,
|
|
523
1277
|
docdexId,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1278
|
+
adminDocdexId,
|
|
1279
|
+
adapter: primarySpec.adapter,
|
|
1280
|
+
adminAdapter: adminSpec?.adapter,
|
|
1281
|
+
agentMetadata: primarySpec.agentMetadata,
|
|
1282
|
+
adminAgentMetadata: adminSpec?.agentMetadata,
|
|
1283
|
+
openapi_admin_required: adminRequired,
|
|
1284
|
+
}),
|
|
527
1285
|
});
|
|
528
1286
|
await this.jobService.finishCommandRun(commandRun.id, "succeeded");
|
|
529
1287
|
if (options.rateAgents && agentUsed) {
|
|
@@ -545,15 +1303,48 @@ export class OpenApiService {
|
|
|
545
1303
|
jobId: job.id,
|
|
546
1304
|
commandRunId: commandRun.id,
|
|
547
1305
|
outputPath: options.dryRun ? undefined : outputPath,
|
|
548
|
-
spec: specYaml,
|
|
1306
|
+
spec: primarySpec.specYaml,
|
|
1307
|
+
adminOutputPath: options.dryRun ? undefined : adminSpec ? adminOutputPath : undefined,
|
|
1308
|
+
adminSpec: adminSpec?.specYaml,
|
|
549
1309
|
docdexId,
|
|
1310
|
+
adminDocdexId,
|
|
550
1311
|
warnings,
|
|
551
1312
|
};
|
|
552
1313
|
}
|
|
553
1314
|
catch (error) {
|
|
554
|
-
|
|
555
|
-
|
|
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
|
+
}
|
|
556
1344
|
throw error;
|
|
557
1345
|
}
|
|
1346
|
+
finally {
|
|
1347
|
+
heartbeat.stop();
|
|
1348
|
+
}
|
|
558
1349
|
}
|
|
559
1350
|
}
|