sela-core 1.0.6 → 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 +12 -9
- package/dist/engine/SelaEngine.d.ts.map +1 -1
- package/dist/engine/SelaEngine.js +249 -72
- 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 +42 -0
- package/dist/services/HealReportService.d.ts.map +1 -1
- package/dist/services/HealReportService.js +80 -1
- 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 +17 -1
- package/dist/services/LLMService.d.ts.map +1 -1
- package/dist/services/LLMService.js +18 -8
- package/dist/services/PRAutomationService.d.ts +14 -1
- package/dist/services/PRAutomationService.d.ts.map +1 -1
- package/dist/services/PRAutomationService.js +260 -69
- 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 +27 -1
- package/dist/services/SafetyGuard.d.ts.map +1 -1
- package/dist/services/SafetyGuard.js +58 -12
- 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,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;
|
|
@@ -42,6 +44,13 @@ export interface SafetyDecision {
|
|
|
42
44
|
level: RiskLevel;
|
|
43
45
|
reason: string;
|
|
44
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;
|
|
45
54
|
/** Structured info for policy-aware log messages in FixwrightEngine. */
|
|
46
55
|
meta?: {
|
|
47
56
|
auditorVerdict?: "CONSISTENT" | "SUSPICIOUS" | "INCONSISTENT";
|
|
@@ -56,10 +65,27 @@ export interface SafetyDecision {
|
|
|
56
65
|
};
|
|
57
66
|
}
|
|
58
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
|
+
}
|
|
59
79
|
export declare class SafetyGuard {
|
|
60
80
|
private intentAuditor;
|
|
61
81
|
private thresholds;
|
|
62
|
-
|
|
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;
|
|
63
89
|
evaluate(context: HealContext, aiFix: AIFixSummary): Promise<SafetyDecision>;
|
|
64
90
|
private isSemanticOpposite;
|
|
65
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":"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"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.SafetyGuard = void 0;
|
|
5
5
|
const IntentAuditor_1 = require("./IntentAuditor");
|
|
6
|
+
const SelaError_1 = require("../errors/SelaError");
|
|
6
7
|
// ═══════════════════════════════════════════════════════════════════
|
|
7
8
|
// DEFAULT THRESHOLDS (match pre-config hardcoded values exactly)
|
|
8
9
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -55,6 +56,9 @@ const SEMANTIC_OPPOSITE_PAIRS = new Set([
|
|
|
55
56
|
"valid|invalid",
|
|
56
57
|
"invalid|valid",
|
|
57
58
|
]);
|
|
59
|
+
function escapeRegExp(s) {
|
|
60
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
}
|
|
58
62
|
const ROLE_KEYWORDS = new Map([
|
|
59
63
|
[
|
|
60
64
|
"SUBMIT",
|
|
@@ -185,15 +189,35 @@ const ROLE_KEYWORDS = new Map([
|
|
|
185
189
|
]),
|
|
186
190
|
],
|
|
187
191
|
]);
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
192
|
+
// Pre-compiled word-boundary regexes for the role classifier. Built
|
|
193
|
+
// once at module load — classifyFunctionalRole() is on the hot heal
|
|
194
|
+
// path, so we don't pay for regex construction per call.
|
|
195
|
+
const ROLE_KEYWORD_REGEXES = Array.from(ROLE_KEYWORDS, ([role, keywords]) => ({
|
|
196
|
+
role,
|
|
197
|
+
regexes: Array.from(keywords, (kw) => new RegExp(`\\b${escapeRegExp(kw)}\\b`, "i")),
|
|
198
|
+
}));
|
|
191
199
|
class SafetyGuard {
|
|
192
200
|
intentAuditor;
|
|
193
201
|
thresholds;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Accepts either the legacy positional `thresholds` argument or an
|
|
204
|
+
* options bag (DI-friendly). Existing call sites keep working
|
|
205
|
+
* unchanged.
|
|
206
|
+
*/
|
|
207
|
+
constructor(arg) {
|
|
208
|
+
const opts = SafetyGuard.normalizeArgs(arg);
|
|
209
|
+
this.thresholds = opts.thresholds ?? DEFAULT_THRESHOLDS;
|
|
210
|
+
this.intentAuditor = opts.intentAuditor ?? new IntentAuditor_1.IntentAuditor();
|
|
211
|
+
}
|
|
212
|
+
static normalizeArgs(arg) {
|
|
213
|
+
if (!arg)
|
|
214
|
+
return {};
|
|
215
|
+
// Heuristic: an options bag has either `intentAuditor` or a nested
|
|
216
|
+
// `thresholds` field; otherwise the value itself is a ResolvedThresholds.
|
|
217
|
+
if ("intentAuditor" in arg || "thresholds" in arg) {
|
|
218
|
+
return arg;
|
|
219
|
+
}
|
|
220
|
+
return { thresholds: arg };
|
|
197
221
|
}
|
|
198
222
|
// ─────────────────────────────────────────────────────────────
|
|
199
223
|
// PUBLIC ENTRY POINT
|
|
@@ -217,6 +241,7 @@ class SafetyGuard {
|
|
|
217
241
|
if (vis.isGhost) {
|
|
218
242
|
return {
|
|
219
243
|
level: "FAIL_HARD",
|
|
244
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_VISIBILITY_GHOST,
|
|
220
245
|
reason: `Visibility Gate: candidate element is a Ghost (${vis.ghostReason}) — ` +
|
|
221
246
|
`functionally absent from the user's perspective`,
|
|
222
247
|
canProceed: false,
|
|
@@ -227,6 +252,7 @@ class SafetyGuard {
|
|
|
227
252
|
if (isMainBranch) {
|
|
228
253
|
return {
|
|
229
254
|
level: "FAIL_HARD",
|
|
255
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_VISIBILITY_OCCLUDED,
|
|
230
256
|
reason: `Visibility Gate: candidate element is occluded by an overlay on branch "${context.branch}" — ` +
|
|
231
257
|
`hard-fail policy active on main`,
|
|
232
258
|
canProceed: false,
|
|
@@ -246,6 +272,7 @@ class SafetyGuard {
|
|
|
246
272
|
if (confidence < this.thresholds.confidenceHardStop) {
|
|
247
273
|
return {
|
|
248
274
|
level: "BLOCKED",
|
|
275
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_LOW_CONFIDENCE,
|
|
249
276
|
reason: `Confidence too low (${confidence}/100 < ${this.thresholds.confidenceHardStop}) — refusing to heal`,
|
|
250
277
|
canProceed: false,
|
|
251
278
|
};
|
|
@@ -290,6 +317,7 @@ class SafetyGuard {
|
|
|
290
317
|
if (this.isSemanticOpposite(oldText, newText)) {
|
|
291
318
|
return {
|
|
292
319
|
level: "FAIL_HARD",
|
|
320
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_SEMANTIC_INVERSION,
|
|
293
321
|
reason: `Semantic inversion detected: "${oldText}" ↔ "${newText}" — ` +
|
|
294
322
|
`state inversions mask logical regressions`,
|
|
295
323
|
canProceed: false,
|
|
@@ -305,6 +333,7 @@ class SafetyGuard {
|
|
|
305
333
|
if (isStatusInversion) {
|
|
306
334
|
return {
|
|
307
335
|
level: "FAIL_HARD",
|
|
336
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_STATUS_INVERSION,
|
|
308
337
|
reason: `Status inversion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
|
|
309
338
|
`masking a logic failure is not permitted`,
|
|
310
339
|
canProceed: false,
|
|
@@ -319,6 +348,7 @@ class SafetyGuard {
|
|
|
319
348
|
// function being found is almost certainly a wrong element fix.
|
|
320
349
|
return {
|
|
321
350
|
level: "BLOCKED",
|
|
351
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_ROLE_SWAP_ASSERTION,
|
|
322
352
|
reason: `Functional role swap in assertion: "${oldText}" (${oldRole}) → "${newText}" (${newRole}) — ` +
|
|
323
353
|
`the assertion was verifying a specific functional state`,
|
|
324
354
|
canProceed: false,
|
|
@@ -361,6 +391,7 @@ class SafetyGuard {
|
|
|
361
391
|
if (auditVerdict.verdict === "INCONSISTENT") {
|
|
362
392
|
return {
|
|
363
393
|
level: "FAIL_HARD",
|
|
394
|
+
code: SelaError_1.SAFETY_GUARD_CODES.FAIL_HARD_AUDITOR_INCONSISTENT,
|
|
364
395
|
reason: `Intent Auditor — INCONSISTENT: ${auditVerdict.reason}` +
|
|
365
396
|
(auditVerdict.inversionType
|
|
366
397
|
? ` [inversion: ${auditVerdict.inversionType}]`
|
|
@@ -377,6 +408,7 @@ class SafetyGuard {
|
|
|
377
408
|
if (!this.thresholds.allowSuspicious && mode === "assertion") {
|
|
378
409
|
return {
|
|
379
410
|
level: "BLOCKED",
|
|
411
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_AUDITOR_SUSPICIOUS,
|
|
380
412
|
reason: `Intent Auditor — SUSPICIOUS in assertion mode: ${auditVerdict.reason}`,
|
|
381
413
|
canProceed: false,
|
|
382
414
|
meta,
|
|
@@ -421,6 +453,7 @@ class SafetyGuard {
|
|
|
421
453
|
if (similarity < this.thresholds.assertionSimilarityFloor) {
|
|
422
454
|
return {
|
|
423
455
|
level: "BLOCKED",
|
|
456
|
+
code: SelaError_1.SAFETY_GUARD_CODES.BLOCKED_SIMILARITY_FLOOR,
|
|
424
457
|
reason: `Text divergence too high in assertion: "${oldText}" → "${newText}" ` +
|
|
425
458
|
`(similarity=${(similarity * 100).toFixed(1)}% < ${(this.thresholds.assertionSimilarityFloor * 100).toFixed(0)}%) — ` +
|
|
426
459
|
`possible content regression`,
|
|
@@ -455,16 +488,29 @@ class SafetyGuard {
|
|
|
455
488
|
// ─────────────────────────────────────────────────────────────
|
|
456
489
|
// FUNCTIONAL ROLE CLASSIFICATION
|
|
457
490
|
//
|
|
458
|
-
// Returns the first role whose keyword
|
|
459
|
-
// (
|
|
491
|
+
// Returns the first role whose keyword matches the text as a whole
|
|
492
|
+
// word (delimited by \b boundaries). Returns UNKNOWN when no role
|
|
493
|
+
// matches.
|
|
494
|
+
//
|
|
495
|
+
// Why \b instead of `.includes(kw)`:
|
|
496
|
+
// - "unauthorized".includes("authorize") → true ← bug
|
|
497
|
+
// - "offline".includes("on") → true ← bug
|
|
498
|
+
// - "disconnected".includes("connected") → true ← bug
|
|
499
|
+
// - "inactive".includes("active") → true ← bug
|
|
500
|
+
// The substring approach mis-classified negations into the
|
|
501
|
+
// positive role of their root word, silently bypassing
|
|
502
|
+
// FAIL_HARD_STATUS_INVERSION. Word-boundary regex eliminates
|
|
503
|
+
// these collisions while keeping multi-word keywords ("sign in")
|
|
504
|
+
// and punctuation-flanked matches ("Submit!") working.
|
|
460
505
|
// ─────────────────────────────────────────────────────────────
|
|
461
506
|
classifyFunctionalRole(text) {
|
|
462
507
|
const normalized = text.toLowerCase().trim();
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
508
|
+
if (normalized.length === 0)
|
|
509
|
+
return "UNKNOWN";
|
|
510
|
+
for (const { role, regexes } of ROLE_KEYWORD_REGEXES) {
|
|
511
|
+
for (const re of regexes) {
|
|
512
|
+
if (re.test(normalized))
|
|
466
513
|
return role;
|
|
467
|
-
}
|
|
468
514
|
}
|
|
469
515
|
}
|
|
470
516
|
return "UNKNOWN";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SourceUpdater.d.ts","sourceRoot":"","sources":["../../src/services/SourceUpdater.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AA4BtD,UAAU,YAAY;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAqZD,qBAAa,aAAa;IAMxB,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,MAAM,EAC5B,UAAU,CAAC,EAAE,eAAe,EAAE,EAC9B,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EACpD,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,EACxC,iBAAiB,CAAC,EAAE,iBAAiB,EAAE,EACvC,cAAc,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,QAAQ,EACrD,UAAU,CAAC,EAAE,OAAO,GACnB,YAAY;IAoDf,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAwC/B,OAAO,CAAC,MAAM,CAAC,WAAW;IAiT1B,OAAO,CAAC,MAAM,CAAC,2BAA2B;IA8E1C,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA8EvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAoErC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAsBjC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IA6DpC,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAuCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IA6CvC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqCrC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAqGrC,OAAO,CAAC,MAAM,CAAC,kBAAkB;IA0BjC,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAatC,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAgBlD,MAAM,CAAC,cAAc,CACnB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY;IA0Cf,0FAA0F;IAC1F,MAAM,CAAC,eAAe,IAAI,IAAI;CAG/B"}
|