sela-core 0.1.0-alpha.3 → 0.1.0-alpha.32
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/cli/commands/pr.d.ts +16 -0
- package/dist/cli/commands/pr.d.ts.map +1 -0
- package/dist/cli/commands/pr.js +81 -0
- package/dist/cli/index.js +3 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +10 -59
- package/dist/errors/SelaError.d.ts +4 -0
- package/dist/errors/SelaError.d.ts.map +1 -1
- package/dist/errors/SelaError.js +4 -0
- package/dist/reporter/SelaReporter.d.ts +17 -1
- package/dist/reporter/SelaReporter.d.ts.map +1 -1
- package/dist/reporter/SelaReporter.js +64 -2
- package/dist/services/PRAutomationService.d.ts +16 -0
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +99 -0
- package/dist/services/PendingPromptLedger.d.ts +32 -1
- package/dist/services/PendingPromptLedger.d.ts.map +1 -1
- package/dist/services/PendingPromptLedger.js +67 -10
- package/dist/services/SafetyGuard.d.ts +1 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +41 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -2
- package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -1
- package/dist/services/safety/rules/DangerousSinkRule.js +37 -16
- package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -1
- package/dist/services/safety/rules/IntentAuditorRule.js +36 -4
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -1
- package/dist/services/safety/rules/StructuralOnlyRule.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `sela pr aggregate --ledger-dir <path>`
|
|
4
|
+
*
|
|
5
|
+
* CI fan-in: after all sharded Playwright runners complete and upload their
|
|
6
|
+
* `.sela/pending-prompts` artifacts, the downstream merge job downloads
|
|
7
|
+
* everything into one directory and calls this command to open a single
|
|
8
|
+
* unified PR for the entire test suite.
|
|
9
|
+
*
|
|
10
|
+
* Typical GHA usage:
|
|
11
|
+
* - Upload: actions/upload-artifact@v4 path: .sela/pending-prompts/
|
|
12
|
+
* - Download: actions/download-artifact@v4 merge-multiple: true path: sela-ledgers/
|
|
13
|
+
* - Run: npx sela pr aggregate --ledger-dir ./sela-ledgers
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerPr(parent: Command): void;
|
|
16
|
+
//# sourceMappingURL=pr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAazC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAgDhD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerPr = registerPr;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const PendingPromptLedger_js_1 = require("../../services/PendingPromptLedger.js");
|
|
39
|
+
const SelaReporter_js_1 = require("../../reporter/SelaReporter.js");
|
|
40
|
+
const PRAutomationService_js_1 = require("../../services/PRAutomationService.js");
|
|
41
|
+
/**
|
|
42
|
+
* `sela pr aggregate --ledger-dir <path>`
|
|
43
|
+
*
|
|
44
|
+
* CI fan-in: after all sharded Playwright runners complete and upload their
|
|
45
|
+
* `.sela/pending-prompts` artifacts, the downstream merge job downloads
|
|
46
|
+
* everything into one directory and calls this command to open a single
|
|
47
|
+
* unified PR for the entire test suite.
|
|
48
|
+
*
|
|
49
|
+
* Typical GHA usage:
|
|
50
|
+
* - Upload: actions/upload-artifact@v4 path: .sela/pending-prompts/
|
|
51
|
+
* - Download: actions/download-artifact@v4 merge-multiple: true path: sela-ledgers/
|
|
52
|
+
* - Run: npx sela pr aggregate --ledger-dir ./sela-ledgers
|
|
53
|
+
*/
|
|
54
|
+
function registerPr(parent) {
|
|
55
|
+
const pr = parent
|
|
56
|
+
.command("pr")
|
|
57
|
+
.description("Pull-request automation commands");
|
|
58
|
+
pr.command("aggregate")
|
|
59
|
+
.description("Aggregate sharded ledger files and open a single unified PR (CI fan-in)")
|
|
60
|
+
.requiredOption("--ledger-dir <path>", "Directory containing all downloaded shard ledger JSON files")
|
|
61
|
+
.option("--cwd <path>", "Repository root used for git operations (defaults to process.cwd())", process.cwd())
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
const ledgerDir = path.resolve(opts.cwd, opts.ledgerDir);
|
|
64
|
+
const payloads = (0, PendingPromptLedger_js_1.readGitPayloadsFromDir)(ledgerDir);
|
|
65
|
+
if (payloads.length === 0) {
|
|
66
|
+
console.error(`[sela pr aggregate] No ledger payloads found in ${ledgerDir} — nothing to do.`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const healedEvents = (0, SelaReporter_js_1.deduplicateByStableId)(payloads.flatMap((p) => p.healedEvents));
|
|
70
|
+
if (healedEvents.length === 0) {
|
|
71
|
+
console.error("[sela pr aggregate] No healed events after deduplication — nothing to do.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const protectedEvents = payloads.flatMap((p) => p.protectedEvents);
|
|
75
|
+
await (0, PRAutomationService_js_1.runCentralizedGitFlow)({
|
|
76
|
+
cwd: opts.cwd,
|
|
77
|
+
healedEvents,
|
|
78
|
+
protectedEvents,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -54,6 +54,7 @@ const bulk_js_1 = require("./commands/bulk.js");
|
|
|
54
54
|
const sync_js_1 = require("./commands/sync.js");
|
|
55
55
|
const showReport_js_1 = require("./commands/showReport.js");
|
|
56
56
|
const merge_js_1 = require("./commands/merge.js");
|
|
57
|
+
const pr_js_1 = require("./commands/pr.js");
|
|
57
58
|
(0, ErrorHandler_js_1.registerGlobalHandlers)();
|
|
58
59
|
// Safety net: route any stray third-party console.log through the Logger's
|
|
59
60
|
// verbosity-gated debug channel (stderr) so stdout stays pipe-clean and the
|
|
@@ -77,6 +78,8 @@ program
|
|
|
77
78
|
(0, showReport_js_1.registerShowReport)(program);
|
|
78
79
|
// ── sela merge - CI fan-in: aggregate sharded reports into one ────────────────
|
|
79
80
|
(0, merge_js_1.registerMerge)(program);
|
|
81
|
+
// ── sela pr <subcommand> - pull-request automation ───────────────────────────
|
|
82
|
+
(0, pr_js_1.registerPr)(program);
|
|
80
83
|
// ── sela dna <subcommand> ─────────────────────────────────────────────────────
|
|
81
84
|
const dna = new commander_1.Command("dna")
|
|
82
85
|
.description("DNA management commands")
|
|
@@ -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;AAcxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAe,QAAQ,EAAkB,MAAM,yBAAyB,CAAC;AAoFhF,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAc;IACjC,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;;IAuBnD,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;IA0DnB,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,GAC5B,OAAO,CAAC,MAAM,CAAC;IAirBlB,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;IAItE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;YAoGtB,aAAa;IAsB3B,OAAO,CAAC,0BAA0B;IA+BlC,OAAO,CAAC,WAAW;IA6Bb,wBAAwB,CAC5B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;CAqBnB;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -42,7 +42,6 @@ const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
|
42
42
|
const logger_1 = require("../utils/logger");
|
|
43
43
|
const SnapshotService_1 = require("../services/SnapshotService");
|
|
44
44
|
const DOMUtils_1 = require("../utils/DOMUtils");
|
|
45
|
-
const FileLock_1 = require("../utils/FileLock");
|
|
46
45
|
const SourceUpdater_1 = require("../services/SourceUpdater");
|
|
47
46
|
const ASTSourceUpdater_1 = require("../services/ASTSourceUpdater");
|
|
48
47
|
const SafetyGuard_1 = require("../services/SafetyGuard");
|
|
@@ -51,7 +50,6 @@ const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
|
51
50
|
const DryRunGuard_1 = require("../config/DryRunGuard");
|
|
52
51
|
const HealBudget_1 = require("../services/HealBudget");
|
|
53
52
|
const HealReportService_1 = require("../services/HealReportService");
|
|
54
|
-
const PRAutomationService_1 = require("../services/PRAutomationService");
|
|
55
53
|
const PendingPromptLedger_1 = require("../services/PendingPromptLedger");
|
|
56
54
|
const SelaError_1 = require("../errors/SelaError");
|
|
57
55
|
const SelectorSanitizer_1 = require("../services/SelectorSanitizer");
|
|
@@ -979,70 +977,23 @@ If the text has NOT changed, omit the "contentChange" field entirely.`;
|
|
|
979
977
|
}
|
|
980
978
|
}
|
|
981
979
|
}
|
|
982
|
-
// ──
|
|
980
|
+
// ── Cross-process worker ledger ──────────────────────────────────────
|
|
983
981
|
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
986
|
-
//
|
|
987
|
-
//
|
|
988
|
-
//
|
|
982
|
+
// Workers serialise their state to a per-worker JSON file so the master
|
|
983
|
+
// process (SelaReporter.onEnd) can aggregate everything after all workers
|
|
984
|
+
// have exited. The ledger carries:
|
|
985
|
+
// • PendingPromptEntry[] — for local-DX diff display / vscode deeplinks.
|
|
986
|
+
// • WorkerGitPayload — for CI centralized git/PR flow.
|
|
989
987
|
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
|
|
993
|
-
if (isCiEnvironment()) {
|
|
994
|
-
if (this.config.prAutomation.enabled) {
|
|
995
|
-
// SELA-5: serialise PR automation (git stash / checkout / commit /
|
|
996
|
-
// push) behind a repo-root lock. CI normally runs one worker per
|
|
997
|
-
// sharded machine, so the lock is uncontended; if a machine is
|
|
998
|
-
// misconfigured with workers>1 it prevents concurrent git operations
|
|
999
|
-
// from corrupting the index / worktree. NOTE: this guards against git
|
|
1000
|
-
// corruption only — aggregating heals from multiple workers into a
|
|
1001
|
-
// single PR is intentionally out of scope (run one worker per shard).
|
|
1002
|
-
const prLock = new FileLock_1.FileLock((0, FileLock_1.lockDirFor)(path.join(process.cwd(), ".sela-pr")), { staleMs: 120_000 });
|
|
1003
|
-
try {
|
|
1004
|
-
const ran = await prLock.withLock(async () => {
|
|
1005
|
-
const workerIdx = process.env["TEST_WORKER_INDEX"];
|
|
1006
|
-
if (workerIdx !== undefined && workerIdx !== "0") {
|
|
1007
|
-
logger_1.logger.warn("[PR] PR automation under workers>1 is unsupported — " +
|
|
1008
|
-
"run one worker per CI shard. Serialising to protect the git " +
|
|
1009
|
-
"index; heals from other workers may land in separate commits.");
|
|
1010
|
-
}
|
|
1011
|
-
const branchInfo = (0, PRAutomationService_1.detectBranch)(process.cwd());
|
|
1012
|
-
const decision = (0, PRAutomationService_1.decideEffectiveStrategy)(this.config.prAutomation, branchInfo.branch, healedEvents);
|
|
1013
|
-
if (decision.downgradeReason) {
|
|
1014
|
-
logger_1.logger.warn(`[PR] Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
|
|
1015
|
-
`(reason: ${decision.downgradeReason}, ` +
|
|
1016
|
-
`minAI: ${fmtPctEngine(decision.minAiConfidence)}, ` +
|
|
1017
|
-
`minAuditor: ${fmtPctEngine(decision.minAuditorConfidence)})`);
|
|
1018
|
-
}
|
|
1019
|
-
else {
|
|
1020
|
-
logger_1.logger.debug(`[PR] Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
|
|
1021
|
-
}
|
|
1022
|
-
const ctx = { cwd: process.cwd(), reportHtmlPath };
|
|
1023
|
-
await (0, PRAutomationService_1.execute)(this.config.prAutomation, decision, healedEvents, branchInfo, ctx);
|
|
1024
|
-
await (0, PRAutomationService_1.handleBugDetected)(this.config.prAutomation.onBugDetected, protectedEvents, branchInfo, ctx);
|
|
1025
|
-
});
|
|
1026
|
-
if (ran === undefined) {
|
|
1027
|
-
logger_1.logger.warn("[PR] Skipped PR automation — another worker holds the PR lock.");
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
catch (err) {
|
|
1031
|
-
logger_1.logger.warn(`[PR] PR automation step failed: ${err.message}`);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
else if (healedEvents.length > 0) {
|
|
1036
|
-
// Local DX: serialise the mutated snapshots to a cross-process
|
|
1037
|
-
// ledger. The SelaReporter drains it from `onEnd()` in the main
|
|
1038
|
-
// process and prints a non-blocking summary (diff + vscode://
|
|
1039
|
-
// deeplink). Mutations are already on disk - the ledger only
|
|
1040
|
-
// exists so the reporter knows what to narrate.
|
|
988
|
+
// Both CI and local paths write the ledger. The master process decides
|
|
989
|
+
// which parts to consume based on its own environment check.
|
|
990
|
+
if (healedEvents.length > 0 || protectedEvents.length > 0) {
|
|
1041
991
|
try {
|
|
1042
992
|
(0, PendingPromptLedger_1.writePendingPromptsLedger)({
|
|
1043
993
|
cwd: process.cwd(),
|
|
1044
994
|
workspace: WorkspaceSnapshotService_1.sharedWorkspaceSnapshot,
|
|
1045
995
|
healedEvents,
|
|
996
|
+
protectedEvents,
|
|
1046
997
|
});
|
|
1047
998
|
}
|
|
1048
999
|
catch (err) {
|
|
@@ -63,6 +63,10 @@ export declare const SAFETY_GUARD_CODES: {
|
|
|
63
63
|
readonly BLOCKED_ROLE_SWAP_ASSERTION: "SG_BLOCKED_ROLE_SWAP_ASSERTION";
|
|
64
64
|
readonly FAIL_HARD_AUDITOR_INCONSISTENT: "SG_FAIL_HARD_AUDITOR_INCONSISTENT";
|
|
65
65
|
readonly BLOCKED_AUDITOR_SUSPICIOUS: "SG_BLOCKED_AUDITOR_SUSPICIOUS";
|
|
66
|
+
/** Structural pre-LLM check: old element had a destructive/auth role and
|
|
67
|
+
* the candidate does not — replacing a DELETE/AUTH action is always
|
|
68
|
+
* semantically inconsistent regardless of AI confidence. */
|
|
69
|
+
readonly FAIL_HARD_DESTRUCTIVE_ROLE_INVERSION: "SG_FAIL_HARD_DESTRUCTIVE_ROLE_INVERSION";
|
|
66
70
|
/**
|
|
67
71
|
* Heal was REQUIRES_REVIEW → quarantined (decision #1): proposed but not
|
|
68
72
|
* written to source and not executed this run, pending explicit approval.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaError.d.ts","sourceRoot":"","sources":["../../src/errors/SelaError.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,aAAa,GACrB,aAAa,GACb,eAAe,GACf,YAAY,GACZ,OAAO,GACP,YAAY,GACZ,UAAU,GACV,gBAAgB,GAChB,QAAQ,GACR,QAAQ,CAAC;AAMb,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,iBAAiB,GAAG,MAAM,CAAC;IACnE,0EAA0E;IAC1E,cAAc,CAAC,EAAE,YAAY,GAAG,cAAc,GAAG,YAAY,CAAC;IAC9D,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAMD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,aAAa,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,aAAa,CAAC;IACzB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAYD,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAEvB,IAAI,EAAE,aAAa;IAsC/B,MAAM,CAAC,aAAa,CAClB,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM;IAIT,MAAM,IAAI,aAAa;CAexB;AAMD,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,SAAS,CAE1D;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,aAAa,GACvB,GAAG,IAAI,SAAS,CAElB;AAUD,eAAO,MAAM,kBAAkB;;;;;;;;;IAS7B;;;;OAIG;;IAEH,+EAA+E;;CAEvE,CAAC;AACX,MAAM,MAAM,eAAe,GACzB,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC;AAE/D,eAAO,MAAM,oBAAoB;IAC/B,wEAAwE;;;;CAIhE,CAAC;AACX,MAAM,MAAM,iBAAiB,GAC3B,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,OAAO,oBAAoB,CAAC,CAAC;AAEnE,eAAO,MAAM,iBAAiB;;;;;;CAMpB,CAAC;AACX,MAAM,MAAM,cAAc,GACxB,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAE7D,eAAO,MAAM,iBAAiB;;;;;;;IAO5B;;;;OAIG;;CAEK,CAAC;AACX,MAAM,MAAM,cAAc,GACxB,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAE7D,eAAO,MAAM,WAAW;;;;CAId,CAAC;AACX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAEvE,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,CAAC;AAEhF,eAAO,MAAM,YAAY;IACvB,2EAA2E;;IAE3E,wDAAwD;;IAExD,gEAAgE;;IAEhE,gDAAgD;;IAEhD,8CAA8C;;IAE9C;;;;OAIG;;IAEH,gFAAgF;;CAExE,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE1E,eAAO,MAAM,qBAAqB;;;;;;CAMxB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAC5B,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,OAAO,qBAAqB,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"SelaError.d.ts","sourceRoot":"","sources":["../../src/errors/SelaError.ts"],"names":[],"mappings":"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"}
|
package/dist/errors/SelaError.js
CHANGED
|
@@ -94,6 +94,10 @@ exports.SAFETY_GUARD_CODES = {
|
|
|
94
94
|
BLOCKED_ROLE_SWAP_ASSERTION: "SG_BLOCKED_ROLE_SWAP_ASSERTION",
|
|
95
95
|
FAIL_HARD_AUDITOR_INCONSISTENT: "SG_FAIL_HARD_AUDITOR_INCONSISTENT",
|
|
96
96
|
BLOCKED_AUDITOR_SUSPICIOUS: "SG_BLOCKED_AUDITOR_SUSPICIOUS",
|
|
97
|
+
/** Structural pre-LLM check: old element had a destructive/auth role and
|
|
98
|
+
* the candidate does not — replacing a DELETE/AUTH action is always
|
|
99
|
+
* semantically inconsistent regardless of AI confidence. */
|
|
100
|
+
FAIL_HARD_DESTRUCTIVE_ROLE_INVERSION: "SG_FAIL_HARD_DESTRUCTIVE_ROLE_INVERSION",
|
|
97
101
|
/**
|
|
98
102
|
* Heal was REQUIRES_REVIEW → quarantined (decision #1): proposed but not
|
|
99
103
|
* written to source and not executed this run, pending explicit approval.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Reporter, TestCase, TestResult, FullConfig, Suite, FullResult } from "@playwright/test/reporter";
|
|
2
2
|
import { HealReportService } from "../services/HealReportService.js";
|
|
3
3
|
import type { SelaEngine } from "../engine/SelaEngine.js";
|
|
4
|
-
import { type PendingPromptEntry } from "../services/PendingPromptLedger.js";
|
|
4
|
+
import { type PendingPromptEntry, type WorkerGitPayload } from "../services/PendingPromptLedger.js";
|
|
5
5
|
import { type PrintReviewDeps } from "../services/InteractiveReview.js";
|
|
6
6
|
export interface ReporterEngine {
|
|
7
7
|
commitUpdates(): Promise<void>;
|
|
@@ -61,6 +61,12 @@ export interface SelaReporterOpts {
|
|
|
61
61
|
* polluted by leftover handlers between cases.
|
|
62
62
|
*/
|
|
63
63
|
installSignalHandlers?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Override the centralized git/PR flow executed in CI after all workers
|
|
66
|
+
* finish. Tests stub this to avoid real git ops. Defaults to reading the
|
|
67
|
+
* worker ledger and calling PRAutomationService.runCentralizedGitFlow.
|
|
68
|
+
*/
|
|
69
|
+
runCentralizedGitFlow?: (cwd: string) => Promise<void>;
|
|
64
70
|
}
|
|
65
71
|
export declare class SelaReporter implements Reporter {
|
|
66
72
|
private readonly engine;
|
|
@@ -75,8 +81,10 @@ export declare class SelaReporter implements Reporter {
|
|
|
75
81
|
private readonly isCi;
|
|
76
82
|
private readonly mergeShards;
|
|
77
83
|
private readonly shouldInstallSignalHandlers;
|
|
84
|
+
private readonly runCentralizedGitFlowFn;
|
|
78
85
|
private shutdownHandler;
|
|
79
86
|
private shuttingDown;
|
|
87
|
+
private _isShardedRun;
|
|
80
88
|
/** Total tests in the suite, captured at onBegin for the end-of-run summary. */
|
|
81
89
|
private totalTests;
|
|
82
90
|
/**
|
|
@@ -161,5 +169,13 @@ export declare class SelaReporter implements Reporter {
|
|
|
161
169
|
*/
|
|
162
170
|
private extractScreenshotBase64;
|
|
163
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Deduplicate HealedEvents by stableId. When the same selector was healed
|
|
174
|
+
* across multiple retries or workers, the last entry wins — it represents
|
|
175
|
+
* the canonical final state of that mutation.
|
|
176
|
+
*
|
|
177
|
+
* Exported for use by `sela pr aggregate` (cross-shard fan-in).
|
|
178
|
+
*/
|
|
179
|
+
export declare function deduplicateByStableId(events: WorkerGitPayload["healedEvents"]): WorkerGitPayload["healedEvents"];
|
|
164
180
|
export default SelaReporter;
|
|
165
181
|
//# sourceMappingURL=SelaReporter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,UAAU,EACV,KAAK,EACL,UAAU,EACX,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAEL,iBAAiB,EAIlB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,
|
|
1
|
+
{"version":3,"file":"SelaReporter.d.ts","sourceRoot":"","sources":["../../src/reporter/SelaReporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -62,6 +62,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
62
62
|
})();
|
|
63
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
64
|
exports.SelaReporter = void 0;
|
|
65
|
+
exports.deduplicateByStableId = deduplicateByStableId;
|
|
65
66
|
const fs = __importStar(require("fs"));
|
|
66
67
|
const path = __importStar(require("path"));
|
|
67
68
|
const HealReportService_js_1 = require("../services/HealReportService.js");
|
|
@@ -69,6 +70,7 @@ const ReportMergeService_js_1 = require("../services/ReportMergeService.js");
|
|
|
69
70
|
const singleton_js_1 = require("../engine/singleton.js");
|
|
70
71
|
const SelaError_js_1 = require("../errors/SelaError.js");
|
|
71
72
|
const PendingPromptLedger_js_1 = require("../services/PendingPromptLedger.js");
|
|
73
|
+
const PRAutomationService_js_1 = require("../services/PRAutomationService.js");
|
|
72
74
|
const InteractiveReview_js_1 = require("../services/InteractiveReview.js");
|
|
73
75
|
const logger_js_1 = require("../utils/logger.js");
|
|
74
76
|
const SummaryTable_js_1 = require("../utils/SummaryTable.js");
|
|
@@ -88,8 +90,10 @@ class SelaReporter {
|
|
|
88
90
|
isCi;
|
|
89
91
|
mergeShards;
|
|
90
92
|
shouldInstallSignalHandlers;
|
|
93
|
+
runCentralizedGitFlowFn;
|
|
91
94
|
shutdownHandler = null;
|
|
92
95
|
shuttingDown = false;
|
|
96
|
+
_isShardedRun = false;
|
|
93
97
|
/** Total tests in the suite, captured at onBegin for the end-of-run summary. */
|
|
94
98
|
totalTests = 0;
|
|
95
99
|
/**
|
|
@@ -120,6 +124,8 @@ class SelaReporter {
|
|
|
120
124
|
this.isCi = opts.isCi ?? defaultIsCi;
|
|
121
125
|
this.mergeShards = opts.mergeShards ?? defaultMergeShards;
|
|
122
126
|
this.shouldInstallSignalHandlers = opts.installSignalHandlers ?? true;
|
|
127
|
+
this.runCentralizedGitFlowFn =
|
|
128
|
+
opts.runCentralizedGitFlow ?? defaultRunCentralizedGitFlow;
|
|
123
129
|
}
|
|
124
130
|
// ─────────────────────────────────────────────────────────────
|
|
125
131
|
// Playwright Reporter API
|
|
@@ -129,6 +135,11 @@ class SelaReporter {
|
|
|
129
135
|
// finalisation. Today the fixture still calls engine.commitUpdates()
|
|
130
136
|
// itself; the env flag is reserved for the planned dedup guard.
|
|
131
137
|
process.env.SELA_REPORTER_ACTIVE = "1";
|
|
138
|
+
// Detect Playwright CI sharding (--shard=N/M). When sharded, each runner
|
|
139
|
+
// must skip the Git/PR flow and let the downstream fan-in job handle it.
|
|
140
|
+
if (_config?.shard != null) {
|
|
141
|
+
this._isShardedRun = true;
|
|
142
|
+
}
|
|
132
143
|
// Snapshot the suite size for the end-of-run summary table. Guarded:
|
|
133
144
|
// allTests() exists on real Playwright suites but not on test stubs.
|
|
134
145
|
try {
|
|
@@ -218,6 +229,25 @@ class SelaReporter {
|
|
|
218
229
|
this.logger.error(`[Sela] shard merge failed: ${msg}`);
|
|
219
230
|
}
|
|
220
231
|
}
|
|
232
|
+
// ── CI: centralized git/PR flow ──────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// All workers have exited by the time onEnd() fires. Their git-flow
|
|
235
|
+
// payloads are aggregated from the per-worker ledger files and fed
|
|
236
|
+
// into a single runCentralizedGitFlow call — no lock needed, no race
|
|
237
|
+
// on the git index, no "another worker holds the PR lock" warning.
|
|
238
|
+
//
|
|
239
|
+
// Sharded CI runs (--shard=N/M) skip this entirely: each shard runner
|
|
240
|
+
// only flushes its ledger; the downstream `sela pr aggregate` fan-in
|
|
241
|
+
// job opens the single unified PR after all shards complete.
|
|
242
|
+
if (this.isCi() && !this._isShardedRun) {
|
|
243
|
+
try {
|
|
244
|
+
await this.runCentralizedGitFlowFn(this.cwd);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
+
this.logger.error(`[Sela] centralized git flow failed: ${msg}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
221
251
|
// ── Local DX - Write-and-notify summary ─────────────────────
|
|
222
252
|
//
|
|
223
253
|
// Workers serialise their mutated FileSnapshots to
|
|
@@ -226,8 +256,7 @@ class SelaReporter {
|
|
|
226
256
|
// deeplink + HTML report deeplink). No Y/N prompt - the files are
|
|
227
257
|
// already mutated on disk by the time we get here.
|
|
228
258
|
//
|
|
229
|
-
// CI runs skip the notify entirely - PR automation
|
|
230
|
-
// the mutations on its isolated branch.
|
|
259
|
+
// CI runs skip the notify entirely - PR automation is handled above.
|
|
231
260
|
if (!this.isCi()) {
|
|
232
261
|
try {
|
|
233
262
|
const entries = this.readLedger(this.cwd);
|
|
@@ -512,6 +541,39 @@ function defaultMergeShards(cwd) {
|
|
|
512
541
|
(0, HealReportService_js_1.clearReportShards)(cwd);
|
|
513
542
|
}
|
|
514
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Default centralized git flow: aggregate all workers' git payloads from the
|
|
546
|
+
* pending-prompts ledger, deduplicate healed events by stableId (last retry
|
|
547
|
+
* wins), then delegate to PRAutomationService.runCentralizedGitFlow.
|
|
548
|
+
*/
|
|
549
|
+
async function defaultRunCentralizedGitFlow(cwd) {
|
|
550
|
+
const payloads = (0, PendingPromptLedger_js_1.readGitPayloads)(cwd);
|
|
551
|
+
if (payloads.length === 0)
|
|
552
|
+
return;
|
|
553
|
+
const allHealedEvents = deduplicateByStableId(payloads.flatMap((p) => p.healedEvents));
|
|
554
|
+
const allProtectedEvents = payloads.flatMap((p) => p.protectedEvents);
|
|
555
|
+
if (allHealedEvents.length === 0)
|
|
556
|
+
return;
|
|
557
|
+
await (0, PRAutomationService_js_1.runCentralizedGitFlow)({
|
|
558
|
+
cwd,
|
|
559
|
+
healedEvents: allHealedEvents,
|
|
560
|
+
protectedEvents: allProtectedEvents,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Deduplicate HealedEvents by stableId. When the same selector was healed
|
|
565
|
+
* across multiple retries or workers, the last entry wins — it represents
|
|
566
|
+
* the canonical final state of that mutation.
|
|
567
|
+
*
|
|
568
|
+
* Exported for use by `sela pr aggregate` (cross-shard fan-in).
|
|
569
|
+
*/
|
|
570
|
+
function deduplicateByStableId(events) {
|
|
571
|
+
const seen = new Map();
|
|
572
|
+
for (const ev of events) {
|
|
573
|
+
seen.set(ev.stableId, ev);
|
|
574
|
+
}
|
|
575
|
+
return [...seen.values()];
|
|
576
|
+
}
|
|
515
577
|
// Default export so Playwright can resolve `reporter: [['sela-core/reporter']]`
|
|
516
578
|
// without needing the consumer to spell out the named import.
|
|
517
579
|
exports.default = SelaReporter;
|
|
@@ -47,4 +47,20 @@ export declare function execute(cfg: ResolvedPRAutomation, decision: StrategyDec
|
|
|
47
47
|
/** Injectable for tests - defaults to the module singleton. */
|
|
48
48
|
workspace?: WorkspaceSnapshotService): Promise<ExecutionResult>;
|
|
49
49
|
export declare function handleBugDetected(action: BugAction, events: ProtectedEvent[], branchInfo: BranchInfo, ctx: ExecuteContext): Promise<void>;
|
|
50
|
+
export interface CentralizedGitFlowOpts {
|
|
51
|
+
cwd: string;
|
|
52
|
+
healedEvents: HealedEvent[];
|
|
53
|
+
protectedEvents: ProtectedEvent[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run the full git/PR automation ONCE in the master process after all
|
|
57
|
+
* Playwright workers have exited. Called from SelaReporter.onEnd().
|
|
58
|
+
*
|
|
59
|
+
* Unlike the old per-worker path this function:
|
|
60
|
+
* - Needs no FileLock (single caller, single process).
|
|
61
|
+
* - Resolves `contentBefore` via `git show HEAD` — retry-proof, never
|
|
62
|
+
* polluted by a prior MUTATE_IN_PLACE run on the same branch.
|
|
63
|
+
* - Accepts pre-aggregated + de-duplicated events from all workers.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runCentralizedGitFlow(opts: CentralizedGitFlowOpts): Promise<void>;
|
|
50
66
|
//# sourceMappingURL=PRAutomationService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PRAutomationService.d.ts","sourceRoot":"","sources":["../../src/services/PRAutomationService.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,SAAS,EACV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAQpC,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,UAAU,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,kBAAkB,GAAG,gBAAgB,GAAG,aAAa,GAAG,IAAI,CAAC;IAC9E;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,6EAA6E;IAC7E,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAiFD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,UAAU,CA+EpE;AAMD,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,EACzB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,WAAW,EAAE,GACnB,gBAAgB,CAgElB;AAMD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEzC;AA6FD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,oBAAoB,EACzB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,WAAW,EAAE,EACpB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc;AACnB,+DAA+D;AAC/D,SAAS,GAAE,wBAAkD,GAC5D,OAAO,CAAC,eAAe,CAAC,CAkG1B;AAsTD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,cAAc,EAAE,EACxB,UAAU,EAAE,UAAU,EACtB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAgGf;AAMD,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|
|
@@ -51,9 +51,11 @@ exports.decideEffectiveStrategy = decideEffectiveStrategy;
|
|
|
51
51
|
exports.fmtPct = fmtPct;
|
|
52
52
|
exports.execute = execute;
|
|
53
53
|
exports.handleBugDetected = handleBugDetected;
|
|
54
|
+
exports.runCentralizedGitFlow = runCentralizedGitFlow;
|
|
54
55
|
const fs = __importStar(require("fs"));
|
|
55
56
|
const path = __importStar(require("path"));
|
|
56
57
|
const child_process_1 = require("child_process");
|
|
58
|
+
const ConfigLoader_1 = require("../config/ConfigLoader");
|
|
57
59
|
const WorkspaceSnapshotService_1 = require("./WorkspaceSnapshotService");
|
|
58
60
|
const IsolatedDiff_1 = require("../utils/IsolatedDiff");
|
|
59
61
|
const logger_1 = require("../utils/logger");
|
|
@@ -746,6 +748,103 @@ async function handleBugDetected(action, events, branchInfo, ctx) {
|
|
|
746
748
|
}
|
|
747
749
|
}
|
|
748
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* Run the full git/PR automation ONCE in the master process after all
|
|
753
|
+
* Playwright workers have exited. Called from SelaReporter.onEnd().
|
|
754
|
+
*
|
|
755
|
+
* Unlike the old per-worker path this function:
|
|
756
|
+
* - Needs no FileLock (single caller, single process).
|
|
757
|
+
* - Resolves `contentBefore` via `git show HEAD` — retry-proof, never
|
|
758
|
+
* polluted by a prior MUTATE_IN_PLACE run on the same branch.
|
|
759
|
+
* - Accepts pre-aggregated + de-duplicated events from all workers.
|
|
760
|
+
*/
|
|
761
|
+
async function runCentralizedGitFlow(opts) {
|
|
762
|
+
const cfg = ConfigLoader_1.ConfigLoader.getInstance();
|
|
763
|
+
const prCfg = cfg.prAutomation;
|
|
764
|
+
if (!prCfg.enabled)
|
|
765
|
+
return;
|
|
766
|
+
if (opts.healedEvents.length === 0)
|
|
767
|
+
return;
|
|
768
|
+
const fileSnapshots = resolveGitBaselines(opts.healedEvents, opts.cwd);
|
|
769
|
+
if (fileSnapshots.length === 0) {
|
|
770
|
+
logger_1.logger.warn("[PR] Centralized flow: no mutated files resolved from git baseline — skipping.");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const branchInfo = detectBranch(opts.cwd);
|
|
774
|
+
const decision = decideEffectiveStrategy(prCfg, branchInfo.branch, opts.healedEvents);
|
|
775
|
+
if (decision.downgradeReason) {
|
|
776
|
+
logger_1.logger.warn(`[PR] Strategy downgraded: '${decision.configured}' → '${decision.effective}' ` +
|
|
777
|
+
`(reason: ${decision.downgradeReason}, ` +
|
|
778
|
+
`minAI: ${fmtPct(decision.minAiConfidence)}, ` +
|
|
779
|
+
`minAuditor: ${fmtPct(decision.minAuditorConfidence)})`);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
logger_1.logger.debug(`[PR] Effective strategy: '${decision.effective}' on branch '${branchInfo.branch ?? "unknown"}'`);
|
|
783
|
+
}
|
|
784
|
+
// Minimal workspace shim — execute() only calls getMutatedSnapshots().
|
|
785
|
+
const fakeWorkspace = {
|
|
786
|
+
getMutatedSnapshots: () => fileSnapshots,
|
|
787
|
+
};
|
|
788
|
+
const ctx = { cwd: opts.cwd, reportHtmlPath: null };
|
|
789
|
+
try {
|
|
790
|
+
await execute(prCfg, decision, opts.healedEvents, branchInfo, ctx, fakeWorkspace);
|
|
791
|
+
await handleBugDetected(prCfg.onBugDetected, opts.protectedEvents, branchInfo, ctx);
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
logger_1.logger.warn(`[PR] Centralized git flow failed: ${err?.message ?? err}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Build FileSnapshot[] for the centralized git flow by resolving each
|
|
799
|
+
* mutated file's baseline from `git show HEAD`. This is retry-proof:
|
|
800
|
+
* HEAD always reflects the committed state before any worker's
|
|
801
|
+
* MUTATE_IN_PLACE touched the working tree.
|
|
802
|
+
*
|
|
803
|
+
* contentBefore = headContent = git HEAD content (CI worktree is clean).
|
|
804
|
+
* contentAfter = current file on disk (workers wrote mutations in-place).
|
|
805
|
+
*/
|
|
806
|
+
function resolveGitBaselines(heals, cwd) {
|
|
807
|
+
const filePaths = new Set();
|
|
808
|
+
for (const h of heals) {
|
|
809
|
+
const target = h.definitionSite?.file ?? h.sourceFile;
|
|
810
|
+
if (target) {
|
|
811
|
+
filePaths.add(path.isAbsolute(target) ? target : path.resolve(cwd, target));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const snapshots = [];
|
|
815
|
+
for (const absPath of filePaths) {
|
|
816
|
+
const rel = path.relative(cwd, absPath).replace(/\\/g, "/");
|
|
817
|
+
const gitResult = sh(`git show HEAD:"${rel}"`, cwd);
|
|
818
|
+
const contentAfter = fs.existsSync(absPath)
|
|
819
|
+
? fs.readFileSync(absPath, "utf8")
|
|
820
|
+
: "";
|
|
821
|
+
if (!gitResult.ok) {
|
|
822
|
+
// File was not tracked at HEAD — created by Sela or untracked.
|
|
823
|
+
snapshots.push({
|
|
824
|
+
absolutePath: absPath,
|
|
825
|
+
relativePath: rel,
|
|
826
|
+
contentBefore: "",
|
|
827
|
+
contentAfter,
|
|
828
|
+
headContent: null,
|
|
829
|
+
hadUncommittedChanges: false,
|
|
830
|
+
createdBySela: true,
|
|
831
|
+
});
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const headContent = gitResult.stdout;
|
|
835
|
+
snapshots.push({
|
|
836
|
+
absolutePath: absPath,
|
|
837
|
+
relativePath: rel,
|
|
838
|
+
contentBefore: headContent,
|
|
839
|
+
contentAfter,
|
|
840
|
+
headContent,
|
|
841
|
+
hadUncommittedChanges: false,
|
|
842
|
+
createdBySela: false,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
// Only return files where Sela actually changed something.
|
|
846
|
+
return snapshots.filter((s) => s.contentAfter !== null && s.contentAfter !== s.contentBefore);
|
|
847
|
+
}
|
|
749
848
|
// ═══════════════════════════════════════════════════════════════════
|
|
750
849
|
// SHELL QUOTING - cross-platform (POSIX single-quote escape;
|
|
751
850
|
// on Windows execSync uses cmd.exe which handles double-quotes natively).
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { WorkspaceSnapshotService } from "./WorkspaceSnapshotService";
|
|
2
|
-
import type { HealedEvent } from "./HealReportService";
|
|
2
|
+
import type { HealedEvent, ProtectedEvent } from "./HealReportService";
|
|
3
|
+
/**
|
|
4
|
+
* The subset of worker state that the master process needs to run the
|
|
5
|
+
* centralized git/PR flow in SelaReporter.onEnd(). Written by every worker
|
|
6
|
+
* alongside the local-DX PendingPromptEntry list so the master can aggregate
|
|
7
|
+
* all workers' mutations and fire a single `gh pr create` after every worker
|
|
8
|
+
* has exited — without any FileLock contention.
|
|
9
|
+
*/
|
|
10
|
+
export interface WorkerGitPayload {
|
|
11
|
+
/** Absolute paths of every source file this worker mutated. */
|
|
12
|
+
mutatedFiles: string[];
|
|
13
|
+
healedEvents: HealedEvent[];
|
|
14
|
+
protectedEvents: ProtectedEvent[];
|
|
15
|
+
}
|
|
3
16
|
export interface PendingPromptEntry {
|
|
4
17
|
absolutePath: string;
|
|
5
18
|
relativePath: string;
|
|
@@ -36,16 +49,34 @@ export declare function writePendingPromptsLedger(opts: {
|
|
|
36
49
|
cwd: string;
|
|
37
50
|
workspace: WorkspaceSnapshotService;
|
|
38
51
|
healedEvents: HealedEvent[];
|
|
52
|
+
protectedEvents?: ProtectedEvent[];
|
|
39
53
|
}): string | null;
|
|
40
54
|
/**
|
|
41
55
|
* Drain every per-worker JSON file under the ledger directory and merge
|
|
42
56
|
* their entries into a single de-duplicated list (keyed by absolutePath).
|
|
43
57
|
* The most-recent contentAfter wins on collision - that's the disk truth.
|
|
44
58
|
*
|
|
59
|
+
* Handles both v1 (bare PendingPromptEntry[]) and v2 (LedgerFileV2) files.
|
|
60
|
+
*
|
|
45
61
|
* The directory is NOT deleted here so the reporter can inspect entries
|
|
46
62
|
* before calling `clearPendingPromptsLedger()`.
|
|
47
63
|
*/
|
|
48
64
|
export declare function readPendingPromptsLedger(cwd: string): PendingPromptEntry[];
|
|
65
|
+
/**
|
|
66
|
+
* Read the git-flow payloads written by each worker. Used by the master
|
|
67
|
+
* process (SelaReporter.onEnd) to aggregate all workers' mutations and
|
|
68
|
+
* fire a single centralized PR creation after all workers have exited.
|
|
69
|
+
*
|
|
70
|
+
* Only v2 ledger files carry a `git` payload; v1 files are skipped silently.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readGitPayloads(cwd: string): WorkerGitPayload[];
|
|
73
|
+
/**
|
|
74
|
+
* Same as readGitPayloads but scans an arbitrary directory directly.
|
|
75
|
+
* Used by `sela pr aggregate --ledger-dir <path>` where the CI fan-in
|
|
76
|
+
* job downloads shard ledger artifacts into a flat directory rather than
|
|
77
|
+
* the standard `.sela/pending-prompts` subpath.
|
|
78
|
+
*/
|
|
79
|
+
export declare function readGitPayloadsFromDir(dir: string): WorkerGitPayload[];
|
|
49
80
|
/** Remove the ledger directory and every file beneath it. Idempotent. */
|
|
50
81
|
export declare function clearPendingPromptsLedger(cwd: string): void;
|
|
51
82
|
//# sourceMappingURL=PendingPromptLedger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"PendingPromptLedger.d.ts","sourceRoot":"","sources":["../../src/services/PendingPromptLedger.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,wBAAwB,EACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMvE;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+DAA+D;IAC/D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,EAAE,cAAc,EAAE,CAAC;CACnC;AAcD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAID;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,wBAAwB,CAAC;IACpC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,cAAc,EAAE,CAAC;CACpC,GAAG,MAAM,GAAG,IAAI,CA8BhB;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAgC1E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAmBtE;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3D"}
|
|
@@ -55,6 +55,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
exports.ledgerDir = ledgerDir;
|
|
56
56
|
exports.writePendingPromptsLedger = writePendingPromptsLedger;
|
|
57
57
|
exports.readPendingPromptsLedger = readPendingPromptsLedger;
|
|
58
|
+
exports.readGitPayloads = readGitPayloads;
|
|
59
|
+
exports.readGitPayloadsFromDir = readGitPayloadsFromDir;
|
|
58
60
|
exports.clearPendingPromptsLedger = clearPendingPromptsLedger;
|
|
59
61
|
const fs = __importStar(require("fs"));
|
|
60
62
|
const path = __importStar(require("path"));
|
|
@@ -78,15 +80,26 @@ function ledgerDir(cwd) {
|
|
|
78
80
|
* empty-mutation runs).
|
|
79
81
|
*/
|
|
80
82
|
function writePendingPromptsLedger(opts) {
|
|
83
|
+
const protectedEvents = opts.protectedEvents ?? [];
|
|
81
84
|
const snaps = opts.workspace.getMutatedSnapshots();
|
|
82
|
-
if (snaps.length === 0)
|
|
83
|
-
return null;
|
|
84
85
|
const entries = snaps.map((s) => snapshotToEntry(s, opts.healedEvents, opts.cwd));
|
|
86
|
+
const git = {
|
|
87
|
+
mutatedFiles: snaps.map((s) => s.absolutePath),
|
|
88
|
+
healedEvents: opts.healedEvents,
|
|
89
|
+
protectedEvents,
|
|
90
|
+
};
|
|
91
|
+
// Nothing to persist when there are no mutations AND no healed/protected events.
|
|
92
|
+
if (snaps.length === 0 && opts.healedEvents.length === 0 && protectedEvents.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
85
95
|
const dir = ledgerDir(opts.cwd);
|
|
86
96
|
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
-
|
|
97
|
+
// Filename includes workerIndex so concurrent workers never collide.
|
|
98
|
+
const workerIdx = process.env["TEST_WORKER_INDEX"] ?? "0";
|
|
99
|
+
const fileName = `${workerIdx}-${process.pid}-${Date.now()}-${randomTag()}.json`;
|
|
88
100
|
const full = path.join(dir, fileName);
|
|
89
|
-
|
|
101
|
+
const payload = { version: 2, entries, git };
|
|
102
|
+
fs.writeFileSync(full, JSON.stringify(payload, null, 2), "utf8");
|
|
90
103
|
return full;
|
|
91
104
|
}
|
|
92
105
|
/**
|
|
@@ -94,6 +107,8 @@ function writePendingPromptsLedger(opts) {
|
|
|
94
107
|
* their entries into a single de-duplicated list (keyed by absolutePath).
|
|
95
108
|
* The most-recent contentAfter wins on collision - that's the disk truth.
|
|
96
109
|
*
|
|
110
|
+
* Handles both v1 (bare PendingPromptEntry[]) and v2 (LedgerFileV2) files.
|
|
111
|
+
*
|
|
97
112
|
* The directory is NOT deleted here so the reporter can inspect entries
|
|
98
113
|
* before calling `clearPendingPromptsLedger()`.
|
|
99
114
|
*/
|
|
@@ -105,18 +120,24 @@ function readPendingPromptsLedger(cwd) {
|
|
|
105
120
|
const files = fs
|
|
106
121
|
.readdirSync(dir)
|
|
107
122
|
.filter((f) => f.endsWith(".json"))
|
|
108
|
-
.sort(); // pid-timestamp prefix gives a stable, time-ordered merge
|
|
123
|
+
.sort(); // workerIdx-pid-timestamp prefix gives a stable, time-ordered merge
|
|
109
124
|
for (const f of files) {
|
|
110
125
|
const full = path.join(dir, f);
|
|
111
126
|
try {
|
|
112
127
|
const raw = fs.readFileSync(full, "utf8");
|
|
113
128
|
const parsed = JSON.parse(raw);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
// v2: { version: 2, entries: [...], git: {...} }
|
|
130
|
+
// v1: bare PendingPromptEntry[]
|
|
131
|
+
const entryList = Array.isArray(parsed)
|
|
132
|
+
? parsed
|
|
133
|
+
: parsed.version === 2
|
|
134
|
+
? parsed.entries
|
|
135
|
+
: [];
|
|
136
|
+
for (const entry of entryList) {
|
|
137
|
+
const e = entry;
|
|
138
|
+
if (!e || typeof e.absolutePath !== "string")
|
|
118
139
|
continue;
|
|
119
|
-
merged.set(path.normalize(
|
|
140
|
+
merged.set(path.normalize(e.absolutePath), e);
|
|
120
141
|
}
|
|
121
142
|
}
|
|
122
143
|
catch {
|
|
@@ -125,6 +146,42 @@ function readPendingPromptsLedger(cwd) {
|
|
|
125
146
|
}
|
|
126
147
|
return Array.from(merged.values());
|
|
127
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Read the git-flow payloads written by each worker. Used by the master
|
|
151
|
+
* process (SelaReporter.onEnd) to aggregate all workers' mutations and
|
|
152
|
+
* fire a single centralized PR creation after all workers have exited.
|
|
153
|
+
*
|
|
154
|
+
* Only v2 ledger files carry a `git` payload; v1 files are skipped silently.
|
|
155
|
+
*/
|
|
156
|
+
function readGitPayloads(cwd) {
|
|
157
|
+
return readGitPayloadsFromDir(ledgerDir(cwd));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Same as readGitPayloads but scans an arbitrary directory directly.
|
|
161
|
+
* Used by `sela pr aggregate --ledger-dir <path>` where the CI fan-in
|
|
162
|
+
* job downloads shard ledger artifacts into a flat directory rather than
|
|
163
|
+
* the standard `.sela/pending-prompts` subpath.
|
|
164
|
+
*/
|
|
165
|
+
function readGitPayloadsFromDir(dir) {
|
|
166
|
+
if (!fs.existsSync(dir))
|
|
167
|
+
return [];
|
|
168
|
+
const payloads = [];
|
|
169
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const full = path.join(dir, f);
|
|
172
|
+
try {
|
|
173
|
+
const raw = fs.readFileSync(full, "utf8");
|
|
174
|
+
const parsed = JSON.parse(raw);
|
|
175
|
+
if (parsed?.version === 2 && parsed.git) {
|
|
176
|
+
payloads.push(parsed.git);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Corrupt file - skip.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return payloads;
|
|
184
|
+
}
|
|
128
185
|
/** Remove the ledger directory and every file beneath it. Idempotent. */
|
|
129
186
|
function clearPendingPromptsLedger(cwd) {
|
|
130
187
|
const dir = ledgerDir(cwd);
|
|
@@ -115,6 +115,7 @@ export declare class SafetyGuard {
|
|
|
115
115
|
constructor(arg?: ResolvedThresholds | SafetyGuardOptions);
|
|
116
116
|
private static normalizeArgs;
|
|
117
117
|
evaluate(context: HealContext, aiFix: AIFixSummary): Promise<SafetyDecision>;
|
|
118
|
+
private applyTextDriftGuard;
|
|
118
119
|
classifyFunctionalRole(text: string): FunctionalRole;
|
|
119
120
|
}
|
|
120
121
|
//# sourceMappingURL=SafetyGuard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SafetyGuard.d.ts","sourceRoot":"","sources":["../../src/services/SafetyGuard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAEL,cAAc,EACf,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"SafetyGuard.d.ts","sourceRoot":"","sources":["../../src/services/SafetyGuard.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ const IntentAuditor_1 = require("./IntentAuditor");
|
|
|
7
7
|
const PipelineContext_1 = require("./safety/PipelineContext");
|
|
8
8
|
const rules_1 = require("./safety/rules");
|
|
9
9
|
const roleClassifier_1 = require("./safety/roleClassifier");
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
10
11
|
function dispositionFor(level) {
|
|
11
12
|
switch (level) {
|
|
12
13
|
case "SAFE":
|
|
@@ -61,6 +62,24 @@ class SafetyGuard {
|
|
|
61
62
|
// ─────────────────────────────────────────────────────────────
|
|
62
63
|
async evaluate(context, aiFix) {
|
|
63
64
|
const ctx = (0, PipelineContext_1.buildContext)(context, aiFix, this.thresholds, this.intentAuditor);
|
|
65
|
+
// ── Text-Drift Pre-Flight ─────────────────────────────────────
|
|
66
|
+
// Compare DNA baseline text vs live candidate text BEFORE any rule
|
|
67
|
+
// runs. This is the primary defence against the "Delete System →
|
|
68
|
+
// Update System same-container mis-identification" class of bug:
|
|
69
|
+
//
|
|
70
|
+
// 1. Drift confirmed (both texts present, differ):
|
|
71
|
+
// synthesise effectiveContentChange so StructuralOnlyRule
|
|
72
|
+
// cannot short-circuit to SAFE, and set forceAudit so the
|
|
73
|
+
// Intent Auditor is mandatory regardless of confidence.
|
|
74
|
+
//
|
|
75
|
+
// 2. Drift unverifiable (DNA text present but liveText absent
|
|
76
|
+
// AND AI did not self-report a contentChange):
|
|
77
|
+
// set forceAudit so StructuralOnlyRule downgrades SAFE → HOLD.
|
|
78
|
+
//
|
|
79
|
+
// This guard runs once, synchronously, before the rule loop.
|
|
80
|
+
// ZeroTrustRule (step 2) may re-derive effectiveContentChange from
|
|
81
|
+
// the same inputs; that is harmless — it will set the same value.
|
|
82
|
+
this.applyTextDriftGuard(ctx, aiFix);
|
|
64
83
|
for (const rule of rules_1.SAFETY_RULES) {
|
|
65
84
|
const decision = await rule.run(ctx);
|
|
66
85
|
if (decision) {
|
|
@@ -73,6 +92,28 @@ class SafetyGuard {
|
|
|
73
92
|
// Unreachable: ConfidenceTierRule is terminal and always decides.
|
|
74
93
|
throw new Error("[SafetyGuard] rule pipeline exhausted without a decision - this is a bug");
|
|
75
94
|
}
|
|
95
|
+
applyTextDriftGuard(ctx, aiFix) {
|
|
96
|
+
const dnaText = aiFix.dnaBaselineText?.trim();
|
|
97
|
+
const liveText = aiFix.liveText?.trim();
|
|
98
|
+
if (dnaText && liveText && dnaText !== liveText) {
|
|
99
|
+
// Drift confirmed — synthesise contentChange if ZeroTrustRule has not
|
|
100
|
+
// yet run (it will run next and will produce the same pair; no conflict).
|
|
101
|
+
if (!ctx.effectiveContentChange) {
|
|
102
|
+
ctx.effectiveContentChange = { oldText: dnaText, newText: liveText };
|
|
103
|
+
}
|
|
104
|
+
ctx.forceAudit = true;
|
|
105
|
+
logger_1.logger.warn(`SafetyGuard text-drift pre-flight: DNA="${dnaText}" ≠ live="${liveText}" — ` +
|
|
106
|
+
`forceAudit=true, Intent Auditor is mandatory`);
|
|
107
|
+
}
|
|
108
|
+
else if (dnaText && !liveText && !aiFix.contentChange) {
|
|
109
|
+
// Cannot verify: DNA baseline exists but live text could not be fetched
|
|
110
|
+
// and the AI did not self-report a contentChange. Conservative stance:
|
|
111
|
+
// force a hold so StructuralOnlyRule cannot silently approve as SAFE.
|
|
112
|
+
ctx.forceAudit = true;
|
|
113
|
+
logger_1.logger.warn(`SafetyGuard text-drift pre-flight: liveText unavailable, ` +
|
|
114
|
+
`DNA="${dnaText}" unverified — forceAudit=true`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
76
117
|
// ─────────────────────────────────────────────────────────────
|
|
77
118
|
// FUNCTIONAL ROLE CLASSIFICATION (public for tests / external use)
|
|
78
119
|
//
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { FunctionalRole } from "../roleClassifier";
|
|
2
2
|
import type { SafetyRule } from "../PipelineContext";
|
|
3
3
|
/**
|
|
4
|
-
* Roles whose
|
|
5
|
-
* obviously mean "delete" or "authenticate" before, but now does.
|
|
4
|
+
* Roles whose arrival OR departure is treated as high-risk.
|
|
6
5
|
*/
|
|
7
6
|
export declare const DANGEROUS_SINK_ROLES: ReadonlySet<FunctionalRole>;
|
|
7
|
+
/** Transition INTO a dangerous role (e.g. UNKNOWN → DELETE). */
|
|
8
8
|
export declare function isDangerousSinkTransition(oldRole: FunctionalRole, newRole: FunctionalRole): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Transition OUT OF a dangerous role (e.g. DELETE → UNKNOWN / SUBMIT).
|
|
11
|
+
*
|
|
12
|
+
* Replacing a destructive/auth control with any other role is semantically
|
|
13
|
+
* inconsistent: the test was authored against a destructive action and the
|
|
14
|
+
* proposed candidate performs a different operation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isDangerousSourceTransition(oldRole: FunctionalRole, newRole: FunctionalRole): boolean;
|
|
9
17
|
export declare const DangerousSinkRule: SafetyRule;
|
|
10
18
|
//# sourceMappingURL=DangerousSinkRule.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DangerousSinkRule.d.ts","sourceRoot":"","sources":["../../../../src/services/safety/rules/DangerousSinkRule.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DangerousSinkRule.d.ts","sourceRoot":"","sources":["../../../../src/services/safety/rules/DangerousSinkRule.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,KAAK,EAAE,UAAU,EAAqC,MAAM,oBAAoB,CAAC;AAExF;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,cAAc,CAG3D,CAAC;AAEH,gEAAgE;AAChE,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,cAAc,GACtB,OAAO,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,cAAc,GACtB,OAAO,CAET;AAED,eAAO,MAAM,iBAAiB,EAAE,UA4B/B,CAAC"}
|
|
@@ -1,40 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/services/safety/rules/DangerousSinkRule.ts
|
|
3
3
|
//
|
|
4
|
-
// Step 5.5 - Dangerous-Sink escalation (context-mutator,
|
|
4
|
+
// Step 5.5 - Dangerous-Sink / Dangerous-Source escalation (context-mutator,
|
|
5
|
+
// returns null).
|
|
5
6
|
//
|
|
6
|
-
// Closes
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// Closes two related blind spots:
|
|
8
|
+
//
|
|
9
|
+
// SINK (X → DELETE/AUTH): a control that did not obviously mean "delete"
|
|
10
|
+
// or "authenticate" before, but now does. The cross-role swap rule
|
|
11
|
+
// (step 5) only fires when BOTH roles are known; this catches
|
|
12
|
+
// UNKNOWN → DELETE.
|
|
13
|
+
//
|
|
14
|
+
// SOURCE (DELETE/AUTH → X): replacing a destructive/auth control with a
|
|
15
|
+
// non-destructive one (e.g. "Delete System" → "Update System")
|
|
16
|
+
// is equally dangerous — it masks the test's original destructive
|
|
17
|
+
// intent. FunctionalRoleRule misses this because newRole=UNKNOWN
|
|
18
|
+
// fails its "both roles known" guard.
|
|
12
19
|
//
|
|
13
20
|
// Per design decision #2, this rule does NOT decide on its own; it FORCES
|
|
14
|
-
// the Intent Auditor to run (ctx.forceAudit = true)
|
|
15
|
-
// a dangerous sink and differs from the old role. The auditor's existing
|
|
21
|
+
// the Intent Auditor to run (ctx.forceAudit = true). The auditor's existing
|
|
16
22
|
// verdict mapping then applies: INCONSISTENT → FAIL_HARD, SUSPICIOUS →
|
|
17
|
-
// BLOCKED/REQUIRES_REVIEW, CONSISTENT → continue.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// is already short-circuited by FunctionalRoleRule before reaching here, so
|
|
21
|
-
// in practice this rule fires for the UNKNOWN→sink case it was built for.
|
|
23
|
+
// BLOCKED/REQUIRES_REVIEW, CONSISTENT → continue. In practice the
|
|
24
|
+
// pre-LLM structural check in IntentAuditorRule will FAIL_HARD immediately
|
|
25
|
+
// for SOURCE transitions before the LLM is even consulted.
|
|
22
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
27
|
exports.DangerousSinkRule = exports.DANGEROUS_SINK_ROLES = void 0;
|
|
24
28
|
exports.isDangerousSinkTransition = isDangerousSinkTransition;
|
|
29
|
+
exports.isDangerousSourceTransition = isDangerousSourceTransition;
|
|
25
30
|
const roleClassifier_1 = require("../roleClassifier");
|
|
26
31
|
const logger_1 = require("../../../utils/logger");
|
|
27
32
|
/**
|
|
28
|
-
* Roles whose
|
|
29
|
-
* obviously mean "delete" or "authenticate" before, but now does.
|
|
33
|
+
* Roles whose arrival OR departure is treated as high-risk.
|
|
30
34
|
*/
|
|
31
35
|
exports.DANGEROUS_SINK_ROLES = new Set([
|
|
32
36
|
"DELETE",
|
|
33
37
|
"AUTH",
|
|
34
38
|
]);
|
|
39
|
+
/** Transition INTO a dangerous role (e.g. UNKNOWN → DELETE). */
|
|
35
40
|
function isDangerousSinkTransition(oldRole, newRole) {
|
|
36
41
|
return exports.DANGEROUS_SINK_ROLES.has(newRole) && newRole !== oldRole;
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Transition OUT OF a dangerous role (e.g. DELETE → UNKNOWN / SUBMIT).
|
|
45
|
+
*
|
|
46
|
+
* Replacing a destructive/auth control with any other role is semantically
|
|
47
|
+
* inconsistent: the test was authored against a destructive action and the
|
|
48
|
+
* proposed candidate performs a different operation.
|
|
49
|
+
*/
|
|
50
|
+
function isDangerousSourceTransition(oldRole, newRole) {
|
|
51
|
+
return exports.DANGEROUS_SINK_ROLES.has(oldRole) && newRole !== oldRole;
|
|
52
|
+
}
|
|
38
53
|
exports.DangerousSinkRule = {
|
|
39
54
|
id: "dangerous-sink-escalation",
|
|
40
55
|
run(ctx) {
|
|
@@ -48,6 +63,12 @@ exports.DangerousSinkRule = {
|
|
|
48
63
|
`forcing Intent Auditor`);
|
|
49
64
|
ctx.forceAudit = true;
|
|
50
65
|
}
|
|
66
|
+
if (isDangerousSourceTransition(oldRole, newRole)) {
|
|
67
|
+
logger_1.logger.warn(`SafetyGuard dangerous-source transition: ` +
|
|
68
|
+
`"${ctx.oldText}" (${oldRole}) → "${ctx.newText}" (${newRole}) - ` +
|
|
69
|
+
`outbound from destructive role, forcing Intent Auditor`);
|
|
70
|
+
ctx.forceAudit = true;
|
|
71
|
+
}
|
|
51
72
|
return null;
|
|
52
73
|
},
|
|
53
74
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntentAuditorRule.d.ts","sourceRoot":"","sources":["../../../../src/services/safety/rules/IntentAuditorRule.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"IntentAuditorRule.d.ts","sourceRoot":"","sources":["../../../../src/services/safety/rules/IntentAuditorRule.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,UAAU,EAAqC,MAAM,oBAAoB,CAAC;AAExF,eAAO,MAAM,iBAAiB,EAAE,UAoI/B,CAAC"}
|
|
@@ -5,18 +5,25 @@
|
|
|
5
5
|
// LLM auditor when:
|
|
6
6
|
// a) zero-trust synthesized the contentChange (AI silent → extra scrutiny), OR
|
|
7
7
|
// b) we are in assertion mode (the test explicitly checked the text), OR
|
|
8
|
-
// c) both roles are UNKNOWN (no functional category to fall back on)
|
|
8
|
+
// c) both roles are UNKNOWN (no functional category to fall back on), OR
|
|
9
|
+
// d) forceAudit was set by the text-drift pre-flight or DangerousSinkRule.
|
|
10
|
+
//
|
|
11
|
+
// PRE-LLM STRUCTURAL CHECK (runs before shouldAudit):
|
|
12
|
+
// Transitioning OUT of a destructive/auth role into any non-destructive role
|
|
13
|
+
// is always semantically inconsistent — the test was authored against a
|
|
14
|
+
// DELETE/AUTH action and the candidate performs a different operation. This
|
|
15
|
+
// is caught here rather than relying solely on the LLM auditor because the
|
|
16
|
+
// LLM can return CONSISTENT for superficially similar labels (e.g. "Delete
|
|
17
|
+
// System" → "Update System" sharing the same container at 95% confidence).
|
|
9
18
|
//
|
|
10
19
|
// INCONSISTENT → FAIL_HARD. SUSPICIOUS → BLOCKED in assertion (unless
|
|
11
20
|
// allowSuspicious) else REQUIRES_REVIEW. CONSISTENT → fall through,
|
|
12
21
|
// stashing the auditor score breakdown on the context for the final tier.
|
|
13
|
-
//
|
|
14
|
-
// Extracted verbatim - identical trigger condition, reasons, codes,
|
|
15
|
-
// logs, and meta shape.
|
|
16
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
23
|
exports.IntentAuditorRule = void 0;
|
|
18
24
|
const SelaError_1 = require("../../../errors/SelaError");
|
|
19
25
|
const logger_1 = require("../../../utils/logger");
|
|
26
|
+
const DangerousSinkRule_1 = require("./DangerousSinkRule");
|
|
20
27
|
exports.IntentAuditorRule = {
|
|
21
28
|
id: "intent-auditor-gate",
|
|
22
29
|
async run(ctx) {
|
|
@@ -24,6 +31,31 @@ exports.IntentAuditorRule = {
|
|
|
24
31
|
const newText = ctx.newText;
|
|
25
32
|
const oldRole = ctx.oldRole;
|
|
26
33
|
const newRole = ctx.newRole;
|
|
34
|
+
// ── Pre-LLM structural check: destructive role inversion ─────────
|
|
35
|
+
// When the DNA element had a destructive/auth role and the candidate does
|
|
36
|
+
// NOT, reject immediately without consulting the LLM. AI confidence is
|
|
37
|
+
// irrelevant — no valid heal should replace a DELETE/AUTH control with a
|
|
38
|
+
// non-destructive one. The LLM can return CONSISTENT for "Delete System →
|
|
39
|
+
// Update System" because both appear in the same container; this pure-code
|
|
40
|
+
// gate closes that bypass regardless of the shouldAudit flag below.
|
|
41
|
+
if (DangerousSinkRule_1.DANGEROUS_SINK_ROLES.has(oldRole) &&
|
|
42
|
+
!DangerousSinkRule_1.DANGEROUS_SINK_ROLES.has(newRole)) {
|
|
43
|
+
logger_1.logger.warn(`SafetyGuard destructive role inversion (pre-LLM): ` +
|
|
44
|
+
`"${oldText}" (${oldRole}) → "${newText}" (${newRole}) - FAIL_HARD`);
|
|
45
|
+
return {
|
|
46
|
+
level: "FAIL_HARD",
|
|
47
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_DESTRUCTIVE_ROLE_INVERSION,
|
|
48
|
+
reason: `Destructive role inversion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
|
|
49
|
+
`replacing a ${oldRole} action is semantically inconsistent regardless of confidence`,
|
|
50
|
+
canProceed: false,
|
|
51
|
+
meta: {
|
|
52
|
+
auditorVerdict: "INCONSISTENT",
|
|
53
|
+
auditorConfidence: 100,
|
|
54
|
+
auditorReason: `Source role ${oldRole} is destructive/auth; ` +
|
|
55
|
+
`candidate role ${newRole} is not — structural semantic mismatch`,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
27
59
|
const shouldAudit = ctx.forceAudit === true ||
|
|
28
60
|
ctx.source === "zero-trust" ||
|
|
29
61
|
ctx.mode === "assertion" ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StructuralOnlyRule.d.ts","sourceRoot":"","sources":["../../../../src/services/safety/rules/StructuralOnlyRule.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAqC,MAAM,oBAAoB,CAAC;AAExF,eAAO,MAAM,kBAAkB,EAAE,
|
|
1
|
+
{"version":3,"file":"StructuralOnlyRule.d.ts","sourceRoot":"","sources":["../../../../src/services/safety/rules/StructuralOnlyRule.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAqC,MAAM,oBAAoB,CAAC;AAExF,eAAO,MAAM,kBAAkB,EAAE,UAwChC,CAAC"}
|
|
@@ -19,6 +19,19 @@ exports.StructuralOnlyRule = {
|
|
|
19
19
|
id: "structural-only-heal",
|
|
20
20
|
run(ctx) {
|
|
21
21
|
if (!ctx.effectiveContentChange) {
|
|
22
|
+
// Text-drift pre-flight set forceAudit because live text was unavailable
|
|
23
|
+
// and we cannot verify the candidate has the same text as the DNA
|
|
24
|
+
// baseline. Approving as SAFE here would bypass every downstream guard
|
|
25
|
+
// (semantic-opposite, functional-role, intent-auditor). Downgrade to
|
|
26
|
+
// REQUIRES_REVIEW (HOLD) so the heal is quarantined pending manual review.
|
|
27
|
+
if (ctx.forceAudit) {
|
|
28
|
+
return {
|
|
29
|
+
level: "REQUIRES_REVIEW",
|
|
30
|
+
reason: `Text drift unverifiable: DNA baseline present but live text unavailable ` +
|
|
31
|
+
`— cannot approve as structural-only (confidence=${ctx.confidence})`,
|
|
32
|
+
canProceed: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
22
35
|
const level = ctx.confidence < ctx.thresholds.confidenceReviewThreshold
|
|
23
36
|
? "REQUIRES_REVIEW"
|
|
24
37
|
: "SAFE";
|
package/package.json
CHANGED