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,198 @@
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.DnaEditorService = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function jsonPath(dir, key) {
10
+ return path_1.default.join(dir, `${key}.json`);
11
+ }
12
+ function tmpPath(dir, key) {
13
+ return path_1.default.join(dir, `${key}.json.tmp`);
14
+ }
15
+ function bakPath(dir, key) {
16
+ return path_1.default.join(dir, `${key}.json.bak`);
17
+ }
18
+ // Derive a best-effort selector from engine snapshot fields when none is stored.
19
+ function deriveSelector(snap) {
20
+ const id = snap['id'];
21
+ const classes = snap['classes'];
22
+ const tagName = snap['tagName'];
23
+ if (typeof id === 'string' && id)
24
+ return `#${id}`;
25
+ if (Array.isArray(classes) && classes.length > 0)
26
+ return `.${classes.join('.')}`;
27
+ return typeof tagName === 'string' ? tagName : 'unknown';
28
+ }
29
+ function dnaError(message, code) {
30
+ const err = new Error(message);
31
+ err.code = code;
32
+ return err;
33
+ }
34
+ class DnaEditorService {
35
+ snapshotDir;
36
+ constructor(snapshotDir) {
37
+ this.snapshotDir = snapshotDir;
38
+ }
39
+ async read(key) {
40
+ const target = jsonPath(this.snapshotDir, key);
41
+ let raw;
42
+ try {
43
+ raw = await promises_1.default.readFile(target, 'utf-8');
44
+ }
45
+ catch {
46
+ throw dnaError(`DNA not found: ${key}`, 'DNA_NOT_FOUND');
47
+ }
48
+ const parsed = JSON.parse(raw);
49
+ // Promote to extended format if selector not stored
50
+ if (!parsed['selector']) {
51
+ parsed['selector'] = deriveSelector(parsed);
52
+ }
53
+ return parsed;
54
+ }
55
+ async write(key, patch) {
56
+ const target = jsonPath(this.snapshotDir, key);
57
+ const bak = bakPath(this.snapshotDir, key);
58
+ const tmp = tmpPath(this.snapshotDir, key);
59
+ // 1. Read existing snapshot
60
+ const original = await this.read(key);
61
+ // 2. Shallow-merge patch (arrays and objects replaced, not deep-merged)
62
+ const merged = { ...original, ...patch };
63
+ // Any CLI write = human has reviewed the DNA → clear auto-heal status flag.
64
+ delete merged['status'];
65
+ // 3. Derive selector only when absent — never override an explicit (possibly invalid) value
66
+ if (merged['selector'] == null) {
67
+ merged['selector'] = deriveSelector(merged);
68
+ }
69
+ // 4. Validate before touching disk
70
+ const validation = this.validate(merged);
71
+ if (!validation.valid) {
72
+ throw dnaError(`Validation failed for ${key}: ${validation.errors.join('; ')}`, 'VALIDATION_FAILED');
73
+ }
74
+ // 5. Backup current file (overwrite previous bak)
75
+ await promises_1.default.writeFile(bak, JSON.stringify(original, null, 2), 'utf-8');
76
+ // 6. Write to tmp
77
+ await promises_1.default.writeFile(tmp, JSON.stringify(merged, null, 2), 'utf-8');
78
+ // 7. Atomic rename tmp → json
79
+ await promises_1.default.rename(tmp, target);
80
+ return bak;
81
+ }
82
+ async rollback(key) {
83
+ const target = jsonPath(this.snapshotDir, key);
84
+ const bak = bakPath(this.snapshotDir, key);
85
+ try {
86
+ await promises_1.default.access(bak);
87
+ }
88
+ catch {
89
+ throw dnaError(`No backup found for: ${key}`, 'DNA_NOT_FOUND');
90
+ }
91
+ await promises_1.default.rename(bak, target);
92
+ }
93
+ validate(candidate) {
94
+ const errors = [];
95
+ if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate)) {
96
+ return { valid: false, errors: ['candidate must be a plain object'] };
97
+ }
98
+ const snap = candidate;
99
+ // ── Required top-level fields ─────────────────────────────────────────────
100
+ for (const field of ['tagName', 'selector', 'schemaVersion', 'timestamp']) {
101
+ if (!(field in snap) || snap[field] === undefined) {
102
+ errors.push(`Missing required field: ${field}`);
103
+ }
104
+ }
105
+ if (snap['schemaVersion'] !== 2) {
106
+ errors.push(`schemaVersion must be 2, got ${String(snap['schemaVersion'])}`);
107
+ }
108
+ // ── selector ─────────────────────────────────────────────────────────────
109
+ const sel = snap['selector'];
110
+ if (typeof sel === 'string') {
111
+ if (sel.trim() === '')
112
+ errors.push('selector must not be empty');
113
+ if (sel === 'undefined')
114
+ errors.push('selector must not be the string "undefined"');
115
+ if (/[\r\n]/.test(sel))
116
+ errors.push('selector must not contain raw newlines');
117
+ }
118
+ else if ('selector' in snap) {
119
+ errors.push('selector must be a string');
120
+ }
121
+ // ── tagName ───────────────────────────────────────────────────────────────
122
+ const tag = snap['tagName'];
123
+ if (typeof tag === 'string') {
124
+ if (!/^[a-z][a-z0-9-]*$/.test(tag)) {
125
+ errors.push('tagName must match /^[a-z][a-z0-9-]*$/');
126
+ }
127
+ }
128
+ else if ('tagName' in snap) {
129
+ errors.push('tagName must be a string');
130
+ }
131
+ // ── text ─────────────────────────────────────────────────────────────────
132
+ const text = snap['text'];
133
+ if (text !== null && text !== undefined) {
134
+ if (typeof text !== 'string') {
135
+ errors.push('text must be a string or null');
136
+ }
137
+ else if (text.length >= 2000) {
138
+ errors.push('text must be fewer than 2000 characters');
139
+ }
140
+ }
141
+ // ── attributes ───────────────────────────────────────────────────────────
142
+ const attrs = snap['attributes'];
143
+ if (attrs !== undefined) {
144
+ if (typeof attrs !== 'object' || attrs === null || Array.isArray(attrs)) {
145
+ errors.push('attributes must be a plain object');
146
+ }
147
+ else {
148
+ for (const [k, v] of Object.entries(attrs)) {
149
+ if (!k)
150
+ errors.push('attributes keys must not be empty');
151
+ if (typeof v !== 'string')
152
+ errors.push(`attributes["${k}"] must be a string`);
153
+ }
154
+ }
155
+ }
156
+ // ── visibility (all 4 keys required) ─────────────────────────────────────
157
+ const vis = snap['visibility'];
158
+ if (typeof vis === 'object' && vis !== null && !Array.isArray(vis)) {
159
+ const visObj = vis;
160
+ for (const key of ['isGhost', 'ghostReason', 'isOccluded', 'opacity']) {
161
+ if (!(key in visObj))
162
+ errors.push(`visibility.${key} is required`);
163
+ }
164
+ }
165
+ else {
166
+ errors.push('visibility block is required and must be an object');
167
+ }
168
+ // ── anchors (all 3 keys required) ────────────────────────────────────────
169
+ const anch = snap['anchors'];
170
+ if (typeof anch === 'object' && anch !== null && !Array.isArray(anch)) {
171
+ const anchObj = anch;
172
+ for (const key of ['closestLabel', 'rowAnchor', 'neighborTexts']) {
173
+ if (!(key in anchObj))
174
+ errors.push(`anchors.${key} is required`);
175
+ }
176
+ const cl = anchObj['closestLabel'];
177
+ if (cl !== null && typeof cl !== 'string') {
178
+ errors.push('anchors.closestLabel must be a string or null');
179
+ }
180
+ const nt = anchObj['neighborTexts'];
181
+ if (!Array.isArray(nt)) {
182
+ errors.push('anchors.neighborTexts must be an array');
183
+ }
184
+ else {
185
+ if (nt.length > 10)
186
+ errors.push('anchors.neighborTexts must have at most 10 elements');
187
+ if (!nt.every((x) => typeof x === 'string')) {
188
+ errors.push('anchors.neighborTexts elements must all be strings');
189
+ }
190
+ }
191
+ }
192
+ else {
193
+ errors.push('anchors block is required and must be an object');
194
+ }
195
+ return { valid: errors.length === 0, errors };
196
+ }
197
+ }
198
+ exports.DnaEditorService = DnaEditorService;
@@ -0,0 +1,24 @@
1
+ import type { DnaRecord, DnaIndex } from './types.js';
2
+ export interface IDnaIndexService {
3
+ buildIndex(snapshotDir: string): Promise<DnaIndex>;
4
+ findBySelector(pattern: string): DnaRecord[];
5
+ findByText(pattern: string): DnaRecord[];
6
+ findBySourceFile(filePath: string): DnaRecord[];
7
+ findByTestTitle(pattern: string): DnaRecord[];
8
+ getByKey(key: string): DnaRecord | null;
9
+ getIndex(): DnaIndex;
10
+ refresh(): void;
11
+ }
12
+ export declare class DnaIndexService implements IDnaIndexService {
13
+ private cachedIndex;
14
+ buildIndex(snapshotDir: string): Promise<DnaIndex>;
15
+ findBySelector(pattern: string): DnaRecord[];
16
+ findByText(pattern: string): DnaRecord[];
17
+ findBySourceFile(filePath: string): DnaRecord[];
18
+ findByTestTitle(pattern: string): DnaRecord[];
19
+ getByKey(key: string): DnaRecord | null;
20
+ getIndex(): DnaIndex;
21
+ refresh(): void;
22
+ private requireIndex;
23
+ }
24
+ //# sourceMappingURL=DnaIndexService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DnaIndexService.d.ts","sourceRoot":"","sources":["../../src/services/DnaIndexService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAatD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IAC7C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IACzC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IAChD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,QAAQ,IAAI,QAAQ,CAAC;IACrB,OAAO,IAAI,IAAI,CAAC;CACjB;AAkED,qBAAa,eAAgB,YAAW,gBAAgB;IACtD,OAAO,CAAC,WAAW,CAAyB;IAEtC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkCxD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAK5C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAKxC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE;IAO/C,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAK7C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIvC,QAAQ,IAAI,QAAQ;IAIpB,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,YAAY;CAQrB"}
@@ -0,0 +1,131 @@
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.DnaIndexService = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const fs_1 = require("fs");
9
+ const path_1 = __importDefault(require("path"));
10
+ // Supports /regex/flags syntax or plain substring (case-insensitive).
11
+ function toRegExp(pattern) {
12
+ const m = pattern.match(/^\/(.+)\/([gimsuy]*)$/);
13
+ if (m)
14
+ return new RegExp(m[1], m[2]);
15
+ return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
16
+ }
17
+ function deriveSelector(snap) {
18
+ if (snap.selector)
19
+ return snap.selector;
20
+ if (snap.id)
21
+ return `#${snap.id}`;
22
+ if (snap.classes.length > 0)
23
+ return `.${snap.classes.join('.')}`;
24
+ return snap.tagName;
25
+ }
26
+ function isWithin7Days(iso) {
27
+ return new Date(iso).getTime() >= Date.now() - 7 * 24 * 60 * 60 * 1000;
28
+ }
29
+ function toRecord(filePath, raw) {
30
+ const key = path_1.default.basename(filePath, '.json');
31
+ const sourceFile = raw.sourceFile ?? '';
32
+ return {
33
+ key,
34
+ filePath,
35
+ testTitle: raw.testTitle ?? '',
36
+ sourceFile,
37
+ sourceLine: raw.sourceLine ?? 0,
38
+ selector: deriveSelector(raw),
39
+ tagName: raw.tagName,
40
+ text: raw.text ?? null,
41
+ lastHealed: raw.lastHealed ?? null,
42
+ schemaVersion: raw.schemaVersion ?? 1,
43
+ // Only flag orphaned when sourceFile is known — engine snapshots have no sourceFile
44
+ isOrphaned: sourceFile !== '' && !(0, fs_1.existsSync)(sourceFile),
45
+ status: raw.status,
46
+ };
47
+ }
48
+ async function collectJsonFiles(dir) {
49
+ const results = [];
50
+ let entries;
51
+ try {
52
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ return results;
56
+ }
57
+ await Promise.all(entries.map(async (entry) => {
58
+ const full = path_1.default.join(dir, entry.name);
59
+ if (entry.isDirectory()) {
60
+ results.push(...(await collectJsonFiles(full)));
61
+ }
62
+ else if (entry.isFile() &&
63
+ entry.name.endsWith('.json') &&
64
+ !entry.name.endsWith('.bak') &&
65
+ !entry.name.endsWith('.tmp')) {
66
+ results.push(full);
67
+ }
68
+ }));
69
+ return results;
70
+ }
71
+ class DnaIndexService {
72
+ cachedIndex = null;
73
+ async buildIndex(snapshotDir) {
74
+ const files = await collectJsonFiles(snapshotDir);
75
+ const settled = await Promise.allSettled(files.map(async (filePath) => {
76
+ const content = await promises_1.default.readFile(filePath, 'utf-8');
77
+ return toRecord(filePath, JSON.parse(content));
78
+ }));
79
+ const records = settled
80
+ .filter((r) => r.status === 'fulfilled')
81
+ .map((r) => r.value);
82
+ const healedAllTime = records.filter((r) => r.lastHealed !== null).length;
83
+ const healedLast7Days = records.filter((r) => r.lastHealed !== null && isWithin7Days(r.lastHealed)).length;
84
+ this.cachedIndex = {
85
+ generatedAt: new Date().toISOString(),
86
+ snapshotDir,
87
+ records,
88
+ stats: {
89
+ total: records.length,
90
+ orphaned: records.filter((r) => r.isOrphaned).length,
91
+ healedLast7Days,
92
+ healedAllTime,
93
+ },
94
+ };
95
+ return this.cachedIndex;
96
+ }
97
+ findBySelector(pattern) {
98
+ const re = toRegExp(pattern);
99
+ return this.requireIndex().records.filter((r) => re.test(r.selector));
100
+ }
101
+ findByText(pattern) {
102
+ const re = toRegExp(pattern);
103
+ return this.requireIndex().records.filter((r) => r.text !== null && re.test(r.text));
104
+ }
105
+ findBySourceFile(filePath) {
106
+ const needle = path_1.default.normalize(filePath).toLowerCase();
107
+ return this.requireIndex().records.filter((r) => path_1.default.normalize(r.sourceFile).toLowerCase().includes(needle));
108
+ }
109
+ findByTestTitle(pattern) {
110
+ const re = toRegExp(pattern);
111
+ return this.requireIndex().records.filter((r) => re.test(r.testTitle));
112
+ }
113
+ getByKey(key) {
114
+ return this.requireIndex().records.find((r) => r.key === key) ?? null;
115
+ }
116
+ getIndex() {
117
+ return this.requireIndex();
118
+ }
119
+ refresh() {
120
+ this.cachedIndex = null;
121
+ }
122
+ requireIndex() {
123
+ if (!this.cachedIndex) {
124
+ const err = new Error('Index not built — call buildIndex() first');
125
+ err.code = 'SNAPSHOT_DIR_MISSING';
126
+ throw err;
127
+ }
128
+ return this.cachedIndex;
129
+ }
130
+ }
131
+ exports.DnaIndexService = DnaIndexService;
@@ -0,0 +1,22 @@
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
+ export interface Advisory {
10
+ code: AdvisoryCode;
11
+ message: string;
12
+ affectedFile: string;
13
+ affectedLine?: number;
14
+ suggestedAction: string;
15
+ }
16
+ export declare class AdvisoryBuffer {
17
+ private items;
18
+ push(advisory: Advisory): void;
19
+ flush(): void;
20
+ get length(): number;
21
+ }
22
+ //# sourceMappingURL=HealingAdvisory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealingAdvisory.d.ts","sourceRoot":"","sources":["../../src/services/HealingAdvisory.ts"],"names":[],"mappings":"AAQA,oBAAY,YAAY;IACtB,oBAAoB,yBAAyB;IAC7C,oBAAoB,yBAAyB;IAC7C,mBAAmB,wBAAwB;IAC3C,8BAA8B,mCAAmC;IACjE,gCAAgC,qCAAqC;IACrE,wBAAwB,6BAA6B;CACtD;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,42 @@
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
+ var AdvisoryCode;
12
+ (function (AdvisoryCode) {
13
+ AdvisoryCode["BLAST_RADIUS_BLOCKED"] = "BLAST_RADIUS_BLOCKED";
14
+ AdvisoryCode["BLAST_RADIUS_WARNING"] = "BLAST_RADIUS_WARNING";
15
+ AdvisoryCode["CROSS_FILE_MUTATION"] = "CROSS_FILE_MUTATION";
16
+ AdvisoryCode["MANUAL_REVIEW_TEMPLATE_COMPLEX"] = "MANUAL_REVIEW_TEMPLATE_COMPLEX";
17
+ AdvisoryCode["MANUAL_REVIEW_CONDITIONAL_RETURN"] = "MANUAL_REVIEW_CONDITIONAL_RETURN";
18
+ AdvisoryCode["POST_MUTATION_TYPE_ERROR"] = "POST_MUTATION_TYPE_ERROR";
19
+ })(AdvisoryCode || (exports.AdvisoryCode = AdvisoryCode = {}));
20
+ class AdvisoryBuffer {
21
+ items = [];
22
+ push(advisory) {
23
+ this.items.push(advisory);
24
+ }
25
+ flush() {
26
+ if (this.items.length === 0)
27
+ return;
28
+ console.log("\n[Sela Advisories] ══════════════════════════════════════");
29
+ for (const a of this.items) {
30
+ const loc = a.affectedLine != null ? `:${a.affectedLine + 1}` : "";
31
+ console.log(` [${a.code}] ${a.message}`);
32
+ console.log(` File: ${a.affectedFile}${loc}`);
33
+ console.log(` Action: ${a.suggestedAction}`);
34
+ }
35
+ console.log("[Sela Advisories] ══════════════════════════════════════\n");
36
+ this.items = [];
37
+ }
38
+ get length() {
39
+ return this.items.length;
40
+ }
41
+ }
42
+ exports.AdvisoryBuffer = AdvisoryBuffer;
@@ -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;AAkBD,qBAAa,mBAAoB,YAAW,oBAAoB;IAC9D,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,YAAY;IAgBvC,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM;CA6ErC"}
@@ -0,0 +1,84 @@
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.HealthReportService = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function isWithin7Days(iso) {
10
+ return new Date(iso).getTime() >= Date.now() - 7 * 24 * 60 * 60 * 1000;
11
+ }
12
+ function topHealedFiles(records) {
13
+ const counts = new Map();
14
+ for (const r of records) {
15
+ if (r.lastHealed !== null && r.sourceFile) {
16
+ counts.set(r.sourceFile, (counts.get(r.sourceFile) ?? 0) + 1);
17
+ }
18
+ }
19
+ return [...counts.entries()]
20
+ .sort((a, b) => b[1] - a[1])
21
+ .map(([sourceFile, count]) => ({ sourceFile, count }));
22
+ }
23
+ class HealthReportService {
24
+ generate(index) {
25
+ const { records, snapshotDir, generatedAt } = index;
26
+ return {
27
+ generatedAt,
28
+ snapshotDir,
29
+ total: records.length,
30
+ orphaned: records.filter((r) => r.isOrphaned),
31
+ unverifiedHeals: records.filter((r) => !r.isOrphaned && r.status === 'healed'),
32
+ recentlyHealed: records.filter((r) => r.lastHealed !== null && isWithin7Days(r.lastHealed)),
33
+ neverHealed: records.filter((r) => r.lastHealed === null),
34
+ topHealedFiles: topHealedFiles(records),
35
+ };
36
+ }
37
+ format(report) {
38
+ const lines = [''];
39
+ lines.push(chalk_1.default.bold.cyan(' Sela Health Report') +
40
+ chalk_1.default.dim(` (${new Date(report.generatedAt).toISOString()})`));
41
+ lines.push('');
42
+ const row = (label, value) => ` ${chalk_1.default.dim(label.padEnd(20))} ${value}`;
43
+ lines.push(row('Total DNAs', chalk_1.default.bold(String(report.total))));
44
+ const orphanCount = report.orphaned.length;
45
+ lines.push(row('Orphaned', orphanCount > 0
46
+ ? chalk_1.default.yellow(`${orphanCount} ⚠`)
47
+ : chalk_1.default.green('0')));
48
+ const unverifiedCount = report.unverifiedHeals.length;
49
+ lines.push(row('Unverified heals', unverifiedCount > 0
50
+ ? chalk_1.default.yellow(`${unverifiedCount} ⚠ ${chalk_1.default.dim('(AI-healed, needs review)')}`)
51
+ : chalk_1.default.green('0')));
52
+ const brokenTotal = orphanCount + unverifiedCount;
53
+ const brokenRatio = report.total > 0
54
+ ? ((brokenTotal / report.total) * 100).toFixed(1)
55
+ : '0.0';
56
+ lines.push(row('Broken ratio', (brokenTotal > 0 ? chalk_1.default.yellow : chalk_1.default.green)(`${brokenRatio}% ${chalk_1.default.dim(`(${brokenTotal}/${report.total})`)}`)));
57
+ lines.push(row('Healed (7 days)', chalk_1.default.bold(String(report.recentlyHealed.length))));
58
+ lines.push(row('Healed (all time)', chalk_1.default.bold(String(report.total - report.neverHealed.length))));
59
+ lines.push(row('Never healed', chalk_1.default.bold(String(report.neverHealed.length))));
60
+ if (report.topHealedFiles.length > 0) {
61
+ lines.push('');
62
+ lines.push(` ${chalk_1.default.bold('Top healed files:')}`);
63
+ for (const { sourceFile, count } of report.topHealedFiles.slice(0, 5)) {
64
+ const rel = path_1.default.relative(process.cwd(), sourceFile);
65
+ const label = (rel.length < sourceFile.length ? rel : sourceFile).slice(-60).padEnd(62);
66
+ const noun = count === 1 ? 'heal' : 'heals';
67
+ lines.push(` ${chalk_1.default.cyan(label)} ${chalk_1.default.dim('—')} ${chalk_1.default.bold(String(count))} ${chalk_1.default.dim(noun)}`);
68
+ }
69
+ }
70
+ if (report.orphaned.length > 0) {
71
+ lines.push('');
72
+ lines.push(` ${chalk_1.default.yellow.bold('Orphaned DNA files:')}`);
73
+ for (const r of report.orphaned.slice(0, 10)) {
74
+ lines.push(` ${chalk_1.default.dim(r.key)} ${chalk_1.default.yellow(r.sourceFile)}`);
75
+ }
76
+ if (report.orphaned.length > 10) {
77
+ lines.push(chalk_1.default.dim(` … and ${report.orphaned.length - 10} more`));
78
+ }
79
+ }
80
+ lines.push('');
81
+ return lines.join('\n');
82
+ }
83
+ }
84
+ exports.HealthReportService = HealthReportService;
@@ -0,0 +1,16 @@
1
+ import { Node, SourceFile } from "ts-morph";
2
+ export interface InitializerResult {
3
+ lineUpdated: number;
4
+ healedLocatorString: string;
5
+ }
6
+ export declare class InitializerUpdater {
7
+ /**
8
+ * Mutate a StringLiteral or NoSubstitutionTemplateLiteral node to
9
+ * hold newValue, then save the containing source file.
10
+ *
11
+ * Returns null if the node type is not directly mutable or if the
12
+ * mutation throws (caller should fall through to existing strategies).
13
+ */
14
+ static apply(valueNode: Node, sourceFile: SourceFile, newValue: string): InitializerResult | null;
15
+ }
16
+ //# sourceMappingURL=InitializerUpdater.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InitializerUpdater.d.ts","sourceRoot":"","sources":["../../src/services/InitializerUpdater.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAiB,MAAM,UAAU,CAAC;AAY3D,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,qBAAa,kBAAkB;IAC7B;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CACV,SAAS,EAAE,IAAI,EACf,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,iBAAiB,GAAG,IAAI;CAqB5B"}
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InitializerUpdater = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ const TemplateDiffService_js_1 = require("./TemplateDiffService.js");
6
+ class InitializerUpdater {
7
+ /**
8
+ * Mutate a StringLiteral or NoSubstitutionTemplateLiteral node to
9
+ * hold newValue, then save the containing source file.
10
+ *
11
+ * Returns null if the node type is not directly mutable or if the
12
+ * mutation throws (caller should fall through to existing strategies).
13
+ */
14
+ static apply(valueNode, sourceFile, newValue) {
15
+ try {
16
+ if (ts_morph_1.Node.isStringLiteral(valueNode)) {
17
+ valueNode.setLiteralValue(newValue);
18
+ }
19
+ else if (ts_morph_1.Node.isNoSubstitutionTemplateLiteral(valueNode)) {
20
+ valueNode.replaceWithText("`" + (0, TemplateDiffService_js_1.escapeForTemplate)(newValue) + "`");
21
+ }
22
+ else {
23
+ return null;
24
+ }
25
+ sourceFile.saveSync();
26
+ return {
27
+ lineUpdated: valueNode.getStartLineNumber() - 1,
28
+ healedLocatorString: newValue,
29
+ };
30
+ }
31
+ catch (e) {
32
+ console.warn(`[InitializerUpdater] ⚠️ apply failed: ${e.message}`);
33
+ return null;
34
+ }
35
+ }
36
+ }
37
+ exports.InitializerUpdater = InitializerUpdater;
@@ -0,0 +1,39 @@
1
+ import { AncestorNode, AnchorInfo } from "../types";
2
+ export interface AuditRequest {
3
+ dnaText: string;
4
+ dnaRole?: string;
5
+ liveText: string;
6
+ liveRole?: string;
7
+ healMode: "assertion" | "action";
8
+ /** Ancestry chain from the DNA snapshot (empty for migrated v1 snapshots). */
9
+ dnaAncestry?: AncestorNode[];
10
+ /** Ancestry chain freshly captured from the candidate element at heal-time. */
11
+ liveAncestry?: AncestorNode[];
12
+ /** Label/row anchors from the DNA snapshot. */
13
+ dnaAnchors?: AnchorInfo;
14
+ /** Label/row anchors freshly captured from the candidate element. */
15
+ liveAnchors?: AnchorInfo;
16
+ }
17
+ export type AuditVerdictValue = "CONSISTENT" | "INCONSISTENT" | "SUSPICIOUS";
18
+ export interface AuditVerdict {
19
+ verdict: AuditVerdictValue;
20
+ confidence: number;
21
+ reason: string;
22
+ inversionType: "polarity" | "quantitative" | "state" | "identity" | null;
23
+ /** Populated when structural scoring adjusted the raw LLM confidence. */
24
+ scoreBreakdown?: {
25
+ rawConfidence: number;
26
+ penalty: number;
27
+ bonus: number;
28
+ };
29
+ }
30
+ export declare class IntentAuditor {
31
+ private anthropic;
32
+ private readonly disabled;
33
+ constructor();
34
+ auditIntent(req: AuditRequest): Promise<AuditVerdict>;
35
+ computeAncestryPenalty(dnaPath: AncestorNode[], livePath: AncestorNode[]): number;
36
+ computeAnchorBonus(dnaAnchors: AnchorInfo | null | undefined, liveAnchors: AnchorInfo | null | undefined): number;
37
+ private parseVerdict;
38
+ }
39
+ //# sourceMappingURL=IntentAuditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IntentAuditor.d.ts","sourceRoot":"","sources":["../../src/services/IntentAuditor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAOpD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,WAAW,GAAG,QAAQ,CAAC;IAEjC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,qEAAqE;IACrE,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,UAAU,GAAG,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;IACzE,yEAAyE;IACzE,cAAc,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5E;AAkED,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;;IAgB7B,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IA4G3D,sBAAsB,CACpB,OAAO,EAAE,YAAY,EAAE,EACvB,QAAQ,EAAE,YAAY,EAAE,GACvB,MAAM;IA4CT,kBAAkB,CAChB,UAAU,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EACzC,WAAW,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GACzC,MAAM;IAmCT,OAAO,CAAC,YAAY;CA8CrB"}