sela-core 1.0.8 → 1.0.9
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 +27 -17
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/merge.d.ts +13 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +119 -0
- package/dist/cli/index.js +4 -1
- package/dist/config/SelaConfig.d.ts +1 -1
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +1 -1
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +54 -2
- package/dist/errors/SelaError.d.ts +8 -0
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +8 -0
- package/dist/fixtures/expectProxy.d.ts +1 -1
- package/dist/fixtures/expectProxy.js +6 -6
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +34 -14
- package/dist/fixtures/moduleExpect.js +7 -7
- package/dist/fixtures/proxyTag.js +1 -1
- package/dist/services/ASTSourceUpdater.d.ts +45 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +201 -47
- package/dist/services/AnchorResolver.d.ts +157 -0
- package/dist/services/AnchorResolver.d.ts.map +1 -0
- package/dist/services/AnchorResolver.js +289 -0
- package/dist/services/DecisionEngine.d.ts +51 -0
- package/dist/services/DecisionEngine.d.ts.map +1 -0
- package/dist/services/DecisionEngine.js +260 -0
- package/dist/services/HealReportService.d.ts +31 -55
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +863 -1243
- package/dist/services/HealingAdvisory.d.ts +9 -1
- package/dist/services/HealingAdvisory.d.ts.map +1 -1
- package/dist/services/HealingAdvisory.js +8 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +10 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -1
- package/dist/services/InteractiveReview.js +4 -1
- package/dist/services/MutationApplier.d.ts +55 -0
- package/dist/services/MutationApplier.d.ts.map +1 -0
- package/dist/services/MutationApplier.js +322 -0
- package/dist/services/PendingPromptLedger.d.ts +7 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -1
- package/dist/services/PendingPromptLedger.js +25 -0
- package/dist/services/ReportGenerator.d.ts +116 -30
- package/dist/services/ReportGenerator.d.ts.map +1 -1
- package/dist/services/ReportGenerator.js +150 -63
- package/dist/services/ReportMergeService.d.ts +95 -0
- package/dist/services/ReportMergeService.d.ts.map +1 -0
- package/dist/services/ReportMergeService.js +0 -0
- package/dist/services/SafetyGuard.d.ts +1 -1
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SelectorSanitizer.d.ts +52 -0
- package/dist/services/SelectorSanitizer.d.ts.map +1 -0
- package/dist/services/SelectorSanitizer.js +318 -0
- package/dist/services/SnapshotService.js +1 -1
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +17 -0
- package/dist/services/TraceBackEngine.d.ts +67 -0
- package/dist/services/TraceBackEngine.d.ts.map +1 -0
- package/dist/services/TraceBackEngine.js +672 -0
- package/dist/utils/DOMUtils.d.ts +18 -2
- package/dist/utils/DOMUtils.d.ts.map +1 -1
- package/dist/utils/DOMUtils.js +335 -49
- package/package.json +1 -1
|
@@ -4,7 +4,15 @@ export declare enum AdvisoryCode {
|
|
|
4
4
|
CROSS_FILE_MUTATION = "CROSS_FILE_MUTATION",
|
|
5
5
|
MANUAL_REVIEW_TEMPLATE_COMPLEX = "MANUAL_REVIEW_TEMPLATE_COMPLEX",
|
|
6
6
|
MANUAL_REVIEW_CONDITIONAL_RETURN = "MANUAL_REVIEW_CONDITIONAL_RETURN",
|
|
7
|
-
POST_MUTATION_TYPE_ERROR = "POST_MUTATION_TYPE_ERROR"
|
|
7
|
+
POST_MUTATION_TYPE_ERROR = "POST_MUTATION_TYPE_ERROR",
|
|
8
|
+
/**
|
|
9
|
+
* A definition-site string constant was healed to a valid PUBLIC selector
|
|
10
|
+
* string (e.g. `role=button[name="…"]`) because the constant is consumed by
|
|
11
|
+
* `.locator(VAR)` and cannot hold a semantic locator. The AI's preferred
|
|
12
|
+
* semantic method form (e.g. `getByRole(...)`) is suggested as a cleaner,
|
|
13
|
+
* optional refactor.
|
|
14
|
+
*/
|
|
15
|
+
SEMANTIC_UPGRADE_SUGGESTED = "SEMANTIC_UPGRADE_SUGGESTED"
|
|
8
16
|
}
|
|
9
17
|
export interface Advisory {
|
|
10
18
|
code: AdvisoryCode;
|
|
@@ -1 +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;
|
|
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;IACrD;;;;;;OAMG;IACH,0BAA0B,+BAA+B;CAC1D;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAkB;IAE/B,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAI9B,KAAK,IAAI,IAAI;IAab,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF"}
|
|
@@ -16,6 +16,14 @@ var AdvisoryCode;
|
|
|
16
16
|
AdvisoryCode["MANUAL_REVIEW_TEMPLATE_COMPLEX"] = "MANUAL_REVIEW_TEMPLATE_COMPLEX";
|
|
17
17
|
AdvisoryCode["MANUAL_REVIEW_CONDITIONAL_RETURN"] = "MANUAL_REVIEW_CONDITIONAL_RETURN";
|
|
18
18
|
AdvisoryCode["POST_MUTATION_TYPE_ERROR"] = "POST_MUTATION_TYPE_ERROR";
|
|
19
|
+
/**
|
|
20
|
+
* A definition-site string constant was healed to a valid PUBLIC selector
|
|
21
|
+
* string (e.g. `role=button[name="…"]`) because the constant is consumed by
|
|
22
|
+
* `.locator(VAR)` and cannot hold a semantic locator. The AI's preferred
|
|
23
|
+
* semantic method form (e.g. `getByRole(...)`) is suggested as a cleaner,
|
|
24
|
+
* optional refactor.
|
|
25
|
+
*/
|
|
26
|
+
AdvisoryCode["SEMANTIC_UPGRADE_SUGGESTED"] = "SEMANTIC_UPGRADE_SUGGESTED";
|
|
19
27
|
})(AdvisoryCode || (exports.AdvisoryCode = AdvisoryCode = {}));
|
|
20
28
|
class AdvisoryBuffer {
|
|
21
29
|
items = [];
|
|
@@ -1 +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;
|
|
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;AAe3D,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;CAkD5B"}
|
|
@@ -5,6 +5,7 @@ const ts_morph_1 = require("ts-morph");
|
|
|
5
5
|
const TemplateDiffService_js_1 = require("./TemplateDiffService.js");
|
|
6
6
|
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
7
7
|
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
8
|
+
const SelectorSanitizer_1 = require("./SelectorSanitizer");
|
|
8
9
|
class InitializerUpdater {
|
|
9
10
|
/**
|
|
10
11
|
* Mutate a StringLiteral or NoSubstitutionTemplateLiteral node to
|
|
@@ -14,6 +15,15 @@ class InitializerUpdater {
|
|
|
14
15
|
* mutation throws (caller should fall through to existing strategies).
|
|
15
16
|
*/
|
|
16
17
|
static apply(valueNode, sourceFile, newValue) {
|
|
18
|
+
// ABSOLUTE GUARD: never serialise a Playwright internal engine selector
|
|
19
|
+
// into a string-literal definition. `.locator()` strings cannot express
|
|
20
|
+
// a semantic locator (getByRole/…), so writing `internal:role=…` here is
|
|
21
|
+
// the bug that leaks engine internals into user source. Refuse → the
|
|
22
|
+
// caller escalates to a semantic-guard (no regex fallback corruption).
|
|
23
|
+
if ((0, SelectorSanitizer_1.isInternalEngineSelector)(newValue)) {
|
|
24
|
+
console.warn(`[InitializerUpdater] ⛔ refusing to write internal engine selector into source: "${newValue.slice(0, 64)}"`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
17
27
|
try {
|
|
18
28
|
if (ts_morph_1.Node.isStringLiteral(valueNode)) {
|
|
19
29
|
valueNode.setLiteralValue(newValue);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InteractiveReview.d.ts","sourceRoot":"","sources":["../../src/services/InteractiveReview.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,uEAAuE;IACvE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"InteractiveReview.d.ts","sourceRoot":"","sources":["../../src/services/InteractiveReview.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,uEAAuE;IACvE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,CA6B3D"}
|
|
@@ -67,7 +67,10 @@ function printHealReview(deps) {
|
|
|
67
67
|
return;
|
|
68
68
|
log("");
|
|
69
69
|
if (entries.length > 0) {
|
|
70
|
-
|
|
70
|
+
// Count actual healed selectors (a file can hold several), not files.
|
|
71
|
+
const fileCount = entries.length;
|
|
72
|
+
const fixCount = entries.reduce((n, e) => n + (e.healCount ?? 1), 0);
|
|
73
|
+
log(chalk_1.default.bold.bgCyan.black(` [Sela] Local DX Review - ${fixCount} fix${fixCount === 1 ? "" : "es"} applied across ${fileCount} file${fileCount === 1 ? "" : "s"} `));
|
|
71
74
|
for (const entry of entries) {
|
|
72
75
|
renderEntry(entry, log);
|
|
73
76
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AnchorKey, SourceSpan } from "./AnchorResolver";
|
|
2
|
+
/**
|
|
3
|
+
* A single edit. `replace` edits carry an `anchor` and are RE-RESOLVED at
|
|
4
|
+
* apply-time (span recomputed from the fresh parse). `insert` edits (forks)
|
|
5
|
+
* carry a literal `span` computed against the same content being applied.
|
|
6
|
+
*/
|
|
7
|
+
export interface AnchoredEdit {
|
|
8
|
+
kind: "replace" | "insert";
|
|
9
|
+
replacement: string;
|
|
10
|
+
/** Replace target — re-resolved via resolveAnchor at apply time. */
|
|
11
|
+
anchor?: AnchorKey;
|
|
12
|
+
/** Literal span for inserts (and as a fallback for anchorless replaces). */
|
|
13
|
+
span?: SourceSpan;
|
|
14
|
+
/**
|
|
15
|
+
* Require the replacement to be a Locator-returning expression. Defaults to
|
|
16
|
+
* true for `replace`. Set false for inserts / non-locator edits.
|
|
17
|
+
*/
|
|
18
|
+
requireLocator?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface FileBatch {
|
|
21
|
+
filePath: string;
|
|
22
|
+
edits: AnchoredEdit[];
|
|
23
|
+
}
|
|
24
|
+
export type ApplyStatus = "applied" | "rolled-back" | "no-op" | "skipped";
|
|
25
|
+
export interface ApplyOutcome {
|
|
26
|
+
filePath: string;
|
|
27
|
+
status: ApplyStatus;
|
|
28
|
+
reason: string;
|
|
29
|
+
written: boolean;
|
|
30
|
+
/** Content after the run — the new content when applied, else the original. */
|
|
31
|
+
finalContent: string;
|
|
32
|
+
/** Edits that could not be applied (stale anchor, type-incompat, …). */
|
|
33
|
+
skipped: Array<{
|
|
34
|
+
reason: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface ApplyOptions {
|
|
38
|
+
/** When false, compute + gate but never touch disk. Default true. */
|
|
39
|
+
write?: boolean;
|
|
40
|
+
/** Honour SELA_DRY_RUN by leaving disk untouched. Default true. */
|
|
41
|
+
respectDryRun?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare class MutationApplier {
|
|
44
|
+
/** Apply many batches; each file is processed independently. */
|
|
45
|
+
static applyBatches(batches: FileBatch[], opts?: ApplyOptions): ApplyOutcome[];
|
|
46
|
+
static applyFileBatch(batch: FileBatch, opts?: ApplyOptions): ApplyOutcome;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Cheap return-type pre-check. The replacement must be an expression whose
|
|
50
|
+
* OUTERMOST call is a Locator-returning Playwright method (locator / getBy* /
|
|
51
|
+
* frameLocator / nth / first / last / filter / and / or). Rejects actions
|
|
52
|
+
* (.click()), string queries (.title(), .textContent()), bare literals, etc.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isLocatorReturningExpr(text: string): boolean;
|
|
55
|
+
//# sourceMappingURL=MutationApplier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MutationApplier.d.ts","sourceRoot":"","sources":["../../src/services/MutationApplier.ts"],"names":[],"mappings":"AA2BA,OAAO,EACL,SAAS,EAET,UAAU,EACX,MAAM,kBAAkB,CAAC;AAS1B;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAE1E,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAeD,qBAAa,eAAe;IAC1B,gEAAgE;IAChE,MAAM,CAAC,YAAY,CACjB,OAAO,EAAE,SAAS,EAAE,EACpB,IAAI,GAAE,YAAiB,GACtB,YAAY,EAAE;IAIjB,MAAM,CAAC,cAAc,CACnB,KAAK,EAAE,SAAS,EAChB,IAAI,GAAE,YAAiB,GACtB,YAAY;CA6JhB;AA2CD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkB5D"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/MutationApplier.ts
|
|
3
|
+
//
|
|
4
|
+
// Ticket #2 — Stage 4: Write-Discipline & the Drain Loop.
|
|
5
|
+
//
|
|
6
|
+
// Applies MutationPlans to disk under three hard contracts:
|
|
7
|
+
//
|
|
8
|
+
// 1. ZERO LAYOUT NOISE (§4.1) — surgical string splice over the EXACT node
|
|
9
|
+
// span [getStart(), getEnd()). getStart() excludes leading trivia, so
|
|
10
|
+
// comments / indentation / Prettier layout outside the span are byte-
|
|
11
|
+
// identical. We never call sourceFile.formatText() / ts.Printer.
|
|
12
|
+
//
|
|
13
|
+
// 2. TYPE-SAFETY GATE + ROLLBACK (§4.3) — after splicing in memory we
|
|
14
|
+
// re-parse and baseline-diff the diagnostics. A mutation that introduces
|
|
15
|
+
// a NEW real error (syntax break, non-Locator return) is rolled back and
|
|
16
|
+
// the file is left byte-for-byte untouched. A cheap Locator-shape pre-
|
|
17
|
+
// check rejects obviously wrong return types before the splice.
|
|
18
|
+
//
|
|
19
|
+
// 3. BATCH APPLICATION (§3.3) — mutations are grouped by file, anchors are
|
|
20
|
+
// RE-RESOLVED against the fresh parse (never trusting flag-time offsets),
|
|
21
|
+
// edits applied highest-offset-first, then the file is written atomically
|
|
22
|
+
// (temp + rename).
|
|
23
|
+
//
|
|
24
|
+
// Stage 1 (resolveAnchor) provides line-shift-immune relocation; Stage-1
|
|
25
|
+
// applyEdits provides the reverse-offset splice + overlap guard.
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.MutationApplier = void 0;
|
|
61
|
+
exports.isLocatorReturningExpr = isLocatorReturningExpr;
|
|
62
|
+
const fs = __importStar(require("fs"));
|
|
63
|
+
const ts_morph_1 = require("ts-morph");
|
|
64
|
+
const AnchorResolver_1 = require("./AnchorResolver");
|
|
65
|
+
const DecisionEngine_1 = require("./DecisionEngine");
|
|
66
|
+
const TraceBackEngine_1 = require("./TraceBackEngine");
|
|
67
|
+
const SelectorSanitizer_1 = require("./SelectorSanitizer");
|
|
68
|
+
// Diagnostic codes that are pure "missing types" noise in an in-memory parse
|
|
69
|
+
// without node_modules — identical allowlist to ASTSourceUpdater's validator.
|
|
70
|
+
// Syntax/binder errors (codes 1000–1999) are NEVER allowlisted.
|
|
71
|
+
const NOISE_CODES = new Set([
|
|
72
|
+
2304, 2339, 2345, 2554, 2571, 2580, 2503, 7006, 7016, 7031, 18004,
|
|
73
|
+
]);
|
|
74
|
+
const LOCATOR_RETURNING_CATEGORIES = new Set(["selector", "frame", "modifier"]);
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
76
|
+
// MutationApplier
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
78
|
+
class MutationApplier {
|
|
79
|
+
/** Apply many batches; each file is processed independently. */
|
|
80
|
+
static applyBatches(batches, opts = {}) {
|
|
81
|
+
return batches.map((b) => MutationApplier.applyFileBatch(b, opts));
|
|
82
|
+
}
|
|
83
|
+
static applyFileBatch(batch, opts = {}) {
|
|
84
|
+
const write = opts.write ?? true;
|
|
85
|
+
const respectDryRun = opts.respectDryRun ?? true;
|
|
86
|
+
const dryRun = respectDryRun && process.env.SELA_DRY_RUN === "1";
|
|
87
|
+
const { filePath } = batch;
|
|
88
|
+
const skipped = [];
|
|
89
|
+
let original;
|
|
90
|
+
try {
|
|
91
|
+
original = fs.readFileSync(filePath, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return {
|
|
95
|
+
filePath,
|
|
96
|
+
status: "skipped",
|
|
97
|
+
reason: `cannot read file: ${e.message}`,
|
|
98
|
+
written: false,
|
|
99
|
+
finalContent: "",
|
|
100
|
+
skipped,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Fresh parse — the single source of truth for span resolution.
|
|
104
|
+
const project = newGateProject();
|
|
105
|
+
const sf = project.createSourceFile(filePath, original, { overwrite: true });
|
|
106
|
+
const baseline = realDiagnosticCounts(sf);
|
|
107
|
+
// ── Resolve each edit to a concrete span ─────────────────────────
|
|
108
|
+
const concrete = [];
|
|
109
|
+
for (const e of batch.edits) {
|
|
110
|
+
let replacement = e.replacement;
|
|
111
|
+
// ABSOLUTE GUARD: translate any internal engine selector to its public
|
|
112
|
+
// method form before writing; refuse if it cannot be translated. No
|
|
113
|
+
// `internal:role=…` string ever reaches the developer's source.
|
|
114
|
+
if (e.kind === "replace") {
|
|
115
|
+
const san = (0, SelectorSanitizer_1.sanitizeLocatorExpression)(replacement);
|
|
116
|
+
if (!san.safe) {
|
|
117
|
+
skipped.push({
|
|
118
|
+
reason: `internal-engine-selector: cannot translate "${truncate(replacement)}" to a public Playwright method`,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
replacement = san.text;
|
|
123
|
+
}
|
|
124
|
+
const requireLocator = e.requireLocator ?? e.kind === "replace";
|
|
125
|
+
if (requireLocator && !isLocatorReturningExpr(replacement)) {
|
|
126
|
+
skipped.push({
|
|
127
|
+
reason: `type-incompat: replacement is not a Locator expression ("${truncate(replacement)}")`,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (e.kind === "replace" && e.anchor) {
|
|
132
|
+
const r = (0, AnchorResolver_1.resolveAnchor)(sf, e.anchor);
|
|
133
|
+
if (r.status !== "resolved") {
|
|
134
|
+
skipped.push({
|
|
135
|
+
reason: `anchor ${r.status}: ${"reason" in r ? r.reason : ""}`,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
concrete.push({
|
|
140
|
+
kind: "replace",
|
|
141
|
+
span: { start: r.node.getStart(), end: r.node.getEnd() },
|
|
142
|
+
replacement,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else if (e.span) {
|
|
146
|
+
concrete.push({
|
|
147
|
+
kind: e.kind,
|
|
148
|
+
span: e.span,
|
|
149
|
+
replacement,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
skipped.push({ reason: "edit has neither anchor nor span" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (concrete.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
filePath,
|
|
159
|
+
status: "no-op",
|
|
160
|
+
reason: "no applicable edits after resolution",
|
|
161
|
+
written: false,
|
|
162
|
+
finalContent: original,
|
|
163
|
+
skipped,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ── Splice (reverse-offset; overlap-guarded) ─────────────────────
|
|
167
|
+
let newContent;
|
|
168
|
+
try {
|
|
169
|
+
newContent = (0, DecisionEngine_1.applyEdits)(original, concrete);
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
return {
|
|
173
|
+
filePath,
|
|
174
|
+
status: "rolled-back",
|
|
175
|
+
reason: `splice failed: ${e.message}`,
|
|
176
|
+
written: false,
|
|
177
|
+
finalContent: original,
|
|
178
|
+
skipped,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (newContent === original) {
|
|
182
|
+
return {
|
|
183
|
+
filePath,
|
|
184
|
+
status: "no-op",
|
|
185
|
+
reason: "edits produced identical content",
|
|
186
|
+
written: false,
|
|
187
|
+
finalContent: original,
|
|
188
|
+
skipped,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ── Type-safety gate: baseline-diff diagnostics ──────────────────
|
|
192
|
+
const afterSf = project.createSourceFile(filePath, newContent, {
|
|
193
|
+
overwrite: true,
|
|
194
|
+
});
|
|
195
|
+
const after = realDiagnosticCounts(afterSf);
|
|
196
|
+
const regression = diagnosticRegression(baseline, after);
|
|
197
|
+
if (regression) {
|
|
198
|
+
return {
|
|
199
|
+
filePath,
|
|
200
|
+
status: "rolled-back",
|
|
201
|
+
reason: `type-safety gate: mutation introduced a new error (${regression}) — file left untouched`,
|
|
202
|
+
written: false,
|
|
203
|
+
finalContent: original,
|
|
204
|
+
skipped,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// ── Atomic write ─────────────────────────────────────────────────
|
|
208
|
+
if (write && !dryRun) {
|
|
209
|
+
try {
|
|
210
|
+
atomicWrite(filePath, newContent);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return {
|
|
214
|
+
filePath,
|
|
215
|
+
status: "rolled-back",
|
|
216
|
+
reason: `atomic write failed: ${e.message}`,
|
|
217
|
+
written: false,
|
|
218
|
+
finalContent: original,
|
|
219
|
+
skipped,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
filePath,
|
|
225
|
+
status: "applied",
|
|
226
|
+
reason: `${concrete.length} edit(s) applied${dryRun ? " (dry-run, not written)" : ""}`,
|
|
227
|
+
written: write && !dryRun,
|
|
228
|
+
finalContent: newContent,
|
|
229
|
+
skipped,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.MutationApplier = MutationApplier;
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
235
|
+
// TYPE-SAFETY HELPERS
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
237
|
+
function newGateProject() {
|
|
238
|
+
return new ts_morph_1.Project({
|
|
239
|
+
useInMemoryFileSystem: true,
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
allowJs: true,
|
|
242
|
+
noEmit: true,
|
|
243
|
+
skipLibCheck: true,
|
|
244
|
+
noResolve: true,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** Map of real (non-noise) diagnostic code → count. */
|
|
249
|
+
function realDiagnosticCounts(sf) {
|
|
250
|
+
const counts = new Map();
|
|
251
|
+
for (const d of sf.getPreEmitDiagnostics()) {
|
|
252
|
+
if (d.getCategory() !== 1 /* Error */)
|
|
253
|
+
continue;
|
|
254
|
+
const code = d.getCode();
|
|
255
|
+
if (NOISE_CODES.has(code))
|
|
256
|
+
continue;
|
|
257
|
+
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
258
|
+
}
|
|
259
|
+
return counts;
|
|
260
|
+
}
|
|
261
|
+
/** Returns a description of the first NEW/elevated error, or null. */
|
|
262
|
+
function diagnosticRegression(baseline, after) {
|
|
263
|
+
for (const [code, n] of after) {
|
|
264
|
+
if (n > (baseline.get(code) ?? 0)) {
|
|
265
|
+
return `TS${code}`;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Cheap return-type pre-check. The replacement must be an expression whose
|
|
272
|
+
* OUTERMOST call is a Locator-returning Playwright method (locator / getBy* /
|
|
273
|
+
* frameLocator / nth / first / last / filter / and / or). Rejects actions
|
|
274
|
+
* (.click()), string queries (.title(), .textContent()), bare literals, etc.
|
|
275
|
+
*/
|
|
276
|
+
function isLocatorReturningExpr(text) {
|
|
277
|
+
const project = newGateProject();
|
|
278
|
+
let sf;
|
|
279
|
+
try {
|
|
280
|
+
sf = project.createSourceFile("_probe.ts", `const __x = ${text};`, {
|
|
281
|
+
overwrite: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const decl = sf.getVariableDeclaration("__x");
|
|
288
|
+
const init = decl?.getInitializer();
|
|
289
|
+
if (!init)
|
|
290
|
+
return false;
|
|
291
|
+
// Outermost expression must be a call whose method is Locator-returning.
|
|
292
|
+
if (!ts_morph_1.Node.isCallExpression(init))
|
|
293
|
+
return false;
|
|
294
|
+
const callee = init.getExpression();
|
|
295
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(callee))
|
|
296
|
+
return false;
|
|
297
|
+
return LOCATOR_RETURNING_CATEGORIES.has((0, TraceBackEngine_1.categorizeMethod)(callee.getName()));
|
|
298
|
+
}
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
300
|
+
// ATOMIC WRITE
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
302
|
+
function atomicWrite(filePath, content) {
|
|
303
|
+
const tmp = `${filePath}.sela-tmp-${process.pid}`;
|
|
304
|
+
fs.writeFileSync(tmp, content, "utf8");
|
|
305
|
+
try {
|
|
306
|
+
fs.renameSync(tmp, filePath); // atomic on POSIX; replace-on-Windows
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Windows can reject rename-over-existing in rare cases — fall back to a
|
|
310
|
+
// direct write, then best-effort remove the temp file.
|
|
311
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
312
|
+
try {
|
|
313
|
+
fs.unlinkSync(tmp);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* best effort */
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function truncate(s, n = 48) {
|
|
321
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
322
|
+
}
|
|
@@ -8,6 +8,13 @@ export interface PendingPromptEntry {
|
|
|
8
8
|
createdBySela: boolean;
|
|
9
9
|
/** 1-based line number for the vscode:// deeplink, or null when unknown. */
|
|
10
10
|
lineNumber: number | null;
|
|
11
|
+
/**
|
|
12
|
+
* Number of healed selectors that landed in THIS file. A single file can
|
|
13
|
+
* absorb several heals in one run (e.g. two locators in the same spec), so
|
|
14
|
+
* the DX banner counts fixes from this field rather than from the entry
|
|
15
|
+
* (= file) count. Older ledgers without the field are treated as 1.
|
|
16
|
+
*/
|
|
17
|
+
healCount: number;
|
|
11
18
|
}
|
|
12
19
|
/**
|
|
13
20
|
* Resolve the ledger directory for a given cwd. The directory is created
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,wBAAwB,CAAC;IACpC,YAAY,EAAE,WAAW,EAAE,CAAC;CAC7B,GAAG,MAAM,GAAG,IAAI,CAehB;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyB1E;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3D"}
|
|
@@ -143,8 +143,33 @@ function snapshotToEntry(snap, heals, cwd) {
|
|
|
143
143
|
contentAfter: snap.contentAfter ?? "",
|
|
144
144
|
createdBySela: snap.createdBySela,
|
|
145
145
|
lineNumber: findLineNumberForSnapshot(snap, heals, cwd),
|
|
146
|
+
healCount: countHealsForSnapshot(snap, heals, cwd),
|
|
146
147
|
};
|
|
147
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Count how many heals mutated `snap`. A heal's mutation target is its
|
|
151
|
+
* `definitionSite.file` when the write crossed into a const/variable
|
|
152
|
+
* declaration, otherwise its `sourceFile`. Resolved to absolute form so a
|
|
153
|
+
* relative event path matches the snapshot's absolute path.
|
|
154
|
+
*
|
|
155
|
+
* Falls back to 1 when no heal resolves to the file - the snapshot only
|
|
156
|
+
* exists because Sela wrote to it, so the file represents at least one fix.
|
|
157
|
+
*/
|
|
158
|
+
function countHealsForSnapshot(snap, heals, cwd) {
|
|
159
|
+
const normSnap = path.normalize(snap.absolutePath);
|
|
160
|
+
let n = 0;
|
|
161
|
+
for (const heal of heals) {
|
|
162
|
+
const target = heal.definitionSite?.file ?? heal.sourceFile;
|
|
163
|
+
if (!target)
|
|
164
|
+
continue;
|
|
165
|
+
const abs = path.isAbsolute(target)
|
|
166
|
+
? path.normalize(target)
|
|
167
|
+
: path.normalize(path.resolve(cwd, target));
|
|
168
|
+
if (abs === normSnap)
|
|
169
|
+
n++;
|
|
170
|
+
}
|
|
171
|
+
return n > 0 ? n : 1;
|
|
172
|
+
}
|
|
148
173
|
/**
|
|
149
174
|
* Resolve a 1-based line number for the vscode:// deeplink. Priority:
|
|
150
175
|
* 1. `definitionSite.line` for a heal whose `definitionSite.file`
|