sela-core 1.0.5 → 1.0.7
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 +1 -1
- package/dist/config/ConfigLoader.d.ts +1 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +10 -0
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +79 -0
- package/dist/config/SelaConfig.d.ts +15 -1
- package/dist/config/SelaConfig.d.ts.map +1 -1
- package/dist/config/SelaConfig.js +27 -1
- package/dist/engine/SelaEngine.d.ts +26 -6
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +329 -77
- package/dist/errors/SelaError.d.ts +133 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +155 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -1
- package/dist/fixtures/expectProxy.js +7 -0
- package/dist/fixtures/index.d.ts +7 -1
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +15 -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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/reporter/SelaReporter.d.ts +110 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +328 -0
- package/dist/services/ASTSourceUpdater.d.ts +19 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
- package/dist/services/ASTSourceUpdater.js +173 -29
- package/dist/services/HealReportService.d.ts +62 -3
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +184 -14
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +460 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -1
- package/dist/services/InitializerUpdater.js +20 -1
- package/dist/services/IntentAuditor.d.ts +18 -1
- package/dist/services/IntentAuditor.d.ts.map +1 -1
- package/dist/services/IntentAuditor.js +19 -6
- package/dist/services/InteractiveReview.d.ts +24 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +218 -0
- package/dist/services/LLMService.d.ts +23 -2
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +48 -12
- package/dist/services/PRAutomationService.d.ts +23 -3
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +325 -79
- package/dist/services/PendingPromptLedger.d.ts +44 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +180 -0
- package/dist/services/ReportGenerator.d.ts +70 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +191 -0
- package/dist/services/SafetyGuard.d.ts +34 -2
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +65 -13
- package/dist/services/SourceUpdater.d.ts.map +1 -1
- package/dist/services/SourceUpdater.js +44 -13
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +202 -0
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +379 -0
- package/package.json +71 -67
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/PendingPromptLedger.ts
|
|
3
|
+
//
|
|
4
|
+
// Cross-process ledger for the Local DX dual-mode lifecycle.
|
|
5
|
+
//
|
|
6
|
+
// Playwright workers run in separate Node processes from the reporter
|
|
7
|
+
// (main process). The in-memory `WorkspaceSnapshotService` ledger is
|
|
8
|
+
// per-process, so we can't drive an interactive prompt from the reporter
|
|
9
|
+
// using only worker-side state.
|
|
10
|
+
//
|
|
11
|
+
// Solution: each worker SERIALISES its mutated FileSnapshots to a JSON
|
|
12
|
+
// file under `<cwd>/.sela/pending-prompts/`, one file per worker process
|
|
13
|
+
// (keyed by pid + monotonic timestamp). The reporter's onEnd hook reads
|
|
14
|
+
// every file in that directory, drives `runInteractiveReview()` against
|
|
15
|
+
// the merged list, then deletes the directory.
|
|
16
|
+
//
|
|
17
|
+
// CRITICAL: workers MUST NOT call `readline` / block on stdin — they have
|
|
18
|
+
// no TTY attached. Blocking the event loop here causes the Playwright
|
|
19
|
+
// test timeout to fire. This ledger is the boundary between silent
|
|
20
|
+
// worker-side execution and TTY-bound reporter-side prompting.
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.ledgerDir = ledgerDir;
|
|
56
|
+
exports.writePendingPromptsLedger = writePendingPromptsLedger;
|
|
57
|
+
exports.readPendingPromptsLedger = readPendingPromptsLedger;
|
|
58
|
+
exports.clearPendingPromptsLedger = clearPendingPromptsLedger;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
const LEDGER_SUBDIR = path.join(".sela", "pending-prompts");
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the ledger directory for a given cwd. The directory is created
|
|
64
|
+
* lazily on the first write so we never clutter repos that aren't running
|
|
65
|
+
* Sela.
|
|
66
|
+
*/
|
|
67
|
+
function ledgerDir(cwd) {
|
|
68
|
+
return path.join(cwd, LEDGER_SUBDIR);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Persist this worker's mutated snapshots to a fresh JSON file under
|
|
72
|
+
* `<cwd>/.sela/pending-prompts/`. Idempotent within a single worker —
|
|
73
|
+
* subsequent calls in the same process append a NEW file (rare in
|
|
74
|
+
* practice but keeps the contract simple).
|
|
75
|
+
*
|
|
76
|
+
* Returns the absolute path of the written file, or `null` when there is
|
|
77
|
+
* nothing to persist (so callers can skip the prompt entirely in CI /
|
|
78
|
+
* empty-mutation runs).
|
|
79
|
+
*/
|
|
80
|
+
function writePendingPromptsLedger(opts) {
|
|
81
|
+
const snaps = opts.workspace.getMutatedSnapshots();
|
|
82
|
+
if (snaps.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const entries = snaps.map((s) => snapshotToEntry(s, opts.healedEvents, opts.cwd));
|
|
85
|
+
const dir = ledgerDir(opts.cwd);
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
const fileName = `${process.pid}-${Date.now()}-${randomTag()}.json`;
|
|
88
|
+
const full = path.join(dir, fileName);
|
|
89
|
+
fs.writeFileSync(full, JSON.stringify(entries, null, 2), "utf8");
|
|
90
|
+
return full;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Drain every per-worker JSON file under the ledger directory and merge
|
|
94
|
+
* their entries into a single de-duplicated list (keyed by absolutePath).
|
|
95
|
+
* The most-recent contentAfter wins on collision — that's the disk truth.
|
|
96
|
+
*
|
|
97
|
+
* The directory is NOT deleted here so the reporter can inspect entries
|
|
98
|
+
* before calling `clearPendingPromptsLedger()`.
|
|
99
|
+
*/
|
|
100
|
+
function readPendingPromptsLedger(cwd) {
|
|
101
|
+
const dir = ledgerDir(cwd);
|
|
102
|
+
if (!fs.existsSync(dir))
|
|
103
|
+
return [];
|
|
104
|
+
const merged = new Map();
|
|
105
|
+
const files = fs
|
|
106
|
+
.readdirSync(dir)
|
|
107
|
+
.filter((f) => f.endsWith(".json"))
|
|
108
|
+
.sort(); // pid-timestamp prefix gives a stable, time-ordered merge
|
|
109
|
+
for (const f of files) {
|
|
110
|
+
const full = path.join(dir, f);
|
|
111
|
+
try {
|
|
112
|
+
const raw = fs.readFileSync(full, "utf8");
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
if (!Array.isArray(parsed))
|
|
115
|
+
continue;
|
|
116
|
+
for (const entry of parsed) {
|
|
117
|
+
if (!entry || typeof entry.absolutePath !== "string")
|
|
118
|
+
continue;
|
|
119
|
+
merged.set(path.normalize(entry.absolutePath), entry);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Corrupt ledger file — skip rather than crashing the whole review.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return Array.from(merged.values());
|
|
127
|
+
}
|
|
128
|
+
/** Remove the ledger directory and every file beneath it. Idempotent. */
|
|
129
|
+
function clearPendingPromptsLedger(cwd) {
|
|
130
|
+
const dir = ledgerDir(cwd);
|
|
131
|
+
if (!fs.existsSync(dir))
|
|
132
|
+
return;
|
|
133
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
// ───────────────────────────────────────────────────────────────────
|
|
136
|
+
// Helpers
|
|
137
|
+
// ───────────────────────────────────────────────────────────────────
|
|
138
|
+
function snapshotToEntry(snap, heals, cwd) {
|
|
139
|
+
return {
|
|
140
|
+
absolutePath: snap.absolutePath,
|
|
141
|
+
relativePath: snap.relativePath,
|
|
142
|
+
contentBefore: snap.contentBefore,
|
|
143
|
+
contentAfter: snap.contentAfter ?? "",
|
|
144
|
+
createdBySela: snap.createdBySela,
|
|
145
|
+
lineNumber: findLineNumberForSnapshot(snap, heals, cwd),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a 1-based line number for the vscode:// deeplink. Priority:
|
|
150
|
+
* 1. `definitionSite.line` for a heal whose `definitionSite.file`
|
|
151
|
+
* resolves to this snapshot.
|
|
152
|
+
* 2. `newLineNumber` reported on a heal whose `sourceFile` resolves to
|
|
153
|
+
* this snapshot.
|
|
154
|
+
* 3. `null` (link emitted without a line suffix).
|
|
155
|
+
*/
|
|
156
|
+
function findLineNumberForSnapshot(snap, heals, cwd) {
|
|
157
|
+
const normSnap = path.normalize(snap.absolutePath);
|
|
158
|
+
for (const heal of heals) {
|
|
159
|
+
if (heal.definitionSite) {
|
|
160
|
+
const defAbs = path.isAbsolute(heal.definitionSite.file)
|
|
161
|
+
? path.normalize(heal.definitionSite.file)
|
|
162
|
+
: path.normalize(path.resolve(cwd, heal.definitionSite.file));
|
|
163
|
+
if (defAbs === normSnap && heal.definitionSite.line > 0) {
|
|
164
|
+
return heal.definitionSite.line;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const heal of heals) {
|
|
169
|
+
const healAbs = path.isAbsolute(heal.sourceFile)
|
|
170
|
+
? path.normalize(heal.sourceFile)
|
|
171
|
+
: path.normalize(path.resolve(cwd, heal.sourceFile));
|
|
172
|
+
if (healAbs === normSnap && heal.newLineNumber > 0) {
|
|
173
|
+
return heal.newLineNumber;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function randomTag() {
|
|
179
|
+
return Math.random().toString(36).slice(2, 8);
|
|
180
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { HealEvent, HealEventKind } from "./HealReportService";
|
|
2
|
+
export interface ReportStats {
|
|
3
|
+
total: number;
|
|
4
|
+
healed: number;
|
|
5
|
+
failed: number;
|
|
6
|
+
protectedCnt: number;
|
|
7
|
+
iframeCnt: number;
|
|
8
|
+
shadowCnt: number;
|
|
9
|
+
/** Estimated developer time saved, in minutes. */
|
|
10
|
+
timeSavedMin: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The canonical JSON payload embedded into sela-report.html and
|
|
14
|
+
* mirrored verbatim into sela-report.json. Treat this as a public
|
|
15
|
+
* contract — additive changes only.
|
|
16
|
+
*/
|
|
17
|
+
export interface ReportData {
|
|
18
|
+
generatedAt: string;
|
|
19
|
+
startedAt: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
events: HealEvent[];
|
|
22
|
+
stats: ReportStats;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Time-saved heuristics (minutes per event). Exposed as a constant
|
|
26
|
+
* so tests pin the math against the same source as production.
|
|
27
|
+
*/
|
|
28
|
+
export declare const TIME_SAVED_CONSTANTS: {
|
|
29
|
+
readonly PER_HEAL: 12;
|
|
30
|
+
readonly PER_PROTECT: 18;
|
|
31
|
+
readonly PER_FAILED: 4;
|
|
32
|
+
};
|
|
33
|
+
export declare const MANDATORY_FIELDS_BY_KIND: Readonly<Record<HealEventKind, ReadonlyArray<string>>>;
|
|
34
|
+
export declare class ReportGenerator {
|
|
35
|
+
/**
|
|
36
|
+
* Validates a single event against MANDATORY_FIELDS_BY_KIND. Throws
|
|
37
|
+
* a SelaError(subsystem="Report") on the first violation found. No
|
|
38
|
+
* partial diagnosis: the report is all-or-nothing.
|
|
39
|
+
*/
|
|
40
|
+
static validateEvent(event: unknown): asserts event is HealEvent;
|
|
41
|
+
/**
|
|
42
|
+
* Validates every event in the buffer. Returns the array unchanged
|
|
43
|
+
* when all events pass. Throws on the first violation.
|
|
44
|
+
*/
|
|
45
|
+
static validateAll(events: ReadonlyArray<unknown>): HealEvent[];
|
|
46
|
+
/**
|
|
47
|
+
* Pure stats computation. No clock, no IO — easy to pin in tests.
|
|
48
|
+
*/
|
|
49
|
+
static computeStats(events: ReadonlyArray<HealEvent>): ReportStats;
|
|
50
|
+
/**
|
|
51
|
+
* Build the ReportData payload from a raw event buffer. Validates
|
|
52
|
+
* every event first. `now` / `startedAt` are injectable so tests
|
|
53
|
+
* pin them deterministically.
|
|
54
|
+
*/
|
|
55
|
+
static generate(events: ReadonlyArray<unknown>, opts?: {
|
|
56
|
+
cwd?: string;
|
|
57
|
+
startedAt?: string;
|
|
58
|
+
now?: () => Date;
|
|
59
|
+
}): ReportData;
|
|
60
|
+
/**
|
|
61
|
+
* Format a confidence value for UI consumption. Returns a
|
|
62
|
+
* percentage string ("87%") for finite numbers, "n/a" for anything
|
|
63
|
+
* else. NEVER emits literal "undefined" / "null" / "NaN".
|
|
64
|
+
*
|
|
65
|
+
* Public so the HTML template's `fmtPct()` can be replaced by this
|
|
66
|
+
* single source of truth in a future refactor.
|
|
67
|
+
*/
|
|
68
|
+
static formatConfidence(value: unknown): string;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=ReportGenerator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportGenerator.d.ts","sourceRoot":"","sources":["../../src/services/ReportGenerator.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAId,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;CAIvB,CAAC;AAmCX,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CACpD,aAAa,EACb,aAAa,CAAC,MAAM,CAAC,CACtB,CAIA,CAAC;AAsBF,qBAAa,eAAe;IAC1B;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,SAAS;IAuChE;;;OAGG;IACH,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,SAAS,EAAE;IAK/D;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,GAAG,WAAW;IA4BlE;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CACb,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAC9B,IAAI,GAAE;QACJ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;KACb,GACL,UAAU;IAYb;;;;;;;OAOG;IACH,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;CAOhD"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/services/ReportGenerator.ts
|
|
3
|
+
//
|
|
4
|
+
// Pure (no-IO) report layer. Takes the HealReportService event buffer
|
|
5
|
+
// and produces a stable, validated JSON shape that the HTML template
|
|
6
|
+
// and any external consumer (CI dashboards, telemetry forwarders)
|
|
7
|
+
// can rely on.
|
|
8
|
+
//
|
|
9
|
+
// Design goals:
|
|
10
|
+
// 1. SCHEMA ENFORCED. Every event passed through ReportGenerator
|
|
11
|
+
// is validated against MANDATORY_FIELDS_BY_KIND — a SelaError
|
|
12
|
+
// with subsystem="Report" is thrown for any malformed input.
|
|
13
|
+
// The HTML template never sees a partial event.
|
|
14
|
+
//
|
|
15
|
+
// 2. CONFIDENCE NEVER LEAKS. formatConfidence() and the rendered
|
|
16
|
+
// `confidenceLabel` field always produce either a "<n>%" string
|
|
17
|
+
// or "n/a" — never `undefined`, `null`, or a stringified
|
|
18
|
+
// "undefined". The JS embedded in HealReportService already
|
|
19
|
+
// uses fmtPct() for the same reason; this module is the
|
|
20
|
+
// authoritative implementation.
|
|
21
|
+
//
|
|
22
|
+
// 3. STATS DETERMINISTIC. computeStats() exposes the time-saved
|
|
23
|
+
// constants via TIME_SAVED_CONSTANTS so tests can pin the math
|
|
24
|
+
// without duplicating the multipliers.
|
|
25
|
+
//
|
|
26
|
+
// 4. NO IO. Disk writes still live in HealReportService.flushToDisk
|
|
27
|
+
// so this module stays trivially unit-testable.
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.ReportGenerator = exports.MANDATORY_FIELDS_BY_KIND = exports.TIME_SAVED_CONSTANTS = void 0;
|
|
30
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
31
|
+
/**
|
|
32
|
+
* Time-saved heuristics (minutes per event). Exposed as a constant
|
|
33
|
+
* so tests pin the math against the same source as production.
|
|
34
|
+
*/
|
|
35
|
+
exports.TIME_SAVED_CONSTANTS = {
|
|
36
|
+
PER_HEAL: 12,
|
|
37
|
+
PER_PROTECT: 18,
|
|
38
|
+
PER_FAILED: 4,
|
|
39
|
+
};
|
|
40
|
+
// ───────────────────────────────────────────────────────────────────
|
|
41
|
+
// Field manifests — mandatory for each event kind
|
|
42
|
+
//
|
|
43
|
+
// "Mandatory" = the HTML template will render an unintelligible card
|
|
44
|
+
// without this field. Anything optional belongs in the interface
|
|
45
|
+
// declarations in HealReportService.ts but NOT here.
|
|
46
|
+
// ───────────────────────────────────────────────────────────────────
|
|
47
|
+
const COMMON_MANDATORY = [
|
|
48
|
+
"kind",
|
|
49
|
+
"stableId",
|
|
50
|
+
"sourceFile",
|
|
51
|
+
"sourceLine",
|
|
52
|
+
"timestamp",
|
|
53
|
+
];
|
|
54
|
+
const HEALED_MANDATORY = [
|
|
55
|
+
"oldSelector",
|
|
56
|
+
"newSelector",
|
|
57
|
+
"dnaBefore",
|
|
58
|
+
];
|
|
59
|
+
const FAILED_MANDATORY = [
|
|
60
|
+
"oldSelector",
|
|
61
|
+
"reason",
|
|
62
|
+
];
|
|
63
|
+
const PROTECTED_MANDATORY = [
|
|
64
|
+
"oldSelector",
|
|
65
|
+
"reason",
|
|
66
|
+
"safetyLevel",
|
|
67
|
+
];
|
|
68
|
+
exports.MANDATORY_FIELDS_BY_KIND = {
|
|
69
|
+
HEALED: [...COMMON_MANDATORY, ...HEALED_MANDATORY],
|
|
70
|
+
FAILED: [...COMMON_MANDATORY, ...FAILED_MANDATORY],
|
|
71
|
+
PROTECTED: [...COMMON_MANDATORY, ...PROTECTED_MANDATORY],
|
|
72
|
+
};
|
|
73
|
+
const VALID_KINDS = new Set(["HEALED", "FAILED", "PROTECTED"]);
|
|
74
|
+
// ───────────────────────────────────────────────────────────────────
|
|
75
|
+
// Helpers
|
|
76
|
+
// ───────────────────────────────────────────────────────────────────
|
|
77
|
+
function throwReport(code, reason, ctx = {}) {
|
|
78
|
+
throw new SelaError_1.SelaError({ subsystem: "Report", code, reason, context: ctx });
|
|
79
|
+
}
|
|
80
|
+
function isPresent(value) {
|
|
81
|
+
if (value === undefined || value === null)
|
|
82
|
+
return false;
|
|
83
|
+
if (typeof value === "string" && value.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
// ───────────────────────────────────────────────────────────────────
|
|
88
|
+
// ReportGenerator
|
|
89
|
+
// ───────────────────────────────────────────────────────────────────
|
|
90
|
+
class ReportGenerator {
|
|
91
|
+
/**
|
|
92
|
+
* Validates a single event against MANDATORY_FIELDS_BY_KIND. Throws
|
|
93
|
+
* a SelaError(subsystem="Report") on the first violation found. No
|
|
94
|
+
* partial diagnosis: the report is all-or-nothing.
|
|
95
|
+
*/
|
|
96
|
+
static validateEvent(event) {
|
|
97
|
+
if (!event || typeof event !== "object") {
|
|
98
|
+
throwReport(SelaError_1.REPORT_CODES.MALFORMED_EVENT, `Event must be an object, received ${event === null ? "null" : typeof event}`);
|
|
99
|
+
}
|
|
100
|
+
const ev = event;
|
|
101
|
+
if (!ev.kind || !VALID_KINDS.has(ev.kind)) {
|
|
102
|
+
throwReport(SelaError_1.REPORT_CODES.UNKNOWN_KIND, `Unknown event.kind "${String(ev.kind)}" — must be HEALED|FAILED|PROTECTED`, { stableId: ev.stableId });
|
|
103
|
+
}
|
|
104
|
+
const required = exports.MANDATORY_FIELDS_BY_KIND[ev.kind];
|
|
105
|
+
for (const field of required) {
|
|
106
|
+
const val = ev[field];
|
|
107
|
+
if (!isPresent(val)) {
|
|
108
|
+
throwReport(SelaError_1.REPORT_CODES.MISSING_FIELD, `${ev.kind} event missing mandatory field "${field}"`, { stableId: ev.stableId, kind: ev.kind, field });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Field-type sanity for the fields we publicly mandate.
|
|
112
|
+
if (typeof ev.sourceLine !== "number") {
|
|
113
|
+
throwReport(SelaError_1.REPORT_CODES.INVALID_FIELD_TYPE, `Field "sourceLine" must be a number, received ${typeof ev.sourceLine}`, { stableId: ev.stableId, kind: ev.kind, field: "sourceLine" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validates every event in the buffer. Returns the array unchanged
|
|
118
|
+
* when all events pass. Throws on the first violation.
|
|
119
|
+
*/
|
|
120
|
+
static validateAll(events) {
|
|
121
|
+
for (const e of events)
|
|
122
|
+
ReportGenerator.validateEvent(e);
|
|
123
|
+
return events;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Pure stats computation. No clock, no IO — easy to pin in tests.
|
|
127
|
+
*/
|
|
128
|
+
static computeStats(events) {
|
|
129
|
+
let healed = 0;
|
|
130
|
+
let failed = 0;
|
|
131
|
+
let protectedCnt = 0;
|
|
132
|
+
let iframeCnt = 0;
|
|
133
|
+
let shadowCnt = 0;
|
|
134
|
+
for (const e of events) {
|
|
135
|
+
if (e.kind === "HEALED")
|
|
136
|
+
healed++;
|
|
137
|
+
else if (e.kind === "FAILED")
|
|
138
|
+
failed++;
|
|
139
|
+
else if (e.kind === "PROTECTED")
|
|
140
|
+
protectedCnt++;
|
|
141
|
+
if (e.inIframe)
|
|
142
|
+
iframeCnt++;
|
|
143
|
+
if (e.inShadowDom)
|
|
144
|
+
shadowCnt++;
|
|
145
|
+
}
|
|
146
|
+
const timeSavedMin = healed * exports.TIME_SAVED_CONSTANTS.PER_HEAL +
|
|
147
|
+
protectedCnt * exports.TIME_SAVED_CONSTANTS.PER_PROTECT +
|
|
148
|
+
failed * exports.TIME_SAVED_CONSTANTS.PER_FAILED;
|
|
149
|
+
return {
|
|
150
|
+
total: events.length,
|
|
151
|
+
healed,
|
|
152
|
+
failed,
|
|
153
|
+
protectedCnt,
|
|
154
|
+
iframeCnt,
|
|
155
|
+
shadowCnt,
|
|
156
|
+
timeSavedMin,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build the ReportData payload from a raw event buffer. Validates
|
|
161
|
+
* every event first. `now` / `startedAt` are injectable so tests
|
|
162
|
+
* pin them deterministically.
|
|
163
|
+
*/
|
|
164
|
+
static generate(events, opts = {}) {
|
|
165
|
+
const validated = ReportGenerator.validateAll(events);
|
|
166
|
+
const now = opts.now ?? (() => new Date());
|
|
167
|
+
return {
|
|
168
|
+
generatedAt: now().toISOString(),
|
|
169
|
+
startedAt: opts.startedAt ?? now().toISOString(),
|
|
170
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
171
|
+
events: validated,
|
|
172
|
+
stats: ReportGenerator.computeStats(validated),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Format a confidence value for UI consumption. Returns a
|
|
177
|
+
* percentage string ("87%") for finite numbers, "n/a" for anything
|
|
178
|
+
* else. NEVER emits literal "undefined" / "null" / "NaN".
|
|
179
|
+
*
|
|
180
|
+
* Public so the HTML template's `fmtPct()` can be replaced by this
|
|
181
|
+
* single source of truth in a future refactor.
|
|
182
|
+
*/
|
|
183
|
+
static formatConfidence(value) {
|
|
184
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
185
|
+
const clamped = Math.max(0, Math.min(100, Math.round(value)));
|
|
186
|
+
return `${clamped}%`;
|
|
187
|
+
}
|
|
188
|
+
return "n/a";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.ReportGenerator = ReportGenerator;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { IntentAuditor } from "./IntentAuditor";
|
|
1
2
|
import { ResolvedThresholds } from "../config/SelaConfig";
|
|
2
3
|
import { VisibilityInfo, AncestorNode, AnchorInfo } from "../types";
|
|
4
|
+
import { SafetyGuardCode } from "../errors/SelaError";
|
|
3
5
|
export type HealMode = "assertion" | "action";
|
|
4
6
|
export interface HealContext {
|
|
5
7
|
mode: HealMode;
|
|
@@ -8,7 +10,13 @@ export interface HealContext {
|
|
|
8
10
|
branch?: string;
|
|
9
11
|
}
|
|
10
12
|
export interface AIFixSummary {
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Raw AI confidence 0-100. `undefined` when the LLM omitted the field —
|
|
15
|
+
* SafetyGuard's hard-stop / review-threshold logic treats absence as the
|
|
16
|
+
* conservative worst case (0) so unverified heals are NOT silently let
|
|
17
|
+
* through, while downstream report renderers display "n/a".
|
|
18
|
+
*/
|
|
19
|
+
confidence?: number;
|
|
12
20
|
contentChange?: {
|
|
13
21
|
oldText: string;
|
|
14
22
|
newText: string;
|
|
@@ -36,6 +44,13 @@ export interface SafetyDecision {
|
|
|
36
44
|
level: RiskLevel;
|
|
37
45
|
reason: string;
|
|
38
46
|
canProceed: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Machine-readable error code from SAFETY_GUARD_CODES. Always populated
|
|
49
|
+
* for rejection decisions (BLOCKED / FAIL_HARD) so callers can throw a
|
|
50
|
+
* SelaError without re-parsing `reason`. Pass-through decisions (SAFE /
|
|
51
|
+
* REQUIRES_REVIEW) may omit it.
|
|
52
|
+
*/
|
|
53
|
+
code?: SafetyGuardCode;
|
|
39
54
|
/** Structured info for policy-aware log messages in FixwrightEngine. */
|
|
40
55
|
meta?: {
|
|
41
56
|
auditorVerdict?: "CONSISTENT" | "SUSPICIOUS" | "INCONSISTENT";
|
|
@@ -50,10 +65,27 @@ export interface SafetyDecision {
|
|
|
50
65
|
};
|
|
51
66
|
}
|
|
52
67
|
type FunctionalRole = "SUBMIT" | "CANCEL" | "DELETE" | "NAVIGATE" | "AUTH" | "STATUS_POSITIVE" | "STATUS_NEGATIVE" | "UNKNOWN";
|
|
68
|
+
/**
|
|
69
|
+
* Constructor options for SafetyGuard.
|
|
70
|
+
*
|
|
71
|
+
* - `thresholds`: per-instance threshold overrides (existing behavior).
|
|
72
|
+
* - `intentAuditor`: pre-built IntentAuditor. Tests inject a stub so
|
|
73
|
+
* SafetyGuard can be unit-tested without an Anthropic key.
|
|
74
|
+
*/
|
|
75
|
+
export interface SafetyGuardOptions {
|
|
76
|
+
thresholds?: ResolvedThresholds;
|
|
77
|
+
intentAuditor?: IntentAuditor;
|
|
78
|
+
}
|
|
53
79
|
export declare class SafetyGuard {
|
|
54
80
|
private intentAuditor;
|
|
55
81
|
private thresholds;
|
|
56
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Accepts either the legacy positional `thresholds` argument or an
|
|
84
|
+
* options bag (DI-friendly). Existing call sites keep working
|
|
85
|
+
* unchanged.
|
|
86
|
+
*/
|
|
87
|
+
constructor(arg?: ResolvedThresholds | SafetyGuardOptions);
|
|
88
|
+
private static normalizeArgs;
|
|
57
89
|
evaluate(context: HealContext, aiFix: AIFixSummary): Promise<SafetyDecision>;
|
|
58
90
|
private isSemanticOpposite;
|
|
59
91
|
classifyFunctionalRole(text: string): FunctionalRole;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SafetyGuard.d.ts","sourceRoot":"","sources":["../../src/services/SafetyGuard.ts"],"names":[],"mappings":"
|
|
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,EAAsB,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAM1E,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,uEAAuE;IACvE,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,YAAY,EAAE,CAAC;IAC7B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,iBAAiB,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,wEAAwE;IACxE,IAAI,CAAC,EAAE;QACL,cAAc,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,cAAc,CAAC;QAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,qFAAqF;QACrF,mBAAmB,CAAC,EAAE;YACpB,aAAa,EAAE,MAAM,CAAC;YACtB,OAAO,EAAE,MAAM,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC;YACd,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC;CACH;AA6DD,KAAK,cAAc,GACf,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,iBAAiB,GACjB,iBAAiB,GACjB,SAAS,CAAC;AA0Jd;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAqB;IAEvC;;;;OAIG;gBACS,GAAG,CAAC,EAAE,kBAAkB,GAAG,kBAAkB;IAMzD,OAAO,CAAC,MAAM,CAAC,aAAa;IAgBtB,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAkUlF,OAAO,CAAC,kBAAkB;IAwB1B,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;IAqBpD,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvD,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,iBAAiB;CAY1B"}
|