sela-core 0.1.0-alpha.1

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 (267) hide show
  1. package/README.md +103 -0
  2. package/bin/sela.js +3 -0
  3. package/dist/cli/ErrorHandler.d.ts +10 -0
  4. package/dist/cli/ErrorHandler.d.ts.map +1 -0
  5. package/dist/cli/ErrorHandler.js +70 -0
  6. package/dist/cli/commands/bulk.d.ts +3 -0
  7. package/dist/cli/commands/bulk.d.ts.map +1 -0
  8. package/dist/cli/commands/bulk.js +158 -0
  9. package/dist/cli/commands/find.d.ts +3 -0
  10. package/dist/cli/commands/find.d.ts.map +1 -0
  11. package/dist/cli/commands/find.js +51 -0
  12. package/dist/cli/commands/init.d.ts +3 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +166 -0
  15. package/dist/cli/commands/list.d.ts +3 -0
  16. package/dist/cli/commands/list.d.ts.map +1 -0
  17. package/dist/cli/commands/list.js +56 -0
  18. package/dist/cli/commands/merge.d.ts +13 -0
  19. package/dist/cli/commands/merge.d.ts.map +1 -0
  20. package/dist/cli/commands/merge.js +119 -0
  21. package/dist/cli/commands/refactor.d.ts +3 -0
  22. package/dist/cli/commands/refactor.d.ts.map +1 -0
  23. package/dist/cli/commands/refactor.js +30 -0
  24. package/dist/cli/commands/showReport.d.ts +3 -0
  25. package/dist/cli/commands/showReport.d.ts.map +1 -0
  26. package/dist/cli/commands/showReport.js +82 -0
  27. package/dist/cli/commands/status.d.ts +3 -0
  28. package/dist/cli/commands/status.d.ts.map +1 -0
  29. package/dist/cli/commands/status.js +51 -0
  30. package/dist/cli/commands/sync.d.ts +3 -0
  31. package/dist/cli/commands/sync.d.ts.map +1 -0
  32. package/dist/cli/commands/sync.js +131 -0
  33. package/dist/cli/index.d.ts +2 -0
  34. package/dist/cli/index.d.ts.map +1 -0
  35. package/dist/cli/index.js +94 -0
  36. package/dist/cli/ui/DnaTable.d.ts +3 -0
  37. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  38. package/dist/cli/ui/DnaTable.js +77 -0
  39. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  40. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  41. package/dist/cli/ui/LocatorPicker.js +33 -0
  42. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  43. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  44. package/dist/cli/ui/ProgressReporter.js +33 -0
  45. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  46. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  47. package/dist/cli/ui/RefactorWizard.js +295 -0
  48. package/dist/config/ConfigLoader.d.ts +16 -0
  49. package/dist/config/ConfigLoader.d.ts.map +1 -0
  50. package/dist/config/ConfigLoader.js +185 -0
  51. package/dist/config/DryRunGuard.d.ts +14 -0
  52. package/dist/config/DryRunGuard.d.ts.map +1 -0
  53. package/dist/config/DryRunGuard.js +80 -0
  54. package/dist/config/PathScopeGuard.d.ts +42 -0
  55. package/dist/config/PathScopeGuard.d.ts.map +1 -0
  56. package/dist/config/PathScopeGuard.js +192 -0
  57. package/dist/config/SelaConfig.d.ts +178 -0
  58. package/dist/config/SelaConfig.d.ts.map +1 -0
  59. package/dist/config/SelaConfig.js +141 -0
  60. package/dist/config/defineConfig.d.ts +3 -0
  61. package/dist/config/defineConfig.d.ts.map +1 -0
  62. package/dist/config/defineConfig.js +9 -0
  63. package/dist/engine/FixwrightEngine.d.ts +24 -0
  64. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  65. package/dist/engine/FixwrightEngine.js +403 -0
  66. package/dist/engine/HealingRegistry.d.ts +40 -0
  67. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  68. package/dist/engine/HealingRegistry.js +99 -0
  69. package/dist/engine/SelaEngine.d.ts +57 -0
  70. package/dist/engine/SelaEngine.d.ts.map +1 -0
  71. package/dist/engine/SelaEngine.js +1146 -0
  72. package/dist/engine/singleton.d.ts +3 -0
  73. package/dist/engine/singleton.d.ts.map +1 -0
  74. package/dist/engine/singleton.js +5 -0
  75. package/dist/errors/SelaError.d.ts +146 -0
  76. package/dist/errors/SelaError.d.ts.map +1 -0
  77. package/dist/errors/SelaError.js +168 -0
  78. package/dist/fixtures/expectProxy.d.ts +12 -0
  79. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  80. package/dist/fixtures/expectProxy.js +246 -0
  81. package/dist/fixtures/index.d.ts +30 -0
  82. package/dist/fixtures/index.d.ts.map +1 -0
  83. package/dist/fixtures/index.js +765 -0
  84. package/dist/fixtures/moduleExpect.d.ts +2 -0
  85. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  86. package/dist/fixtures/moduleExpect.js +54 -0
  87. package/dist/fixtures/proxyTag.d.ts +12 -0
  88. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  89. package/dist/fixtures/proxyTag.js +45 -0
  90. package/dist/index.d.ts +9 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +31 -0
  93. package/dist/reporter/SelaReporter.d.ts +165 -0
  94. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  95. package/dist/reporter/SelaReporter.js +517 -0
  96. package/dist/services/ASTSourceUpdater.d.ts +143 -0
  97. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  98. package/dist/services/ASTSourceUpdater.js +3514 -0
  99. package/dist/services/AnchorResolver.d.ts +157 -0
  100. package/dist/services/AnchorResolver.d.ts.map +1 -0
  101. package/dist/services/AnchorResolver.js +289 -0
  102. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  103. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  104. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  105. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  106. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  107. package/dist/services/BlastRadiusAnalyzer.js +107 -0
  108. package/dist/services/ChainValidator.d.ts +76 -0
  109. package/dist/services/ChainValidator.d.ts.map +1 -0
  110. package/dist/services/ChainValidator.js +570 -0
  111. package/dist/services/CrossFileHealer.d.ts +6 -0
  112. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  113. package/dist/services/CrossFileHealer.js +135 -0
  114. package/dist/services/DecisionEngine.d.ts +51 -0
  115. package/dist/services/DecisionEngine.d.ts.map +1 -0
  116. package/dist/services/DecisionEngine.js +260 -0
  117. package/dist/services/DefinitionTracer.d.ts +41 -0
  118. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  119. package/dist/services/DefinitionTracer.js +366 -0
  120. package/dist/services/DnaEditorService.d.ts +31 -0
  121. package/dist/services/DnaEditorService.d.ts.map +1 -0
  122. package/dist/services/DnaEditorService.js +217 -0
  123. package/dist/services/DnaIndexService.d.ts +24 -0
  124. package/dist/services/DnaIndexService.d.ts.map +1 -0
  125. package/dist/services/DnaIndexService.js +131 -0
  126. package/dist/services/HealBudget.d.ts +32 -0
  127. package/dist/services/HealBudget.d.ts.map +1 -0
  128. package/dist/services/HealBudget.js +85 -0
  129. package/dist/services/HealReportService.d.ts +161 -0
  130. package/dist/services/HealReportService.d.ts.map +1 -0
  131. package/dist/services/HealReportService.js +1039 -0
  132. package/dist/services/HealingAdvisory.d.ts +30 -0
  133. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  134. package/dist/services/HealingAdvisory.js +51 -0
  135. package/dist/services/HealingCacheService.d.ts +135 -0
  136. package/dist/services/HealingCacheService.d.ts.map +1 -0
  137. package/dist/services/HealingCacheService.js +404 -0
  138. package/dist/services/HealthReportService.d.ts +10 -0
  139. package/dist/services/HealthReportService.d.ts.map +1 -0
  140. package/dist/services/HealthReportService.js +84 -0
  141. package/dist/services/InitializerUpdater.d.ts +16 -0
  142. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  143. package/dist/services/InitializerUpdater.js +73 -0
  144. package/dist/services/IntentAuditor.d.ts +56 -0
  145. package/dist/services/IntentAuditor.d.ts.map +1 -0
  146. package/dist/services/IntentAuditor.js +326 -0
  147. package/dist/services/InteractiveReview.d.ts +15 -0
  148. package/dist/services/InteractiveReview.d.ts.map +1 -0
  149. package/dist/services/InteractiveReview.js +128 -0
  150. package/dist/services/LLMService.d.ts +121 -0
  151. package/dist/services/LLMService.d.ts.map +1 -0
  152. package/dist/services/LLMService.js +475 -0
  153. package/dist/services/MutationApplier.d.ts +69 -0
  154. package/dist/services/MutationApplier.d.ts.map +1 -0
  155. package/dist/services/MutationApplier.js +360 -0
  156. package/dist/services/PRAutomationService.d.ts +50 -0
  157. package/dist/services/PRAutomationService.d.ts.map +1 -0
  158. package/dist/services/PRAutomationService.js +760 -0
  159. package/dist/services/PendingPromptLedger.d.ts +51 -0
  160. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  161. package/dist/services/PendingPromptLedger.js +205 -0
  162. package/dist/services/ReportGenerator.d.ts +156 -0
  163. package/dist/services/ReportGenerator.d.ts.map +1 -0
  164. package/dist/services/ReportGenerator.js +278 -0
  165. package/dist/services/ReportMergeService.d.ts +95 -0
  166. package/dist/services/ReportMergeService.d.ts.map +1 -0
  167. package/dist/services/ReportMergeService.js +0 -0
  168. package/dist/services/SafetyGuard.d.ts +112 -0
  169. package/dist/services/SafetyGuard.d.ts.map +1 -0
  170. package/dist/services/SafetyGuard.js +87 -0
  171. package/dist/services/Sanitizer.d.ts +28 -0
  172. package/dist/services/Sanitizer.d.ts.map +1 -0
  173. package/dist/services/Sanitizer.js +142 -0
  174. package/dist/services/SanitizingAnthropic.d.ts +19 -0
  175. package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
  176. package/dist/services/SanitizingAnthropic.js +84 -0
  177. package/dist/services/SelectorSanitizer.d.ts +52 -0
  178. package/dist/services/SelectorSanitizer.d.ts.map +1 -0
  179. package/dist/services/SelectorSanitizer.js +318 -0
  180. package/dist/services/SnapshotService.d.ts +11 -0
  181. package/dist/services/SnapshotService.d.ts.map +1 -0
  182. package/dist/services/SnapshotService.js +352 -0
  183. package/dist/services/SourceLinkService.d.ts +26 -0
  184. package/dist/services/SourceLinkService.d.ts.map +1 -0
  185. package/dist/services/SourceLinkService.js +165 -0
  186. package/dist/services/SourceUpdater.d.ts +45 -0
  187. package/dist/services/SourceUpdater.d.ts.map +1 -0
  188. package/dist/services/SourceUpdater.js +1091 -0
  189. package/dist/services/TemplateDiffService.d.ts +25 -0
  190. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  191. package/dist/services/TemplateDiffService.js +334 -0
  192. package/dist/services/TraceBackEngine.d.ts +67 -0
  193. package/dist/services/TraceBackEngine.d.ts.map +1 -0
  194. package/dist/services/TraceBackEngine.js +672 -0
  195. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  196. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  197. package/dist/services/WorkspaceSnapshotService.js +200 -0
  198. package/dist/services/safety/PipelineContext.d.ts +63 -0
  199. package/dist/services/safety/PipelineContext.d.ts.map +1 -0
  200. package/dist/services/safety/PipelineContext.js +34 -0
  201. package/dist/services/safety/roleClassifier.d.ts +4 -0
  202. package/dist/services/safety/roleClassifier.d.ts.map +1 -0
  203. package/dist/services/safety/roleClassifier.js +233 -0
  204. package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
  205. package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
  206. package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
  207. package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
  208. package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
  209. package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
  210. package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
  211. package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
  212. package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
  213. package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
  214. package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
  215. package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
  216. package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
  217. package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
  218. package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
  219. package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
  220. package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
  221. package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
  222. package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
  223. package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
  224. package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
  225. package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
  226. package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
  227. package/dist/services/safety/rules/VisibilityRule.js +53 -0
  228. package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
  229. package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
  230. package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
  231. package/dist/services/safety/rules/index.d.ts +3 -0
  232. package/dist/services/safety/rules/index.d.ts.map +1 -0
  233. package/dist/services/safety/rules/index.js +34 -0
  234. package/dist/services/types.d.ts +59 -0
  235. package/dist/services/types.d.ts.map +1 -0
  236. package/dist/services/types.js +2 -0
  237. package/dist/storage/SnapshotManager.d.ts +5 -0
  238. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  239. package/dist/storage/SnapshotManager.js +32 -0
  240. package/dist/types/index.d.ts +95 -0
  241. package/dist/types/index.d.ts.map +1 -0
  242. package/dist/types/index.js +7 -0
  243. package/dist/utils/DOMUtils.d.ts +49 -0
  244. package/dist/utils/DOMUtils.d.ts.map +1 -0
  245. package/dist/utils/DOMUtils.js +466 -0
  246. package/dist/utils/FileLock.d.ts +44 -0
  247. package/dist/utils/FileLock.d.ts.map +1 -0
  248. package/dist/utils/FileLock.js +189 -0
  249. package/dist/utils/IsolatedDiff.d.ts +45 -0
  250. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  251. package/dist/utils/IsolatedDiff.js +382 -0
  252. package/dist/utils/StackUtils.d.ts +11 -0
  253. package/dist/utils/StackUtils.d.ts.map +1 -0
  254. package/dist/utils/StackUtils.js +120 -0
  255. package/dist/utils/SummaryTable.d.ts +17 -0
  256. package/dist/utils/SummaryTable.d.ts.map +1 -0
  257. package/dist/utils/SummaryTable.js +37 -0
  258. package/dist/utils/atomicWrite.d.ts +9 -0
  259. package/dist/utils/atomicWrite.d.ts.map +1 -0
  260. package/dist/utils/atomicWrite.js +83 -0
  261. package/dist/utils/logger.d.ts +59 -0
  262. package/dist/utils/logger.d.ts.map +1 -0
  263. package/dist/utils/logger.js +123 -0
  264. package/dist/vendor/enquirer.d.ts +33 -0
  265. package/dist/vendor/enquirer.d.ts.map +1 -0
  266. package/dist/vendor/enquirer.js +11 -0
  267. package/package.json +71 -0
