@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.
Files changed (216) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +2 -2
  3. package/dist/api/AgentsApi.d.ts +9 -1
  4. package/dist/api/AgentsApi.d.ts.map +1 -1
  5. package/dist/api/AgentsApi.js +201 -6
  6. package/dist/api/QaTasksApi.d.ts.map +1 -1
  7. package/dist/api/QaTasksApi.js +6 -0
  8. package/dist/api/TasksApi.d.ts.map +1 -1
  9. package/dist/api/TasksApi.js +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/prompts/PdrPrompts.d.ts.map +1 -1
  14. package/dist/prompts/PdrPrompts.js +9 -1
  15. package/dist/prompts/SdsPrompts.d.ts.map +1 -1
  16. package/dist/prompts/SdsPrompts.js +9 -0
  17. package/dist/services/agents/AgentRatingFormula.d.ts +27 -0
  18. package/dist/services/agents/AgentRatingFormula.d.ts.map +1 -0
  19. package/dist/services/agents/AgentRatingFormula.js +45 -0
  20. package/dist/services/agents/AgentRatingService.d.ts +60 -0
  21. package/dist/services/agents/AgentRatingService.d.ts.map +1 -0
  22. package/dist/services/agents/AgentRatingService.js +363 -0
  23. package/dist/services/agents/GatewayAgentService.d.ts +11 -0
  24. package/dist/services/agents/GatewayAgentService.d.ts.map +1 -1
  25. package/dist/services/agents/GatewayAgentService.js +525 -84
  26. package/dist/services/agents/GatewayHandoff.d.ts +11 -0
  27. package/dist/services/agents/GatewayHandoff.d.ts.map +1 -0
  28. package/dist/services/agents/GatewayHandoff.js +141 -0
  29. package/dist/services/agents/RoutingService.d.ts +1 -0
  30. package/dist/services/agents/RoutingService.d.ts.map +1 -1
  31. package/dist/services/agents/RoutingService.js +4 -4
  32. package/dist/services/backlog/BacklogService.d.ts +23 -0
  33. package/dist/services/backlog/BacklogService.d.ts.map +1 -1
  34. package/dist/services/backlog/BacklogService.js +62 -7
  35. package/dist/services/backlog/TaskOrderingHeuristics.d.ts +12 -0
  36. package/dist/services/backlog/TaskOrderingHeuristics.d.ts.map +1 -0
  37. package/dist/services/backlog/TaskOrderingHeuristics.js +56 -0
  38. package/dist/services/backlog/TaskOrderingService.d.ts +17 -4
  39. package/dist/services/backlog/TaskOrderingService.d.ts.map +1 -1
  40. package/dist/services/backlog/TaskOrderingService.js +538 -79
  41. package/dist/services/docs/DocInventory.d.ts +11 -0
  42. package/dist/services/docs/DocInventory.d.ts.map +1 -0
  43. package/dist/services/docs/DocInventory.js +230 -0
  44. package/dist/services/docs/DocgenRunContext.d.ts +59 -0
  45. package/dist/services/docs/DocgenRunContext.d.ts.map +1 -0
  46. package/dist/services/docs/DocgenRunContext.js +4 -0
  47. package/dist/services/docs/DocsService.d.ts +70 -3
  48. package/dist/services/docs/DocsService.d.ts.map +1 -1
  49. package/dist/services/docs/DocsService.js +1930 -89
  50. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts +23 -0
  51. package/dist/services/docs/alignment/DocAlignmentGraph.d.ts.map +1 -0
  52. package/dist/services/docs/alignment/DocAlignmentGraph.js +78 -0
  53. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts +19 -0
  54. package/dist/services/docs/alignment/DocAlignmentPatcher.d.ts.map +1 -0
  55. package/dist/services/docs/alignment/DocAlignmentPatcher.js +222 -0
  56. package/dist/services/docs/patch/DocPatchEngine.d.ts +57 -0
  57. package/dist/services/docs/patch/DocPatchEngine.d.ts.map +1 -0
  58. package/dist/services/docs/patch/DocPatchEngine.js +331 -0
  59. package/dist/services/docs/review/Glossary.d.ts +16 -0
  60. package/dist/services/docs/review/Glossary.d.ts.map +1 -0
  61. package/dist/services/docs/review/Glossary.js +47 -0
  62. package/dist/services/docs/review/ReviewReportRenderer.d.ts +3 -0
  63. package/dist/services/docs/review/ReviewReportRenderer.d.ts.map +1 -0
  64. package/dist/services/docs/review/ReviewReportRenderer.js +133 -0
  65. package/dist/services/docs/review/ReviewReportSchema.d.ts +39 -0
  66. package/dist/services/docs/review/ReviewReportSchema.d.ts.map +1 -0
  67. package/dist/services/docs/review/ReviewReportSchema.js +47 -0
  68. package/dist/services/docs/review/ReviewTypes.d.ts +76 -0
  69. package/dist/services/docs/review/ReviewTypes.d.ts.map +1 -0
  70. package/dist/services/docs/review/ReviewTypes.js +94 -0
  71. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts +7 -0
  72. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.d.ts.map +1 -0
  73. package/dist/services/docs/review/gates/AdminOpenApiSpecGate.js +93 -0
  74. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts +7 -0
  75. package/dist/services/docs/review/gates/ApiPathConsistencyGate.d.ts.map +1 -0
  76. package/dist/services/docs/review/gates/ApiPathConsistencyGate.js +308 -0
  77. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts +8 -0
  78. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.d.ts.map +1 -0
  79. package/dist/services/docs/review/gates/BuildReadyCompletenessGate.js +278 -0
  80. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts +8 -0
  81. package/dist/services/docs/review/gates/DeploymentBlueprintGate.d.ts.map +1 -0
  82. package/dist/services/docs/review/gates/DeploymentBlueprintGate.js +487 -0
  83. package/dist/services/docs/review/gates/NoMaybesGate.d.ts +8 -0
  84. package/dist/services/docs/review/gates/NoMaybesGate.d.ts.map +1 -0
  85. package/dist/services/docs/review/gates/NoMaybesGate.js +145 -0
  86. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts +7 -0
  87. package/dist/services/docs/review/gates/OpenApiCoverageGate.d.ts.map +1 -0
  88. package/dist/services/docs/review/gates/OpenApiCoverageGate.js +266 -0
  89. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts +7 -0
  90. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.d.ts.map +1 -0
  91. package/dist/services/docs/review/gates/OpenApiSchemaSanityGate.js +59 -0
  92. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts +7 -0
  93. package/dist/services/docs/review/gates/OpenQuestionsGate.d.ts.map +1 -0
  94. package/dist/services/docs/review/gates/OpenQuestionsGate.js +200 -0
  95. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts +7 -0
  96. package/dist/services/docs/review/gates/PdrInterfacesGate.d.ts.map +1 -0
  97. package/dist/services/docs/review/gates/PdrInterfacesGate.js +159 -0
  98. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts +8 -0
  99. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.d.ts.map +1 -0
  100. package/dist/services/docs/review/gates/PdrOpenQuestionsGate.js +129 -0
  101. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts +7 -0
  102. package/dist/services/docs/review/gates/PdrOwnershipGate.d.ts.map +1 -0
  103. package/dist/services/docs/review/gates/PdrOwnershipGate.js +169 -0
  104. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts +10 -0
  105. package/dist/services/docs/review/gates/PlaceholderArtifactGate.d.ts.map +1 -0
  106. package/dist/services/docs/review/gates/PlaceholderArtifactGate.js +261 -0
  107. package/dist/services/docs/review/gates/RfpConsentGate.d.ts +6 -0
  108. package/dist/services/docs/review/gates/RfpConsentGate.d.ts.map +1 -0
  109. package/dist/services/docs/review/gates/RfpConsentGate.js +127 -0
  110. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts +7 -0
  111. package/dist/services/docs/review/gates/RfpDefinitionGate.d.ts.map +1 -0
  112. package/dist/services/docs/review/gates/RfpDefinitionGate.js +173 -0
  113. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts +7 -0
  114. package/dist/services/docs/review/gates/SdsAdaptersGate.d.ts.map +1 -0
  115. package/dist/services/docs/review/gates/SdsAdaptersGate.js +196 -0
  116. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts +7 -0
  117. package/dist/services/docs/review/gates/SdsDecisionsGate.d.ts.map +1 -0
  118. package/dist/services/docs/review/gates/SdsDecisionsGate.js +89 -0
  119. package/dist/services/docs/review/gates/SdsOpsGate.d.ts +7 -0
  120. package/dist/services/docs/review/gates/SdsOpsGate.d.ts.map +1 -0
  121. package/dist/services/docs/review/gates/SdsOpsGate.js +162 -0
  122. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts +7 -0
  123. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.d.ts.map +1 -0
  124. package/dist/services/docs/review/gates/SdsPolicyTelemetryGate.js +166 -0
  125. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts +7 -0
  126. package/dist/services/docs/review/gates/SqlRequiredTablesGate.d.ts.map +1 -0
  127. package/dist/services/docs/review/gates/SqlRequiredTablesGate.js +273 -0
  128. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts +7 -0
  129. package/dist/services/docs/review/gates/SqlSyntaxGate.d.ts.map +1 -0
  130. package/dist/services/docs/review/gates/SqlSyntaxGate.js +203 -0
  131. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts +9 -0
  132. package/dist/services/docs/review/gates/TerminologyNormalizationGate.d.ts.map +1 -0
  133. package/dist/services/docs/review/gates/TerminologyNormalizationGate.js +217 -0
  134. package/dist/services/docs/review/glossary.json +47 -0
  135. package/dist/services/estimate/EstimateService.d.ts +2 -0
  136. package/dist/services/estimate/EstimateService.d.ts.map +1 -1
  137. package/dist/services/estimate/EstimateService.js +66 -18
  138. package/dist/services/estimate/VelocityService.d.ts +4 -0
  139. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  140. package/dist/services/estimate/VelocityService.js +179 -36
  141. package/dist/services/estimate/types.d.ts +1 -0
  142. package/dist/services/estimate/types.d.ts.map +1 -1
  143. package/dist/services/execution/GatewayTrioService.d.ts +200 -0
  144. package/dist/services/execution/GatewayTrioService.d.ts.map +1 -0
  145. package/dist/services/execution/GatewayTrioService.js +2492 -0
  146. package/dist/services/execution/QaApiRunner.d.ts +30 -0
  147. package/dist/services/execution/QaApiRunner.d.ts.map +1 -0
  148. package/dist/services/execution/QaApiRunner.js +881 -0
  149. package/dist/services/execution/QaFollowupService.d.ts +2 -0
  150. package/dist/services/execution/QaFollowupService.d.ts.map +1 -1
  151. package/dist/services/execution/QaFollowupService.js +9 -2
  152. package/dist/services/execution/QaPlanValidator.d.ts +10 -0
  153. package/dist/services/execution/QaPlanValidator.d.ts.map +1 -0
  154. package/dist/services/execution/QaPlanValidator.js +128 -0
  155. package/dist/services/execution/QaProfileService.d.ts +27 -1
  156. package/dist/services/execution/QaProfileService.d.ts.map +1 -1
  157. package/dist/services/execution/QaProfileService.js +354 -7
  158. package/dist/services/execution/QaTasksService.d.ts +59 -1
  159. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  160. package/dist/services/execution/QaTasksService.js +3347 -318
  161. package/dist/services/execution/QaTestCommandBuilder.d.ts +51 -0
  162. package/dist/services/execution/QaTestCommandBuilder.d.ts.map +1 -0
  163. package/dist/services/execution/QaTestCommandBuilder.js +495 -0
  164. package/dist/services/execution/TaskSelectionService.d.ts +4 -2
  165. package/dist/services/execution/TaskSelectionService.d.ts.map +1 -1
  166. package/dist/services/execution/TaskSelectionService.js +144 -28
  167. package/dist/services/execution/TaskStateService.d.ts +19 -6
  168. package/dist/services/execution/TaskStateService.d.ts.map +1 -1
  169. package/dist/services/execution/TaskStateService.js +128 -13
  170. package/dist/services/execution/WorkOnTasksService.d.ts +32 -1
  171. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  172. package/dist/services/execution/WorkOnTasksService.js +4667 -722
  173. package/dist/services/jobs/JobInsightsService.d.ts +4 -0
  174. package/dist/services/jobs/JobInsightsService.d.ts.map +1 -1
  175. package/dist/services/jobs/JobInsightsService.js +51 -5
  176. package/dist/services/jobs/JobResumeService.d.ts.map +1 -1
  177. package/dist/services/jobs/JobResumeService.js +23 -10
  178. package/dist/services/jobs/JobService.d.ts +56 -4
  179. package/dist/services/jobs/JobService.d.ts.map +1 -1
  180. package/dist/services/jobs/JobService.js +232 -1
  181. package/dist/services/openapi/OpenApiService.d.ts +51 -0
  182. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  183. package/dist/services/openapi/OpenApiService.js +953 -106
  184. package/dist/services/planning/CreateTasksService.d.ts +21 -0
  185. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  186. package/dist/services/planning/CreateTasksService.js +569 -31
  187. package/dist/services/planning/RefineTasksService.d.ts +9 -0
  188. package/dist/services/planning/RefineTasksService.d.ts.map +1 -1
  189. package/dist/services/planning/RefineTasksService.js +409 -59
  190. package/dist/services/review/CodeReviewService.d.ts +18 -0
  191. package/dist/services/review/CodeReviewService.d.ts.map +1 -1
  192. package/dist/services/review/CodeReviewService.js +1309 -167
  193. package/dist/services/review/ReviewNormalizer.d.ts +9 -0
  194. package/dist/services/review/ReviewNormalizer.d.ts.map +1 -0
  195. package/dist/services/review/ReviewNormalizer.js +147 -0
  196. package/dist/services/shared/AuthErrors.d.ts +3 -0
  197. package/dist/services/shared/AuthErrors.d.ts.map +1 -0
  198. package/dist/services/shared/AuthErrors.js +17 -0
  199. package/dist/services/shared/DocdexGuidance.d.ts +7 -0
  200. package/dist/services/shared/DocdexGuidance.d.ts.map +1 -0
  201. package/dist/services/shared/DocdexGuidance.js +12 -0
  202. package/dist/services/shared/ProjectGuidance.d.ts +17 -0
  203. package/dist/services/shared/ProjectGuidance.d.ts.map +1 -0
  204. package/dist/services/shared/ProjectGuidance.js +78 -0
  205. package/dist/services/system/ToolDenylist.d.ts +13 -0
  206. package/dist/services/system/ToolDenylist.d.ts.map +1 -0
  207. package/dist/services/system/ToolDenylist.js +85 -0
  208. package/dist/services/tasks/TaskCommentFormatter.d.ts +20 -0
  209. package/dist/services/tasks/TaskCommentFormatter.d.ts.map +1 -0
  210. package/dist/services/tasks/TaskCommentFormatter.js +54 -0
  211. package/dist/services/telemetry/TelemetryService.d.ts.map +1 -1
  212. package/dist/services/telemetry/TelemetryService.js +39 -7
  213. package/dist/workspace/WorkspaceManager.d.ts +26 -0
  214. package/dist/workspace/WorkspaceManager.d.ts.map +1 -1
  215. package/dist/workspace/WorkspaceManager.js +206 -32
  216. package/package.json +6 -5
