sela-core 0.1.0-alpha.43 → 0.1.0-alpha.44
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/dist/engine/SelaEngine.d.ts +1 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +194 -85
- package/dist/errors/SelectorForensics.d.ts +31 -0
- package/dist/errors/SelectorForensics.d.ts.map +1 -1
- package/dist/errors/SelectorForensics.js +53 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +8 -5
- package/dist/services/HealCircuitBreaker.d.ts +28 -0
- package/dist/services/HealCircuitBreaker.d.ts.map +1 -0
- package/dist/services/HealCircuitBreaker.js +103 -0
- package/dist/services/PRAutomationService.d.ts +8 -0
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +103 -11
- package/dist/services/SnapshotService.d.ts +2 -0
- package/dist/services/SnapshotService.d.ts.map +1 -1
- package/dist/services/SnapshotService.js +4 -0
- package/dist/services/vcs/GitHubProvider.d.ts +4 -0
- package/dist/services/vcs/GitHubProvider.d.ts.map +1 -1
- package/dist/services/vcs/GitHubProvider.js +13 -0
- package/dist/services/vcs/GitLabProvider.d.ts +4 -0
- package/dist/services/vcs/GitLabProvider.d.ts.map +1 -1
- package/dist/services/vcs/GitLabProvider.js +13 -0
- package/dist/services/vcs/VCSFailureDiagnosis.d.ts +5 -0
- package/dist/services/vcs/VCSFailureDiagnosis.d.ts.map +1 -0
- package/dist/services/vcs/VCSFailureDiagnosis.js +77 -0
- package/dist/services/vcs/VCSProvider.d.ts +11 -0
- package/dist/services/vcs/VCSProvider.d.ts.map +1 -1
- package/dist/utils/DOMUtils.d.ts.map +1 -1
- package/dist/utils/DOMUtils.js +51 -9
- package/dist/utils/atomicWrite.d.ts.map +1 -1
- package/dist/utils/atomicWrite.js +42 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAsBxC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAejF,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AA+HhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,mBAAmB,CAAmB;IAC9C,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,eAAe,CAAkC;IAMzD,OAAO,CAAC,yBAAyB,CAA6C;IAK9E,OAAO,CAAC,qBAAqB,CAAkC;IAI/D,OAAO,CAAC,iBAAiB,CAAmC;IAM5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA4B;IAMzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;;IAkDE,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;IAqEzB;;;;;OAKG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAQlC;;;;;;OAMG;YACW,sBAAsB;IA4CpC;;;;;OAKG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI9B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB,CAAkC;IAE5D,cAAc,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IAInD;;;;;;;OAOG;IACH,2BAA2B,CAAC,KAAK,EAAE;QACjC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,qEAAqE;QACrE,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,kEAAkE;QAClE,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,IAAI;IAkCF,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,EAC7B,cAAc,CAAC,EAAE,MAAM,EAKvB,eAAe,CAAC,EAAE;QAAE,eAAe,EAAE,MAAM,CAAA;KAAE,EAG7C,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,MAAM,CAAC;IAinDlB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAerB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,2BAA2B;IA4BnC,OAAO,CAAC,yBAAyB;IAgDjC,OAAO,CAAC,kBAAkB;IA8D1B,OAAO,CAAC,oBAAoB;IAyD5B,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;IAI5E,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAuPpC,OAAO,CAAC,iBAAiB;YAwCX,mBAAmB;YAyCnB,qBAAqB;YAiCrB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,WAAW;IA8Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM;IAyFrB,OAAO,CAAC,MAAM,CAAC,oBAAoB;CAWpC;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const LLMService_1 = require("../services/LLMService");
|
|
40
40
|
const WorkspaceSnapshotService_1 = require("../services/WorkspaceSnapshotService");
|
|
41
|
+
const FileLock_1 = require("../utils/FileLock");
|
|
41
42
|
const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
42
43
|
const logger_1 = require("../utils/logger");
|
|
43
44
|
const SnapshotService_1 = require("../services/SnapshotService");
|
|
@@ -61,6 +62,7 @@ const ProviderFactory_1 = require("../providers/ProviderFactory");
|
|
|
61
62
|
const IntentAuditor_1 = require("../services/IntentAuditor");
|
|
62
63
|
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
63
64
|
const HealBudget_1 = require("../services/HealBudget");
|
|
65
|
+
const HealCircuitBreaker_1 = require("../services/HealCircuitBreaker");
|
|
64
66
|
const HealReportService_1 = require("../services/HealReportService");
|
|
65
67
|
const SharedLocatorGuard_1 = require("../services/SharedLocatorGuard");
|
|
66
68
|
const RegressionManifestService_1 = require("../services/RegressionManifestService");
|
|
@@ -146,6 +148,10 @@ class SelaEngine {
|
|
|
146
148
|
// immediate in-memory hit without a second LLM round-trip or any page.evaluate
|
|
147
149
|
// call (which would fail when the page is already closed, e.g. unawaited promises).
|
|
148
150
|
bypassedSelectorCache = new Map();
|
|
151
|
+
// Memoized fail-fast directive verdicts per "file:line" (P1-5): the
|
|
152
|
+
// ts-morph spec-file parse runs once per call-site, not once per failure —
|
|
153
|
+
// critical when a systemic failure floods heal() with repeats.
|
|
154
|
+
directiveVerdicts = new Map();
|
|
149
155
|
// Lifecycle state machine — owns the transient (session-scoped) NO_HEAL locks
|
|
150
156
|
// and hydrates the persistent v3 selector ledger into the HealingRegistry at
|
|
151
157
|
// session start. Replaces the old ad-hoc, worker-lifetime `noHealCache` Map so
|
|
@@ -402,15 +408,85 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
402
408
|
criticalityOverrides: this.config.healing.criticality,
|
|
403
409
|
};
|
|
404
410
|
const businessCriticality = (0, BusinessContextParser_1.deriveBusinessCriticality)(businessIntent);
|
|
411
|
+
// ── NO_HEAL cache pre-check ──────────────────────────────────────────
|
|
412
|
+
// FIRST gate (P1-5): a pure in-memory Map hit, so it runs before the
|
|
413
|
+
// AST directive scan — in a mass-failure stampede the old order parsed
|
|
414
|
+
// the spec file with ts-morph on EVERY repeat of an already-blocked
|
|
415
|
+
// selector. If this selector was already identified as an application
|
|
416
|
+
// logic/state failure in this run, skip LLM, DNA load, and DOM
|
|
417
|
+
// extraction entirely. One LLM call pays for 50+ identical state-bug
|
|
418
|
+
// failures in large suites.
|
|
419
|
+
const cachedNoHealReason = this.stateManager.selectorBlockReason(elementSelectorOnly);
|
|
420
|
+
if (cachedNoHealReason !== undefined) {
|
|
421
|
+
logger_1.logger.warn(`[Sela] NO_HEAL cache hit — skipping LLM: ${cachedNoHealReason}`);
|
|
422
|
+
HealReportService_1.sharedHealReport.record({
|
|
423
|
+
kind: "FAILED",
|
|
424
|
+
stableId,
|
|
425
|
+
deterministicKey: (0, HealReportService_1.computeEventDeterministicKey)(reportFile, line, elementSelectorOnly),
|
|
426
|
+
sourceFile: reportFile,
|
|
427
|
+
sourceLine: line,
|
|
428
|
+
testTitle,
|
|
429
|
+
timestamp: new Date().toISOString(),
|
|
430
|
+
framePath: [],
|
|
431
|
+
inIframe: false,
|
|
432
|
+
inShadowDom: false,
|
|
433
|
+
oldSelector: elementSelectorOnly,
|
|
434
|
+
reason: `NO_HEAL (cached) — ${cachedNoHealReason}`,
|
|
435
|
+
dnaBefore: null,
|
|
436
|
+
});
|
|
437
|
+
return "";
|
|
438
|
+
}
|
|
439
|
+
// ── Systemic-drift circuit breaker (P1-5) ────────────────────────────
|
|
440
|
+
// A burst of DISTINCT structural failures signals one application change
|
|
441
|
+
// (redeploy / renamed root container), not N independent selector
|
|
442
|
+
// drifts. Past the threshold, refuse further LLM heals for this run and
|
|
443
|
+
// say so once, loudly. Blocking the selector routes every repeat through
|
|
444
|
+
// the cheap NO_HEAL cache above.
|
|
445
|
+
HealCircuitBreaker_1.sharedHealCircuitBreaker.recordAttempt();
|
|
446
|
+
if (HealCircuitBreaker_1.sharedHealCircuitBreaker.isTripped()) {
|
|
447
|
+
const breakerReason = HealCircuitBreaker_1.sharedHealCircuitBreaker.reason();
|
|
448
|
+
if (HealCircuitBreaker_1.sharedHealCircuitBreaker.shouldWarnOnce()) {
|
|
449
|
+
logger_1.logger.warn(`[Sela] Heal BLOCKED — ${breakerReason}`);
|
|
450
|
+
}
|
|
451
|
+
this.stateManager.blockSelector(elementSelectorOnly, "CIRCUIT_BREAKER — systemic drift suspected");
|
|
452
|
+
HealReportService_1.sharedHealReport.record({
|
|
453
|
+
kind: "FAILED",
|
|
454
|
+
stableId,
|
|
455
|
+
deterministicKey: (0, HealReportService_1.computeEventDeterministicKey)(reportFile, line, elementSelectorOnly),
|
|
456
|
+
sourceFile: reportFile,
|
|
457
|
+
sourceLine: line,
|
|
458
|
+
testTitle,
|
|
459
|
+
timestamp: new Date().toISOString(),
|
|
460
|
+
framePath: [],
|
|
461
|
+
inIframe: false,
|
|
462
|
+
inShadowDom: false,
|
|
463
|
+
oldSelector: elementSelectorOnly,
|
|
464
|
+
reason: `CIRCUIT_BREAKER — ${breakerReason}`,
|
|
465
|
+
dnaBefore: null,
|
|
466
|
+
});
|
|
467
|
+
return "";
|
|
468
|
+
}
|
|
405
469
|
// ── Developer fail-fast directive pre-gate ──────────────────────
|
|
406
470
|
// Scan the failing statement's leading comments for `// sela-fail-fast`
|
|
407
471
|
// BEFORE the expensive LLM round-trip. When present, record a
|
|
408
472
|
// "Skipped by Developer" FailedEvent and propagate the canonical
|
|
409
473
|
// SelaError(AST_HEAL_DISABLED_BY_DIRECTIVE) so retries are skipped too.
|
|
410
|
-
|
|
411
|
-
|
|
474
|
+
// The verdict is memoized per call-site (P1-5): the ts-morph parse of
|
|
475
|
+
// the spec file runs once per (file, line), not once per failure.
|
|
476
|
+
const directiveKey = `${filePath}:${line}`;
|
|
477
|
+
let directiveVerdict = this.directiveVerdicts.get(directiveKey);
|
|
478
|
+
if (directiveVerdict === undefined) {
|
|
479
|
+
try {
|
|
480
|
+
this.astDirectiveScanner.enforceFailFastDirective({ filePath, line });
|
|
481
|
+
directiveVerdict = null;
|
|
482
|
+
}
|
|
483
|
+
catch (scanErr) {
|
|
484
|
+
directiveVerdict = scanErr;
|
|
485
|
+
}
|
|
486
|
+
this.directiveVerdicts.set(directiveKey, directiveVerdict);
|
|
412
487
|
}
|
|
413
|
-
|
|
488
|
+
if (directiveVerdict) {
|
|
489
|
+
const directiveErr = directiveVerdict;
|
|
414
490
|
if ((0, SelaError_1.isSelaErrorFrom)(directiveErr, "ASTUpdater") &&
|
|
415
491
|
directiveErr.code === SelaError_1.AST_UPDATER_CODES.HEAL_DISABLED_BY_DIRECTIVE) {
|
|
416
492
|
logger_1.logger.warn(`Skipped by Developer - ${directiveErr.reason}`);
|
|
@@ -434,30 +510,6 @@ CRITICAL: Do NOT return a CSS selector like "#my-select". Return plain text only
|
|
|
434
510
|
}
|
|
435
511
|
throw directiveErr;
|
|
436
512
|
}
|
|
437
|
-
// ── NO_HEAL cache pre-check ──────────────────────────────────────────
|
|
438
|
-
// If this selector was already identified as an application logic/state
|
|
439
|
-
// failure in this run, skip LLM, DNA load, and DOM extraction entirely.
|
|
440
|
-
// One LLM call pays for 50+ identical state-bug failures in large suites.
|
|
441
|
-
const cachedNoHealReason = this.stateManager.selectorBlockReason(elementSelectorOnly);
|
|
442
|
-
if (cachedNoHealReason !== undefined) {
|
|
443
|
-
logger_1.logger.warn(`[Sela] NO_HEAL cache hit — skipping LLM: ${cachedNoHealReason}`);
|
|
444
|
-
HealReportService_1.sharedHealReport.record({
|
|
445
|
-
kind: "FAILED",
|
|
446
|
-
stableId,
|
|
447
|
-
deterministicKey: (0, HealReportService_1.computeEventDeterministicKey)(reportFile, line, elementSelectorOnly),
|
|
448
|
-
sourceFile: reportFile,
|
|
449
|
-
sourceLine: line,
|
|
450
|
-
testTitle,
|
|
451
|
-
timestamp: new Date().toISOString(),
|
|
452
|
-
framePath: [],
|
|
453
|
-
inIframe: false,
|
|
454
|
-
inShadowDom: false,
|
|
455
|
-
oldSelector: elementSelectorOnly,
|
|
456
|
-
reason: `NO_HEAL (cached) — ${cachedNoHealReason}`,
|
|
457
|
-
dnaBefore: null,
|
|
458
|
-
});
|
|
459
|
-
return "";
|
|
460
|
-
}
|
|
461
513
|
// Interception is now active for this call-site — enter HEALING.
|
|
462
514
|
this.stateManager.beginHeal({ filePath, line });
|
|
463
515
|
// Snapshot for report event context (loaded once below, used at all exit paths).
|
|
@@ -945,6 +997,45 @@ Decision rule (overrides any typo/Levenshtein-distance heuristic):
|
|
|
945
997
|
logger_1.logger.info(`[SELA GUARD] Re-prompt escaped the volatile family — proceeding with ` +
|
|
946
998
|
`stable anchor "${fmtSelector(newFullSelector)}".`);
|
|
947
999
|
}
|
|
1000
|
+
// ── Anchor-oscillation gate (P1-7 — matrix ping-pong) ───────────
|
|
1001
|
+
// The token ladder above catches id/class FACTORY rotation; this
|
|
1002
|
+
// catches stable anchors flipping A→B→A across runs — the signature
|
|
1003
|
+
// of i18n matrix legs, feature-flag variants, and canary deploys
|
|
1004
|
+
// serving N concurrent DOM truths. Healing the flip-back would just
|
|
1005
|
+
// arm the next flip (dueling PRs rewriting one line). No re-prompt:
|
|
1006
|
+
// the context conflict is real, the LLM would re-propose the same.
|
|
1007
|
+
const oscillation = (0, SelectorForensics_1.detectAnchorOscillation)({
|
|
1008
|
+
proposal: elementOnlySelector,
|
|
1009
|
+
previousSourceSelector: lastKnownState?.sourceSelector,
|
|
1010
|
+
healHistory: (lastKnownState?.healHistory ?? []).map((h) => h.selector),
|
|
1011
|
+
});
|
|
1012
|
+
if (oscillation.oscillating) {
|
|
1013
|
+
const reason = `ANCHOR OSCILLATION — ${oscillation.reason} Sela refuses to flip ` +
|
|
1014
|
+
`the selector back. The element is Protected until the run ` +
|
|
1015
|
+
`contexts are reconciled: pin a variant-invariant anchor ` +
|
|
1016
|
+
`(data-testid / stable attribute), or exclude this suite leg via ` +
|
|
1017
|
+
`healing.exclude.`;
|
|
1018
|
+
logger_1.logger.warn(`[SELA GUARD] ${reason}`);
|
|
1019
|
+
this.stateManager.failNoHeal({ filePath, line }, elementSelectorOnly, reason);
|
|
1020
|
+
HealReportService_1.sharedHealReport.record({
|
|
1021
|
+
kind: "FAILED",
|
|
1022
|
+
stableId,
|
|
1023
|
+
deterministicKey: (0, HealReportService_1.computeEventDeterministicKey)(reportFile, line, elementSelectorOnly),
|
|
1024
|
+
sourceFile: reportFile,
|
|
1025
|
+
sourceLine: line,
|
|
1026
|
+
testTitle,
|
|
1027
|
+
timestamp: new Date().toISOString(),
|
|
1028
|
+
framePath: successfulPath,
|
|
1029
|
+
inIframe: successfulPath.length > 0,
|
|
1030
|
+
inShadowDom: false,
|
|
1031
|
+
oldSelector: elementSelectorOnly,
|
|
1032
|
+
reason,
|
|
1033
|
+
aiConfidence: aiFix?.confidence,
|
|
1034
|
+
aiExplanation: aiFix?.explanation,
|
|
1035
|
+
dnaBefore: (0, HealReportService_1.summariseSnapshot)(lastKnownStateForReport),
|
|
1036
|
+
});
|
|
1037
|
+
return "";
|
|
1038
|
+
}
|
|
948
1039
|
// ── Step 3.5: Fetch live text (lightweight zero-trust probe) ─
|
|
949
1040
|
const liveText = await this.fetchLiveText(page, elementOnlySelector, successfulPath);
|
|
950
1041
|
const dnaBaselineText = oldText ?? undefined;
|
|
@@ -1854,59 +1945,74 @@ Decision rule (overrides any typo/Levenshtein-distance heuristic):
|
|
|
1854
1945
|
// preserve any tombstones already on disk (a re-heal must never erase
|
|
1855
1946
|
// the record of selectors reviewers rejected).
|
|
1856
1947
|
const alias = this.aliasBuffer.get(key);
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1948
|
+
// P1-4: load → merge → save below is a read-modify-write of shared
|
|
1949
|
+
// ledger state. It runs under the per-key mkdir FileLock so a peer
|
|
1950
|
+
// worker flushing the SAME key (shared POM element exercised from
|
|
1951
|
+
// two workers) cannot interleave — an unlocked interleave is a lost
|
|
1952
|
+
// update that can silently resurrect reviewer-rejected tombstones.
|
|
1953
|
+
// Contended ⇒ skip with a warning; the DNA memo is re-captured on
|
|
1954
|
+
// the next run. staleMs reclaims locks orphaned by a SIGKILLed run.
|
|
1955
|
+
const keyLock = new FileLock_1.FileLock((0, FileLock_1.lockDirFor)(this.snapshotService.filePathFor(key)), { staleMs: 60_000 });
|
|
1956
|
+
const wrote = await keyLock.withLock(async () => {
|
|
1957
|
+
const existingSnap = await this.snapshotService.load(key);
|
|
1958
|
+
let withAlias;
|
|
1959
|
+
if (alias) {
|
|
1960
|
+
withAlias = {
|
|
1961
|
+
...toSave,
|
|
1962
|
+
schemaVersion: 4,
|
|
1871
1963
|
sourceSelector: alias.source,
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
existingSnap.
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1964
|
+
healedSelector: alias.healed,
|
|
1965
|
+
status: "healed",
|
|
1966
|
+
lastHealed: new Date().toISOString(),
|
|
1967
|
+
healContext: { filePath: alias.filePath, line: alias.line },
|
|
1968
|
+
anchor: (0, LedgerAnchor_1.buildLedgerAnchor)({
|
|
1969
|
+
filePath: alias.filePath,
|
|
1970
|
+
line: alias.line,
|
|
1971
|
+
sourceSelector: alias.source,
|
|
1972
|
+
}) ?? existingSnap?.anchor,
|
|
1973
|
+
review: { state: "PENDING_REVIEW" },
|
|
1974
|
+
tombstones: existingSnap?.tombstones,
|
|
1975
|
+
// Rolling heal log (capped) — SelectorForensics S0 proves an
|
|
1976
|
+
// id-prefix ping-pong deterministically across sessions.
|
|
1977
|
+
healHistory: [
|
|
1978
|
+
...(existingSnap?.healHistory ?? []),
|
|
1979
|
+
{ selector: alias.healed, at: new Date().toISOString() },
|
|
1980
|
+
].slice(-10),
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
else if (existingSnap?.healedSelector) {
|
|
1984
|
+
// No new heal this run, but the ledger already holds an alias —
|
|
1985
|
+
// typically a RESOLVED_VIA_DNA run whose post-success DNA memo
|
|
1986
|
+
// landed in dnaBuffer. A bare `toSave` write would WIPE the alias
|
|
1987
|
+
// ("alias amnesia": heal → seeded pass → alias lost → re-heal on
|
|
1988
|
+
// run 3 — an LLM-cost idempotency leak). Refresh the structural
|
|
1989
|
+
// DNA, preserve the durable ledger fields verbatim.
|
|
1990
|
+
withAlias = {
|
|
1991
|
+
...toSave,
|
|
1992
|
+
schemaVersion: 4,
|
|
1993
|
+
sourceSelector: existingSnap.sourceSelector,
|
|
1994
|
+
healedSelector: existingSnap.healedSelector,
|
|
1995
|
+
status: existingSnap.status,
|
|
1996
|
+
lastHealed: existingSnap.lastHealed,
|
|
1997
|
+
healContext: existingSnap.healContext,
|
|
1998
|
+
anchor: existingSnap.anchor,
|
|
1999
|
+
review: existingSnap.review,
|
|
2000
|
+
tombstones: existingSnap.tombstones,
|
|
2001
|
+
healHistory: existingSnap.healHistory,
|
|
2002
|
+
visualSnapshot: toSave.visualSnapshot ??
|
|
2003
|
+
existingSnap.visualSnapshot,
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
else {
|
|
2007
|
+
withAlias = toSave;
|
|
2008
|
+
}
|
|
2009
|
+
await this.snapshotService.save(key, withAlias);
|
|
2010
|
+
return true;
|
|
2011
|
+
});
|
|
2012
|
+
if (!wrote) {
|
|
2013
|
+
logger_1.logger.warn(`Ledger flush skipped for '${key}' — a peer worker holds the ` +
|
|
2014
|
+
`lock; the snapshot will be re-captured on the next run.`);
|
|
1908
2015
|
}
|
|
1909
|
-
await this.snapshotService.save(key, withAlias);
|
|
1910
2016
|
}
|
|
1911
2017
|
this.dnaBuffer.clear();
|
|
1912
2018
|
this.healMetaBuffer.clear();
|
|
@@ -1921,13 +2027,16 @@ Decision rule (overrides any typo/Levenshtein-distance heuristic):
|
|
|
1921
2027
|
const healedEvents = HealReportService_1.sharedHealReport.getHealedEvents();
|
|
1922
2028
|
const protectedEvents = HealReportService_1.sharedHealReport.getProtectedEvents();
|
|
1923
2029
|
if (HealReportService_1.sharedHealReport.hasContent()) {
|
|
1924
|
-
// SELA-5
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1928
|
-
//
|
|
1929
|
-
|
|
1930
|
-
|
|
2030
|
+
// SELA-5 single-writer invariant: the write path is decided by PROCESS
|
|
2031
|
+
// ROLE, never by environment. A worker process shares one cwd with its
|
|
2032
|
+
// peers, so a direct flushToDisk is a last-writer-wins race on
|
|
2033
|
+
// sela-report.json (and the regression manifest) — locally AND in CI:
|
|
2034
|
+
// real CI runners execute `--workers=N` inside every shard. Workers
|
|
2035
|
+
// therefore ALWAYS write a private shard; the single main-process
|
|
2036
|
+
// reporter fans them in at onEnd (or `sela merge` across sharded
|
|
2037
|
+
// machines). Only a non-worker process may flush the shared files.
|
|
2038
|
+
const inWorkerProcess = process.env["TEST_WORKER_INDEX"] !== undefined;
|
|
2039
|
+
if (inWorkerProcess) {
|
|
1931
2040
|
try {
|
|
1932
2041
|
const shard = HealReportService_1.sharedHealReport.flushShard(process.cwd());
|
|
1933
2042
|
if (shard) {
|
|
@@ -35,4 +35,35 @@ export declare function analyzeSelectorVolatility(input: VolatilityInput): Volat
|
|
|
35
35
|
* text is guidance; this predicate is what actually breaks the ping-pong.
|
|
36
36
|
*/
|
|
37
37
|
export declare function proposalViolatesVolatility(proposedSelector: string, verdict: VolatilityVerdict): boolean;
|
|
38
|
+
export interface AnchorOscillationInput {
|
|
39
|
+
/** The LLM's proposed replacement selector (element-only form). */
|
|
40
|
+
proposal: string;
|
|
41
|
+
/** Ledger `sourceSelector` — what this element was last healed AWAY from. */
|
|
42
|
+
previousSourceSelector: string | undefined;
|
|
43
|
+
/** Ledger `healHistory` selectors, oldest → newest. */
|
|
44
|
+
healHistory: string[];
|
|
45
|
+
}
|
|
46
|
+
export interface AnchorOscillationVerdict {
|
|
47
|
+
oscillating: boolean;
|
|
48
|
+
reason: string;
|
|
49
|
+
/** The historical selector the proposal flips back to. */
|
|
50
|
+
flippedBackTo?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Detects the A→B→A flip the token-level volatility ladder cannot see:
|
|
54
|
+
* stable, human-readable anchors oscillating because the application
|
|
55
|
+
* legitimately serves N concurrent DOM truths — i18n matrix legs, feature
|
|
56
|
+
* flag / A-B variants, canary deploys. A proposal that flips BACK to a
|
|
57
|
+
* selector this element was already healed away from is context variance,
|
|
58
|
+
* not selector drift; healing it would just arm the next flip (and, in CI,
|
|
59
|
+
* dueling PRs rewriting the same line forever).
|
|
60
|
+
*
|
|
61
|
+
* Two deterministic signals, evidence strongest first:
|
|
62
|
+
* 1. proposal == ledger `sourceSelector` — the exact selector the last
|
|
63
|
+
* heal replaced (catches the FIRST re-flip).
|
|
64
|
+
* 2. proposal == an OLDER `healHistory` entry — healed in previously,
|
|
65
|
+
* later replaced. The LATEST entry is intentionally exempt: it is the
|
|
66
|
+
* element's current selector, owned by the same-selector guards.
|
|
67
|
+
*/
|
|
68
|
+
export declare function detectAnchorOscillation(input: AnchorOscillationInput): AnchorOscillationVerdict;
|
|
38
69
|
//# sourceMappingURL=SelectorForensics.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectorForensics.d.ts","sourceRoot":"","sources":["../../src/errors/SelectorForensics.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACpD;;+DAE2D;IAC3D,oBAAoB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACxD,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAyFD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,eAAe,GACrB,iBAAiB,CA+KnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAmBT"}
|
|
1
|
+
{"version":3,"file":"SelectorForensics.d.ts","sourceRoot":"","sources":["../../src/errors/SelectorForensics.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACpD;;+DAE2D;IAC3D,oBAAoB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACxD,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAyFD,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,eAAe,GACrB,iBAAiB,CA+KnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAmBT;AAMD,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,uDAAuD;IACvD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAQD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,sBAAsB,GAC5B,wBAAwB,CAmC1B"}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
30
|
exports.analyzeSelectorVolatility = analyzeSelectorVolatility;
|
|
31
31
|
exports.proposalViolatesVolatility = proposalViolatesVolatility;
|
|
32
|
+
exports.detectAnchorOscillation = detectAnchorOscillation;
|
|
32
33
|
const INERT = {
|
|
33
34
|
volatile: false,
|
|
34
35
|
volatileTokens: [],
|
|
@@ -280,3 +281,55 @@ function proposalViolatesVolatility(proposedSelector, verdict) {
|
|
|
280
281
|
}
|
|
281
282
|
return false;
|
|
282
283
|
}
|
|
284
|
+
const OSC_INERT = { oscillating: false, reason: "" };
|
|
285
|
+
function normalizeSelector(s) {
|
|
286
|
+
return s.replace(/\s+/g, " ").trim();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Detects the A→B→A flip the token-level volatility ladder cannot see:
|
|
290
|
+
* stable, human-readable anchors oscillating because the application
|
|
291
|
+
* legitimately serves N concurrent DOM truths — i18n matrix legs, feature
|
|
292
|
+
* flag / A-B variants, canary deploys. A proposal that flips BACK to a
|
|
293
|
+
* selector this element was already healed away from is context variance,
|
|
294
|
+
* not selector drift; healing it would just arm the next flip (and, in CI,
|
|
295
|
+
* dueling PRs rewriting the same line forever).
|
|
296
|
+
*
|
|
297
|
+
* Two deterministic signals, evidence strongest first:
|
|
298
|
+
* 1. proposal == ledger `sourceSelector` — the exact selector the last
|
|
299
|
+
* heal replaced (catches the FIRST re-flip).
|
|
300
|
+
* 2. proposal == an OLDER `healHistory` entry — healed in previously,
|
|
301
|
+
* later replaced. The LATEST entry is intentionally exempt: it is the
|
|
302
|
+
* element's current selector, owned by the same-selector guards.
|
|
303
|
+
*/
|
|
304
|
+
function detectAnchorOscillation(input) {
|
|
305
|
+
const proposal = normalizeSelector(input.proposal ?? "");
|
|
306
|
+
if (!proposal)
|
|
307
|
+
return OSC_INERT;
|
|
308
|
+
const prevSource = input.previousSourceSelector
|
|
309
|
+
? normalizeSelector(input.previousSourceSelector)
|
|
310
|
+
: undefined;
|
|
311
|
+
if (prevSource && proposal === prevSource) {
|
|
312
|
+
return {
|
|
313
|
+
oscillating: true,
|
|
314
|
+
flippedBackTo: prevSource,
|
|
315
|
+
reason: `Proposal "${proposal}" is the exact selector this element was healed away from ` +
|
|
316
|
+
`(A→B→A flip). This pattern means the application serves BOTH selector variants ` +
|
|
317
|
+
`across runs (i18n matrix leg, feature flag / A-B variant, or canary deploy) — ` +
|
|
318
|
+
`context variance, not selector drift.`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const history = (input.healHistory ?? []).map(normalizeSelector);
|
|
322
|
+
// Exclude the newest entry — it IS the current selector.
|
|
323
|
+
for (const past of history.slice(0, -1)) {
|
|
324
|
+
if (past && proposal === past) {
|
|
325
|
+
return {
|
|
326
|
+
oscillating: true,
|
|
327
|
+
flippedBackTo: past,
|
|
328
|
+
reason: `Proposal "${proposal}" was already healed in previously and later replaced ` +
|
|
329
|
+
`(ledger healHistory). Re-healing it re-arms the ping-pong — the application ` +
|
|
330
|
+
`likely serves multiple DOM variants across runs (i18n / flags / canary).`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return OSC_INERT;
|
|
335
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -219,11 +219,14 @@ class SelaReporter {
|
|
|
219
219
|
this.logger.error(`[Sela] reporter commitUpdates failed: ${msg}`);
|
|
220
220
|
}
|
|
221
221
|
// ── SELA-5: fan in per-worker heal shards ───────────────────
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
// main-process writer, so it
|
|
225
|
-
// final sela-report.{html,json}
|
|
226
|
-
|
|
222
|
+
// Workers ALWAYS write one shard each (commitUpdates) — locally and in
|
|
223
|
+
// CI, since real CI runners execute `--workers=N` inside every shard.
|
|
224
|
+
// The reporter is the single main-process writer, so it merges them into
|
|
225
|
+
// the final sela-report.{html,json} whenever this machine is the whole
|
|
226
|
+
// run. The one exception is a SHARDED CI run (`--shard=N/M`): the raw
|
|
227
|
+
// shards must survive for the artifact upload step, and the downstream
|
|
228
|
+
// cross-machine `sela merge` job owns the fan-in (P0-1).
|
|
229
|
+
if (!this.isCi() || !this._isShardedRun) {
|
|
227
230
|
// Capture raw bypass events from shards BEFORE merge — the merge runs
|
|
228
231
|
// normalizeEventsForReport() which prettifies internal:role=... selector
|
|
229
232
|
// strings to page.getByRole(...) form. The manifest needs the raw
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface HealCircuitBreakerOptions {
|
|
2
|
+
maxAttempts?: number;
|
|
3
|
+
windowMs?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class HealCircuitBreaker {
|
|
6
|
+
private readonly opts;
|
|
7
|
+
/** Injectable clock for tests. */
|
|
8
|
+
private readonly now;
|
|
9
|
+
private attempts;
|
|
10
|
+
private tripped;
|
|
11
|
+
private trippedAtCount;
|
|
12
|
+
private warned;
|
|
13
|
+
constructor(opts?: HealCircuitBreakerOptions,
|
|
14
|
+
/** Injectable clock for tests. */
|
|
15
|
+
now?: () => number);
|
|
16
|
+
private maxAttempts;
|
|
17
|
+
private windowMs;
|
|
18
|
+
/** Record one heal attempt (called on entry to the LLM heal pipeline). */
|
|
19
|
+
recordAttempt(): void;
|
|
20
|
+
isTripped(): boolean;
|
|
21
|
+
reason(): string;
|
|
22
|
+
/** One-shot warning latch (mirrors HealBudget.shouldWarnOnce). */
|
|
23
|
+
shouldWarnOnce(): boolean;
|
|
24
|
+
/** Test isolation / new session. */
|
|
25
|
+
reset(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare const sharedHealCircuitBreaker: HealCircuitBreaker;
|
|
28
|
+
//# sourceMappingURL=HealCircuitBreaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HealCircuitBreaker.d.ts","sourceRoot":"","sources":["../../src/services/HealCircuitBreaker.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmBD,qBAAa,kBAAkB;IAO3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,MAAM,CAAS;gBAGJ,IAAI,GAAE,yBAA8B;IACrD,kCAAkC;IACjB,GAAG,GAAE,MAAM,MAAyB;IAGvD,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,QAAQ;IAIhB,0EAA0E;IAC1E,aAAa,IAAI,IAAI;IAerB,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,MAAM;IAWhB,kEAAkE;IAClE,cAAc,IAAI,OAAO;IAMzB,oCAAoC;IACpC,KAAK,IAAI,IAAI;CAMd;AAED,eAAO,MAAM,wBAAwB,oBAA2B,CAAC"}
|