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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/config/ConfigLoader.d.ts +1 -0
  3. package/dist/config/ConfigLoader.d.ts.map +1 -1
  4. package/dist/config/ConfigLoader.js +10 -0
  5. package/dist/config/DryRunGuard.d.ts +14 -0
  6. package/dist/config/DryRunGuard.d.ts.map +1 -0
  7. package/dist/config/DryRunGuard.js +79 -0
  8. package/dist/config/SelaConfig.d.ts +15 -1
  9. package/dist/config/SelaConfig.d.ts.map +1 -1
  10. package/dist/config/SelaConfig.js +27 -1
  11. package/dist/engine/SelaEngine.d.ts +12 -9
  12. package/dist/engine/SelaEngine.d.ts.map +1 -1
  13. package/dist/engine/SelaEngine.js +249 -72
  14. package/dist/errors/SelaError.d.ts +133 -0
  15. package/dist/errors/SelaError.d.ts.map +1 -0
  16. package/dist/errors/SelaError.js +155 -0
  17. package/dist/fixtures/expectProxy.d.ts.map +1 -1
  18. package/dist/fixtures/expectProxy.js +7 -0
  19. package/dist/fixtures/index.d.ts +7 -1
  20. package/dist/fixtures/index.d.ts.map +1 -1
  21. package/dist/fixtures/index.js +15 -0
  22. package/dist/fixtures/proxyTag.d.ts +12 -0
  23. package/dist/fixtures/proxyTag.d.ts.map +1 -0
  24. package/dist/fixtures/proxyTag.js +45 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +6 -1
  28. package/dist/reporter/SelaReporter.d.ts +110 -0
  29. package/dist/reporter/SelaReporter.d.ts.map +1 -0
  30. package/dist/reporter/SelaReporter.js +328 -0
  31. package/dist/services/ASTSourceUpdater.d.ts +19 -0
  32. package/dist/services/ASTSourceUpdater.d.ts.map +1 -1
  33. package/dist/services/ASTSourceUpdater.js +173 -29
  34. package/dist/services/HealReportService.d.ts +42 -0
  35. package/dist/services/HealReportService.d.ts.map +1 -1
  36. package/dist/services/HealReportService.js +80 -1
  37. package/dist/services/HealingCacheService.d.ts +135 -0
  38. package/dist/services/HealingCacheService.d.ts.map +1 -0
  39. package/dist/services/HealingCacheService.js +460 -0
  40. package/dist/services/InitializerUpdater.d.ts.map +1 -1
  41. package/dist/services/InitializerUpdater.js +20 -1
  42. package/dist/services/IntentAuditor.d.ts +18 -1
  43. package/dist/services/IntentAuditor.d.ts.map +1 -1
  44. package/dist/services/IntentAuditor.js +19 -6
  45. package/dist/services/InteractiveReview.d.ts +24 -0
  46. package/dist/services/InteractiveReview.d.ts.map +1 -0
  47. package/dist/services/InteractiveReview.js +218 -0
  48. package/dist/services/LLMService.d.ts +17 -1
  49. package/dist/services/LLMService.d.ts.map +1 -1
  50. package/dist/services/LLMService.js +18 -8
  51. package/dist/services/PRAutomationService.d.ts +14 -1
  52. package/dist/services/PRAutomationService.d.ts.map +1 -1
  53. package/dist/services/PRAutomationService.js +260 -69
  54. package/dist/services/PendingPromptLedger.d.ts +44 -0
  55. package/dist/services/PendingPromptLedger.d.ts.map +1 -0
  56. package/dist/services/PendingPromptLedger.js +180 -0
  57. package/dist/services/ReportGenerator.d.ts +70 -0
  58. package/dist/services/ReportGenerator.d.ts.map +1 -0
  59. package/dist/services/ReportGenerator.js +191 -0
  60. package/dist/services/SafetyGuard.d.ts +27 -1
  61. package/dist/services/SafetyGuard.d.ts.map +1 -1
  62. package/dist/services/SafetyGuard.js +58 -12
  63. package/dist/services/SourceUpdater.d.ts.map +1 -1
  64. package/dist/services/SourceUpdater.js +44 -13
  65. package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
  66. package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
  67. package/dist/services/WorkspaceSnapshotService.js +202 -0
  68. package/dist/utils/IsolatedDiff.d.ts +45 -0
  69. package/dist/utils/IsolatedDiff.d.ts.map +1 -0
  70. package/dist/utils/IsolatedDiff.js +379 -0
  71. 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
- constructor(thresholds?: ResolvedThresholds);
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":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAMpE,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,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;AA2Id,qBAAa,WAAW;IACtB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,UAAU,CAAqB;gBAE3B,UAAU,CAAC,EAAE,kBAAkB;IASrC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAyTlF,OAAO,CAAC,kBAAkB;IAY1B,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;IAsBpD,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvD,OAAO,CAAC,mBAAmB;IAyB3B,OAAO,CAAC,iBAAiB;CAY1B"}
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
- // SafetyGuard
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
- constructor(thresholds) {
195
- this.thresholds = thresholds ?? DEFAULT_THRESHOLDS;
196
- this.intentAuditor = new IntentAuditor_1.IntentAuditor();
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 set contains the text
459
- // (exact match or substring). Returns UNKNOWN when no role matches.
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
- for (const [role, keywords] of ROLE_KEYWORDS) {
464
- for (const kw of keywords) {
465
- if (normalized === kw || normalized.includes(kw)) {
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;AAMtD,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;AA0YD,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;IAwS1B,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"}
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"}