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.
Files changed (33) hide show
  1. package/dist/engine/SelaEngine.d.ts +1 -0
  2. package/dist/engine/SelaEngine.d.ts.map +1 -1
  3. package/dist/engine/SelaEngine.js +194 -85
  4. package/dist/errors/SelectorForensics.d.ts +31 -0
  5. package/dist/errors/SelectorForensics.d.ts.map +1 -1
  6. package/dist/errors/SelectorForensics.js +53 -0
  7. package/dist/reporter/SelaReporter.d.ts.map +1 -1
  8. package/dist/reporter/SelaReporter.js +8 -5
  9. package/dist/services/HealCircuitBreaker.d.ts +28 -0
  10. package/dist/services/HealCircuitBreaker.d.ts.map +1 -0
  11. package/dist/services/HealCircuitBreaker.js +103 -0
  12. package/dist/services/PRAutomationService.d.ts +8 -0
  13. package/dist/services/PRAutomationService.d.ts.map +1 -1
  14. package/dist/services/PRAutomationService.js +103 -11
  15. package/dist/services/SnapshotService.d.ts +2 -0
  16. package/dist/services/SnapshotService.d.ts.map +1 -1
  17. package/dist/services/SnapshotService.js +4 -0
  18. package/dist/services/vcs/GitHubProvider.d.ts +4 -0
  19. package/dist/services/vcs/GitHubProvider.d.ts.map +1 -1
  20. package/dist/services/vcs/GitHubProvider.js +13 -0
  21. package/dist/services/vcs/GitLabProvider.d.ts +4 -0
  22. package/dist/services/vcs/GitLabProvider.d.ts.map +1 -1
  23. package/dist/services/vcs/GitLabProvider.js +13 -0
  24. package/dist/services/vcs/VCSFailureDiagnosis.d.ts +5 -0
  25. package/dist/services/vcs/VCSFailureDiagnosis.d.ts.map +1 -0
  26. package/dist/services/vcs/VCSFailureDiagnosis.js +77 -0
  27. package/dist/services/vcs/VCSProvider.d.ts +11 -0
  28. package/dist/services/vcs/VCSProvider.d.ts.map +1 -1
  29. package/dist/utils/DOMUtils.d.ts.map +1 -1
  30. package/dist/utils/DOMUtils.js +51 -9
  31. package/dist/utils/atomicWrite.d.ts.map +1 -1
  32. package/dist/utils/atomicWrite.js +42 -2
  33. package/package.json +1 -1
@@ -14,6 +14,7 @@ export declare class SelaEngine {
14
14
  private testTitleBuffer;
15
15
  private capturedSelectorSnapshots;
16
16
  private bypassedSelectorCache;
17
+ private directiveVerdicts;
17
18
  private readonly stateManager;
18
19
  private readonly aliasBuffer;
19
20
  constructor();
@@ -1 +1 @@
1
- {"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SelaEngine.d.ts","sourceRoot":"","sources":["../../src/engine/SelaEngine.ts"],"names":[],"mappings":"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"}
@@ -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
- try {
411
- this.astDirectiveScanner.enforceFailFastDirective({ filePath, line });
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
- catch (directiveErr) {
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
- const existingSnap = await this.snapshotService.load(key);
1858
- let withAlias;
1859
- if (alias) {
1860
- withAlias = {
1861
- ...toSave,
1862
- schemaVersion: 4,
1863
- sourceSelector: alias.source,
1864
- healedSelector: alias.healed,
1865
- status: "healed",
1866
- lastHealed: new Date().toISOString(),
1867
- healContext: { filePath: alias.filePath, line: alias.line },
1868
- anchor: (0, LedgerAnchor_1.buildLedgerAnchor)({
1869
- filePath: alias.filePath,
1870
- line: alias.line,
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
- }) ?? existingSnap?.anchor,
1873
- review: { state: "PENDING_REVIEW" },
1874
- tombstones: existingSnap?.tombstones,
1875
- // Rolling heal log (capped) SelectorForensics S0 proves an
1876
- // id-prefix ping-pong deterministically across sessions.
1877
- healHistory: [
1878
- ...(existingSnap?.healHistory ?? []),
1879
- { selector: alias.healed, at: new Date().toISOString() },
1880
- ].slice(-10),
1881
- };
1882
- }
1883
- else if (existingSnap?.healedSelector) {
1884
- // No new heal this run, but the ledger already holds an alias —
1885
- // typically a RESOLVED_VIA_DNA run whose post-success DNA memo
1886
- // landed in dnaBuffer. A bare `toSave` write would WIPE the alias
1887
- // ("alias amnesia": heal → seeded pass → alias lost re-heal on
1888
- // run 3 — an LLM-cost idempotency leak). Refresh the structural
1889
- // DNA, preserve the durable ledger fields verbatim.
1890
- withAlias = {
1891
- ...toSave,
1892
- schemaVersion: 4,
1893
- sourceSelector: existingSnap.sourceSelector,
1894
- healedSelector: existingSnap.healedSelector,
1895
- status: existingSnap.status,
1896
- lastHealed: existingSnap.lastHealed,
1897
- healContext: existingSnap.healContext,
1898
- anchor: existingSnap.anchor,
1899
- review: existingSnap.review,
1900
- tombstones: existingSnap.tombstones,
1901
- healHistory: existingSnap.healHistory,
1902
- visualSnapshot: toSave.visualSnapshot ??
1903
- existingSnap.visualSnapshot,
1904
- };
1905
- }
1906
- else {
1907
- withAlias = toSave;
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: in a LOCAL multi-worker run every worker shares one cwd, so a
1925
- // direct flushToDisk is a last-writer-wins race on sela-report.json.
1926
- // Each worker instead writes a private shard; the single main-process
1927
- // reporter fans them in at onEnd. CI keeps the direct flushit runs
1928
- // one worker per sharded machine and merges across machines separately.
1929
- const inLocalWorker = !isCiEnvironment() && process.env["TEST_WORKER_INDEX"] !== undefined;
1930
- if (inLocalWorker) {
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
- // Local multi-worker runs write one shard per worker (commitUpdates) to
223
- // avoid clobbering a shared report file. The reporter is the single
224
- // main-process writer, so it is the safe place to merge them into the
225
- // final sela-report.{html,json}. CI uses cross-machine `sela merge`.
226
- if (!this.isCi()) {
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"}