sela-core 1.0.0
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 +93 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +140 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +133 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- package/dist/cli/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +123 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +70 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +269 -0
- package/dist/config/ConfigLoader.d.ts +15 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +174 -0
- package/dist/config/SelaConfig.d.ts +67 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +57 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +98 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +228 -0
- package/dist/fixtures/index.d.ts +6 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +688 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +46 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/services/ASTSourceUpdater.d.ts +79 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3177 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +103 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +569 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +134 -0
- package/dist/services/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +350 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +198 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealingAdvisory.d.ts +22 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +42 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +37 -0
- package/dist/services/IntentAuditor.d.ts +39 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +302 -0
- package/dist/services/LLMService.d.ts +100 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +439 -0
- package/dist/services/SafetyGuard.d.ts +65 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +524 -0
- package/dist/services/SnapshotService.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +349 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +156 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1021 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +331 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +31 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +33 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +179 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- package/package.json +67 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FixwrightEngine = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const LLMService_1 = require("../services/LLMService");
|
|
40
|
+
const SnapshotService_1 = require("../services/SnapshotService");
|
|
41
|
+
const DOMUtils_1 = require("../utils/DOMUtils");
|
|
42
|
+
const SourceUpdater_1 = require("../services/SourceUpdater");
|
|
43
|
+
const SafetyGuard_1 = require("../services/SafetyGuard");
|
|
44
|
+
const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
45
|
+
class FixwrightEngine {
|
|
46
|
+
llmService;
|
|
47
|
+
snapshotService;
|
|
48
|
+
safetyGuard;
|
|
49
|
+
config;
|
|
50
|
+
dnaBuffer = new Map();
|
|
51
|
+
healMetaBuffer = new Map();
|
|
52
|
+
testTitleBuffer = new Map();
|
|
53
|
+
constructor() {
|
|
54
|
+
this.config = ConfigLoader_1.ConfigLoader.getInstance();
|
|
55
|
+
this.llmService = new LLMService_1.LLMService();
|
|
56
|
+
this.snapshotService = new SnapshotService_1.SnapshotService();
|
|
57
|
+
this.safetyGuard = new SafetyGuard_1.SafetyGuard(this.config.thresholds);
|
|
58
|
+
}
|
|
59
|
+
// ─────────────────────────────────────────────────────────────
|
|
60
|
+
// healArgument — repairs a wrong action argument (e.g. selectOption)
|
|
61
|
+
// ─────────────────────────────────────────────────────────────
|
|
62
|
+
async healArgument(page, selector, action, oldArgument, stableId, filePath, line) {
|
|
63
|
+
console.log(`[Fixwright] 🔧 HEALING ARGUMENT for action '${action}': "${oldArgument}"`);
|
|
64
|
+
try {
|
|
65
|
+
const { dom } = await DOMUtils_1.DOMUtils.getNeighborhoodDom(page, selector);
|
|
66
|
+
const neighborhoodDom = dom;
|
|
67
|
+
const targetIntent = action === "selectOption"
|
|
68
|
+
? `ARGUMENT_HEALING: The selectOption argument "${oldArgument}" does not exist.
|
|
69
|
+
Look at the <select> element in the DOM and find all <option> elements inside it.
|
|
70
|
+
Return in 'new_selector' ONLY the TEXT CONTENT of the closest matching <option>.
|
|
71
|
+
For example: if the options are "Male" and "Female" and the old argument was "Man", return "Male".
|
|
72
|
+
CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only, exactly as it appears in the option.`
|
|
73
|
+
: `Fix the broken argument "${oldArgument}" for action "${action}". Return the correct value.`;
|
|
74
|
+
const aiFix = await this.llmService.getFix({
|
|
75
|
+
targetIntent,
|
|
76
|
+
failedSelector: `${selector}::${action}("${oldArgument}")`,
|
|
77
|
+
previousState: {},
|
|
78
|
+
currentDom: neighborhoodDom,
|
|
79
|
+
});
|
|
80
|
+
if (aiFix?.status === "FIXED" && aiFix.new_selector) {
|
|
81
|
+
const newArgument = aiFix.new_selector;
|
|
82
|
+
console.log(`[Fixwright] 🔧 New argument: "${newArgument}"`);
|
|
83
|
+
SourceUpdater_1.SourceUpdater.updateArgument({ filePath, line }, action, oldArgument, newArgument);
|
|
84
|
+
return newArgument;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error(`[Fixwright] ❌ Argument healing error:`, error.message);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// ─────────────────────────────────────────────────────────────
|
|
93
|
+
// heal — repairs a broken selector
|
|
94
|
+
// ─────────────────────────────────────────────────────────────
|
|
95
|
+
async heal(page, fullSelector, elementSelectorOnly, stableId, filePath, line, healMode = "action") {
|
|
96
|
+
console.log(`\n[Fixwright] 🛠️ HEALING START for: ${stableId}`);
|
|
97
|
+
try {
|
|
98
|
+
// load() now always returns ElementSnapshotV2 (v1 snapshots are
|
|
99
|
+
// migrated transparently; v2 fields default to safe values).
|
|
100
|
+
const lastKnownState = await this.snapshotService.load(stableId);
|
|
101
|
+
// ── Extract v2 DNA context ────────────────────────────────
|
|
102
|
+
const dnaAncestry = lastKnownState?.ancestry ?? [];
|
|
103
|
+
const dnaAnchors = lastKnownState?.anchors;
|
|
104
|
+
console.log(`[Fixwright] 🔍 Step 2: Extracting neighborhood DOM...`);
|
|
105
|
+
const { dom: neighborhoodDom, successfulPath } = await DOMUtils_1.DOMUtils.getNeighborhoodDom(page, fullSelector);
|
|
106
|
+
if (!neighborhoodDom || neighborhoodDom.length === 0) {
|
|
107
|
+
throw new Error("Failed to extract any DOM context for AI");
|
|
108
|
+
}
|
|
109
|
+
console.log(`[Fixwright] 🔍 Step 2: DOM Extracted (${neighborhoodDom.length} chars) ✅`);
|
|
110
|
+
const contextHint = successfulPath.length > 0
|
|
111
|
+
? `\n\n[CONTEXT HINT]: The provided DOM is extracted from INSIDE this frame path: "${successfulPath.join(" >> ")}". ` +
|
|
112
|
+
`If you find the element, your suggested selector should be relative to THIS context.`
|
|
113
|
+
: "";
|
|
114
|
+
const oldText = lastKnownState?.text ?? null;
|
|
115
|
+
const dnaRole = lastKnownState?.role ?? undefined;
|
|
116
|
+
const intentDescription = lastKnownState
|
|
117
|
+
? `The element that was a ${lastKnownState.tagName} with text "${lastKnownState.text}"`
|
|
118
|
+
: `Element identified by ${stableId}`;
|
|
119
|
+
const contentChangeInstructions = `
|
|
120
|
+
|
|
121
|
+
[CONTENT CHANGE DETECTION]:
|
|
122
|
+
If the element's VISIBLE TEXT has changed compared to its previous value${oldText ? ` ("${oldText}")` : ""}, you MUST include a "contentChange" field.
|
|
123
|
+
NOTE: This change will NOT be applied automatically to the source code to prevent masking potential bugs.
|
|
124
|
+
It will only be presented as a suggested fix in the terminal.
|
|
125
|
+
{
|
|
126
|
+
"contentChange": {
|
|
127
|
+
"oldText": "<the previous visible text>",
|
|
128
|
+
"newText": "<the new visible text as it appears in the current DOM>"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
This is required so that any expect(...).toHaveText("${oldText ?? "..."}") assertions in the test file can be automatically updated to match the new text.
|
|
132
|
+
If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
133
|
+
// ── Semantic anchor context hint for the LLM ─────────────────────
|
|
134
|
+
// When DNA v2 anchor data exists, tell the AI which label and row
|
|
135
|
+
// key the element was associated with so it can use semantic landmarks
|
|
136
|
+
// to disambiguate identical controls (e.g. multiple "Edit" buttons).
|
|
137
|
+
const anchorContextHint = (() => {
|
|
138
|
+
const anchors = lastKnownState?.anchors;
|
|
139
|
+
if (!anchors)
|
|
140
|
+
return "";
|
|
141
|
+
const lines = [];
|
|
142
|
+
if (anchors.closestLabel) {
|
|
143
|
+
lines.push(`- Label: "${anchors.closestLabel}" — the UI label or heading associated with this element.`);
|
|
144
|
+
}
|
|
145
|
+
if (anchors.rowAnchor) {
|
|
146
|
+
lines.push(`- Row key: "${anchors.rowAnchor}" — unique cell text identifying the table row this element belonged to.`);
|
|
147
|
+
}
|
|
148
|
+
if (lines.length === 0)
|
|
149
|
+
return "";
|
|
150
|
+
return ("\n\n[ELEMENT SEMANTIC CONTEXT]:\n" +
|
|
151
|
+
"The DNA snapshot recorded these semantic landmarks for the element you must find:\n" +
|
|
152
|
+
lines.join("\n") +
|
|
153
|
+
"\n" +
|
|
154
|
+
"Prioritize selectors that preserve these relationships (e.g. filter by row key, then target by label).");
|
|
155
|
+
})();
|
|
156
|
+
console.log(`[Fixwright] 🔍 Step 3: Requesting AI fix from LLM...`);
|
|
157
|
+
const aiFix = await this.llmService.getFix({
|
|
158
|
+
targetIntent: intentDescription,
|
|
159
|
+
failedSelector: fullSelector,
|
|
160
|
+
previousState: lastKnownState || {},
|
|
161
|
+
currentDom: neighborhoodDom +
|
|
162
|
+
contextHint +
|
|
163
|
+
anchorContextHint +
|
|
164
|
+
contentChangeInstructions,
|
|
165
|
+
});
|
|
166
|
+
if (aiFix && aiFix.status === "FIXED") {
|
|
167
|
+
console.log(`[Fixwright] 🔍 Step 3: AI responded ✅`);
|
|
168
|
+
// ── Build new selector ────────────────────────────────────
|
|
169
|
+
const aiSegments = aiFix.segments || [];
|
|
170
|
+
const newSegmentsFromAI = aiSegments.filter((aiSeg) => !successfulPath.includes(aiSeg.selector));
|
|
171
|
+
const newFullSelector = [
|
|
172
|
+
...successfulPath,
|
|
173
|
+
...newSegmentsFromAI.map((s) => s.selector),
|
|
174
|
+
].join(" >> ");
|
|
175
|
+
const rawElementSelector = newSegmentsFromAI.map((s) => s.selector).join(" >> ") ||
|
|
176
|
+
newFullSelector;
|
|
177
|
+
const framePrefix = successfulPath.join(" >> ");
|
|
178
|
+
const elementOnlySelector = framePrefix && rawElementSelector.startsWith(framePrefix + " >> ")
|
|
179
|
+
? rawElementSelector.slice(framePrefix.length + 4)
|
|
180
|
+
: rawElementSelector;
|
|
181
|
+
// ── Step 3.5: Fetch live text (lightweight zero-trust probe) ─
|
|
182
|
+
const liveText = await this.fetchLiveText(page, elementOnlySelector, successfulPath);
|
|
183
|
+
const dnaBaselineText = oldText ?? undefined;
|
|
184
|
+
if (liveText !== null) {
|
|
185
|
+
console.log(`[Fixwright] 🔍 Step 3.5: Live text fetched: "${liveText}"` +
|
|
186
|
+
(dnaBaselineText ? ` (DNA baseline: "${dnaBaselineText}")` : ""));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(`[Fixwright] 🔍 Step 3.5: Live text fetch skipped (element not yet rendered or non-text)`);
|
|
190
|
+
}
|
|
191
|
+
// ── Step 3.6: Atomic v2 capture of the candidate element ──
|
|
192
|
+
// Single page.evaluate() call — collects visibility, ancestry,
|
|
193
|
+
// and anchors of the healed element for SafetyGuard use.
|
|
194
|
+
// Returns null on any failure; never blocks the heal.
|
|
195
|
+
const liveSnapshot = await this.snapshotService.captureElement(page, elementOnlySelector, successfulPath);
|
|
196
|
+
const liveVisibility = liveSnapshot?.visibility;
|
|
197
|
+
const liveAncestry = liveSnapshot?.ancestry;
|
|
198
|
+
const liveAnchors = liveSnapshot?.anchors;
|
|
199
|
+
if (liveSnapshot) {
|
|
200
|
+
const ghostFlag = liveVisibility?.isGhost
|
|
201
|
+
? ` 👻 GHOST(${liveVisibility.ghostReason})`
|
|
202
|
+
: "";
|
|
203
|
+
const occFlag = liveVisibility?.isOccluded ? " 🫥 OCCLUDED" : "";
|
|
204
|
+
console.log(`[Fixwright] 🔍 Step 3.6: Live v2 capture complete` +
|
|
205
|
+
`${ghostFlag}${occFlag} — ancestry depth: ${liveAncestry?.length ?? 0}`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(`[Fixwright] 🔍 Step 3.6: Live v2 capture skipped`);
|
|
209
|
+
}
|
|
210
|
+
// ── Safety: delegate all go/no-go decisions to SafetyGuard ─
|
|
211
|
+
const branch = ConfigLoader_1.ConfigLoader.getBranch() ?? undefined;
|
|
212
|
+
const safetyDecision = await this.safetyGuard.evaluate({ mode: healMode, branch }, {
|
|
213
|
+
confidence: aiFix.confidence,
|
|
214
|
+
contentChange: aiFix.contentChange,
|
|
215
|
+
explanation: aiFix.explanation,
|
|
216
|
+
liveText: liveText ?? undefined,
|
|
217
|
+
dnaBaselineText,
|
|
218
|
+
dnaRole,
|
|
219
|
+
// v2 structural context
|
|
220
|
+
liveVisibility,
|
|
221
|
+
liveAncestry,
|
|
222
|
+
liveAnchors,
|
|
223
|
+
dnaAncestry,
|
|
224
|
+
dnaAnchors,
|
|
225
|
+
});
|
|
226
|
+
console.log(`[SafetyGuard] ${safetyDecision.canProceed ? "✅" : "❌"} [${safetyDecision.level}] ${safetyDecision.reason}`);
|
|
227
|
+
if (!safetyDecision.canProceed) {
|
|
228
|
+
if (safetyDecision.level === "BLOCKED" &&
|
|
229
|
+
safetyDecision.meta?.auditorVerdict === "SUSPICIOUS") {
|
|
230
|
+
const strictness = this.config.thresholds.auditorStrictness;
|
|
231
|
+
const conf = safetyDecision.meta.auditorConfidence ?? "?";
|
|
232
|
+
const suggestion = strictness === "strict" ? "Balanced" : "Loose";
|
|
233
|
+
console.warn(`[Sela] ⚠️ Auditor status: SUSPICIOUS (Confidence: ${conf}%)`);
|
|
234
|
+
console.warn(`[Sela] 🚫 Policy: '${strictness}' (Branch: ${branch ?? "unknown"}). Auto-fix blocked.`);
|
|
235
|
+
console.warn(`[Sela] 💡 To allow this, change 'auditorStrictness' to '${suggestion}' in sela.config.ts.`);
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`[SafetyGuard] ${safetyDecision.level}: ${safetyDecision.reason}`);
|
|
238
|
+
}
|
|
239
|
+
// ── Heal-completion score summary ─────────────────────────────────
|
|
240
|
+
// Emits a single consolidated line showing AI confidence plus the
|
|
241
|
+
// auditor's structural adjustments (ancestry drift / anchor bonus).
|
|
242
|
+
const breakdown = safetyDecision.meta?.auditScoreBreakdown;
|
|
243
|
+
if (breakdown && (breakdown.penalty !== 0 || breakdown.bonus !== 0)) {
|
|
244
|
+
const parts = [];
|
|
245
|
+
if (breakdown.penalty !== 0)
|
|
246
|
+
parts.push(`${breakdown.penalty} ancestry drift`);
|
|
247
|
+
if (breakdown.bonus !== 0)
|
|
248
|
+
parts.push(`+${breakdown.bonus} anchor match`);
|
|
249
|
+
console.log(`[Fixwright] 📊 Score — AI: ${aiFix.confidence}% | ` +
|
|
250
|
+
`Auditor: ${breakdown.adjustedConfidence}% ` +
|
|
251
|
+
`(raw ${breakdown.rawConfidence}%, ${parts.join(", ")})`);
|
|
252
|
+
}
|
|
253
|
+
console.log(`[Fixwright] 🚀 Rebuilt Full Runtime Selector (Clean): ${newFullSelector}`);
|
|
254
|
+
const updateResult = SourceUpdater_1.SourceUpdater.update({ filePath, line }, elementSelectorOnly, newFullSelector, neighborhoodDom, undefined, fullSelector, aiFix.segments, aiFix.contentChange, aiFix.chainSegments, aiFix.originalChainHint, this.config.updateStrategy, this.config.autoCommit);
|
|
255
|
+
const canonicalSelector = updateResult.healedLocatorString ?? newFullSelector;
|
|
256
|
+
console.log(`[Fixwright] ✅ Canonical selector (disk == runtime): "${canonicalSelector}"`);
|
|
257
|
+
// Buffer CLI-ready metadata — merged into DNA at commitUpdates() time.
|
|
258
|
+
// Sanitize filePath: strip any residual "at " stack-trace prefix + whitespace,
|
|
259
|
+
// then resolve to an absolute path before converting to project-relative form.
|
|
260
|
+
const match = filePath.match(/at\s+(.*)/i);
|
|
261
|
+
// 2. אם מצאנו התאמה ניקח אותה, אחרת ניקח את המחרוזת המקורית.
|
|
262
|
+
// בנוסף, ננקה רווחים וסוגריים (לפעמים נתיבים ב-Stack trace מוקפים בסוגריים).
|
|
263
|
+
const cleanFilePath = (match ? match[1] : filePath)
|
|
264
|
+
.replace(/^\(|\)$/g, "") // מסיר סוגריים אם קיימים ( )
|
|
265
|
+
.trim();
|
|
266
|
+
// 3. הפיכה לנתיב אבסולוטי ואז ליחסי (כמו שעשית, שזה מעולה)
|
|
267
|
+
const absFilePath = cleanFilePath && path.isAbsolute(cleanFilePath)
|
|
268
|
+
? cleanFilePath
|
|
269
|
+
: cleanFilePath
|
|
270
|
+
? path.resolve(process.cwd(), cleanFilePath)
|
|
271
|
+
: "";
|
|
272
|
+
const relativeSourceFile = absFilePath
|
|
273
|
+
? path.relative(process.cwd(), absFilePath)
|
|
274
|
+
: "";
|
|
275
|
+
this.healMetaBuffer.set(stableId, {
|
|
276
|
+
selector: canonicalSelector,
|
|
277
|
+
sourceFile: relativeSourceFile,
|
|
278
|
+
sourceLine: line,
|
|
279
|
+
originalSelector: elementSelectorOnly,
|
|
280
|
+
healedSelector: canonicalSelector,
|
|
281
|
+
lastHealed: new Date().toISOString(),
|
|
282
|
+
status: "healed",
|
|
283
|
+
definitionSite: updateResult.definitionSite,
|
|
284
|
+
blastRadius: updateResult.blastRadius,
|
|
285
|
+
});
|
|
286
|
+
return canonicalSelector;
|
|
287
|
+
}
|
|
288
|
+
throw new Error("AI could not provide a fix");
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.error(`[Fixwright] ❌ Healing Failure:`, error.message);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
preloadTestTitle(stableId, testTitle) {
|
|
296
|
+
this.testTitleBuffer.set(stableId, testTitle);
|
|
297
|
+
}
|
|
298
|
+
async saveSnapshot(stableId, data) {
|
|
299
|
+
this.dnaBuffer.set(stableId, data);
|
|
300
|
+
}
|
|
301
|
+
async commitUpdates() {
|
|
302
|
+
if (this.dnaBuffer.size === 0)
|
|
303
|
+
return;
|
|
304
|
+
for (const [key, dna] of this.dnaBuffer.entries()) {
|
|
305
|
+
const meta = this.healMetaBuffer.get(key);
|
|
306
|
+
const toSave = meta ? { ...dna, ...meta } : dna;
|
|
307
|
+
await this.snapshotService.save(key, toSave);
|
|
308
|
+
}
|
|
309
|
+
this.dnaBuffer.clear();
|
|
310
|
+
this.healMetaBuffer.clear();
|
|
311
|
+
this.testTitleBuffer.clear();
|
|
312
|
+
SourceUpdater_1.SourceUpdater.flushAdvisories();
|
|
313
|
+
}
|
|
314
|
+
// ─────────────────────────────────────────────────────────────
|
|
315
|
+
// fetchLiveText — frame-aware innerText fetch (zero-trust probe)
|
|
316
|
+
//
|
|
317
|
+
// Navigates the frame chain encoded in successfulPath, then calls
|
|
318
|
+
// .first().innerText() on the element-only selector. Uses a short
|
|
319
|
+
// 1.5 s timeout so it never adds meaningful latency. Returns null
|
|
320
|
+
// on ANY failure — the caller must treat null as "no data" and skip
|
|
321
|
+
// live verification rather than blocking the heal.
|
|
322
|
+
// ─────────────────────────────────────────────────────────────
|
|
323
|
+
async fetchLiveText(page, elementOnlySelector, framePath) {
|
|
324
|
+
try {
|
|
325
|
+
let ctx = page;
|
|
326
|
+
for (const frameSel of framePath) {
|
|
327
|
+
ctx = ctx.frameLocator(frameSel);
|
|
328
|
+
}
|
|
329
|
+
const text = await ctx
|
|
330
|
+
.locator(elementOnlySelector)
|
|
331
|
+
.first()
|
|
332
|
+
.innerText({ timeout: 1500 });
|
|
333
|
+
return text.trim().length > 0 ? text.trim() : null;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
_rebuildSelectorWithFrames(fullSelector, elementSelectorOnly, aiNewSelector) {
|
|
340
|
+
const fullParts = fullSelector.split(" >> ").map((s) => s.trim());
|
|
341
|
+
const elementParts = elementSelectorOnly.split(" >> ").map((s) => s.trim());
|
|
342
|
+
const aiParts = aiNewSelector.split(" >> ").map((s) => s.trim());
|
|
343
|
+
const frameParts = fullParts.slice(0, fullParts.length - elementParts.length);
|
|
344
|
+
if (frameParts.length === 0)
|
|
345
|
+
return aiNewSelector;
|
|
346
|
+
const aiHasFrameSegments = aiParts.some((p) => p.includes("-frame") ||
|
|
347
|
+
p === "iframe" ||
|
|
348
|
+
p === "frame" ||
|
|
349
|
+
p.includes("frame-"));
|
|
350
|
+
if (aiHasFrameSegments) {
|
|
351
|
+
console.log(`[Fixwright] 🔗 AI returned full frame path — using directly: ${aiNewSelector}`);
|
|
352
|
+
return aiNewSelector;
|
|
353
|
+
}
|
|
354
|
+
return [...frameParts, ...aiParts].join(" >> ");
|
|
355
|
+
}
|
|
356
|
+
getAllFiles(dirPath, arrayOfFiles = []) {
|
|
357
|
+
const files = fs.readdirSync(dirPath);
|
|
358
|
+
const ignoreList = [
|
|
359
|
+
"node_modules",
|
|
360
|
+
".git",
|
|
361
|
+
"fixwright-snapshots",
|
|
362
|
+
"dist",
|
|
363
|
+
"test-results",
|
|
364
|
+
"tests/reset-demo.ts",
|
|
365
|
+
];
|
|
366
|
+
files.forEach((file) => {
|
|
367
|
+
const fullPath = path.join(dirPath, file);
|
|
368
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
369
|
+
if (!ignoreList.includes(file))
|
|
370
|
+
this.getAllFiles(fullPath, arrayOfFiles);
|
|
371
|
+
}
|
|
372
|
+
else if (file.endsWith(".ts") || file.endsWith(".js")) {
|
|
373
|
+
arrayOfFiles.push(fullPath);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return arrayOfFiles;
|
|
377
|
+
}
|
|
378
|
+
// ─────────────────────────────────────────────────────────────
|
|
379
|
+
// captureSuccessfulElement — capture and buffer a v2 DNA snapshot.
|
|
380
|
+
//
|
|
381
|
+
// Replaces the previous DOMUtils.getElementDNA() path with a single
|
|
382
|
+
// captureElement() call that collects all v2 metrics atomically.
|
|
383
|
+
// ─────────────────────────────────────────────────────────────
|
|
384
|
+
async captureSuccessfulElement(page, selector, stableId) {
|
|
385
|
+
try {
|
|
386
|
+
const snapshot = await this.snapshotService.captureElement(page, selector);
|
|
387
|
+
if (snapshot) {
|
|
388
|
+
const testTitle = this.testTitleBuffer.get(stableId);
|
|
389
|
+
const enriched = {
|
|
390
|
+
...snapshot,
|
|
391
|
+
selector,
|
|
392
|
+
...(testTitle ? { testTitle } : {}),
|
|
393
|
+
};
|
|
394
|
+
this.dnaBuffer.set(stableId, enriched);
|
|
395
|
+
console.log(`[Fixwright] 🧬 DNA v2 captured for: ${stableId}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.debug(`[Fixwright] 🧬 DNA capture skipped: ${error.message}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
exports.FixwrightEngine = FixwrightEngine;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type HealListener = (fingerprint: string, oldSelector: string, newSelector: string) => void;
|
|
2
|
+
export interface SelectorContext {
|
|
3
|
+
filePath: string;
|
|
4
|
+
line: number;
|
|
5
|
+
/** Ordered parent selectors above this call, e.g. ["#outer-frame", ".user-item"] */
|
|
6
|
+
parentChain?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare class HealingRegistry {
|
|
9
|
+
private static instance;
|
|
10
|
+
private readonly listeners;
|
|
11
|
+
/**
|
|
12
|
+
* Two-level store:
|
|
13
|
+
* fingerprint → (oldSelector → newSelector)
|
|
14
|
+
*
|
|
15
|
+
* A fingerprint is deterministic per call-site, so heals from one test
|
|
16
|
+
* can NEVER overwrite a different call-site's mapping, even if the raw
|
|
17
|
+
* selector string is identical (e.g. "internal:role=button").
|
|
18
|
+
*/
|
|
19
|
+
private readonly healMap;
|
|
20
|
+
private constructor();
|
|
21
|
+
static getInstance(): HealingRegistry;
|
|
22
|
+
static fingerprint(ctx: SelectorContext): string;
|
|
23
|
+
broadcast(ctx: SelectorContext, oldSelector: string, newSelector: string): void;
|
|
24
|
+
broadcastCascade(ctx: SelectorContext, pairs: Array<{
|
|
25
|
+
oldSelector: string;
|
|
26
|
+
newSelector: string;
|
|
27
|
+
}>): void;
|
|
28
|
+
subscribe(listener: HealListener): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a selector within a specific call-site context.
|
|
31
|
+
* Falls back to a context-free lookup ONLY when the fingerprint
|
|
32
|
+
* has no entry — prevents poisoning while still enabling same-file reuse.
|
|
33
|
+
*/
|
|
34
|
+
resolveSelector(selector: string, ctx?: SelectorContext): string;
|
|
35
|
+
private walkChain;
|
|
36
|
+
clearForTest(): void;
|
|
37
|
+
clearAll(): void;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=HealingRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HealingRegistry.d.ts","sourceRoot":"","sources":["../../src/engine/HealingRegistry.ts"],"names":[],"mappings":"AAIA,KAAK,YAAY,GAAG,CAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;AAEV,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAkB;IAEzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;IAErD;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;IAElE,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,eAAe;IASrC,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM;IAUhD,SAAS,CACP,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI;IAqBP,gBAAgB,CACd,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,GACzD,IAAI;IAQP,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,IAAI;IAK7C;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,eAAe,GAAG,MAAM;IAYhE,OAAO,CAAC,SAAS;IAWjB,YAAY,IAAI,IAAI;IAMpB,QAAQ,IAAI,IAAI;CAIjB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/engine/HealingRegistry.ts — full replacement
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.HealingRegistry = void 0;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
class HealingRegistry {
|
|
7
|
+
static instance;
|
|
8
|
+
listeners = new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Two-level store:
|
|
11
|
+
* fingerprint → (oldSelector → newSelector)
|
|
12
|
+
*
|
|
13
|
+
* A fingerprint is deterministic per call-site, so heals from one test
|
|
14
|
+
* can NEVER overwrite a different call-site's mapping, even if the raw
|
|
15
|
+
* selector string is identical (e.g. "internal:role=button").
|
|
16
|
+
*/
|
|
17
|
+
healMap = new Map();
|
|
18
|
+
constructor() { }
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!HealingRegistry.instance) {
|
|
21
|
+
HealingRegistry.instance = new HealingRegistry();
|
|
22
|
+
}
|
|
23
|
+
return HealingRegistry.instance;
|
|
24
|
+
}
|
|
25
|
+
// ── Fingerprint generation ───────────────────────────────────
|
|
26
|
+
static fingerprint(ctx) {
|
|
27
|
+
const chain = (ctx.parentChain ?? []).join("||");
|
|
28
|
+
return (0, crypto_1.createHash)("sha1")
|
|
29
|
+
.update(`${ctx.filePath}:${ctx.line}:${chain}`)
|
|
30
|
+
.digest("hex")
|
|
31
|
+
.slice(0, 16);
|
|
32
|
+
}
|
|
33
|
+
// ── Write side ───────────────────────────────────────────────
|
|
34
|
+
broadcast(ctx, oldSelector, newSelector) {
|
|
35
|
+
if (!oldSelector || !newSelector || oldSelector === newSelector)
|
|
36
|
+
return;
|
|
37
|
+
const fp = HealingRegistry.fingerprint(ctx);
|
|
38
|
+
if (!this.healMap.has(fp)) {
|
|
39
|
+
this.healMap.set(fp, new Map());
|
|
40
|
+
}
|
|
41
|
+
this.healMap.get(fp).set(oldSelector, newSelector);
|
|
42
|
+
console.log(`[HealingRegistry] 📡 [${fp}] "${oldSelector}" → "${newSelector}"`);
|
|
43
|
+
for (const listener of this.listeners) {
|
|
44
|
+
try {
|
|
45
|
+
listener(fp, oldSelector, newSelector);
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
broadcastCascade(ctx, pairs) {
|
|
51
|
+
for (const { oldSelector, newSelector } of pairs) {
|
|
52
|
+
this.broadcast(ctx, oldSelector, newSelector);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// ── Read side ────────────────────────────────────────────────
|
|
56
|
+
subscribe(listener) {
|
|
57
|
+
this.listeners.add(listener);
|
|
58
|
+
return () => this.listeners.delete(listener);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a selector within a specific call-site context.
|
|
62
|
+
* Falls back to a context-free lookup ONLY when the fingerprint
|
|
63
|
+
* has no entry — prevents poisoning while still enabling same-file reuse.
|
|
64
|
+
*/
|
|
65
|
+
resolveSelector(selector, ctx) {
|
|
66
|
+
if (ctx) {
|
|
67
|
+
const fp = HealingRegistry.fingerprint(ctx);
|
|
68
|
+
const fpMap = this.healMap.get(fp);
|
|
69
|
+
if (fpMap) {
|
|
70
|
+
const resolved = this.walkChain(selector, fpMap);
|
|
71
|
+
if (resolved !== selector)
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return selector; // No global fallback — intentional
|
|
76
|
+
}
|
|
77
|
+
walkChain(selector, store) {
|
|
78
|
+
let current = selector;
|
|
79
|
+
const visited = new Set();
|
|
80
|
+
while (store.has(current)) {
|
|
81
|
+
if (visited.has(current))
|
|
82
|
+
break;
|
|
83
|
+
visited.add(current);
|
|
84
|
+
current = store.get(current);
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
clearForTest() {
|
|
89
|
+
this.listeners.clear();
|
|
90
|
+
// Intentionally preserve healMap so intra-worker test N+1 can reuse heals
|
|
91
|
+
// from test N at the SAME call-site fingerprint (file + line match).
|
|
92
|
+
}
|
|
93
|
+
clearAll() {
|
|
94
|
+
this.listeners.clear();
|
|
95
|
+
this.healMap.clear();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.HealingRegistry = HealingRegistry;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../../src/engine/singleton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,eAAO,MAAM,YAAY,iBAAwB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Locator } from "@playwright/test";
|
|
2
|
+
import { FixwrightEngine } from "../engine/FixwrightEngine";
|
|
3
|
+
/**
|
|
4
|
+
* createHealingExpect — drop-in replacement for Playwright's expect().
|
|
5
|
+
*
|
|
6
|
+
* @param resolveProxy Optional callback that unwraps a Fixwright proxy
|
|
7
|
+
* to its current live Locator. Supplied by
|
|
8
|
+
* fixtures/index.ts which owns proxyToActiveLocator.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createHealingExpect(engine: FixwrightEngine, page: import("@playwright/test").Page, filePath: string, initialLine: number, // השורה מהפיקסצ'ר (לרוב תהיה לא מדויקת או 0)
|
|
11
|
+
resolveProxy?: (value: unknown) => Locator | null): (locatorOrValue: unknown) => any;
|
|
12
|
+
//# sourceMappingURL=expectProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expectProxy.d.ts","sourceRoot":"","sources":["../../src/fixtures/expectProxy.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,OAAO,EAER,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA2N5D;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,OAAO,kBAAkB,EAAE,IAAI,EACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,6CAA6C;AAClE,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,IAAI,GAChD,CAAC,cAAc,EAAE,OAAO,KAAK,GAAG,CA2DlC"}
|