@@ -14,11 +14,13 @@ export interface FollowupSuggestion {
14
14
  testName?: string;
15
15
  evidenceUrl?: string;
16
16
  artifacts?: string[];
17
+ followupSlug?: string;
17
18
  }
18
19
  export declare class QaFollowupService {
19
20
  private workspaceRepo;
20
21
  private workspaceRoot;
21
22
  constructor(workspaceRepo: WorkspaceRepository, workspaceRoot: string);
23
+ private get mcodaDir();
22
24
  private get cachePath();
23
25
  private readCache;
24
26
  private writeCache;
@@ -1 +1 @@
1
- {"version":3,"file":"QaFollowupService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/QaFollowupService.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,OAAO,EACP,mBAAmB,EACpB,MAAM,WAAW,CAAC;AAMnB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAgCD,qBAAa,iBAAiB;IAChB,OAAO,CAAC,aAAa;IAAuB,OAAO,CAAC,aAAa;gBAAzD,aAAa,EAAE,mBAAmB,EAAU,aAAa,EAAE,MAAM;IAErF,OAAO,KAAK,SAAS,GAEpB;YAEa,SAAS;YAST,UAAU;YAKV,kBAAkB;IA+C1B,kBAAkB,CACtB,UAAU,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EAC7D,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC;QAAE,IAAI,EAAE,UAAU,GAAG;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,UAAU,CAAC,EAAE,oBAAoB,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;YA6EnG,sBAAsB;CA+ErC"}
1
+ {"version":3,"file":"QaFollowupService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/QaFollowupService.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,OAAO,EACP,mBAAmB,EACpB,MAAM,WAAW,CAAC;AAMnB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgCD,qBAAa,iBAAiB;IAChB,OAAO,CAAC,aAAa;IAAuB,OAAO,CAAC,aAAa;gBAAzD,aAAa,EAAE,mBAAmB,EAAU,aAAa,EAAE,MAAM;IAErF,OAAO,KAAK,QAAQ,GAEnB;IAED,OAAO,KAAK,SAAS,GAEpB;YAEa,SAAS;YAST,UAAU;YAKV,kBAAkB;IA+C1B,kBAAkB,CACtB,UAAU,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EAC7D,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAAC;QAAE,IAAI,EAAE,UAAU,GAAG;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,UAAU,CAAC,EAAE,oBAAoB,CAAC;QAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;YAiFnG,sBAAsB;CA+ErC"}
@@ -30,8 +30,11 @@ export class QaFollowupService {
30
30
  this.workspaceRepo = workspaceRepo;
31
31
  this.workspaceRoot = workspaceRoot;
32
32
  }
33
+ get mcodaDir() {
34
+ return PathHelper.getWorkspaceDir(this.workspaceRoot);
35
+ }
33
36
  get cachePath() {
34
- return path.join(this.workspaceRoot, '.mcoda', 'qa-containers.json');
37
+ return path.join(this.mcodaDir, 'qa-containers.json');
35
38
  }
36
39
  async readCache() {
37
40
  try {
@@ -94,9 +97,13 @@ export class QaFollowupService {
94
97
  const keyGen = createTaskKeyGenerator(storyKeyBase, existingKeys);
95
98
  const followupKey = keyGen();
96
99
  const now = new Date().toISOString();
100
+ const storyPoints = suggestion.storyPoints ?? 1;
101
+ const boundedPoints = Math.min(10, Math.max(1, Math.round(storyPoints)));
97
102
  const metadata = {
98
103
  tags: ['qa-found', 'auto-created', 'ready-for-ai-dev', 'source=qa', ...(suggestion.tags ?? [])],
99
104
  source_task: sourceTask.key,
105
+ complexity: boundedPoints,
106
+ ...(suggestion.followupSlug ? { qa_followup_slug: suggestion.followupSlug } : {}),
100
107
  ...(suggestion.components ? { components: suggestion.components } : {}),
101
108
  ...(suggestion.docLinks ? { doc_links: suggestion.docLinks } : {}),
102
109
  ...(suggestion.testName ? { failing_test: suggestion.testName } : {}),
@@ -123,7 +130,7 @@ export class QaFollowupService {
123
130
  description,
124
131
  type: suggestion.type ?? 'bug',
125
132
  status: 'not_started',
126
- storyPoints: suggestion.storyPoints ?? 1,
133
+ storyPoints: boundedPoints,
127
134
  priority: suggestion.priority ?? 99,
128
135
  metadata,
129
136
  };
@@ -0,0 +1,10 @@
1
+ import { QaTaskPlan } from '@mcoda/shared';
2
+ type NormalizedQaPlan = {
3
+ taskProfiles: Record<string, string[]>;
4
+ taskPlans: Record<string, QaTaskPlan>;
5
+ notes?: string;
6
+ warnings: string[];
7
+ };
8
+ export declare const normalizeQaPlanOutput: (value: unknown) => NormalizedQaPlan;
9
+ export {};
10
+ //# sourceMappingURL=QaPlanValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QaPlanValidator.d.ts","sourceRoot":"","sources":["../../../src/services/execution/QaPlanValidator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,UAAU,EAAE,MAAM,eAAe,CAAC;AAGnD,KAAK,gBAAgB,GAAG;IACtB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAwGF,eAAO,MAAM,qBAAqB,GAAI,OAAO,OAAO,KAAG,gBAoCtD,CAAC"}
@@ -0,0 +1,128 @@
1
+ const toStringList = (value) => {
2
+ if (typeof value === 'string')
3
+ return value.trim() ? [value.trim()] : [];
4
+ if (!Array.isArray(value))
5
+ return [];
6
+ return value
7
+ .filter((entry) => typeof entry === 'string')
8
+ .map((entry) => entry.trim())
9
+ .filter(Boolean);
10
+ };
11
+ const ASSERT_TEXT_OK_MARKER = '__MCODA_ASSERT_OK__';
12
+ const buildAssertTextExpression = (params) => {
13
+ const textExpression = params.selector
14
+ ? `(() => { const el = document.querySelector(${JSON.stringify(params.selector)}); return el ? (el.innerText || el.textContent || '') : ''; })()`
15
+ : `document.body ? (document.body.innerText || document.body.textContent || '') : ''`;
16
+ const expected = JSON.stringify(params.text);
17
+ const comparison = params.contains ? 'actual.includes(expected)' : 'actual.trim() === expected';
18
+ return `(() => { const actual = ${textExpression}; const expected = ${expected}; const ok = ${comparison}; return ok ? ${JSON.stringify(ASSERT_TEXT_OK_MARKER)} : actual; })()`;
19
+ };
20
+ const normalizeBrowserActions = (entries, warnings, taskKey) => {
21
+ const actions = [];
22
+ const context = taskKey ? ` for ${taskKey}` : '';
23
+ for (const entry of entries) {
24
+ if (!entry || typeof entry !== 'object')
25
+ continue;
26
+ const action = entry;
27
+ const rawType = typeof action.type === 'string' ? action.type : '';
28
+ if (rawType === 'assertText' || rawType === 'assert_text') {
29
+ const text = typeof action.text === 'string' ? action.text.trim() : '';
30
+ if (!text) {
31
+ warnings.push(`QA plan browser action ${rawType} missing text${context}.`);
32
+ continue;
33
+ }
34
+ const selector = typeof action.selector === 'string' ? action.selector : undefined;
35
+ const contains = typeof action.contains === 'boolean' ? action.contains : true;
36
+ actions.push({
37
+ type: 'script',
38
+ expression: buildAssertTextExpression({ selector, text, contains }),
39
+ expect: ASSERT_TEXT_OK_MARKER,
40
+ });
41
+ continue;
42
+ }
43
+ actions.push(action);
44
+ }
45
+ return actions;
46
+ };
47
+ const normalizePlanEntry = (value, warnings, taskKey) => {
48
+ if (!value || typeof value !== 'object')
49
+ return undefined;
50
+ const raw = value;
51
+ const profiles = toStringList(raw.profiles);
52
+ const cliCommands = toStringList(raw.cli?.commands);
53
+ const apiRequests = Array.isArray(raw.api?.requests)
54
+ ? raw.api.requests.filter((entry) => typeof entry === 'object' && entry)
55
+ : [];
56
+ const rawBrowserActions = Array.isArray(raw.browser?.actions)
57
+ ? raw.browser.actions.filter((entry) => typeof entry === 'object' && entry)
58
+ : [];
59
+ const browserActions = normalizeBrowserActions(rawBrowserActions, warnings, taskKey);
60
+ const stressApi = Array.isArray(raw.stress?.api)
61
+ ? raw.stress.api.filter((entry) => typeof entry === 'object' && entry)
62
+ : [];
63
+ const stressBrowser = Array.isArray(raw.stress?.browser)
64
+ ? raw.stress.browser.filter((entry) => typeof entry === 'object' && entry)
65
+ : [];
66
+ const plan = {};
67
+ if (profiles.length)
68
+ plan.profiles = profiles;
69
+ if (cliCommands.length)
70
+ plan.cli = { commands: cliCommands };
71
+ if (apiRequests.length || typeof raw.api?.base_url === 'string') {
72
+ plan.api = {
73
+ base_url: typeof raw.api?.base_url === 'string' ? raw.api.base_url : undefined,
74
+ requests: apiRequests,
75
+ };
76
+ }
77
+ if (browserActions.length || typeof raw.browser?.base_url === 'string') {
78
+ plan.browser = {
79
+ base_url: typeof raw.browser?.base_url === 'string' ? raw.browser.base_url : undefined,
80
+ actions: browserActions.length ? browserActions : undefined,
81
+ };
82
+ }
83
+ if (stressApi.length || stressBrowser.length) {
84
+ plan.stress = {
85
+ api: stressApi,
86
+ browser: stressBrowser,
87
+ };
88
+ }
89
+ return Object.keys(plan).length ? plan : undefined;
90
+ };
91
+ export const normalizeQaPlanOutput = (value) => {
92
+ const warnings = [];
93
+ if (!value || typeof value !== 'object') {
94
+ warnings.push('QA plan output is not an object.');
95
+ return { taskProfiles: {}, taskPlans: {}, warnings };
96
+ }
97
+ const raw = value;
98
+ const taskProfilesRaw = raw.task_profiles ?? raw.taskProfiles;
99
+ const taskPlansRaw = raw.task_plans ?? raw.taskPlans ?? raw.tasks;
100
+ const taskProfiles = {};
101
+ if (taskProfilesRaw && typeof taskProfilesRaw === 'object') {
102
+ for (const [key, entry] of Object.entries(taskProfilesRaw)) {
103
+ const list = toStringList(entry);
104
+ if (list.length)
105
+ taskProfiles[key] = list;
106
+ }
107
+ }
108
+ else if (taskProfilesRaw !== undefined) {
109
+ warnings.push('QA plan task_profiles is not an object.');
110
+ }
111
+ const taskPlans = {};
112
+ if (taskPlansRaw && typeof taskPlansRaw === 'object') {
113
+ for (const [key, entry] of Object.entries(taskPlansRaw)) {
114
+ const normalized = normalizePlanEntry(entry, warnings, key);
115
+ if (normalized)
116
+ taskPlans[key] = normalized;
117
+ }
118
+ }
119
+ else if (taskPlansRaw !== undefined) {
120
+ warnings.push('QA plan task_plans is not an object.');
121
+ }
122
+ return {
123
+ taskProfiles,
124
+ taskPlans,
125
+ notes: typeof raw.notes === 'string' ? raw.notes : undefined,
126
+ warnings,
127
+ };
128
+ };
@@ -4,12 +4,34 @@ export interface QaProfileResolutionOptions {
4
4
  profileName?: string;
5
5
  level?: string;
6
6
  defaultLevel?: string;
7
+ runnerPreference?: 'cli' | 'chromium' | 'maestro';
7
8
  }
9
+ type QaRunner = 'cli' | 'chromium' | 'maestro';
8
10
  export declare class QaProfileService {
9
11
  private workspaceRoot;
10
12
  private cache?;
13
+ private webInterfaceCache?;
11
14
  private routingCache?;
12
- constructor(workspaceRoot: string);
15
+ private noRepoWrites;
16
+ constructor(workspaceRoot: string, options?: {
17
+ noRepoWrites?: boolean;
18
+ });
19
+ private get mcodaDir();
20
+ private fileExists;
21
+ private readPackageJson;
22
+ private detectWebInterface;
23
+ private detectUiTask;
24
+ private detectMobileTask;
25
+ private resolveRunnerPlan;
26
+ getRunnerPlan(task: TaskRow & {
27
+ metadata?: any;
28
+ }): Promise<{
29
+ runners: QaRunner[];
30
+ hasWebInterface: boolean;
31
+ uiTask: boolean;
32
+ mobileTask: boolean;
33
+ }>;
34
+ private resolveRunnerPreference;
13
35
  private get profilePath();
14
36
  private get workspaceConfigPath();
15
37
  private getConfiguredDefaultProfileName;
@@ -18,5 +40,9 @@ export declare class QaProfileService {
18
40
  resolveProfileForTask(task: TaskRow & {
19
41
  metadata?: any;
20
42
  }, options?: QaProfileResolutionOptions): Promise<QaProfile | undefined>;
43
+ resolveProfilesForTask(task: TaskRow & {
44
+ metadata?: any;
45
+ }, options?: QaProfileResolutionOptions): Promise<QaProfile[]>;
21
46
  }
47
+ export {};
22
48
  //# sourceMappingURL=QaProfileService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"QaProfileService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/QaProfileService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAG1D,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,gBAAgB;IASf,OAAO,CAAC,aAAa;IARjC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,YAAY,CAAC,CAKnB;gBAEkB,aAAa,EAAE,MAAM;IAEzC,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,mBAAmB,GAE9B;YAEa,+BAA+B;YAU/B,gBAAgB;IAwBxB,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAyBpC,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;KAAE,EAAE,OAAO,GAAE,0BAA+B,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAqE1I"}
1
+ {"version":3,"file":"QaProfileService.d.ts","sourceRoot":"","sources":["../../../src/services/execution/QaProfileService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI1D,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;CACnD;AAED,KAAK,QAAQ,GAAG,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;AAc/C,qBAAa,gBAAgB;IAWf,OAAO,CAAC,aAAa;IAVjC,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,iBAAiB,CAAC,CAAU;IACpC,OAAO,CAAC,YAAY,CAAC,CAKnB;IACF,OAAO,CAAC,YAAY,CAAU;gBAEV,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAO;IAInF,OAAO,KAAK,QAAQ,GAEnB;YAEa,UAAU;YASV,eAAe;YAUf,kBAAkB;IAyDhC,OAAO,CAAC,YAAY;IAwGpB,OAAO,CAAC,gBAAgB;YAqBV,iBAAiB;IAezB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC;QAC/D,OAAO,EAAE,QAAQ,EAAE,CAAC;QACpB,eAAe,EAAE,OAAO,CAAC;QACzB,MAAM,EAAE,OAAO,CAAC;QAChB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;YAIY,uBAAuB;IAQrC,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,mBAAmB,GAE9B;YAEa,+BAA+B;YAU/B,gBAAgB;IAwBxB,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IA2BpC,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;KAAE,EAAE,OAAO,GAAE,0BAA+B,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAoGnI,sBAAsB,CAC1B,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,CAAC,EAAE,GAAG,CAAA;KAAE,EAClC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,SAAS,EAAE,CAAC;CA8DxB"}
@@ -1,15 +1,263 @@
1
1
  import path from 'node:path';
2
2
  import { PathHelper } from '@mcoda/shared';
3
+ import { classifyTask } from '../backlog/TaskOrderingHeuristics.js';
3
4
  import fs from 'node:fs/promises';
5
+ const DEFAULT_QA_PROFILES = [
6
+ {
7
+ name: 'cli',
8
+ runner: 'cli',
9
+ default: true,
10
+ },
11
+ {
12
+ name: 'chromium',
13
+ runner: 'chromium',
14
+ },
15
+ ];
4
16
  export class QaProfileService {
5
- constructor(workspaceRoot) {
17
+ constructor(workspaceRoot, options = {}) {
6
18
  this.workspaceRoot = workspaceRoot;
19
+ this.noRepoWrites = Boolean(options.noRepoWrites);
20
+ }
21
+ get mcodaDir() {
22
+ return PathHelper.getWorkspaceDir(this.workspaceRoot);
23
+ }
24
+ async fileExists(targetPath) {
25
+ try {
26
+ await fs.access(targetPath);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async readPackageJson() {
34
+ const pkgPath = path.join(this.workspaceRoot, 'package.json');
35
+ try {
36
+ const raw = await fs.readFile(pkgPath, 'utf8');
37
+ return JSON.parse(raw);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ async detectWebInterface() {
44
+ if (this.webInterfaceCache !== undefined)
45
+ return this.webInterfaceCache;
46
+ const markers = [
47
+ 'client',
48
+ 'frontend',
49
+ 'web',
50
+ 'ui',
51
+ 'apps/web',
52
+ 'apps/client',
53
+ 'packages/web',
54
+ 'packages/client',
55
+ 'src/public',
56
+ 'src/public/index.html',
57
+ 'public/index.html',
58
+ 'index.html',
59
+ 'src/App.tsx',
60
+ 'src/main.tsx',
61
+ 'src/App.jsx',
62
+ 'src/main.jsx',
63
+ 'next.config.js',
64
+ 'next.config.mjs',
65
+ 'vite.config.ts',
66
+ 'vite.config.js',
67
+ 'svelte.config.js',
68
+ 'angular.json',
69
+ 'nuxt.config.js',
70
+ 'astro.config.mjs',
71
+ 'remix.config.js',
72
+ ];
73
+ for (const marker of markers) {
74
+ if (await this.fileExists(path.join(this.workspaceRoot, marker))) {
75
+ this.webInterfaceCache = true;
76
+ return true;
77
+ }
78
+ }
79
+ const pkg = await this.readPackageJson();
80
+ const deps = {
81
+ ...(pkg?.dependencies ?? {}),
82
+ ...(pkg?.devDependencies ?? {}),
83
+ ...(pkg?.peerDependencies ?? {}),
84
+ };
85
+ const uiDeps = [
86
+ 'react',
87
+ 'next',
88
+ 'vue',
89
+ 'nuxt',
90
+ 'svelte',
91
+ 'astro',
92
+ '@angular/core',
93
+ '@remix-run/react',
94
+ 'solid-js',
95
+ ];
96
+ const hasUiDep = uiDeps.some((dep) => typeof deps?.[dep] === 'string');
97
+ this.webInterfaceCache = hasUiDep;
98
+ return hasUiDep;
99
+ }
100
+ detectUiTask(task) {
101
+ const metadata = task.metadata ?? {};
102
+ const files = Array.isArray(metadata.files) ? metadata.files : [];
103
+ const reviewFiles = Array.isArray(metadata.last_review_changed_paths)
104
+ ? metadata.last_review_changed_paths
105
+ : [];
106
+ const key = String(task.key ?? '').toLowerCase();
107
+ const isUiKey = key.startsWith('web-') ||
108
+ key.startsWith('ui-') ||
109
+ key.startsWith('fe-') ||
110
+ key.startsWith('ux-') ||
111
+ key.includes('-web-') ||
112
+ key.includes('-ui-') ||
113
+ key.includes('-fe-') ||
114
+ key.includes('-ux-');
115
+ if (isUiKey)
116
+ return true;
117
+ const isBackendKey = key.startsWith('bck-') ||
118
+ key.startsWith('backend-') ||
119
+ key.startsWith('api-') ||
120
+ key.startsWith('ops-') ||
121
+ key.startsWith('infra-') ||
122
+ key.includes('-bck-') ||
123
+ key.includes('-backend-') ||
124
+ key.includes('-api-') ||
125
+ key.includes('-ops-') ||
126
+ key.includes('-infra-');
127
+ const tags = Array.isArray(metadata.tags)
128
+ ? metadata.tags.map((tag) => String(tag).toLowerCase())
129
+ : [];
130
+ const tagHints = new Set(['ui', 'ux', 'frontend', 'front-end', 'web', 'client']);
131
+ const hasUiTag = tags.some((tag) => tagHints.has(tag));
132
+ const components = Array.isArray(metadata.components)
133
+ ? metadata.components.map((component) => String(component).toLowerCase())
134
+ : [];
135
+ const componentHints = new Set(['ui', 'ux', 'frontend', 'front-end', 'web', 'client']);
136
+ const hasUiComponent = components.some((component) => componentHints.has(component));
137
+ const uiHints = [
138
+ '/ui/',
139
+ '/frontend/',
140
+ '/client/',
141
+ '/web/',
142
+ '/components/',
143
+ '/pages/',
144
+ '/app/',
145
+ '/public/',
146
+ '/styles/',
147
+ ];
148
+ const uiExtensions = ['.tsx', '.jsx', '.vue', '.svelte', '.astro', '.html', '.css', '.scss', '.less'];
149
+ const hasUiFileHint = (paths, options = {}) => {
150
+ for (const file of paths) {
151
+ const normalized = String(file).toLowerCase();
152
+ const extensionMatch = uiExtensions.some((ext) => normalized.endsWith(ext));
153
+ if (extensionMatch)
154
+ return true;
155
+ if (options.requireExtension)
156
+ continue;
157
+ if (uiHints.some((hint) => normalized.includes(hint)))
158
+ return true;
159
+ }
160
+ return false;
161
+ };
162
+ const type = typeof task.type === 'string' ? task.type.toLowerCase() : '';
163
+ const hasUiType = ['frontend', 'front-end', 'ui', 'web', 'client'].some((hint) => type.includes(hint));
164
+ if (hasUiTag || hasUiComponent || hasUiType)
165
+ return true;
166
+ if (hasUiFileHint(files, { requireExtension: isBackendKey }))
167
+ return true;
168
+ if (isBackendKey)
169
+ return false;
170
+ if (hasUiFileHint(reviewFiles))
171
+ return true;
172
+ const acceptance = Array.isArray(task.acceptanceCriteria)
173
+ ? task.acceptanceCriteria
174
+ : [];
175
+ const text = [task.key, task.title, task.description, task.type, ...acceptance]
176
+ .filter(Boolean)
177
+ .join(' ')
178
+ .toLowerCase();
179
+ if (text) {
180
+ const uiPhrases = [
181
+ 'user interface',
182
+ 'front end',
183
+ 'frontend',
184
+ 'front-end',
185
+ 'web ui',
186
+ 'web-',
187
+ 'ui-',
188
+ 'ui ',
189
+ 'page',
190
+ 'screen',
191
+ 'view',
192
+ 'component',
193
+ 'browser',
194
+ 'html',
195
+ 'css',
196
+ 'responsive',
197
+ 'accessibility',
198
+ ];
199
+ if (uiPhrases.some((phrase) => text.includes(phrase)))
200
+ return true;
201
+ }
202
+ const classification = classifyTask({
203
+ title: task.title,
204
+ description: task.description,
205
+ type: task.type ?? undefined,
206
+ });
207
+ if (classification.stage === 'frontend')
208
+ return true;
209
+ return false;
210
+ }
211
+ detectMobileTask(task) {
212
+ const metadata = task.metadata ?? {};
213
+ const tags = Array.isArray(metadata.tags) ? metadata.tags.map((t) => t.toLowerCase()) : [];
214
+ const type = typeof task.type === 'string' ? task.type.toLowerCase() : '';
215
+ const mobileTags = new Set(['mobile', 'ios', 'android', 'maestro', 'react-native', 'rn']);
216
+ if (tags.some((tag) => mobileTags.has(tag)))
217
+ return true;
218
+ if (mobileTags.has(type))
219
+ return true;
220
+ const files = Array.isArray(metadata.files) ? metadata.files : [];
221
+ const reviewFiles = Array.isArray(metadata.last_review_changed_paths)
222
+ ? metadata.last_review_changed_paths
223
+ : [];
224
+ const combined = [...files, ...reviewFiles];
225
+ const mobileHints = ['/ios/', '/android/', '/mobile/'];
226
+ for (const file of combined) {
227
+ const normalized = String(file).toLowerCase();
228
+ if (mobileHints.some((hint) => normalized.includes(hint)))
229
+ return true;
230
+ if (normalized.endsWith('.maestro.yml') || normalized.endsWith('.maestro.yaml'))
231
+ return true;
232
+ }
233
+ return false;
234
+ }
235
+ async resolveRunnerPlan(task) {
236
+ const hasWebInterface = await this.detectWebInterface();
237
+ const uiTask = this.detectUiTask(task);
238
+ const mobileTask = this.detectMobileTask(task);
239
+ const runners = ['cli'];
240
+ if (uiTask && hasWebInterface)
241
+ runners.push('chromium');
242
+ if (mobileTask)
243
+ runners.push('maestro');
244
+ return { runners, hasWebInterface, uiTask, mobileTask };
245
+ }
246
+ async getRunnerPlan(task) {
247
+ return this.resolveRunnerPlan(task);
248
+ }
249
+ async resolveRunnerPreference(task) {
250
+ if (task && this.detectUiTask(task)) {
251
+ const hasUi = await this.detectWebInterface();
252
+ return hasUi ? 'chromium' : 'cli';
253
+ }
254
+ return 'cli';
7
255
  }
8
256
  get profilePath() {
9
- return path.join(this.workspaceRoot, '.mcoda', 'qa-profiles.json');
257
+ return path.join(this.mcodaDir, 'qa-profiles.json');
10
258
  }
11
259
  get workspaceConfigPath() {
12
- return path.join(this.workspaceRoot, '.mcoda', 'config.json');
260
+ return path.join(this.mcodaDir, 'config.json');
13
261
  }
14
262
  async getConfiguredDefaultProfileName() {
15
263
  try {
@@ -44,7 +292,9 @@ export class QaProfileService {
44
292
  async loadProfiles() {
45
293
  if (this.cache)
46
294
  return this.cache;
47
- await PathHelper.ensureDir(path.join(this.workspaceRoot, '.mcoda'));
295
+ if (!this.noRepoWrites) {
296
+ await PathHelper.ensureDir(this.mcodaDir);
297
+ }
48
298
  try {
49
299
  const raw = await fs.readFile(this.profilePath, 'utf8');
50
300
  const parsed = JSON.parse(raw);
@@ -56,12 +306,12 @@ export class QaProfileService {
56
306
  this.cache = parsed.profiles;
57
307
  return this.cache;
58
308
  }
59
- this.cache = [];
309
+ this.cache = DEFAULT_QA_PROFILES;
60
310
  return this.cache;
61
311
  }
62
312
  catch (error) {
63
313
  if (error?.code === 'ENOENT') {
64
- this.cache = [];
314
+ this.cache = DEFAULT_QA_PROFILES;
65
315
  return this.cache;
66
316
  }
67
317
  throw error;
@@ -73,6 +323,18 @@ export class QaProfileService {
73
323
  return undefined;
74
324
  const envProfile = process.env.MCODA_QA_PROFILE;
75
325
  const routing = await this.getRoutingConfig();
326
+ const runnerPreference = options.runnerPreference ?? (await this.resolveRunnerPreference(task));
327
+ const normalizeRunner = (profile) => profile.runner ?? 'cli';
328
+ const matchRunner = (profile) => normalizeRunner(profile) === runnerPreference || profile.name === runnerPreference;
329
+ const pickByRunner = () => {
330
+ const matches = profiles.filter(matchRunner);
331
+ if (!matches.length)
332
+ return undefined;
333
+ const defaults = matches.filter((p) => p.default);
334
+ if (defaults.length === 1)
335
+ return defaults[0];
336
+ return matches[0];
337
+ };
76
338
  const configuredDefault = options.profileName ?? envProfile ?? (await this.getConfiguredDefaultProfileName()) ?? routing.defaultProfile;
77
339
  const pickByName = (name) => {
78
340
  if (!name)
@@ -85,6 +347,14 @@ export class QaProfileService {
85
347
  };
86
348
  const explicit = pickByName(configuredDefault);
87
349
  if (explicit) {
350
+ if (options.profileName) {
351
+ return explicit;
352
+ }
353
+ if (normalizeRunner(explicit) !== runnerPreference) {
354
+ const fallback = pickByRunner();
355
+ if (fallback)
356
+ return fallback;
357
+ }
88
358
  return explicit;
89
359
  }
90
360
  const taskTags = Array.isArray(task.metadata?.tags)
@@ -107,8 +377,18 @@ export class QaProfileService {
107
377
  : undefined;
108
378
  const routedName = levelRoute ?? typeRoute ?? tagRoute;
109
379
  const routed = pickByName(routedName);
110
- if (routed)
380
+ if (routed) {
381
+ if (normalizeRunner(routed) !== runnerPreference) {
382
+ const fallback = pickByRunner();
383
+ if (fallback)
384
+ return fallback;
385
+ }
111
386
  return routed;
387
+ }
388
+ }
389
+ const runnerCandidates = candidates.filter(matchRunner);
390
+ if (runnerCandidates.length) {
391
+ candidates = runnerCandidates;
112
392
  }
113
393
  const targetLevel = options.level ?? options.defaultLevel;
114
394
  if (targetLevel) {
@@ -131,6 +411,9 @@ export class QaProfileService {
131
411
  .map((p) => p.name)
132
412
  .join(', ')}`);
133
413
  }
414
+ const runnerDefault = pickByRunner();
415
+ if (runnerDefault)
416
+ return runnerDefault;
134
417
  const defaults = profiles.filter((p) => p.default);
135
418
  if (defaults.length === 1)
136
419
  return defaults[0];
@@ -139,4 +422,68 @@ export class QaProfileService {
139
422
  }
140
423
  return undefined;
141
424
  }
425
+ async resolveProfilesForTask(task, options = {}) {
426
+ if (options.profileName) {
427
+ const explicit = await this.resolveProfileForTask(task, options);
428
+ return explicit ? [explicit] : [];
429
+ }
430
+ const qaProfiles = Array.isArray(task.metadata?.qa?.profiles_expected)
431
+ ? task.metadata.qa.profiles_expected
432
+ .map((value) => String(value).trim())
433
+ .filter(Boolean)
434
+ : [];
435
+ if (qaProfiles.length > 0) {
436
+ const profiles = await this.loadProfiles();
437
+ const byName = new Map(profiles.map((profile) => [profile.name.toLowerCase(), profile]));
438
+ const pickByRunner = (runner) => {
439
+ const matches = profiles.filter((profile) => (profile.runner ?? 'cli') === runner);
440
+ if (!matches.length)
441
+ return undefined;
442
+ const defaults = matches.filter((profile) => profile.default);
443
+ if (defaults.length === 1)
444
+ return defaults[0];
445
+ return matches[0];
446
+ };
447
+ const runnerAliases = {
448
+ api: 'cli',
449
+ cli: 'cli',
450
+ browser: 'chromium',
451
+ web: 'chromium',
452
+ ui: 'chromium',
453
+ chromium: 'chromium',
454
+ maestro: 'maestro',
455
+ mobile: 'maestro',
456
+ };
457
+ const resolved = [];
458
+ const seen = new Set();
459
+ for (const profileName of qaProfiles) {
460
+ const normalized = profileName.toLowerCase();
461
+ let profile = byName.get(normalized);
462
+ if (!profile) {
463
+ const runner = runnerAliases[normalized];
464
+ if (runner) {
465
+ profile = pickByRunner(runner);
466
+ }
467
+ }
468
+ if (profile && !seen.has(profile.name)) {
469
+ resolved.push(profile);
470
+ seen.add(profile.name);
471
+ }
472
+ }
473
+ if (resolved.length > 0) {
474
+ return resolved;
475
+ }
476
+ }
477
+ const plan = await this.resolveRunnerPlan(task);
478
+ const profiles = [];
479
+ const seen = new Set();
480
+ for (const runner of plan.runners) {
481
+ const profile = await this.resolveProfileForTask(task, { ...options, runnerPreference: runner });
482
+ if (profile && !seen.has(profile.name)) {
483
+ profiles.push(profile);
484
+ seen.add(profile.name);
485
+ }
486
+ }
487
+ return profiles;
488
+ }
142
489
  }