sela-core 0.1.0-alpha.1
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 +103 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +158 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +166 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- 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/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/showReport.d.ts +3 -0
- package/dist/cli/commands/showReport.d.ts.map +1 -0
- package/dist/cli/commands/showReport.js +82 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +131 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +94 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +77 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +295 -0
- package/dist/config/ConfigLoader.d.ts +16 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +185 -0
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +80 -0
- package/dist/config/PathScopeGuard.d.ts +42 -0
- package/dist/config/PathScopeGuard.d.ts.map +1 -0
- package/dist/config/PathScopeGuard.js +192 -0
- package/dist/config/SelaConfig.d.ts +178 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +141 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +99 -0
- package/dist/engine/SelaEngine.d.ts +57 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -0
- package/dist/engine/SelaEngine.js +1146 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/errors/SelaError.d.ts +146 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +168 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +246 -0
- package/dist/fixtures/index.d.ts +30 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +765 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +54 -0
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/reporter/SelaReporter.d.ts +165 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +517 -0
- package/dist/services/ASTSourceUpdater.d.ts +143 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3514 -0
- 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/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +107 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +570 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +135 -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/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +366 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +217 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealBudget.d.ts +32 -0
- package/dist/services/HealBudget.d.ts.map +1 -0
- package/dist/services/HealBudget.js +85 -0
- package/dist/services/HealReportService.d.ts +161 -0
- package/dist/services/HealReportService.d.ts.map +1 -0
- package/dist/services/HealReportService.js +1039 -0
- package/dist/services/HealingAdvisory.d.ts +30 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +51 -0
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +404 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +73 -0
- package/dist/services/IntentAuditor.d.ts +56 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +326 -0
- package/dist/services/InteractiveReview.d.ts +15 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +128 -0
- package/dist/services/LLMService.d.ts +121 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +475 -0
- package/dist/services/MutationApplier.d.ts +69 -0
- package/dist/services/MutationApplier.d.ts.map +1 -0
- package/dist/services/MutationApplier.js +360 -0
- package/dist/services/PRAutomationService.d.ts +50 -0
- package/dist/services/PRAutomationService.d.ts.map +1 -0
- package/dist/services/PRAutomationService.js +760 -0
- package/dist/services/PendingPromptLedger.d.ts +51 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +205 -0
- package/dist/services/ReportGenerator.d.ts +156 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +278 -0
- 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 +112 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +87 -0
- package/dist/services/Sanitizer.d.ts +28 -0
- package/dist/services/Sanitizer.d.ts.map +1 -0
- package/dist/services/Sanitizer.js +142 -0
- package/dist/services/SanitizingAnthropic.d.ts +19 -0
- package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
- package/dist/services/SanitizingAnthropic.js +84 -0
- 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.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +352 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +165 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1091 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +334 -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/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +200 -0
- package/dist/services/safety/PipelineContext.d.ts +63 -0
- package/dist/services/safety/PipelineContext.d.ts.map +1 -0
- package/dist/services/safety/PipelineContext.js +34 -0
- package/dist/services/safety/roleClassifier.d.ts +4 -0
- package/dist/services/safety/roleClassifier.d.ts.map +1 -0
- package/dist/services/safety/roleClassifier.js +233 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
- package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
- package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
- package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
- package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
- package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
- package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
- package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
- package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
- package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
- package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
- package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
- package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
- package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
- package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
- package/dist/services/safety/rules/VisibilityRule.js +53 -0
- package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
- package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
- package/dist/services/safety/rules/index.d.ts +3 -0
- package/dist/services/safety/rules/index.d.ts.map +1 -0
- package/dist/services/safety/rules/index.js +34 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +32 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +49 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +466 -0
- package/dist/utils/FileLock.d.ts +44 -0
- package/dist/utils/FileLock.d.ts.map +1 -0
- package/dist/utils/FileLock.js +189 -0
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +382 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/utils/SummaryTable.d.ts +17 -0
- package/dist/utils/SummaryTable.d.ts.map +1 -0
- package/dist/utils/SummaryTable.js +37 -0
- package/dist/utils/atomicWrite.d.ts +9 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +83 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +123 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- package/package.json +71 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/PRAutomationService.ts
|
|
3
|
+
//
|
|
4
|
+
// Git-flow automation layer. Runs ONCE per session at SelaEngine.commitUpdates().
|
|
5
|
+
// Responsibilities:
|
|
6
|
+
// 1. Detect the active branch (CI env vars first, git CLI fallback).
|
|
7
|
+
// 2. Resolve the EFFECTIVE strategy via trust-aware downgrade.
|
|
8
|
+
// 3. Execute the strategy: directCommit | pr | draftPR | readOnly.
|
|
9
|
+
// 4. Handle PROTECTED events per onBugDetected policy.
|
|
10
|
+
//
|
|
11
|
+
// Hard requirements:
|
|
12
|
+
// - GitHub-only in v1 (uses `gh` CLI).
|
|
13
|
+
// - Never throws into the caller. Failures degrade to warnings + readOnly.
|
|
14
|
+
// - Detached HEAD or missing `gh` → skip git ops, emit clear log.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.detectBranch = detectBranch;
|
|
50
|
+
exports.decideEffectiveStrategy = decideEffectiveStrategy;
|
|
51
|
+
exports.fmtPct = fmtPct;
|
|
52
|
+
exports.execute = execute;
|
|
53
|
+
exports.handleBugDetected = handleBugDetected;
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const child_process_1 = require("child_process");
|
|
57
|
+
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
58
|
+
const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
59
|
+
const logger_1 = require("../utils/logger");
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
61
|
+
// SHELL HELPERS
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
63
|
+
function sh(cmd, cwd) {
|
|
64
|
+
try {
|
|
65
|
+
const stdout = (0, child_process_1.execSync)(cmd, {
|
|
66
|
+
cwd,
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
69
|
+
});
|
|
70
|
+
return { ok: true, stdout: stdout.toString(), stderr: "" };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
stdout: err?.stdout?.toString() ?? "",
|
|
76
|
+
stderr: err?.stderr?.toString() ?? err?.message ?? "unknown error",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function ghAvailable(cwd) {
|
|
81
|
+
return sh("gh --version", cwd).ok;
|
|
82
|
+
}
|
|
83
|
+
function gitAvailable(cwd) {
|
|
84
|
+
return sh("git --version", cwd).ok;
|
|
85
|
+
}
|
|
86
|
+
function isDetachedHead(cwd) {
|
|
87
|
+
return !sh("git symbolic-ref HEAD", cwd).ok;
|
|
88
|
+
}
|
|
89
|
+
const SELA_BRANCH_PREFIX = "sela/heal-";
|
|
90
|
+
function isSelaHealBranch(branch) {
|
|
91
|
+
return !!branch && branch.startsWith(SELA_BRANCH_PREFIX);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the repo's default integration branch.
|
|
95
|
+
* 1. origin/HEAD symbolic-ref (set by `git clone`).
|
|
96
|
+
* 2. First existing local ref among: main, master, develop, dev.
|
|
97
|
+
* Returns null if none found - caller must handle that.
|
|
98
|
+
*/
|
|
99
|
+
function findDefaultBranch(cwd) {
|
|
100
|
+
const headRef = sh("git symbolic-ref refs/remotes/origin/HEAD", cwd);
|
|
101
|
+
if (headRef.ok) {
|
|
102
|
+
const m = headRef.stdout.trim().match(/refs\/remotes\/origin\/(.+)$/);
|
|
103
|
+
if (m)
|
|
104
|
+
return m[1];
|
|
105
|
+
}
|
|
106
|
+
for (const candidate of ["main", "master", "develop", "dev"]) {
|
|
107
|
+
if (sh(`git rev-parse --verify --quiet ${candidate}`, cwd).ok) {
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
114
|
+
// TEMPLATING
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
116
|
+
function render(template, vars) {
|
|
117
|
+
return template.replace(/\$\{(\w+)\}/g, (_m, k) => k in vars ? String(vars[k]) : `\${${k}}`);
|
|
118
|
+
}
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
120
|
+
// BRANCH DETECTION
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
122
|
+
function detectBranch(cwd = process.cwd()) {
|
|
123
|
+
// GitHub Actions exposes both REF_NAME (push) and HEAD_REF (PR).
|
|
124
|
+
const ghHeadRef = process.env.GITHUB_HEAD_REF; // PR source branch
|
|
125
|
+
const ghRefName = process.env.GITHUB_REF_NAME; // current branch on push, or "<n>/merge" on PR
|
|
126
|
+
const ghEventName = process.env.GITHUB_EVENT_NAME;
|
|
127
|
+
const ghRef = process.env.GITHUB_REF; // "refs/pull/123/merge"
|
|
128
|
+
// Prefer HEAD_REF on PRs - REF_NAME is the synthetic merge ref.
|
|
129
|
+
if (ghHeadRef && ghHeadRef.length > 0 && !isSelaHealBranch(ghHeadRef)) {
|
|
130
|
+
const prNumber = (() => {
|
|
131
|
+
const m = ghRef?.match(/refs\/pull\/(\d+)\//);
|
|
132
|
+
return m ? Number(m[1]) : null;
|
|
133
|
+
})();
|
|
134
|
+
return { branch: ghHeadRef, isPR: true, prNumber, source: "env" };
|
|
135
|
+
}
|
|
136
|
+
if (ghRefName &&
|
|
137
|
+
ghRefName.length > 0 &&
|
|
138
|
+
!ghRefName.endsWith("/merge") &&
|
|
139
|
+
!isSelaHealBranch(ghRefName)) {
|
|
140
|
+
return {
|
|
141
|
+
branch: ghRefName,
|
|
142
|
+
isPR: ghEventName === "pull_request",
|
|
143
|
+
prNumber: null,
|
|
144
|
+
source: "env",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// GitLab CI fallback
|
|
148
|
+
const gitlabRef = process.env.CI_COMMIT_REF_NAME;
|
|
149
|
+
if (gitlabRef && gitlabRef.length > 0 && !isSelaHealBranch(gitlabRef)) {
|
|
150
|
+
return { branch: gitlabRef, isPR: false, prNumber: null, source: "env" };
|
|
151
|
+
}
|
|
152
|
+
// Generic Jenkins / local fallback
|
|
153
|
+
const jenkinsBranch = process.env.GIT_BRANCH;
|
|
154
|
+
if (jenkinsBranch && jenkinsBranch.length > 0) {
|
|
155
|
+
const cleaned = jenkinsBranch.replace(/^origin\//, "");
|
|
156
|
+
if (!isSelaHealBranch(cleaned)) {
|
|
157
|
+
return { branch: cleaned, isPR: false, prNumber: null, source: "env" };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Local: git CLI
|
|
161
|
+
if (!gitAvailable(cwd)) {
|
|
162
|
+
return { branch: null, isPR: false, prNumber: null, source: "none" };
|
|
163
|
+
}
|
|
164
|
+
const r = sh("git rev-parse --abbrev-ref HEAD", cwd);
|
|
165
|
+
if (!r.ok)
|
|
166
|
+
return { branch: null, isPR: false, prNumber: null, source: "none" };
|
|
167
|
+
const localBranch = r.stdout.trim();
|
|
168
|
+
if (!localBranch || localBranch === "HEAD") {
|
|
169
|
+
return { branch: null, isPR: false, prNumber: null, source: "none" };
|
|
170
|
+
}
|
|
171
|
+
// ── Sela self-branch guard ─────────────────────────────────────────
|
|
172
|
+
// If we're sitting on a previously-created `sela/heal-*` branch (typical
|
|
173
|
+
// local-rerun scenario), do NOT use it as the PR base. Fall back to the
|
|
174
|
+
// repo's default integration branch (main / master / develop) so the new
|
|
175
|
+
// PR targets a real branch, not another sela patch branch.
|
|
176
|
+
if (isSelaHealBranch(localBranch)) {
|
|
177
|
+
const fallback = findDefaultBranch(cwd);
|
|
178
|
+
if (fallback) {
|
|
179
|
+
logger_1.logger.warn(`PR: HEAD is on '${localBranch}' (sela patch branch) - ` +
|
|
180
|
+
`using '${fallback}' as PR base instead.`);
|
|
181
|
+
return { branch: fallback, isPR: false, prNumber: null, source: "git" };
|
|
182
|
+
}
|
|
183
|
+
logger_1.logger.warn(`PR: HEAD is on '${localBranch}' and no default branch found - ` +
|
|
184
|
+
`skipping PR automation to avoid self-targeting.`);
|
|
185
|
+
return { branch: null, isPR: false, prNumber: null, source: "none" };
|
|
186
|
+
}
|
|
187
|
+
return { branch: localBranch, isPR: false, prNumber: null, source: "git" };
|
|
188
|
+
}
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
190
|
+
// STRATEGY DECISION
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
192
|
+
function decideEffectiveStrategy(cfg, branch, heals) {
|
|
193
|
+
// Compute MIN across only the heals that reported a finite numeric
|
|
194
|
+
// confidence. When *no* heal carried one, the result is `undefined` -
|
|
195
|
+
// surfaced as "n/a" in reports rather than synthesised as 100% (which
|
|
196
|
+
// would silently mask the absence of evidence) or 0% (which would
|
|
197
|
+
// incorrectly force a downgrade for every clean run on Sonnet).
|
|
198
|
+
const aiVals = heals
|
|
199
|
+
.map((h) => h.aiConfidence)
|
|
200
|
+
.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
201
|
+
const audVals = heals
|
|
202
|
+
.map((h) => h.auditor?.confidence)
|
|
203
|
+
.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
204
|
+
const minAi = aiVals.length > 0 ? Math.min(...aiVals) : undefined;
|
|
205
|
+
const minAud = audVals.length > 0 ? Math.min(...audVals) : undefined;
|
|
206
|
+
const baseDecision = {
|
|
207
|
+
effective: cfg.strategy,
|
|
208
|
+
configured: cfg.strategy,
|
|
209
|
+
downgradeReason: null,
|
|
210
|
+
minAiConfidence: minAi,
|
|
211
|
+
minAuditorConfidence: minAud,
|
|
212
|
+
};
|
|
213
|
+
if (cfg.strategy !== "directCommit")
|
|
214
|
+
return baseDecision;
|
|
215
|
+
// Rule 1: protected branch → forced downgrade
|
|
216
|
+
if (branch && cfg.protectedBranches.includes(branch)) {
|
|
217
|
+
return {
|
|
218
|
+
...baseDecision,
|
|
219
|
+
effective: cfg.reviewThresholds.downgradeTo,
|
|
220
|
+
downgradeReason: "PROTECTED_BRANCH",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Rule 2: low AI confidence - only triggers when the measured min is
|
|
224
|
+
// actually below the threshold. Missing confidence is "unknown", not
|
|
225
|
+
// "zero", so we do NOT downgrade purely on absence.
|
|
226
|
+
if (cfg.reviewThresholds.minConfidenceForDirectCommit > 0 &&
|
|
227
|
+
typeof minAi === "number" &&
|
|
228
|
+
minAi < cfg.reviewThresholds.minConfidenceForDirectCommit) {
|
|
229
|
+
return {
|
|
230
|
+
...baseDecision,
|
|
231
|
+
effective: cfg.reviewThresholds.downgradeTo,
|
|
232
|
+
downgradeReason: "LOW_CONFIDENCE",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// Rule 3: low auditor score - same "absence ≠ zero" rule as Rule 2.
|
|
236
|
+
if (cfg.reviewThresholds.minAuditorScoreForDirectCommit > 0 &&
|
|
237
|
+
typeof minAud === "number" &&
|
|
238
|
+
minAud < cfg.reviewThresholds.minAuditorScoreForDirectCommit) {
|
|
239
|
+
return {
|
|
240
|
+
...baseDecision,
|
|
241
|
+
effective: cfg.reviewThresholds.downgradeTo,
|
|
242
|
+
downgradeReason: "LOW_AUDITOR",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return baseDecision;
|
|
246
|
+
}
|
|
247
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
248
|
+
// PR BODY RENDERING
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
250
|
+
function fmtPct(v) {
|
|
251
|
+
return typeof v === "number" && Number.isFinite(v) ? `${v}%` : "n/a";
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Strip the file-level preamble from a unified diff so the GitHub Markdown
|
|
255
|
+
* renderer doesn't show `--- a/file` / `+++ b/file` as red/green lines (which
|
|
256
|
+
* read as fake deletions/additions in a ```diff fence). Keeps the `@@` hunk
|
|
257
|
+
* header and every -/+/space content row intact, so the rendered block is
|
|
258
|
+
* still syntactically a valid unified diff that GitHub highlights correctly.
|
|
259
|
+
*/
|
|
260
|
+
function stripDiffPreamble(raw) {
|
|
261
|
+
return raw
|
|
262
|
+
.split(/\r?\n/)
|
|
263
|
+
.filter((ln) => {
|
|
264
|
+
if (ln.startsWith("diff "))
|
|
265
|
+
return false;
|
|
266
|
+
if (ln.startsWith("index "))
|
|
267
|
+
return false;
|
|
268
|
+
if (ln.startsWith("--- "))
|
|
269
|
+
return false;
|
|
270
|
+
if (ln.startsWith("+++ "))
|
|
271
|
+
return false;
|
|
272
|
+
if (ln.startsWith("new file mode"))
|
|
273
|
+
return false;
|
|
274
|
+
if (ln.startsWith("deleted file mode"))
|
|
275
|
+
return false;
|
|
276
|
+
if (ln.startsWith("similarity "))
|
|
277
|
+
return false;
|
|
278
|
+
if (ln.startsWith("rename "))
|
|
279
|
+
return false;
|
|
280
|
+
if (ln.startsWith("Binary files"))
|
|
281
|
+
return false;
|
|
282
|
+
return true;
|
|
283
|
+
})
|
|
284
|
+
.join("\n")
|
|
285
|
+
.replace(/\n+$/, "");
|
|
286
|
+
}
|
|
287
|
+
function buildDefaultBody(heals, branchInfo, reportLink, decision) {
|
|
288
|
+
const lines = [];
|
|
289
|
+
lines.push(`# 🤖 Sela Insights - Automation Suite Healed!`);
|
|
290
|
+
lines.push("");
|
|
291
|
+
lines.push(`Sela kept your pipeline running by healing ${heals.length} broken locator(s).`);
|
|
292
|
+
lines.push("");
|
|
293
|
+
lines.push(`## 📊 Executive Summary`);
|
|
294
|
+
lines.push(`* **Heals:** ${heals.length}`);
|
|
295
|
+
lines.push(`* **Min AI Confidence:** ${fmtPct(decision.minAiConfidence)}`);
|
|
296
|
+
lines.push(`* **Min Auditor Confidence:** ${fmtPct(decision.minAuditorConfidence)}`);
|
|
297
|
+
lines.push(`* **Branch:** \`${branchInfo.branch ?? "unknown"}\``);
|
|
298
|
+
if (decision.downgradeReason) {
|
|
299
|
+
lines.push(`* **⚠️ Strategy Downgrade:** \`${decision.configured}\` → \`${decision.effective}\` (${decision.downgradeReason})`);
|
|
300
|
+
}
|
|
301
|
+
lines.push("");
|
|
302
|
+
lines.push(`## 💻 Per-Heal Diff`);
|
|
303
|
+
for (const h of heals) {
|
|
304
|
+
const lineRef = h.newLineNumber ?? h.sourceLine;
|
|
305
|
+
lines.push(`### \`${h.sourceFile}:${lineRef}\` - ${h.testTitle ?? "(no test title)"}`);
|
|
306
|
+
lines.push("");
|
|
307
|
+
lines.push("```diff");
|
|
308
|
+
if (h.gitUnifiedDiff && h.gitUnifiedDiff.trim().length > 0) {
|
|
309
|
+
// Emit the real git diff verbatim. This natively handles cross-file
|
|
310
|
+
// healing, multi-line edits, and template-literal changes - none of
|
|
311
|
+
// which the legacy single-line -/+ pair could represent.
|
|
312
|
+
lines.push(stripDiffPreamble(h.gitUnifiedDiff));
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
// Fallback when git was unavailable / the file was untracked.
|
|
316
|
+
lines.push(`- ${(h.oldCodeLine ?? "").trim()}`);
|
|
317
|
+
lines.push(`+ ${(h.newCodeLine ?? "").trim()}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push("```");
|
|
320
|
+
if (h.aiExplanation) {
|
|
321
|
+
lines.push(`> 🧠 ${h.aiExplanation}`);
|
|
322
|
+
}
|
|
323
|
+
lines.push(`> AI Confidence: **${fmtPct(h.aiConfidence)}** · Auditor: **${fmtPct(h.auditor?.confidence)}**`);
|
|
324
|
+
lines.push("");
|
|
325
|
+
}
|
|
326
|
+
lines.push(`## 🔍 Visual Evidence`);
|
|
327
|
+
lines.push(`[📄 View Full Interactive Healing Report](${reportLink})`);
|
|
328
|
+
lines.push("");
|
|
329
|
+
lines.push(`---`);
|
|
330
|
+
lines.push(`Generated automatically by \`sela-core\`.`);
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Clean-Room execution lifecycle.
|
|
335
|
+
*
|
|
336
|
+
* The legacy flow committed the developer's whole dirty working tree onto
|
|
337
|
+
* the PR branch - polluting reviewers with `test.only`, commented debug
|
|
338
|
+
* blocks, etc. This refactor isolates Sela's specific AST changes onto an
|
|
339
|
+
* ephemeral branch, leaves the developer's local workspace byte-identical
|
|
340
|
+
* to its pre-Sela state, and is wrapped in a try/finally that restores
|
|
341
|
+
* the workspace even on mid-flight crashes.
|
|
342
|
+
*/
|
|
343
|
+
async function execute(cfg, decision, heals, branchInfo, ctx,
|
|
344
|
+
/** Injectable for tests - defaults to the module singleton. */
|
|
345
|
+
workspace = WorkspaceSnapshotService_1.sharedWorkspaceSnapshot) {
|
|
346
|
+
const result = {
|
|
347
|
+
effective: decision.effective,
|
|
348
|
+
prUrl: null,
|
|
349
|
+
branchPushed: null,
|
|
350
|
+
errors: [],
|
|
351
|
+
};
|
|
352
|
+
if (decision.effective === "readOnly") {
|
|
353
|
+
logger_1.logger.info(`PR: readOnly - no git operations performed.`);
|
|
354
|
+
if (ctx.reportHtmlPath) {
|
|
355
|
+
logger_1.logger.info(`PR: Report artifact: ${ctx.reportHtmlPath}`);
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
if (heals.length === 0) {
|
|
360
|
+
logger_1.logger.info(`PR: No HEALED events - skipping git operations.`);
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
if (!gitAvailable(ctx.cwd)) {
|
|
364
|
+
result.errors.push("git CLI not available");
|
|
365
|
+
logger_1.logger.warn(`PR: git CLI not available - skipping.`);
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
if (isDetachedHead(ctx.cwd)) {
|
|
369
|
+
result.errors.push("detached HEAD");
|
|
370
|
+
logger_1.logger.warn(`PR: Detached HEAD - skipping git operations.`);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
const mutatedSnaps = workspace.getMutatedSnapshots();
|
|
374
|
+
if (mutatedSnaps.length === 0) {
|
|
375
|
+
result.errors.push("no mutated files tracked");
|
|
376
|
+
logger_1.logger.warn(`PR: WorkspaceSnapshotService reports no mutated files - skipping.`);
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
// Render title / body up front - needed by all branches below.
|
|
380
|
+
const first = heals[0];
|
|
381
|
+
const titleVars = {
|
|
382
|
+
testName: first.testTitle ?? "automation",
|
|
383
|
+
fileName: path.basename(first.sourceFile),
|
|
384
|
+
count: heals.length,
|
|
385
|
+
};
|
|
386
|
+
const title = render(cfg.titleTemplate, titleVars);
|
|
387
|
+
const reportLink = ctx.reportHtmlPath
|
|
388
|
+
? path.relative(ctx.cwd, ctx.reportHtmlPath)
|
|
389
|
+
: cfg.reportArtifactPath;
|
|
390
|
+
const body = cfg.bodyTemplate
|
|
391
|
+
? render(cfg.bodyTemplate, {
|
|
392
|
+
...titleVars,
|
|
393
|
+
summary: `${heals.length} heal(s)`,
|
|
394
|
+
reportLink,
|
|
395
|
+
reasoning: heals.map((h) => h.aiExplanation).join("\n\n"),
|
|
396
|
+
})
|
|
397
|
+
: buildDefaultBody(heals, branchInfo, reportLink, decision);
|
|
398
|
+
if (decision.effective === "directCommit") {
|
|
399
|
+
return runCleanRoom({
|
|
400
|
+
mode: "directCommit",
|
|
401
|
+
cfg,
|
|
402
|
+
ctx,
|
|
403
|
+
mutatedSnaps,
|
|
404
|
+
title,
|
|
405
|
+
body,
|
|
406
|
+
branchInfo,
|
|
407
|
+
decision,
|
|
408
|
+
result,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (decision.effective === "pr" || decision.effective === "draftPR") {
|
|
412
|
+
if (!ghAvailable(ctx.cwd)) {
|
|
413
|
+
result.errors.push("gh CLI not available");
|
|
414
|
+
logger_1.logger.warn(`PR: gh CLI not installed - cannot open PR. Run \`gh auth login\` after install.`);
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
return runCleanRoom({
|
|
418
|
+
mode: decision.effective,
|
|
419
|
+
cfg,
|
|
420
|
+
ctx,
|
|
421
|
+
mutatedSnaps,
|
|
422
|
+
title,
|
|
423
|
+
body,
|
|
424
|
+
branchInfo,
|
|
425
|
+
decision,
|
|
426
|
+
result,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
async function runCleanRoom(args) {
|
|
432
|
+
const { mode, cfg, ctx, mutatedSnaps, title, body, branchInfo, decision, result, } = args;
|
|
433
|
+
const id = String(Date.now());
|
|
434
|
+
const stashLabel = `sela:clean-room-${id}`;
|
|
435
|
+
const originalBranch = currentBranch(ctx.cwd);
|
|
436
|
+
const targetBranch = mode === "directCommit"
|
|
437
|
+
? originalBranch
|
|
438
|
+
: render(cfg.branchNameTemplate, { id, timestamp: id });
|
|
439
|
+
// Lifecycle state - used by the finally{} restoration block.
|
|
440
|
+
let didRestoreOriginal = false;
|
|
441
|
+
let didStash = false;
|
|
442
|
+
let didSwitchToTarget = false;
|
|
443
|
+
let didCommit = false;
|
|
444
|
+
try {
|
|
445
|
+
// 1. State Checkpoint - rewind developer's mutated files to pre-Sela
|
|
446
|
+
// state on disk. Other dirty files (unrelated to Sela) stay where
|
|
447
|
+
// they are; the subsequent stash will sweep them up.
|
|
448
|
+
workspaceRestoreOriginal(mutatedSnaps);
|
|
449
|
+
didRestoreOriginal = true;
|
|
450
|
+
// 2. Branch Isolation - stash everything (tracked + untracked) so the
|
|
451
|
+
// worktree mirrors HEAD when we switch / branch off.
|
|
452
|
+
const stash = sh(`git stash push -u -m ${shellQuote(stashLabel)}`, ctx.cwd);
|
|
453
|
+
if (stash.ok) {
|
|
454
|
+
didStash = !/No local changes to save/i.test(stash.stdout);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
// Stash failures are non-fatal when the worktree was already clean.
|
|
458
|
+
logger_1.logger.debug(`PR: stash skipped: ${stash.stderr}`);
|
|
459
|
+
}
|
|
460
|
+
if (mode !== "directCommit") {
|
|
461
|
+
const checkout = sh(`git checkout -b ${targetBranch}`, ctx.cwd);
|
|
462
|
+
if (!checkout.ok) {
|
|
463
|
+
throw new Error(`git checkout -b ${targetBranch}: ${checkout.stderr}`);
|
|
464
|
+
}
|
|
465
|
+
didSwitchToTarget = true;
|
|
466
|
+
}
|
|
467
|
+
// 3. Targeted Hunk Patching - apply ONLY Sela's blocks to each file's
|
|
468
|
+
// clean HEAD content. Refuses to write if dirty edits collide with
|
|
469
|
+
// Sela's mutation site (block not found / not unique).
|
|
470
|
+
for (const snap of mutatedSnaps) {
|
|
471
|
+
const applied = applyMutationToClean(snap);
|
|
472
|
+
if (!applied.ok) {
|
|
473
|
+
throw new Error(`cannot apply Sela mutation to clean base of ${snap.relativePath}: ${applied.reason}`);
|
|
474
|
+
}
|
|
475
|
+
fs.writeFileSync(snap.absolutePath, applied.result, "utf8");
|
|
476
|
+
}
|
|
477
|
+
// 4. Stage, commit, push.
|
|
478
|
+
for (const snap of mutatedSnaps) {
|
|
479
|
+
const add = sh(`git add ${shellQuote(snap.relativePath)}`, ctx.cwd);
|
|
480
|
+
if (!add.ok)
|
|
481
|
+
result.errors.push(`git add ${snap.relativePath}: ${add.stderr}`);
|
|
482
|
+
}
|
|
483
|
+
configureGitIdentityForCI(ctx.cwd);
|
|
484
|
+
const commit = sh(`git commit -m ${shellQuote(title)}`, ctx.cwd);
|
|
485
|
+
if (!commit.ok) {
|
|
486
|
+
throw new Error(`git commit: ${commit.stderr}`);
|
|
487
|
+
}
|
|
488
|
+
didCommit = true;
|
|
489
|
+
logger_1.logger.success(`PR: Clean commit on '${targetBranch ?? "(unknown)"}': ${title}`);
|
|
490
|
+
const pushCmd = mode === "directCommit"
|
|
491
|
+
? `git push`
|
|
492
|
+
: `git push -u origin ${shellQuote(targetBranch)}`;
|
|
493
|
+
const push = sh(pushCmd, ctx.cwd);
|
|
494
|
+
if (!push.ok) {
|
|
495
|
+
throw new Error(`git push: ${push.stderr}`);
|
|
496
|
+
}
|
|
497
|
+
if (mode !== "directCommit") {
|
|
498
|
+
result.branchPushed = targetBranch;
|
|
499
|
+
}
|
|
500
|
+
// 5. (PR modes only) open the PR.
|
|
501
|
+
if (mode === "pr" || mode === "draftPR") {
|
|
502
|
+
const prUrl = openPullRequest({
|
|
503
|
+
cfg,
|
|
504
|
+
ctx,
|
|
505
|
+
mode,
|
|
506
|
+
title,
|
|
507
|
+
body,
|
|
508
|
+
branchInfo,
|
|
509
|
+
id,
|
|
510
|
+
result,
|
|
511
|
+
});
|
|
512
|
+
if (prUrl)
|
|
513
|
+
result.prUrl = prUrl;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
result.errors.push(err?.message ?? String(err));
|
|
518
|
+
logger_1.logger.warn(`PR: Clean-room flow failed: ${err?.message ?? err}`);
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
// 6. Workspace Restoration - ALWAYS attempt, even on mid-flight crash.
|
|
522
|
+
// Order matters:
|
|
523
|
+
// a) Switch back to the developer's original branch (if we left it).
|
|
524
|
+
// b) Pop the stash to restore the dirty pre-Sela working tree.
|
|
525
|
+
// c) Defensively re-write contentBefore for tracked files in case
|
|
526
|
+
// the stash was lost.
|
|
527
|
+
try {
|
|
528
|
+
if (didSwitchToTarget && originalBranch) {
|
|
529
|
+
const back = sh(`git checkout ${shellQuote(originalBranch)}`, ctx.cwd);
|
|
530
|
+
if (!back.ok) {
|
|
531
|
+
logger_1.logger.warn(`PR: Could not switch back to '${originalBranch}': ${back.stderr}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (didStash) {
|
|
535
|
+
const pop = sh(`git stash pop`, ctx.cwd);
|
|
536
|
+
if (!pop.ok) {
|
|
537
|
+
logger_1.logger.warn(`PR: git stash pop failed: ${pop.stderr}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (didRestoreOriginal) {
|
|
541
|
+
// Belt-and-braces - every tracked snapshot back to its dirty
|
|
542
|
+
// pre-Sela state. No-op if stash pop already did the job.
|
|
543
|
+
workspaceRestoreOriginal(mutatedSnaps);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (restoreErr) {
|
|
547
|
+
logger_1.logger.warn(`PR: Workspace restoration error: ${restoreErr?.message ?? restoreErr}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (!didCommit && result.errors.length === 0) {
|
|
551
|
+
result.errors.push("clean-room flow did not produce a commit");
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
function openPullRequest(args) {
|
|
556
|
+
const { cfg, ctx, mode, title, body, branchInfo, id, result } = args;
|
|
557
|
+
const bodyFile = path.join(ctx.cwd, `.sela-pr-body-${id}.md`);
|
|
558
|
+
try {
|
|
559
|
+
fs.writeFileSync(bodyFile, body, "utf8");
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
result.errors.push(`pr body write: ${err?.message ?? err}`);
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const labelFlag = cfg.labels.length > 0
|
|
566
|
+
? cfg.labels.map((l) => `--label ${shellQuote(l)}`).join(" ")
|
|
567
|
+
: "";
|
|
568
|
+
const draftFlag = mode === "draftPR" ? "--draft" : "";
|
|
569
|
+
const baseFlag = branchInfo.branch
|
|
570
|
+
? `--base ${shellQuote(branchInfo.branch)}`
|
|
571
|
+
: "";
|
|
572
|
+
const prCmd = `gh pr create ${draftFlag} ${baseFlag} ` +
|
|
573
|
+
`--title ${shellQuote(title)} ` +
|
|
574
|
+
`--body-file ${shellQuote(bodyFile)} ` +
|
|
575
|
+
`${labelFlag}`;
|
|
576
|
+
const pr = sh(prCmd, ctx.cwd);
|
|
577
|
+
try {
|
|
578
|
+
fs.unlinkSync(bodyFile);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
/* ignore */
|
|
582
|
+
}
|
|
583
|
+
if (!pr.ok) {
|
|
584
|
+
result.errors.push(`gh pr create: ${pr.stderr}`);
|
|
585
|
+
logger_1.logger.warn(`PR: gh pr create failed: ${pr.stderr}`);
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
const url = pr.stdout.trim();
|
|
589
|
+
logger_1.logger.success(`${mode === "draftPR" ? "Draft PR" : "PR"} opened: ${url}`);
|
|
590
|
+
return url;
|
|
591
|
+
}
|
|
592
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
593
|
+
// CLEAN-ROOM HELPERS
|
|
594
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
595
|
+
function currentBranch(cwd) {
|
|
596
|
+
const r = sh("git rev-parse --abbrev-ref HEAD", cwd);
|
|
597
|
+
if (!r.ok)
|
|
598
|
+
return null;
|
|
599
|
+
const b = r.stdout.trim();
|
|
600
|
+
return b && b !== "HEAD" ? b : null;
|
|
601
|
+
}
|
|
602
|
+
function workspaceRestoreOriginal(snaps) {
|
|
603
|
+
for (const snap of snaps) {
|
|
604
|
+
try {
|
|
605
|
+
if (snap.createdBySela) {
|
|
606
|
+
if (fs.existsSync(snap.absolutePath))
|
|
607
|
+
fs.unlinkSync(snap.absolutePath);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
fs.writeFileSync(snap.absolutePath, snap.contentBefore, "utf8");
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
logger_1.logger.warn(`PR: Could not restore ${snap.relativePath}: ${err?.message ?? err}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function applyMutationToClean(snap) {
|
|
618
|
+
const after = snap.contentAfter ?? "";
|
|
619
|
+
// File didn't exist on HEAD (created by Sela or untracked). Write the
|
|
620
|
+
// post-mutation content directly - there's no clean base to merge into.
|
|
621
|
+
if (snap.headContent === null) {
|
|
622
|
+
return { ok: true, result: after };
|
|
623
|
+
}
|
|
624
|
+
// No dirty edits - `headContent === contentBefore`, so the mutation
|
|
625
|
+
// applies cleanly as a 1:1 string equivalence.
|
|
626
|
+
if (snap.headContent === snap.contentBefore) {
|
|
627
|
+
return { ok: true, result: after };
|
|
628
|
+
}
|
|
629
|
+
// Dirty edits present - extract Sela's specific change blocks and
|
|
630
|
+
// re-apply them against the clean HEAD content via unique substring
|
|
631
|
+
// replacement. Fails loudly when the dirty edits overlap Sela's site.
|
|
632
|
+
const blocks = (0, IsolatedDiff_1.extractChangeBlocks)(snap.contentBefore, after);
|
|
633
|
+
if (blocks.length === 0) {
|
|
634
|
+
return { ok: true, result: snap.headContent };
|
|
635
|
+
}
|
|
636
|
+
return (0, IsolatedDiff_1.applyChangeBlocks)(snap.headContent, blocks);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* CI adapters (GitHub Actions, GitLab, Jenkins) often run with no global
|
|
640
|
+
* git identity. `git commit` then fails with `Please tell me who you are`,
|
|
641
|
+
* tanking the whole heal pipeline. Plant a sela bot identity for THIS
|
|
642
|
+
* commit only when no `user.email` / `user.name` is configured - never
|
|
643
|
+
* overwrite an existing identity.
|
|
644
|
+
*/
|
|
645
|
+
function configureGitIdentityForCI(cwd) {
|
|
646
|
+
const isCI = process.env.CI === "true" ||
|
|
647
|
+
process.env.CI === "1" ||
|
|
648
|
+
!!process.env.GITHUB_ACTIONS ||
|
|
649
|
+
!!process.env.GITLAB_CI ||
|
|
650
|
+
!!process.env.JENKINS_URL;
|
|
651
|
+
if (!isCI)
|
|
652
|
+
return;
|
|
653
|
+
const email = sh("git config user.email", cwd);
|
|
654
|
+
if (!email.ok || email.stdout.trim().length === 0) {
|
|
655
|
+
sh(`git config user.email "sela-bot@anthropic.local"`, cwd);
|
|
656
|
+
}
|
|
657
|
+
const name = sh("git config user.name", cwd);
|
|
658
|
+
if (!name.ok || name.stdout.trim().length === 0) {
|
|
659
|
+
sh(`git config user.name "Sela Bot"`, cwd);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
663
|
+
// BUG-DETECTED HANDLING
|
|
664
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
665
|
+
async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
666
|
+
if (events.length === 0)
|
|
667
|
+
return;
|
|
668
|
+
logger_1.logger.warn(`Protection: ${events.length} potential regression(s) detected - auto-fix blocked.`);
|
|
669
|
+
for (const e of events) {
|
|
670
|
+
logger_1.logger.warn(` • ${e.sourceFile}:${e.sourceLine} - ${e.reason} (auditor: ${e.auditor?.verdict ?? "n/a"})`);
|
|
671
|
+
}
|
|
672
|
+
if (action === "failCI") {
|
|
673
|
+
logger_1.logger.warn(`Protection: onBugDetected='failCI' - setting process.exitCode = 1`);
|
|
674
|
+
process.exitCode = 1;
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (action === "createIssue") {
|
|
678
|
+
if (!ghAvailable(ctx.cwd)) {
|
|
679
|
+
logger_1.logger.warn(`Protection: gh CLI unavailable - cannot create issue.`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
for (const e of events) {
|
|
683
|
+
const id = String(Date.now());
|
|
684
|
+
const title = `[Sela Alert] Potential Bug Detected: ${e.testTitle ?? path.basename(e.sourceFile)}`;
|
|
685
|
+
const body = [
|
|
686
|
+
`## 🛑 Sela detected a potential regression`,
|
|
687
|
+
``,
|
|
688
|
+
`* **File:** \`${e.sourceFile}:${e.sourceLine}\``,
|
|
689
|
+
`* **Test:** ${e.testTitle ?? "(unknown)"}`,
|
|
690
|
+
`* **Reason:** ${e.reason}`,
|
|
691
|
+
`* **Safety Level:** ${e.safetyLevel}`,
|
|
692
|
+
`* **Auditor:** ${e.auditor?.verdict ?? "n/a"} (${e.auditor?.confidence ?? "n/a"}%)`,
|
|
693
|
+
``,
|
|
694
|
+
`### Old selector`,
|
|
695
|
+
`\`${e.oldSelector}\``,
|
|
696
|
+
``,
|
|
697
|
+
`### AI candidate (BLOCKED)`,
|
|
698
|
+
`\`${e.candidateNewSelector ?? "(none)"}\``,
|
|
699
|
+
``,
|
|
700
|
+
`### AI explanation`,
|
|
701
|
+
`> ${e.aiExplanation || "(none)"}`,
|
|
702
|
+
].join("\n");
|
|
703
|
+
const bodyFile = path.join(ctx.cwd, `.sela-issue-body-${id}.md`);
|
|
704
|
+
fs.writeFileSync(bodyFile, body, "utf8");
|
|
705
|
+
const cmd = `gh issue create --title ${shellQuote(title)} ` +
|
|
706
|
+
`--body-file ${shellQuote(bodyFile)} ` +
|
|
707
|
+
`--label sela-bug-detected`;
|
|
708
|
+
const r = sh(cmd, ctx.cwd);
|
|
709
|
+
try {
|
|
710
|
+
fs.unlinkSync(bodyFile);
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
/* ignore */
|
|
714
|
+
}
|
|
715
|
+
if (!r.ok) {
|
|
716
|
+
logger_1.logger.warn(`Protection: gh issue create failed: ${r.stderr}`);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
logger_1.logger.success(`Protection: Issue created: ${r.stdout.trim()}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (action === "labelOnly") {
|
|
725
|
+
if (!ghAvailable(ctx.cwd)) {
|
|
726
|
+
logger_1.logger.warn(`Protection: gh CLI unavailable - cannot label PR.`);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (!branchInfo.isPR || !branchInfo.prNumber) {
|
|
730
|
+
logger_1.logger.warn(`Protection: Not running on a PR - labelOnly skipped.`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const labelCmd = `gh pr edit ${branchInfo.prNumber} --add-label sela-bug-detected`;
|
|
734
|
+
const labelRes = sh(labelCmd, ctx.cwd);
|
|
735
|
+
if (!labelRes.ok) {
|
|
736
|
+
logger_1.logger.warn(`Protection: gh pr edit failed: ${labelRes.stderr}`);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const comment = `⚠️ Sela detected ${events.length} potential regression(s). Review before merging.`;
|
|
740
|
+
const commentRes = sh(`gh pr comment ${branchInfo.prNumber} --body ${shellQuote(comment)}`, ctx.cwd);
|
|
741
|
+
if (!commentRes.ok) {
|
|
742
|
+
logger_1.logger.warn(`Protection: gh pr comment failed: ${commentRes.stderr}`);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
logger_1.logger.success(`Protection: PR #${branchInfo.prNumber} labeled + commented.`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
750
|
+
// SHELL QUOTING - cross-platform (POSIX single-quote escape;
|
|
751
|
+
// on Windows execSync uses cmd.exe which handles double-quotes natively).
|
|
752
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
753
|
+
function shellQuote(s) {
|
|
754
|
+
if (process.platform === "win32") {
|
|
755
|
+
// cmd.exe: escape inner double-quotes by doubling them.
|
|
756
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
757
|
+
}
|
|
758
|
+
// POSIX: wrap in single quotes; close-and-reopen for embedded single quotes.
|
|
759
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
760
|
+
}
|