@@ -0,0 +1,30 @@
1
+ export declare enum AdvisoryCode {
2
+ BLAST_RADIUS_BLOCKED = "BLAST_RADIUS_BLOCKED",
3
+ BLAST_RADIUS_WARNING = "BLAST_RADIUS_WARNING",
4
+ CROSS_FILE_MUTATION = "CROSS_FILE_MUTATION",
5
+ MANUAL_REVIEW_TEMPLATE_COMPLEX = "MANUAL_REVIEW_TEMPLATE_COMPLEX",
6
+ MANUAL_REVIEW_CONDITIONAL_RETURN = "MANUAL_REVIEW_CONDITIONAL_RETURN",
7
+ POST_MUTATION_TYPE_ERROR = "POST_MUTATION_TYPE_ERROR",
8
+ /**
9
+ * A definition-site string constant was healed to a valid PUBLIC selector
10
+ * string (e.g. `role=button[name="…"]`) because the constant is consumed by
11
+ * `.locator(VAR)` and cannot hold a semantic locator. The AI's preferred
12
+ * semantic method form (e.g. `getByRole(...)`) is suggested as a cleaner,
13
+ * optional refactor.
14
+ */
15
+ SEMANTIC_UPGRADE_SUGGESTED = "SEMANTIC_UPGRADE_SUGGESTED"
16
+ }
17
+ export interface Advisory {
18
+ code: AdvisoryCode;
19
+ message: string;
20
+ affectedFile: string;
21
+ affectedLine?: number;
22
+ suggestedAction: string;
23
+ }
24
+ export declare class AdvisoryBuffer {
25
+ private items;
26
+ push(advisory: Advisory): void;
27
+ flush(): void;
28
+ get length(): number;
29
+ }
30
+ //# sourceMappingURL=HealingAdvisory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealingAdvisory.d.ts","sourceRoot":"","sources":["../../src/services/HealingAdvisory.ts"],"names":[],"mappings":"AAUA,oBAAY,YAAY;IACtB,oBAAoB,yBAAyB;IAC7C,oBAAoB,yBAAyB;IAC7C,mBAAmB,wBAAwB;IAC3C,8BAA8B,mCAAmC;IACjE,gCAAgC,qCAAqC;IACrE,wBAAwB,6BAA6B;IACrD;;;;;;OAMG;IACH,0BAA0B,+BAA+B;CAC1D;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAkB;IAE/B,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI9B,KAAK,IAAI,IAAI;IAab,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF"}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ // ═══════════════════════════════════════════════════════════════════
3
+ // HealingAdvisory (T-12)
4
+ //
5
+ // Typed advisory system for definition-site mutations.
6
+ // Advisories are buffered during the AST phase and flushed AFTER
7
+ // Playwright prints its test result - prevents interleaving.
8
+ // ═══════════════════════════════════════════════════════════════════
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.AdvisoryBuffer = exports.AdvisoryCode = void 0;
11
+ const logger_1 = require("../utils/logger");
12
+ var AdvisoryCode;
13
+ (function (AdvisoryCode) {
14
+ AdvisoryCode["BLAST_RADIUS_BLOCKED"] = "BLAST_RADIUS_BLOCKED";
15
+ AdvisoryCode["BLAST_RADIUS_WARNING"] = "BLAST_RADIUS_WARNING";
16
+ AdvisoryCode["CROSS_FILE_MUTATION"] = "CROSS_FILE_MUTATION";
17
+ AdvisoryCode["MANUAL_REVIEW_TEMPLATE_COMPLEX"] = "MANUAL_REVIEW_TEMPLATE_COMPLEX";
18
+ AdvisoryCode["MANUAL_REVIEW_CONDITIONAL_RETURN"] = "MANUAL_REVIEW_CONDITIONAL_RETURN";
19
+ AdvisoryCode["POST_MUTATION_TYPE_ERROR"] = "POST_MUTATION_TYPE_ERROR";
20
+ /**
21
+ * A definition-site string constant was healed to a valid PUBLIC selector
22
+ * string (e.g. `role=button[name="…"]`) because the constant is consumed by
23
+ * `.locator(VAR)` and cannot hold a semantic locator. The AI's preferred
24
+ * semantic method form (e.g. `getByRole(...)`) is suggested as a cleaner,
25
+ * optional refactor.
26
+ */
27
+ AdvisoryCode["SEMANTIC_UPGRADE_SUGGESTED"] = "SEMANTIC_UPGRADE_SUGGESTED";
28
+ })(AdvisoryCode || (exports.AdvisoryCode = AdvisoryCode = {}));
29
+ class AdvisoryBuffer {
30
+ items = [];
31
+ push(advisory) {
32
+ this.items.push(advisory);
33
+ }
34
+ flush() {
35
+ if (this.items.length === 0)
36
+ return;
37
+ logger_1.logger.info("\n[Sela Advisories] ══════════════════════════════════════");
38
+ for (const a of this.items) {
39
+ const loc = a.affectedLine != null ? `:${a.affectedLine + 1}` : "";
40
+ logger_1.logger.info(` [${a.code}] ${a.message}`);
41
+ logger_1.logger.info(` File: ${a.affectedFile}${loc}`);
42
+ logger_1.logger.info(` Action: ${a.suggestedAction}`);
43
+ }
44
+ logger_1.logger.info("[Sela Advisories] ══════════════════════════════════════\n");
45
+ this.items = [];
46
+ }
47
+ get length() {
48
+ return this.items.length;
49
+ }
50
+ }
51
+ exports.AdvisoryBuffer = AdvisoryBuffer;
@@ -0,0 +1,135 @@
1
+ import type { SmartChainSegment, SelectorSegment } from "./LLMService";
2
+ export type HealCacheMode = "action" | "assertion";
3
+ export interface CacheKeyInput {
4
+ /** Absolute or project-relative spec path containing the failing locator. */
5
+ filePath: string;
6
+ /** 1-based line of the failing call site - paired with filePath as a fingerprint. */
7
+ line: number;
8
+ /**
9
+ * Stable content hash of the DNA snapshot used to drive the heal. When the
10
+ * DNA rewrites, this changes - every cached entry for the old DNA
11
+ * silently stops matching and ages out via TTL.
12
+ */
13
+ dnaContentHash: string;
14
+ /** The original broken selector. */
15
+ brokenSelector: string;
16
+ /** Heal mode the cached fix was produced under. */
17
+ mode: HealCacheMode;
18
+ }
19
+ export interface HealingCacheEntry {
20
+ /** Full SHA1 of the CacheKeyInput tuple. */
21
+ key: string;
22
+ filePath: string;
23
+ line: number;
24
+ dnaContentHash: string;
25
+ brokenSelector: string;
26
+ mode: HealCacheMode;
27
+ healedSelector: string;
28
+ chainSegments?: SmartChainSegment[];
29
+ segments?: SelectorSegment[];
30
+ confidence?: number;
31
+ contentChange?: {
32
+ oldText: string;
33
+ newText: string;
34
+ };
35
+ explanation?: string;
36
+ alternatives?: string[];
37
+ /**
38
+ * True when SafetyGuard cleared this fix in the originating session
39
+ * (SAFE or REQUIRES_REVIEW). Unsafe entries are never replayed.
40
+ */
41
+ safe: boolean;
42
+ createdAt: string;
43
+ /** ISO timestamp after which the entry is considered expired and ignored. */
44
+ ttlExpiresAt: string;
45
+ lastHitAt?: string;
46
+ hitCount: number;
47
+ }
48
+ interface CacheFile {
49
+ schemaVersion: 1;
50
+ entries: Record<string, HealingCacheEntry>;
51
+ }
52
+ export interface HealingCacheSetInput extends CacheKeyInput {
53
+ healedSelector: string;
54
+ chainSegments?: SmartChainSegment[];
55
+ segments?: SelectorSegment[];
56
+ confidence?: number;
57
+ contentChange?: {
58
+ oldText: string;
59
+ newText: string;
60
+ };
61
+ explanation?: string;
62
+ alternatives?: string[];
63
+ safe: boolean;
64
+ /** Optional override for entry-level TTL (ms). Defaults to env / 7d. */
65
+ ttlMs?: number;
66
+ }
67
+ export declare function computeCacheKey(input: CacheKeyInput): string;
68
+ /**
69
+ * Convenience helper - fingerprint the JSON form of a DNA snapshot so
70
+ * the cache key changes whenever the DNA is rewritten.
71
+ */
72
+ export declare function hashDnaContent(dna: unknown): string;
73
+ export declare class HealingCacheService {
74
+ private readonly cacheDir;
75
+ private readonly filename;
76
+ private readonly lockDir;
77
+ private readonly tmpSuffix;
78
+ private readonly bakSuffix;
79
+ /** Cross-process mutex (Layer 2) — shared mkdir-lock primitive. */
80
+ private readonly lock;
81
+ private inFlight;
82
+ constructor(opts?: {
83
+ cacheDir?: string;
84
+ filename?: string;
85
+ });
86
+ /**
87
+ * Read-only lookup. Bumps `hitCount` / `lastHitAt` IN-MEMORY only on
88
+ * the returned entry reference - never persisted on the get() path.
89
+ * This eliminates the read-write race where parallel workers each
90
+ * call get() and serialise full entries-map snapshots back to disk,
91
+ * overwriting peer set() operations.
92
+ */
93
+ get(input: CacheKeyInput): HealingCacheEntry | null;
94
+ /**
95
+ * Persist a new cache entry. Serialises against in-process writers
96
+ * AND across processes via the mkdir lock. The in-process chain
97
+ * isolates each `await` so a rejected upstream Promise never
98
+ * deadlocks downstream callers.
99
+ */
100
+ set(input: HealingCacheSetInput): Promise<void>;
101
+ /** Synchronous variant - used by teardown / CLI paths. */
102
+ setSync(input: HealingCacheSetInput): void;
103
+ /** Wipe every entry. Used by tests and the (forthcoming) `sela cache clear` CLI. */
104
+ clear(): void;
105
+ /** Returns the parsed cache file (or an empty shell on failure). */
106
+ read(): CacheFile;
107
+ /** Absolute path of the on-disk cache file. */
108
+ filePath(): string;
109
+ /**
110
+ * Prunes expired entries. Cheap O(n) scan - intended for engine
111
+ * startup. Returns the number of entries removed.
112
+ */
113
+ pruneExpired(): number;
114
+ private isExpired;
115
+ private lockedWrite;
116
+ private buildEntry;
117
+ /**
118
+ * Reads + parses the cache file. Tolerates every realistic failure
119
+ * (missing, empty, malformed, wrong schema version) by returning a
120
+ * fresh empty shell - the contract is "cache misses are silent".
121
+ */
122
+ private safeRead;
123
+ /**
124
+ * Atomic write via the shared {@link atomicWriteFile} primitive: write to a
125
+ * pid-temp, copy current to `<file>.bak`, then rename. The rename is the
126
+ * single commit point - a crash before it leaves the prior file intact.
127
+ */
128
+ private atomicWrite;
129
+ private acquireLockWithBackoff;
130
+ private withFileLockSync;
131
+ private releaseLock;
132
+ }
133
+ export declare const sharedHealingCache: HealingCacheService;
134
+ export {};
135
+ //# sourceMappingURL=HealingCacheService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealingCacheService.d.ts","sourceRoot":"","sources":["../../src/services/HealingCacheService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ // src/services/HealingCacheService.ts
3
+ //
4
+ // Local healing cache - short-circuits the LLM round-trip when an
5
+ // identical heal-context has been resolved successfully in a prior
6
+ // session.
7
+ //
8
+ // Cache key (matches DX MVP Phase 6 plan):
9
+ // SHA1(filePath + ":" + line + "::" + dnaContentHash + "::" + brokenSelector + "::" + mode)
10
+ // Including `dnaContentHash` means a DNA rewrite automatically
11
+ // invalidates every entry keyed against that fingerprint - old
12
+ // entries simply stop matching and age out via TTL.
13
+ //
14
+ // Cache value:
15
+ // `{ chainSegments, segments, confidence, contentChange, explanation,
16
+ // alternatives, safe, createdAt, ttlExpiresAt, hitCount, lastHitAt }`.
17
+ // TTL defaults to 7 days; overridable via `SELA_CACHE_TTL_DAYS`.
18
+ //
19
+ // Storage:
20
+ // `.sela-cache.json` in process.cwd() by default (configurable via the
21
+ // constructor for tests). Single-file map of SHA1 keys → entry.
22
+ // Schema-versioned (`schemaVersion: 1`).
23
+ //
24
+ // Concurrency model (three layers, identical to the plan):
25
+ //
26
+ // 1. In-process mutex
27
+ // `inFlight: Promise<void>` chain. Concurrent setters in the SAME
28
+ // Node process queue behind one another via the chained Promise.
29
+ // Any rejection on the chain is swallowed before the next call
30
+ // awaits it, so a single bad write CANNOT deadlock subsequent
31
+ // callers.
32
+ //
33
+ // 2. Cross-process file lock
34
+ // Playwright workers run as separate processes - atomic
35
+ // `mkdir <cacheDir>/.sela-cache.lock` is used as the mutex. Second
36
+ // process gets `EEXIST`, polls with exponential backoff
37
+ // (50ms → 800ms, max 6 retries ≈ 3.15 s ceiling). Lock is held
38
+ // only across the read-modify-write window, never during reads.
39
+ //
40
+ // 3. Atomic write via temp + rename
41
+ // Write to `.sela-cache.json.tmp`, copy the previous file to
42
+ // `.sela-cache.json.bak`, then `fs.renameSync(tmp, target)` -
43
+ // atomic on POSIX and on Windows (Node ≥14) when source and
44
+ // target share a volume (always true here).
45
+ //
46
+ // Read path:
47
+ // memory check → file check (no lock) → on miss the caller (SelaEngine)
48
+ // acquires the LLM via the normal path; cache writes happen post-success.
49
+ //
50
+ // `get()` is strictly read-only on disk: hit-count / lastHitAt are
51
+ // bumped IN-MEMORY only - they never trigger a disk write. This avoids
52
+ // the documented bug where concurrent worker `get()` calls would
53
+ // overwrite valid `set()` operations from peer workers.
54
+ //
55
+ // Failure tolerance: any read failure (missing, malformed JSON, wrong
56
+ // schema, EACCES, …) returns "no entries" instead of throwing. The
57
+ // cache is a latency optimisation - never a hard dependency of the
58
+ // heal pipeline.
59
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
60
+ if (k2 === undefined) k2 = k;
61
+ var desc = Object.getOwnPropertyDescriptor(m, k);
62
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
63
+ desc = { enumerable: true, get: function() { return m[k]; } };
64
+ }
65
+ Object.defineProperty(o, k2, desc);
66
+ }) : (function(o, m, k, k2) {
67
+ if (k2 === undefined) k2 = k;
68
+ o[k2] = m[k];
69
+ }));
70
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
71
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
72
+ }) : function(o, v) {
73
+ o["default"] = v;
74
+ });
75
+ var __importStar = (this && this.__importStar) || (function () {
76
+ var ownKeys = function(o) {
77
+ ownKeys = Object.getOwnPropertyNames || function (o) {
78
+ var ar = [];
79
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
80
+ return ar;
81
+ };
82
+ return ownKeys(o);
83
+ };
84
+ return function (mod) {
85
+ if (mod && mod.__esModule) return mod;
86
+ var result = {};
87
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
88
+ __setModuleDefault(result, mod);
89
+ return result;
90
+ };
91
+ })();
92
+ Object.defineProperty(exports, "__esModule", { value: true });
93
+ exports.sharedHealingCache = exports.HealingCacheService = void 0;
94
+ exports.computeCacheKey = computeCacheKey;
95
+ exports.hashDnaContent = hashDnaContent;
96
+ const fs = __importStar(require("fs"));
97
+ const path = __importStar(require("path"));
98
+ const crypto_1 = require("crypto");
99
+ const DryRunGuard_1 = require("../config/DryRunGuard");
100
+ const FileLock_1 = require("../utils/FileLock");
101
+ const atomicWrite_1 = require("../utils/atomicWrite");
102
+ const logger_1 = require("../utils/logger");
103
+ // ═══════════════════════════════════════════════════════════════════
104
+ // KEY HASH
105
+ // ═══════════════════════════════════════════════════════════════════
106
+ const KEY_SEPARATOR = "::";
107
+ const DEFAULT_TTL_DAYS = 7;
108
+ function defaultTtlMs() {
109
+ const raw = process.env.SELA_CACHE_TTL_DAYS;
110
+ if (!raw)
111
+ return DEFAULT_TTL_DAYS * 24 * 60 * 60 * 1000;
112
+ const n = Number(raw);
113
+ if (!Number.isFinite(n) || n <= 0) {
114
+ return DEFAULT_TTL_DAYS * 24 * 60 * 60 * 1000;
115
+ }
116
+ return n * 24 * 60 * 60 * 1000;
117
+ }
118
+ function computeCacheKey(input) {
119
+ // Normalise filePath separators so a cache entry written from a Windows
120
+ // worker is still found from a POSIX worker hitting the same repo.
121
+ const normPath = path.resolve(input.filePath).split(path.sep).join("/");
122
+ return (0, crypto_1.createHash)("sha1")
123
+ .update([
124
+ normPath + ":" + input.line,
125
+ input.dnaContentHash,
126
+ input.brokenSelector,
127
+ input.mode,
128
+ ].join(KEY_SEPARATOR))
129
+ .digest("hex");
130
+ }
131
+ /**
132
+ * Convenience helper - fingerprint the JSON form of a DNA snapshot so
133
+ * the cache key changes whenever the DNA is rewritten.
134
+ */
135
+ function hashDnaContent(dna) {
136
+ try {
137
+ const json = typeof dna === "string" ? dna : JSON.stringify(dna ?? {});
138
+ return (0, crypto_1.createHash)("sha1").update(json).digest("hex");
139
+ }
140
+ catch {
141
+ return "no-dna";
142
+ }
143
+ }
144
+ // ═══════════════════════════════════════════════════════════════════
145
+ // HealingCacheService
146
+ // ═══════════════════════════════════════════════════════════════════
147
+ class HealingCacheService {
148
+ cacheDir;
149
+ filename;
150
+ lockDir;
151
+ tmpSuffix = ".tmp";
152
+ bakSuffix = ".bak";
153
+ /** Cross-process mutex (Layer 2) — shared mkdir-lock primitive. */
154
+ lock;
155
+ // In-process serialisation. Every new set() chains onto the prior
156
+ // tail Promise. The chain is fault-tolerant - see `set()` below.
157
+ inFlight = Promise.resolve();
158
+ constructor(opts) {
159
+ this.cacheDir = opts?.cacheDir ?? process.cwd();
160
+ this.filename = opts?.filename ?? ".sela-cache.json";
161
+ this.lockDir = path.join(this.cacheDir, ".sela-cache.lock");
162
+ // Preserve the historical schedules: async 50→800ms (≈3.15s ceiling),
163
+ // sync busy-wait 10→100ms. Logged warnings keep the cache's prefix.
164
+ this.lock = new FileLock_1.FileLock(this.lockDir, {
165
+ delaysMs: [50, 100, 200, 400, 800, 800],
166
+ syncDelaysMs: [10, 25, 50, 100],
167
+ logger: { warn: (m) => logger_1.logger.warn(`HealingCache: ${m}`) },
168
+ });
169
+ }
170
+ // ─────────────────────────────────────────────────────────────
171
+ // PUBLIC API
172
+ // ─────────────────────────────────────────────────────────────
173
+ /**
174
+ * Read-only lookup. Bumps `hitCount` / `lastHitAt` IN-MEMORY only on
175
+ * the returned entry reference - never persisted on the get() path.
176
+ * This eliminates the read-write race where parallel workers each
177
+ * call get() and serialise full entries-map snapshots back to disk,
178
+ * overwriting peer set() operations.
179
+ */
180
+ get(input) {
181
+ const key = computeCacheKey(input);
182
+ const file = this.safeRead();
183
+ const entry = file.entries[key];
184
+ if (!entry)
185
+ return null;
186
+ if (!entry.safe)
187
+ return null;
188
+ if (this.isExpired(entry))
189
+ return null;
190
+ // In-memory only - DO NOT write back. Hit-count drift across workers
191
+ // is acceptable; corrupting peer writes is not.
192
+ entry.hitCount = (entry.hitCount ?? 0) + 1;
193
+ entry.lastHitAt = new Date().toISOString();
194
+ return entry;
195
+ }
196
+ /**
197
+ * Persist a new cache entry. Serialises against in-process writers
198
+ * AND across processes via the mkdir lock. The in-process chain
199
+ * isolates each `await` so a rejected upstream Promise never
200
+ * deadlocks downstream callers.
201
+ */
202
+ async set(input) {
203
+ if (DryRunGuard_1.DryRunGuard.active()) {
204
+ // Don't pollute the cache during a SELA_DRY_RUN session.
205
+ return;
206
+ }
207
+ // Chain onto the previous tail, but swallow rejections via a
208
+ // `.catch(noop)` adapter. Without this, an earlier rejected set()
209
+ // would propagate through every subsequent `await prev` and either
210
+ // throw at the wrong caller or - worse - leak through the finally
211
+ // block without releasing the queue. The wrapper guarantees the
212
+ // queue is FIFO and rejection-safe.
213
+ const prev = this.inFlight.catch(() => {
214
+ /* swallow - upstream failure handled by its own caller */
215
+ });
216
+ let release;
217
+ const myTurn = new Promise((res) => {
218
+ release = res;
219
+ });
220
+ this.inFlight = myTurn;
221
+ try {
222
+ await prev;
223
+ await this.lockedWrite(input);
224
+ }
225
+ finally {
226
+ // Always release - even on throw - so the next queued caller
227
+ // unblocks. Errors still propagate up to *this* caller normally.
228
+ release();
229
+ }
230
+ }
231
+ /** Synchronous variant - used by teardown / CLI paths. */
232
+ setSync(input) {
233
+ if (DryRunGuard_1.DryRunGuard.active())
234
+ return;
235
+ this.withFileLockSync(() => {
236
+ const file = this.safeRead();
237
+ const key = computeCacheKey(input);
238
+ file.entries[key] = this.buildEntry(key, input, file.entries[key]);
239
+ this.atomicWrite(file);
240
+ });
241
+ }
242
+ /** Wipe every entry. Used by tests and the (forthcoming) `sela cache clear` CLI. */
243
+ clear() {
244
+ this.withFileLockSync(() => {
245
+ const target = path.join(this.cacheDir, this.filename);
246
+ const bak = target + this.bakSuffix;
247
+ const tmp = target + this.tmpSuffix;
248
+ for (const f of [target, bak, tmp]) {
249
+ try {
250
+ if (fs.existsSync(f))
251
+ fs.unlinkSync(f);
252
+ }
253
+ catch {
254
+ // best-effort
255
+ }
256
+ }
257
+ });
258
+ }
259
+ /** Returns the parsed cache file (or an empty shell on failure). */
260
+ read() {
261
+ return this.safeRead();
262
+ }
263
+ /** Absolute path of the on-disk cache file. */
264
+ filePath() {
265
+ return path.join(this.cacheDir, this.filename);
266
+ }
267
+ /**
268
+ * Prunes expired entries. Cheap O(n) scan - intended for engine
269
+ * startup. Returns the number of entries removed.
270
+ */
271
+ pruneExpired() {
272
+ let removed = 0;
273
+ this.withFileLockSync(() => {
274
+ const file = this.safeRead();
275
+ for (const [k, e] of Object.entries(file.entries)) {
276
+ if (this.isExpired(e)) {
277
+ delete file.entries[k];
278
+ removed++;
279
+ }
280
+ }
281
+ if (removed > 0)
282
+ this.atomicWrite(file);
283
+ });
284
+ return removed;
285
+ }
286
+ // ─────────────────────────────────────────────────────────────
287
+ // INTERNAL - read / write / lock
288
+ // ─────────────────────────────────────────────────────────────
289
+ isExpired(entry) {
290
+ if (!entry.ttlExpiresAt)
291
+ return false;
292
+ const t = Date.parse(entry.ttlExpiresAt);
293
+ if (!Number.isFinite(t))
294
+ return false;
295
+ return t <= Date.now();
296
+ }
297
+ async lockedWrite(input) {
298
+ const acquired = await this.acquireLockWithBackoff();
299
+ if (!acquired) {
300
+ logger_1.logger.warn(`HealingCache: could not acquire lock at ${this.lockDir} - write skipped`);
301
+ return;
302
+ }
303
+ try {
304
+ const file = this.safeRead();
305
+ const key = computeCacheKey(input);
306
+ file.entries[key] = this.buildEntry(key, input, file.entries[key]);
307
+ this.atomicWrite(file);
308
+ }
309
+ finally {
310
+ this.releaseLock();
311
+ }
312
+ }
313
+ buildEntry(key, input, prev) {
314
+ const now = new Date().toISOString();
315
+ const ttlMs = input.ttlMs ?? defaultTtlMs();
316
+ const ttlExpiresAt = new Date(Date.now() + ttlMs).toISOString();
317
+ return {
318
+ key,
319
+ filePath: input.filePath,
320
+ line: input.line,
321
+ dnaContentHash: input.dnaContentHash,
322
+ brokenSelector: input.brokenSelector,
323
+ mode: input.mode,
324
+ healedSelector: input.healedSelector,
325
+ chainSegments: input.chainSegments,
326
+ segments: input.segments,
327
+ confidence: input.confidence,
328
+ contentChange: input.contentChange,
329
+ explanation: input.explanation,
330
+ alternatives: input.alternatives,
331
+ safe: input.safe,
332
+ createdAt: prev?.createdAt ?? now,
333
+ ttlExpiresAt,
334
+ lastHitAt: prev?.lastHitAt,
335
+ hitCount: prev?.hitCount ?? 0,
336
+ };
337
+ }
338
+ /**
339
+ * Reads + parses the cache file. Tolerates every realistic failure
340
+ * (missing, empty, malformed, wrong schema version) by returning a
341
+ * fresh empty shell - the contract is "cache misses are silent".
342
+ */
343
+ safeRead() {
344
+ const target = path.join(this.cacheDir, this.filename);
345
+ let raw;
346
+ try {
347
+ raw = fs.readFileSync(target, "utf8");
348
+ }
349
+ catch {
350
+ return { schemaVersion: 1, entries: {} };
351
+ }
352
+ if (!raw || raw.trim().length === 0) {
353
+ return { schemaVersion: 1, entries: {} };
354
+ }
355
+ let parsed;
356
+ try {
357
+ parsed = JSON.parse(raw);
358
+ }
359
+ catch (err) {
360
+ logger_1.logger.warn(`HealingCache: corrupt cache file at ${target} - starting fresh (${err.message})`);
361
+ return { schemaVersion: 1, entries: {} };
362
+ }
363
+ if (!parsed ||
364
+ typeof parsed !== "object" ||
365
+ Array.isArray(parsed) ||
366
+ parsed.schemaVersion !== 1 ||
367
+ typeof parsed.entries !== "object" ||
368
+ parsed.entries === null ||
369
+ Array.isArray(parsed.entries)) {
370
+ logger_1.logger.warn(`HealingCache: unknown cache schema at ${target} - starting fresh`);
371
+ return { schemaVersion: 1, entries: {} };
372
+ }
373
+ return parsed;
374
+ }
375
+ /**
376
+ * Atomic write via the shared {@link atomicWriteFile} primitive: write to a
377
+ * pid-temp, copy current to `<file>.bak`, then rename. The rename is the
378
+ * single commit point - a crash before it leaves the prior file intact.
379
+ */
380
+ atomicWrite(file) {
381
+ if (!fs.existsSync(this.cacheDir)) {
382
+ fs.mkdirSync(this.cacheDir, { recursive: true });
383
+ }
384
+ const target = path.join(this.cacheDir, this.filename);
385
+ (0, atomicWrite_1.atomicWriteFile)(target, JSON.stringify(file, null, 2), { backup: true });
386
+ }
387
+ // ─────────────────────────────────────────────────────────────
388
+ // FILE LOCK - delegated to the shared mkdir-based mutex (Layer 2)
389
+ // ─────────────────────────────────────────────────────────────
390
+ acquireLockWithBackoff() {
391
+ return this.lock.acquire();
392
+ }
393
+ withFileLockSync(fn) {
394
+ this.lock.withLockSync(fn);
395
+ }
396
+ releaseLock() {
397
+ this.lock.release();
398
+ }
399
+ }
400
+ exports.HealingCacheService = HealingCacheService;
401
+ // ═══════════════════════════════════════════════════════════════════
402
+ // Module-level singleton - engines reuse the same cache file by default
403
+ // ═══════════════════════════════════════════════════════════════════
404
+ exports.sharedHealingCache = new HealingCacheService();
@@ -0,0 +1,10 @@
1
+ import type { DnaIndex, HealthReport } from "./types.js";
2
+ export interface IHealthReportService {
3
+ generate(index: DnaIndex): HealthReport;
4
+ format(report: HealthReport): string;
5
+ }
6
+ export declare class HealthReportService implements IHealthReportService {
7
+ generate(index: DnaIndex): HealthReport;
8
+ format(report: HealthReport): string;
9
+ }
10
+ //# sourceMappingURL=HealthReportService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealthReportService.d.ts","sourceRoot":"","sources":["../../src/services/HealthReportService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAa,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,YAAY,CAAC;IACxC,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAAC;CACtC;AAoBD,qBAAa,mBAAoB,YAAW,oBAAoB;IAC9D,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,YAAY;IAkBvC,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM;CA0FrC"}