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.
Files changed (144) hide show
  1. package/README.md +93 -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 +140 -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 +133 -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/refactor.d.ts +3 -0
  19. package/dist/cli/commands/refactor.d.ts.map +1 -0
  20. package/dist/cli/commands/refactor.js +30 -0
  21. package/dist/cli/commands/status.d.ts +3 -0
  22. package/dist/cli/commands/status.d.ts.map +1 -0
  23. package/dist/cli/commands/status.js +51 -0
  24. package/dist/cli/commands/sync.d.ts +3 -0
  25. package/dist/cli/commands/sync.d.ts.map +1 -0
  26. package/dist/cli/commands/sync.js +123 -0
  27. package/dist/cli/index.d.ts +2 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +42 -0
  30. package/dist/cli/ui/DnaTable.d.ts +3 -0
  31. package/dist/cli/ui/DnaTable.d.ts.map +1 -0
  32. package/dist/cli/ui/DnaTable.js +70 -0
  33. package/dist/cli/ui/LocatorPicker.d.ts +8 -0
  34. package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
  35. package/dist/cli/ui/LocatorPicker.js +33 -0
  36. package/dist/cli/ui/ProgressReporter.d.ts +11 -0
  37. package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
  38. package/dist/cli/ui/ProgressReporter.js +33 -0
  39. package/dist/cli/ui/RefactorWizard.d.ts +26 -0
  40. package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
  41. package/dist/cli/ui/RefactorWizard.js +269 -0
  42. package/dist/config/ConfigLoader.d.ts +15 -0
  43. package/dist/config/ConfigLoader.d.ts.map +1 -0
  44. package/dist/config/ConfigLoader.js +174 -0
  45. package/dist/config/SelaConfig.d.ts +67 -0
  46. package/dist/config/SelaConfig.d.ts.map +1 -0
  47. package/dist/config/SelaConfig.js +57 -0
  48. package/dist/config/defineConfig.d.ts +3 -0
  49. package/dist/config/defineConfig.d.ts.map +1 -0
  50. package/dist/config/defineConfig.js +9 -0
  51. package/dist/engine/FixwrightEngine.d.ts +24 -0
  52. package/dist/engine/FixwrightEngine.d.ts.map +1 -0
  53. package/dist/engine/FixwrightEngine.js +403 -0
  54. package/dist/engine/HealingRegistry.d.ts +40 -0
  55. package/dist/engine/HealingRegistry.d.ts.map +1 -0
  56. package/dist/engine/HealingRegistry.js +98 -0
  57. package/dist/engine/singleton.d.ts +3 -0
  58. package/dist/engine/singleton.d.ts.map +1 -0
  59. package/dist/engine/singleton.js +5 -0
  60. package/dist/fixtures/expectProxy.d.ts +12 -0
  61. package/dist/fixtures/expectProxy.d.ts.map +1 -0
  62. package/dist/fixtures/expectProxy.js +228 -0
  63. package/dist/fixtures/index.d.ts +6 -0
  64. package/dist/fixtures/index.d.ts.map +1 -0
  65. package/dist/fixtures/index.js +688 -0
  66. package/dist/fixtures/moduleExpect.d.ts +2 -0
  67. package/dist/fixtures/moduleExpect.d.ts.map +1 -0
  68. package/dist/fixtures/moduleExpect.js +46 -0
  69. package/dist/index.d.ts +7 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +28 -0
  72. package/dist/services/ASTSourceUpdater.d.ts +79 -0
  73. package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
  74. package/dist/services/ASTSourceUpdater.js +3177 -0
  75. package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
  76. package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
  77. package/dist/services/ArgumentTypeAnalyzer.js +92 -0
  78. package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
  79. package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
  80. package/dist/services/BlastRadiusAnalyzer.js +103 -0
  81. package/dist/services/ChainValidator.d.ts +76 -0
  82. package/dist/services/ChainValidator.d.ts.map +1 -0
  83. package/dist/services/ChainValidator.js +569 -0
  84. package/dist/services/CrossFileHealer.d.ts +6 -0
  85. package/dist/services/CrossFileHealer.d.ts.map +1 -0
  86. package/dist/services/CrossFileHealer.js +134 -0
  87. package/dist/services/DefinitionTracer.d.ts +41 -0
  88. package/dist/services/DefinitionTracer.d.ts.map +1 -0
  89. package/dist/services/DefinitionTracer.js +350 -0
  90. package/dist/services/DnaEditorService.d.ts +31 -0
  91. package/dist/services/DnaEditorService.d.ts.map +1 -0
  92. package/dist/services/DnaEditorService.js +198 -0
  93. package/dist/services/DnaIndexService.d.ts +24 -0
  94. package/dist/services/DnaIndexService.d.ts.map +1 -0
  95. package/dist/services/DnaIndexService.js +131 -0
  96. package/dist/services/HealingAdvisory.d.ts +22 -0
  97. package/dist/services/HealingAdvisory.d.ts.map +1 -0
  98. package/dist/services/HealingAdvisory.js +42 -0
  99. package/dist/services/HealthReportService.d.ts +10 -0
  100. package/dist/services/HealthReportService.d.ts.map +1 -0
  101. package/dist/services/HealthReportService.js +84 -0
  102. package/dist/services/InitializerUpdater.d.ts +16 -0
  103. package/dist/services/InitializerUpdater.d.ts.map +1 -0
  104. package/dist/services/InitializerUpdater.js +37 -0
  105. package/dist/services/IntentAuditor.d.ts +39 -0
  106. package/dist/services/IntentAuditor.d.ts.map +1 -0
  107. package/dist/services/IntentAuditor.js +302 -0
  108. package/dist/services/LLMService.d.ts +100 -0
  109. package/dist/services/LLMService.d.ts.map +1 -0
  110. package/dist/services/LLMService.js +439 -0
  111. package/dist/services/SafetyGuard.d.ts +65 -0
  112. package/dist/services/SafetyGuard.d.ts.map +1 -0
  113. package/dist/services/SafetyGuard.js +524 -0
  114. package/dist/services/SnapshotService.d.ts +11 -0
  115. package/dist/services/SnapshotService.d.ts.map +1 -0
  116. package/dist/services/SnapshotService.js +349 -0
  117. package/dist/services/SourceLinkService.d.ts +26 -0
  118. package/dist/services/SourceLinkService.d.ts.map +1 -0
  119. package/dist/services/SourceLinkService.js +156 -0
  120. package/dist/services/SourceUpdater.d.ts +45 -0
  121. package/dist/services/SourceUpdater.d.ts.map +1 -0
  122. package/dist/services/SourceUpdater.js +1021 -0
  123. package/dist/services/TemplateDiffService.d.ts +25 -0
  124. package/dist/services/TemplateDiffService.d.ts.map +1 -0
  125. package/dist/services/TemplateDiffService.js +331 -0
  126. package/dist/services/types.d.ts +59 -0
  127. package/dist/services/types.d.ts.map +1 -0
  128. package/dist/services/types.js +2 -0
  129. package/dist/storage/SnapshotManager.d.ts +5 -0
  130. package/dist/storage/SnapshotManager.d.ts.map +1 -0
  131. package/dist/storage/SnapshotManager.js +31 -0
  132. package/dist/types/index.d.ts +95 -0
  133. package/dist/types/index.d.ts.map +1 -0
  134. package/dist/types/index.js +7 -0
  135. package/dist/utils/DOMUtils.d.ts +33 -0
  136. package/dist/utils/DOMUtils.d.ts.map +1 -0
  137. package/dist/utils/DOMUtils.js +179 -0
  138. package/dist/utils/StackUtils.d.ts +11 -0
  139. package/dist/utils/StackUtils.d.ts.map +1 -0
  140. package/dist/utils/StackUtils.js +120 -0
  141. package/dist/vendor/enquirer.d.ts +33 -0
  142. package/dist/vendor/enquirer.d.ts.map +1 -0
  143. package/dist/vendor/enquirer.js +11 -0
  144. package/package.json +67 -0
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RefactorWizard = void 0;
7
+ const enquirer_js_1 = require("../../vendor/enquirer.js");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const LocatorPicker_js_1 = require("./LocatorPicker.js");
11
+ // ── Helpers ───────────────────────────────────────────────────────────────────
12
+ function formatValue(v) {
13
+ if (v === null || v === undefined)
14
+ return chalk_1.default.dim('null');
15
+ if (typeof v === 'object')
16
+ return JSON.stringify(v, null, 2);
17
+ return String(v);
18
+ }
19
+ function parseInput(field, raw) {
20
+ if (field === 'text')
21
+ return raw.trim() === '' ? null : raw;
22
+ if (field === 'attributes' || field === 'anchors') {
23
+ try {
24
+ return JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return raw;
28
+ }
29
+ }
30
+ return raw;
31
+ }
32
+ function getFieldValue(field, record, snap) {
33
+ switch (field) {
34
+ case 'selector': return record.selector;
35
+ case 'text': return record.text;
36
+ case 'tagName': return record.tagName;
37
+ case 'attributes': return snap.attributes;
38
+ case 'anchors': return snap.anchors;
39
+ }
40
+ }
41
+ function validateField(field, value) {
42
+ const errs = [];
43
+ switch (field) {
44
+ case 'selector':
45
+ if (typeof value !== 'string' || value.trim() === '')
46
+ errs.push('selector must not be empty');
47
+ if (value === 'undefined')
48
+ errs.push('selector must not be the string "undefined"');
49
+ if (typeof value === 'string' && /[\r\n]/.test(value))
50
+ errs.push('selector must not contain raw newlines');
51
+ break;
52
+ case 'text':
53
+ if (value !== null && typeof value !== 'string')
54
+ errs.push('text must be a string or null');
55
+ if (typeof value === 'string' && value.length >= 2000)
56
+ errs.push('text must be fewer than 2000 characters');
57
+ break;
58
+ case 'tagName':
59
+ if (typeof value !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value))
60
+ errs.push('tagName must match /^[a-z][a-z0-9-]*$/');
61
+ break;
62
+ case 'attributes':
63
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
64
+ errs.push('attributes must be a plain object (JSON)');
65
+ break;
66
+ case 'anchors':
67
+ if (typeof value !== 'object' || value === null)
68
+ errs.push('anchors must be an object (JSON)');
69
+ break;
70
+ }
71
+ return errs;
72
+ }
73
+ function showRecordCard(record) {
74
+ const w = 62;
75
+ const line = (label, value) => {
76
+ const content = ` ${chalk_1.default.dim(label.padEnd(14))} ${value}`;
77
+ process.stdout.write(content + '\n');
78
+ };
79
+ process.stdout.write('\n' + chalk_1.default.dim('─'.repeat(w)) + '\n');
80
+ line('Key', chalk_1.default.dim(record.key));
81
+ line('Selector', chalk_1.default.yellow(record.selector));
82
+ line('Tag', record.tagName);
83
+ line('Text', record.text ? chalk_1.default.cyan(record.text.slice(0, 40)) : chalk_1.default.dim('—'));
84
+ if (record.sourceFile) {
85
+ const rel = path_1.default.relative(process.cwd(), record.sourceFile);
86
+ line('Source', `${rel}:${record.sourceLine}`);
87
+ }
88
+ line('Last Healed', record.lastHealed ? new Date(record.lastHealed).toLocaleDateString() : chalk_1.default.dim('never'));
89
+ line('Status', record.isOrphaned ? chalk_1.default.red('orphaned') : record.lastHealed ? chalk_1.default.green('healed') : chalk_1.default.cyan('active'));
90
+ process.stdout.write(chalk_1.default.dim('─'.repeat(w)) + '\n\n');
91
+ }
92
+ function showDiff(field, oldVal, newVal) {
93
+ process.stdout.write('\n');
94
+ process.stdout.write(` ${chalk_1.default.bold('Preview — ' + field)}\n`);
95
+ process.stdout.write(` ${'─'.repeat(56)}\n`);
96
+ process.stdout.write(` ${chalk_1.default.red('−')} ${chalk_1.default.red(formatValue(oldVal))}\n`);
97
+ process.stdout.write(` ${chalk_1.default.green('+')} ${chalk_1.default.green(formatValue(newVal))}\n`);
98
+ process.stdout.write('\n');
99
+ }
100
+ // ── RefactorWizard ────────────────────────────────────────────────────────────
101
+ class RefactorWizard {
102
+ svc;
103
+ opts;
104
+ constructor(svc, opts = {}) {
105
+ this.svc = svc;
106
+ this.opts = opts;
107
+ }
108
+ async run(initialKey) {
109
+ // ── SELECT_DNA ────────────────────────────────────────────────
110
+ let record = initialKey
111
+ ? this.resolveKey(initialKey)
112
+ : await this.selectDna();
113
+ // Load full snapshot once (needed for attributes/anchors fields)
114
+ let snap = await this.svc.dnaEditor.read(record.key);
115
+ // Edit loop — user can edit multiple fields per session
116
+ for (;;) {
117
+ // ── SHOW_CURRENT ──────────────────────────────────────────
118
+ showRecordCard(record);
119
+ // ── SELECT_FIELD ──────────────────────────────────────────
120
+ const field = await this.selectField();
121
+ // ── LOCATOR_DISAMBIGUATE (selector field only) ────────────
122
+ let locatorIndex = 0;
123
+ if (field === 'selector' && record.sourceFile && record.sourceLine > 0) {
124
+ const locators = this.svc.sourceLink.enumerateLocatorsOnLine(record.sourceFile, record.sourceLine);
125
+ if (locators.length > 1) {
126
+ locatorIndex = await (0, LocatorPicker_js_1.pickLocator)(locators, record.selector);
127
+ }
128
+ }
129
+ // ── EDIT_VALUE + VALIDATE (loop on error) ─────────────────
130
+ const oldValue = getFieldValue(field, record, snap);
131
+ let newValue;
132
+ for (;;) {
133
+ newValue = await this.editValue(field, oldValue);
134
+ const errs = validateField(field, newValue);
135
+ if (errs.length === 0)
136
+ break;
137
+ process.stdout.write('\n');
138
+ for (const e of errs) {
139
+ process.stdout.write(` ${chalk_1.default.red('✗')} ${e}\n`);
140
+ }
141
+ process.stdout.write('\n');
142
+ }
143
+ // ── PREVIEW_DIFF ──────────────────────────────────────────
144
+ showDiff(field, oldValue, newValue);
145
+ // ── CONFIRM ───────────────────────────────────────────────
146
+ const apply = await this.confirm('Apply changes?');
147
+ if (!apply) {
148
+ const again = await this.confirm('Edit another field?');
149
+ if (!again)
150
+ break;
151
+ continue;
152
+ }
153
+ // ── APPLY_SOURCE? ─────────────────────────────────────────
154
+ let applySource = false;
155
+ if (!this.opts.noSource &&
156
+ field === 'selector' &&
157
+ record.sourceFile &&
158
+ record.sourceLine > 0) {
159
+ applySource = await this.confirm('Also update .spec.ts source file?');
160
+ }
161
+ // ── COMMIT ────────────────────────────────────────────────
162
+ const patch = {
163
+ key: record.key,
164
+ field,
165
+ oldValue,
166
+ newValue,
167
+ applySourceUpdate: applySource,
168
+ locatorIndex,
169
+ };
170
+ if (this.opts.dryRun) {
171
+ process.stdout.write(`\n ${chalk_1.default.yellow('[dry-run]')} Would write: ${JSON.stringify({ field, newValue })}\n\n`);
172
+ }
173
+ else {
174
+ const bakPath = await this.svc.dnaEditor.write(record.key, this.buildDnaPatch(field, newValue));
175
+ process.stdout.write(`\n ${chalk_1.default.green('✓')} DNA written. Backup: ${chalk_1.default.dim(path_1.default.basename(bakPath))}\n`);
176
+ if (applySource) {
177
+ const result = await this.svc.sourceLink.applySourceUpdate(record, patch);
178
+ if (result.success) {
179
+ process.stdout.write(` ${chalk_1.default.green('✓')} Source updated on line ${result.lineUpdated ?? record.sourceLine}\n`);
180
+ }
181
+ else {
182
+ process.stdout.write(` ${chalk_1.default.yellow('⚠')} Source update: ${result.reason}\n`);
183
+ }
184
+ }
185
+ // Refresh record from index after write
186
+ snap = await this.svc.dnaEditor.read(record.key);
187
+ const refreshed = this.svc.dnaIndex.getByKey(record.key);
188
+ if (refreshed)
189
+ record = refreshed;
190
+ }
191
+ // ── DONE / CONTINUE? ─────────────────────────────────────
192
+ const another = await this.confirm('Edit another field?');
193
+ if (!another)
194
+ break;
195
+ }
196
+ process.stdout.write('\n' + chalk_1.default.dim('Wizard complete.') + '\n\n');
197
+ }
198
+ // ── Private helpers ───────────────────────────────────────────────────────
199
+ resolveKey(key) {
200
+ const record = this.svc.dnaIndex.getByKey(key);
201
+ if (!record) {
202
+ const err = new Error(`DNA not found: ${key}`);
203
+ err.code = 'DNA_NOT_FOUND';
204
+ throw err;
205
+ }
206
+ return record;
207
+ }
208
+ async selectDna() {
209
+ const records = this.svc.dnaIndex.getIndex().records;
210
+ if (records.length === 0) {
211
+ const err = new Error('No DNA records found in snapshot directory');
212
+ err.code = 'SNAPSHOT_DIR_MISSING';
213
+ throw err;
214
+ }
215
+ const choices = records.map((r) => ({
216
+ name: r.key,
217
+ message: `${chalk_1.default.dim(r.key.slice(0, 8))} ${r.selector.padEnd(40).slice(0, 40)} ${chalk_1.default.dim(r.testTitle.slice(0, 30))}`,
218
+ }));
219
+ const prompt = new enquirer_js_1.AutoComplete({
220
+ name: 'dna',
221
+ message: 'Search DNA record (type to filter)',
222
+ limit: 10,
223
+ choices,
224
+ });
225
+ const key = (await prompt.run());
226
+ return this.resolveKey(key);
227
+ }
228
+ async selectField() {
229
+ const prompt = new enquirer_js_1.Select({
230
+ name: 'field',
231
+ message: 'Which field to edit?',
232
+ choices: [
233
+ { name: 'selector', message: `${chalk_1.default.yellow('selector')} CSS/Playwright selector` },
234
+ { name: 'text', message: `${chalk_1.default.cyan('text')} Visible element text` },
235
+ { name: 'tagName', message: `${chalk_1.default.dim('tagName')} HTML tag name` },
236
+ { name: 'attributes', message: `${chalk_1.default.dim('attributes')} Element attributes (JSON)` },
237
+ { name: 'anchors', message: `${chalk_1.default.dim('anchors')} Anchor context (JSON)` },
238
+ ],
239
+ });
240
+ return (await prompt.run());
241
+ }
242
+ async editValue(field, current) {
243
+ const initialStr = current === null || current === undefined
244
+ ? ''
245
+ : typeof current === 'object'
246
+ ? JSON.stringify(current, null, 2)
247
+ : String(current);
248
+ const hint = field === 'text'
249
+ ? ' (leave blank for null)'
250
+ : field === 'attributes' || field === 'anchors'
251
+ ? ' (JSON)'
252
+ : '';
253
+ const prompt = new enquirer_js_1.Input({
254
+ name: 'value',
255
+ message: `New ${field}${hint}`,
256
+ initial: initialStr,
257
+ });
258
+ const raw = (await prompt.run());
259
+ return parseInput(field, raw);
260
+ }
261
+ async confirm(message) {
262
+ const prompt = new enquirer_js_1.Confirm({ name: 'ok', message });
263
+ return (await prompt.run());
264
+ }
265
+ buildDnaPatch(field, newValue) {
266
+ return { [field]: newValue };
267
+ }
268
+ }
269
+ exports.RefactorWizard = RefactorWizard;
@@ -0,0 +1,15 @@
1
+ import { ResolvedConfig } from './SelaConfig';
2
+ export declare class ConfigLoader {
3
+ private static resolved;
4
+ private static branch;
5
+ /**
6
+ * Returns the fully resolved config. First call does all I/O; subsequent
7
+ * calls return the cached result at zero cost.
8
+ */
9
+ static getInstance(): ResolvedConfig;
10
+ /** Returns the detected Git branch name, or null if detection failed. */
11
+ static getBranch(): string | null;
12
+ /** Clears the singleton cache — useful for testing. */
13
+ static reset(): void;
14
+ }
15
+ //# sourceMappingURL=ConfigLoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigLoader.d.ts","sourceRoot":"","sources":["../../src/config/ConfigLoader.ts"],"names":[],"mappings":"AAQA,OAAO,EAAc,cAAc,EAAiB,MAAM,cAAc,CAAC;AAoGzE,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA+B;IACtD,OAAO,CAAC,MAAM,CAAC,MAAM,CAAuB;IAE5C;;;OAGG;IACH,MAAM,CAAC,WAAW,IAAI,cAAc;IAiCpC,yEAAyE;IACzE,MAAM,CAAC,SAAS,IAAI,MAAM,GAAG,IAAI;IAMjC,uDAAuD;IACvD,MAAM,CAAC,KAAK,IAAI,IAAI;CAIrB"}
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ // src/config/ConfigLoader.ts
3
+ // Locates sela.config.ts|js, detects the current Git branch, deep-merges
4
+ // the base config with any branch override, and caches the resolved result
5
+ // for the lifetime of the process (singleton pattern).
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.ConfigLoader = void 0;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const child_process_1 = require("child_process");
44
+ const SelaConfig_1 = require("./SelaConfig");
45
+ // ─────────────────────────────────────────────────────────────────
46
+ // FILE DISCOVERY
47
+ // ─────────────────────────────────────────────────────────────────
48
+ function findConfigFile(startDir) {
49
+ let dir = startDir;
50
+ while (true) {
51
+ for (const name of ['sela.config.ts', 'sela.config.js']) {
52
+ const candidate = path.join(dir, name);
53
+ if (fs.existsSync(candidate))
54
+ return candidate;
55
+ }
56
+ const parent = path.dirname(dir);
57
+ if (parent === dir)
58
+ return null; // reached filesystem root
59
+ dir = parent;
60
+ }
61
+ }
62
+ // ─────────────────────────────────────────────────────────────────
63
+ // GIT BRANCH DETECTION
64
+ // ─────────────────────────────────────────────────────────────────
65
+ function getCurrentBranch() {
66
+ try {
67
+ // Fail fast if not a git repo or git is unavailable
68
+ const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
69
+ encoding: 'utf-8',
70
+ stdio: ['pipe', 'pipe', 'pipe'],
71
+ }).trim();
72
+ if (branch === 'HEAD') {
73
+ // Detached HEAD (common in CI with explicit SHA checkout)
74
+ console.warn('[Sela] ⚠️ Detached HEAD detected — branch overrides skipped');
75
+ return null;
76
+ }
77
+ return branch;
78
+ }
79
+ catch {
80
+ console.warn('[Sela] ⚠️ Git branch detection failed — branch overrides skipped');
81
+ return null;
82
+ }
83
+ }
84
+ // ─────────────────────────────────────────────────────────────────
85
+ // CONFIG FILE LOADING
86
+ // ─────────────────────────────────────────────────────────────────
87
+ function loadRawConfig(configPath) {
88
+ try {
89
+ // Works in Playwright test context (TypeScript loader registered by @playwright/test)
90
+ // and in Jest context (ts-jest). Falls back gracefully if neither is active.
91
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
92
+ const mod = require(configPath);
93
+ const config = mod?.default ?? mod ?? {};
94
+ if (typeof config !== 'object' || config === null) {
95
+ console.warn(`[Sela] ⚠️ sela.config did not export an object — using defaults`);
96
+ return {};
97
+ }
98
+ return config;
99
+ }
100
+ catch (err) {
101
+ console.warn(`[Sela] ⚠️ Failed to load ${path.basename(configPath)}: ${err.message}`);
102
+ return {};
103
+ }
104
+ }
105
+ // ─────────────────────────────────────────────────────────────────
106
+ // DEEP MERGE (plain objects only — arrays are replaced, not merged)
107
+ // ─────────────────────────────────────────────────────────────────
108
+ function deepMerge(base, override) {
109
+ const result = { ...base };
110
+ for (const key of Object.keys(override)) {
111
+ const baseVal = result[key];
112
+ const overrideVal = override[key];
113
+ if (overrideVal !== null &&
114
+ typeof overrideVal === 'object' &&
115
+ !Array.isArray(overrideVal) &&
116
+ baseVal !== null &&
117
+ typeof baseVal === 'object' &&
118
+ !Array.isArray(baseVal)) {
119
+ result[key] = deepMerge(baseVal, overrideVal);
120
+ }
121
+ else if (overrideVal !== undefined) {
122
+ result[key] = overrideVal;
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ // ─────────────────────────────────────────────────────────────────
128
+ // SINGLETON LOADER
129
+ // ─────────────────────────────────────────────────────────────────
130
+ class ConfigLoader {
131
+ static resolved = null;
132
+ static branch = null;
133
+ /**
134
+ * Returns the fully resolved config. First call does all I/O; subsequent
135
+ * calls return the cached result at zero cost.
136
+ */
137
+ static getInstance() {
138
+ if (ConfigLoader.resolved)
139
+ return ConfigLoader.resolved;
140
+ const configPath = findConfigFile(process.cwd());
141
+ if (!configPath) {
142
+ // Zero-breaking-change guarantee: no config → pure defaults
143
+ ConfigLoader.resolved = (0, SelaConfig_1.resolveConfig)({});
144
+ return ConfigLoader.resolved;
145
+ }
146
+ console.log(`[Sela] 📄 Loaded config from: ${path.relative(process.cwd(), configPath)}`);
147
+ let rawConfig = loadRawConfig(configPath);
148
+ // Apply branch overrides using exact-string matching
149
+ const detectedBranch = getCurrentBranch();
150
+ ConfigLoader.branch = detectedBranch;
151
+ if (rawConfig.branchOverrides && detectedBranch) {
152
+ const override = rawConfig.branchOverrides[detectedBranch];
153
+ if (override) {
154
+ console.log(`[Sela] 🌿 Applying branch override for: ${detectedBranch}`);
155
+ rawConfig = deepMerge(rawConfig, override);
156
+ }
157
+ }
158
+ ConfigLoader.resolved = (0, SelaConfig_1.resolveConfig)(rawConfig);
159
+ return ConfigLoader.resolved;
160
+ }
161
+ /** Returns the detected Git branch name, or null if detection failed. */
162
+ static getBranch() {
163
+ // Ensure getInstance() has been called at least once
164
+ if (!ConfigLoader.resolved)
165
+ ConfigLoader.getInstance();
166
+ return ConfigLoader.branch;
167
+ }
168
+ /** Clears the singleton cache — useful for testing. */
169
+ static reset() {
170
+ ConfigLoader.resolved = null;
171
+ ConfigLoader.branch = null;
172
+ }
173
+ }
174
+ exports.ConfigLoader = ConfigLoader;
@@ -0,0 +1,67 @@
1
+ export interface SelaConfig {
2
+ /** Shorthand preset that expands to concrete threshold values. */
3
+ healingPolicy?: 'conservative' | 'balanced' | 'aggressive';
4
+ /** Fine-grained threshold overrides. Explicit values always win over preset expansion. */
5
+ thresholds?: {
6
+ /** Minimum AI confidence to attempt a heal. Default: 85. Maps to CONFIDENCE_HARD_STOP. */
7
+ minHealerConfidence?: number;
8
+ /** How strictly the Intent Auditor treats SUSPICIOUS verdicts. Default: 'balanced'. */
9
+ auditorStrictness?: 'strict' | 'balanced' | 'loose';
10
+ /** Similarity floor for UNKNOWN-role assertion text changes. Default: 0.70. */
11
+ assertionSimilarityFloor?: number;
12
+ };
13
+ agenticDiscovery?: {
14
+ enabled?: boolean;
15
+ maxSteps?: number;
16
+ forbiddenSelectors?: string[];
17
+ };
18
+ /** How healed selectors are persisted. Default: 'inPlace'. */
19
+ updateStrategy?: 'inPlace' | 'commentOnly' | 'branch';
20
+ /** Automatically `git commit` after each successful in-place heal. Default: false. */
21
+ autoCommit?: boolean;
22
+ prAutomation?: {
23
+ enabled?: boolean;
24
+ titleTemplate?: string;
25
+ labels?: string[];
26
+ };
27
+ /** Directory for DNA snapshots. Default: 'fixwright-snapshots'. */
28
+ dnaStoragePath?: string;
29
+ /**
30
+ * Per-branch config overrides. Keys are exact branch names.
31
+ * Merge order: expandPreset(base) → base.thresholds → expandPreset(override) → override.thresholds
32
+ */
33
+ branchOverrides?: Record<string, Partial<SelaConfig>>;
34
+ }
35
+ export interface ResolvedThresholds {
36
+ /** Minimum AI confidence (0–100). Heals below this are always BLOCKED. */
37
+ confidenceHardStop: number;
38
+ /** Confidence below which a heal is tagged REQUIRES_REVIEW instead of SAFE. */
39
+ confidenceReviewThreshold: number;
40
+ /** Similarity floor for UNKNOWN→UNKNOWN assertion text changes. */
41
+ assertionSimilarityFloor: number;
42
+ /** Minimum auditor confidence to treat a verdict as CONSISTENT. */
43
+ auditorConfidenceMin: number;
44
+ /** When true, SUSPICIOUS in assertion mode yields REQUIRES_REVIEW instead of BLOCKED. */
45
+ allowSuspicious: boolean;
46
+ /** The resolved strictness name — used for policy-aware log messages. */
47
+ auditorStrictness: 'strict' | 'balanced' | 'loose';
48
+ }
49
+ export interface ResolvedConfig {
50
+ thresholds: ResolvedThresholds;
51
+ updateStrategy: 'inPlace' | 'commentOnly' | 'branch';
52
+ autoCommit: boolean;
53
+ dnaStoragePath: string;
54
+ prAutomation: {
55
+ enabled: boolean;
56
+ titleTemplate: string;
57
+ labels: string[];
58
+ };
59
+ agenticDiscovery: {
60
+ enabled: boolean;
61
+ maxSteps: number;
62
+ forbiddenSelectors: string[];
63
+ };
64
+ }
65
+ export declare function resolveThresholds(config: SelaConfig): ResolvedThresholds;
66
+ export declare function resolveConfig(config: SelaConfig): ResolvedConfig;
67
+ //# sourceMappingURL=SelaConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelaConfig.d.ts","sourceRoot":"","sources":["../../src/config/SelaConfig.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,aAAa,CAAC,EAAE,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC;IAE3D,0FAA0F;IAC1F,UAAU,CAAC,EAAE;QACX,0FAA0F;QAC1F,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,uFAAuF;QACvF,iBAAiB,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;QACpD,+EAA+E;QAC/E,wBAAwB,CAAC,EAAE,MAAM,CAAC;KACnC,CAAC;IAEF,gBAAgB,CAAC,EAAE;QACjB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC/B,CAAC;IAEF,8DAA8D;IAC9D,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;IAEtD,sFAAsF;IACtF,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,YAAY,CAAC,EAAE;QACb,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;IAEF,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;CACvD;AAMD,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,yBAAyB,EAAE,MAAM,CAAC;IAClC,mEAAmE;IACnE,wBAAwB,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,yEAAyE;IACzE,iBAAiB,EAAE,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,kBAAkB,CAAC;IAC/B,cAAc,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;IACrD,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,MAAM,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,gBAAgB,EAAE;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,kBAAkB,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH;AAgCD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,kBAAkB,CAkBxE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,cAAc,CAiBhE"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ // src/config/SelaConfig.ts
3
+ // Single source of truth for all Sela configuration types and preset expansion logic.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.resolveThresholds = resolveThresholds;
6
+ exports.resolveConfig = resolveConfig;
7
+ const PRESET_MAP = {
8
+ conservative: { minHealerConfidence: 92, auditorStrictness: 'strict', assertionSimilarityFloor: 0.85 },
9
+ balanced: { minHealerConfidence: 85, auditorStrictness: 'balanced', assertionSimilarityFloor: 0.70 },
10
+ aggressive: { minHealerConfidence: 75, auditorStrictness: 'loose', assertionSimilarityFloor: 0.55 },
11
+ };
12
+ const STRICTNESS_CONFIDENCE = {
13
+ strict: 90,
14
+ balanced: 75,
15
+ loose: 60,
16
+ };
17
+ function expandPreset(policy) {
18
+ return policy ? { ...PRESET_MAP[policy] } : {};
19
+ }
20
+ // ═══════════════════════════════════════════════════════════════════
21
+ // RESOLUTION HELPERS
22
+ // ═══════════════════════════════════════════════════════════════════
23
+ function resolveThresholds(config) {
24
+ const fromPreset = expandPreset(config.healingPolicy);
25
+ // Explicit thresholds always win over preset expansion
26
+ const merged = { ...fromPreset, ...config.thresholds };
27
+ const minHealerConfidence = merged.minHealerConfidence ?? 85;
28
+ const auditorStrictness = merged.auditorStrictness ?? 'balanced';
29
+ const assertionSimilarityFloor = merged.assertionSimilarityFloor ?? 0.7;
30
+ return {
31
+ confidenceHardStop: minHealerConfidence,
32
+ confidenceReviewThreshold: 95,
33
+ assertionSimilarityFloor,
34
+ auditorConfidenceMin: STRICTNESS_CONFIDENCE[auditorStrictness],
35
+ // allowSuspicious is derived — never independently settable to avoid footguns
36
+ allowSuspicious: auditorStrictness === 'loose',
37
+ auditorStrictness,
38
+ };
39
+ }
40
+ function resolveConfig(config) {
41
+ return {
42
+ thresholds: resolveThresholds(config),
43
+ updateStrategy: config.updateStrategy ?? 'inPlace',
44
+ autoCommit: config.autoCommit ?? false,
45
+ dnaStoragePath: config.dnaStoragePath ?? 'fixwright-snapshots',
46
+ prAutomation: {
47
+ enabled: config.prAutomation?.enabled ?? false,
48
+ titleTemplate: config.prAutomation?.titleTemplate ?? 'fix(sela): heal selector in ${filename}',
49
+ labels: config.prAutomation?.labels ?? [],
50
+ },
51
+ agenticDiscovery: {
52
+ enabled: config.agenticDiscovery?.enabled ?? false,
53
+ maxSteps: config.agenticDiscovery?.maxSteps ?? 10,
54
+ forbiddenSelectors: config.agenticDiscovery?.forbiddenSelectors ?? [],
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,3 @@
1
+ import { SelaConfig } from './SelaConfig';
2
+ export declare function defineConfig(config: SelaConfig): SelaConfig;
3
+ //# sourceMappingURL=defineConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defineConfig.d.ts","sourceRoot":"","sources":["../../src/config/defineConfig.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAE3D"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ // src/config/defineConfig.ts
3
+ // Identity helper that provides TypeScript inference for sela.config.ts files
4
+ // without requiring users to manually annotate their export.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.defineConfig = defineConfig;
7
+ function defineConfig(config) {
8
+ return config;
9
+ }
@@ -0,0 +1,24 @@
1
+ import { Page } from "@playwright/test";
2
+ import { ElementSnapshotV2 } from "../types";
3
+ import { HealMode } from "../services/SafetyGuard";
4
+ export declare class FixwrightEngine {
5
+ private llmService;
6
+ private snapshotService;
7
+ private safetyGuard;
8
+ private config;
9
+ private dnaBuffer;
10
+ private healMetaBuffer;
11
+ private testTitleBuffer;
12
+ constructor();
13
+ healArgument(page: Page, selector: string, action: string, oldArgument: string, stableId: string, filePath: string, line: number): Promise<string | null>;
14
+ heal(page: Page, fullSelector: string, elementSelectorOnly: string, stableId: string, filePath: string, line: number, healMode?: HealMode): Promise<string>;
15
+ preloadTestTitle(stableId: string, testTitle: string): void;
16
+ saveSnapshot(stableId: string, data: ElementSnapshotV2): Promise<void>;
17
+ commitUpdates(): Promise<void>;
18
+ private fetchLiveText;
19
+ private _rebuildSelectorWithFrames;
20
+ private getAllFiles;
21
+ captureSuccessfulElement(page: Page, selector: string, stableId: string): Promise<void>;
22
+ }
23
+ export {};
24
+ //# sourceMappingURL=FixwrightEngine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FixwrightEngine.d.ts","sourceRoot":"","sources":["../../src/engine/FixwrightEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAMxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAe,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAkBhE,qBAAa,eAAe;IAC1B,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;;IAanD,YAAY,CAChB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiDnB,IAAI,CACR,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,MAAM,EACpB,mBAAmB,EAAE,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAmB,GAC5B,OAAO,CAAC,MAAM,CAAC;IAuSlB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAIrD,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAuBtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IAiClC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}