speclock 1.7.0 → 2.1.0
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 +28 -13
- package/package.json +9 -3
- package/src/cli/index.js +82 -5
- package/src/core/audit.js +237 -0
- package/src/core/compliance.js +291 -0
- package/src/core/engine.js +48 -68
- package/src/core/license.js +221 -0
- package/src/core/llm-checker.js +239 -0
- package/src/core/semantics.js +1096 -0
- package/src/core/storage.js +9 -0
- package/src/mcp/http-server.js +120 -5
- package/src/mcp/server.js +78 -2
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Compliance Export Engine
|
|
3
|
+
* Generates audit-ready reports for SOC 2, HIPAA, and CSV formats.
|
|
4
|
+
* Designed for enterprise compliance teams and auditors.
|
|
5
|
+
*
|
|
6
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readBrain, readEvents } from "./storage.js";
|
|
10
|
+
import { verifyAuditChain } from "./audit.js";
|
|
11
|
+
|
|
12
|
+
const VERSION = "2.1.0";
|
|
13
|
+
|
|
14
|
+
// PHI-related keywords for HIPAA filtering
|
|
15
|
+
const PHI_KEYWORDS = [
|
|
16
|
+
"patient", "phi", "health", "medical", "hipaa", "ehr", "emr",
|
|
17
|
+
"diagnosis", "treatment", "prescription", "clinical", "healthcare",
|
|
18
|
+
"protected health", "health record", "medical record", "patient data",
|
|
19
|
+
"health information", "insurance", "claims", "billing",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Security-related event types
|
|
23
|
+
const SECURITY_EVENT_TYPES = [
|
|
24
|
+
"lock_added", "lock_removed", "decision_added",
|
|
25
|
+
"goal_updated", "init", "session_started", "session_ended",
|
|
26
|
+
"checkpoint_created", "revert_detected",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if text matches any PHI keywords (case-insensitive).
|
|
31
|
+
*/
|
|
32
|
+
function isPHIRelated(text) {
|
|
33
|
+
if (!text) return false;
|
|
34
|
+
const lower = text.toLowerCase();
|
|
35
|
+
return PHI_KEYWORDS.some((kw) => lower.includes(kw));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate SOC 2 Type II compliance report.
|
|
40
|
+
* Covers: constraint changes, access events, change management, audit integrity.
|
|
41
|
+
*/
|
|
42
|
+
export function exportSOC2(root) {
|
|
43
|
+
const brain = readBrain(root);
|
|
44
|
+
if (!brain) {
|
|
45
|
+
return { error: "SpecLock not initialized. Run speclock init first." };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const events = readEvents(root, { limit: 10000 });
|
|
49
|
+
const auditResult = verifyAuditChain(root);
|
|
50
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active);
|
|
51
|
+
const allLocks = brain.specLock?.items || [];
|
|
52
|
+
|
|
53
|
+
// Group events by type for analysis
|
|
54
|
+
const eventsByType = {};
|
|
55
|
+
for (const e of events) {
|
|
56
|
+
if (!eventsByType[e.type]) eventsByType[e.type] = [];
|
|
57
|
+
eventsByType[e.type].push(e);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Calculate constraint change history
|
|
61
|
+
const constraintChanges = events
|
|
62
|
+
.filter((e) => ["lock_added", "lock_removed"].includes(e.type))
|
|
63
|
+
.map((e) => ({
|
|
64
|
+
timestamp: e.at || e.ts,
|
|
65
|
+
action: e.type === "lock_added" ? "ADDED" : "REMOVED",
|
|
66
|
+
lockId: e.lockId || e.summary?.match(/\[([^\]]+)\]/)?.[1] || "unknown",
|
|
67
|
+
text: e.lockText || e.summary || "",
|
|
68
|
+
source: e.source || "unknown",
|
|
69
|
+
eventId: e.eventId,
|
|
70
|
+
hash: e.hash || null,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// Session history (access log)
|
|
74
|
+
const sessions = (brain.sessions?.history || []).map((s) => ({
|
|
75
|
+
tool: s.tool || "unknown",
|
|
76
|
+
startedAt: s.startedAt,
|
|
77
|
+
endedAt: s.endedAt || null,
|
|
78
|
+
summary: s.summary || "",
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Decision audit trail
|
|
82
|
+
const decisions = (brain.decisions || []).map((d) => ({
|
|
83
|
+
id: d.id,
|
|
84
|
+
text: d.text,
|
|
85
|
+
createdAt: d.createdAt,
|
|
86
|
+
source: d.source || "unknown",
|
|
87
|
+
tags: d.tags || [],
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
report: "SOC 2 Type II — SpecLock Compliance Export",
|
|
92
|
+
version: VERSION,
|
|
93
|
+
generatedAt: new Date().toISOString(),
|
|
94
|
+
project: {
|
|
95
|
+
name: brain.project?.name || "unknown",
|
|
96
|
+
id: brain.project?.id || "unknown",
|
|
97
|
+
createdAt: brain.project?.createdAt,
|
|
98
|
+
goal: brain.goal?.text || "",
|
|
99
|
+
},
|
|
100
|
+
auditChainIntegrity: {
|
|
101
|
+
valid: auditResult.valid,
|
|
102
|
+
totalEvents: auditResult.totalEvents,
|
|
103
|
+
hashedEvents: auditResult.hashedEvents,
|
|
104
|
+
unhashedEvents: auditResult.unhashedEvents,
|
|
105
|
+
brokenAt: auditResult.brokenAt,
|
|
106
|
+
verifiedAt: auditResult.verifiedAt,
|
|
107
|
+
},
|
|
108
|
+
constraintManagement: {
|
|
109
|
+
activeConstraints: activeLocks.length,
|
|
110
|
+
totalConstraints: allLocks.length,
|
|
111
|
+
removedConstraints: allLocks.filter((l) => !l.active).length,
|
|
112
|
+
changeHistory: constraintChanges,
|
|
113
|
+
},
|
|
114
|
+
accessLog: {
|
|
115
|
+
totalSessions: sessions.length,
|
|
116
|
+
sessions,
|
|
117
|
+
},
|
|
118
|
+
decisionAuditTrail: {
|
|
119
|
+
totalDecisions: decisions.length,
|
|
120
|
+
decisions,
|
|
121
|
+
},
|
|
122
|
+
changeManagement: {
|
|
123
|
+
totalEvents: events.length,
|
|
124
|
+
eventBreakdown: Object.fromEntries(
|
|
125
|
+
Object.entries(eventsByType).map(([type, evts]) => [type, evts.length])
|
|
126
|
+
),
|
|
127
|
+
recentChanges: (brain.state?.recentChanges || []).slice(0, 20),
|
|
128
|
+
reverts: brain.state?.reverts || [],
|
|
129
|
+
},
|
|
130
|
+
violations: {
|
|
131
|
+
total: (brain.state?.violations || []).length,
|
|
132
|
+
items: (brain.state?.violations || []).slice(0, 50),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate HIPAA compliance report.
|
|
139
|
+
* Filtered for PHI-related constraints, access events, and encryption status.
|
|
140
|
+
*/
|
|
141
|
+
export function exportHIPAA(root) {
|
|
142
|
+
const brain = readBrain(root);
|
|
143
|
+
if (!brain) {
|
|
144
|
+
return { error: "SpecLock not initialized. Run speclock init first." };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const events = readEvents(root, { limit: 10000 });
|
|
148
|
+
const auditResult = verifyAuditChain(root);
|
|
149
|
+
const allLocks = brain.specLock?.items || [];
|
|
150
|
+
|
|
151
|
+
// Filter PHI-related locks
|
|
152
|
+
const phiLocks = allLocks.filter((l) => isPHIRelated(l.text));
|
|
153
|
+
const activePhiLocks = phiLocks.filter((l) => l.active);
|
|
154
|
+
|
|
155
|
+
// Filter PHI-related events
|
|
156
|
+
const phiEvents = events.filter(
|
|
157
|
+
(e) => isPHIRelated(e.summary || "") || isPHIRelated(e.lockText || "")
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// PHI-related decisions
|
|
161
|
+
const phiDecisions = (brain.decisions || []).filter((d) =>
|
|
162
|
+
isPHIRelated(d.text)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// PHI-related violations
|
|
166
|
+
const phiViolations = (brain.state?.violations || []).filter(
|
|
167
|
+
(v) => isPHIRelated(v.lockText || "") || isPHIRelated(v.action || "")
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Check encryption status
|
|
171
|
+
const encryptionEnabled = !!process.env.SPECLOCK_ENCRYPTION_KEY;
|
|
172
|
+
|
|
173
|
+
// Access controls
|
|
174
|
+
const hasAuth = !!process.env.SPECLOCK_API_KEY;
|
|
175
|
+
const hasAuditChain = auditResult.hashedEvents > 0;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
report: "HIPAA Compliance — SpecLock PHI Protection Report",
|
|
179
|
+
version: VERSION,
|
|
180
|
+
generatedAt: new Date().toISOString(),
|
|
181
|
+
project: {
|
|
182
|
+
name: brain.project?.name || "unknown",
|
|
183
|
+
id: brain.project?.id || "unknown",
|
|
184
|
+
},
|
|
185
|
+
safeguards: {
|
|
186
|
+
technicalSafeguards: {
|
|
187
|
+
auditControls: {
|
|
188
|
+
enabled: hasAuditChain,
|
|
189
|
+
chainValid: auditResult.valid,
|
|
190
|
+
totalAuditedEvents: auditResult.hashedEvents,
|
|
191
|
+
status: hasAuditChain
|
|
192
|
+
? auditResult.valid
|
|
193
|
+
? "COMPLIANT"
|
|
194
|
+
: "NON-COMPLIANT — audit chain broken"
|
|
195
|
+
: "PARTIAL — enable HMAC audit chain for full compliance",
|
|
196
|
+
},
|
|
197
|
+
accessControl: {
|
|
198
|
+
authEnabled: hasAuth,
|
|
199
|
+
status: hasAuth
|
|
200
|
+
? "COMPLIANT"
|
|
201
|
+
: "NON-COMPLIANT — no API key authentication",
|
|
202
|
+
},
|
|
203
|
+
encryption: {
|
|
204
|
+
atRest: encryptionEnabled,
|
|
205
|
+
algorithm: encryptionEnabled ? "AES-256-GCM" : "none",
|
|
206
|
+
status: encryptionEnabled
|
|
207
|
+
? "COMPLIANT"
|
|
208
|
+
: "NON-COMPLIANT — enable SPECLOCK_ENCRYPTION_KEY",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
administrativeSafeguards: {
|
|
212
|
+
constraintEnforcement: {
|
|
213
|
+
totalPhiConstraints: phiLocks.length,
|
|
214
|
+
activePhiConstraints: activePhiLocks.length,
|
|
215
|
+
constraints: activePhiLocks.map((l) => ({
|
|
216
|
+
id: l.id,
|
|
217
|
+
text: l.text,
|
|
218
|
+
createdAt: l.createdAt,
|
|
219
|
+
source: l.source,
|
|
220
|
+
})),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
phiProtection: {
|
|
225
|
+
constraints: phiLocks.map((l) => ({
|
|
226
|
+
id: l.id,
|
|
227
|
+
text: l.text,
|
|
228
|
+
active: l.active,
|
|
229
|
+
createdAt: l.createdAt,
|
|
230
|
+
})),
|
|
231
|
+
decisions: phiDecisions.map((d) => ({
|
|
232
|
+
id: d.id,
|
|
233
|
+
text: d.text,
|
|
234
|
+
createdAt: d.createdAt,
|
|
235
|
+
})),
|
|
236
|
+
violations: phiViolations,
|
|
237
|
+
relatedEvents: phiEvents.length,
|
|
238
|
+
},
|
|
239
|
+
auditTrail: {
|
|
240
|
+
integrity: auditResult,
|
|
241
|
+
phiEventCount: phiEvents.length,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate CSV export of all events.
|
|
248
|
+
* Returns a CSV string suitable for auditor spreadsheets.
|
|
249
|
+
*/
|
|
250
|
+
export function exportCSV(root) {
|
|
251
|
+
const events = readEvents(root, { limit: 10000 });
|
|
252
|
+
|
|
253
|
+
if (!events.length) {
|
|
254
|
+
return "timestamp,event_id,type,summary,files,hash\n";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Reverse back to chronological order (readEvents returns newest first)
|
|
258
|
+
events.reverse();
|
|
259
|
+
|
|
260
|
+
const header = "timestamp,event_id,type,summary,files,hash";
|
|
261
|
+
const rows = events.map((e) => {
|
|
262
|
+
const timestamp = e.at || e.ts || "";
|
|
263
|
+
const eventId = e.eventId || "";
|
|
264
|
+
const type = e.type || "";
|
|
265
|
+
const summary = (e.summary || "").replace(/"/g, '""');
|
|
266
|
+
const files = (e.files || []).join("; ");
|
|
267
|
+
const hash = e.hash || "";
|
|
268
|
+
return `"${timestamp}","${eventId}","${type}","${summary}","${files}","${hash}"`;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return [header, ...rows].join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Main export function — dispatches by format.
|
|
276
|
+
*/
|
|
277
|
+
export function exportCompliance(root, format) {
|
|
278
|
+
switch (format) {
|
|
279
|
+
case "soc2":
|
|
280
|
+
return { format: "soc2", data: exportSOC2(root) };
|
|
281
|
+
case "hipaa":
|
|
282
|
+
return { format: "hipaa", data: exportHIPAA(root) };
|
|
283
|
+
case "csv":
|
|
284
|
+
return { format: "csv", data: exportCSV(root) };
|
|
285
|
+
default:
|
|
286
|
+
return {
|
|
287
|
+
error: `Unknown format: ${format}. Supported: soc2, hipaa, csv`,
|
|
288
|
+
supportedFormats: ["soc2", "hipaa", "csv"],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/core/engine.js
CHANGED
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
} from "./storage.js";
|
|
18
18
|
import { hasGit, getHead, getDefaultBranch, captureDiff, getStagedFiles } from "./git.js";
|
|
19
19
|
import { getTemplateNames, getTemplate } from "./templates.js";
|
|
20
|
+
import { analyzeConflict } from "./semantics.js";
|
|
21
|
+
import { ensureAuditKeyGitignored } from "./audit.js";
|
|
22
|
+
import { verifyAuditChain } from "./audit.js";
|
|
23
|
+
import { exportCompliance } from "./compliance.js";
|
|
24
|
+
import { checkFeature, checkLimits, getLicenseInfo } from "./license.js";
|
|
20
25
|
|
|
21
26
|
// --- Internal helpers ---
|
|
22
27
|
|
|
@@ -40,6 +45,7 @@ function writePatch(root, eventId, content) {
|
|
|
40
45
|
|
|
41
46
|
export function ensureInit(root) {
|
|
42
47
|
ensureSpeclockDirs(root);
|
|
48
|
+
try { ensureAuditKeyGitignored(root); } catch { /* non-critical */ }
|
|
43
49
|
let brain = readBrain(root);
|
|
44
50
|
if (!brain) {
|
|
45
51
|
const gitExists = hasGit(root);
|
|
@@ -251,7 +257,8 @@ export function handleFileEvent(root, brain, type, filePath) {
|
|
|
251
257
|
recordEvent(root, brain, event);
|
|
252
258
|
}
|
|
253
259
|
|
|
254
|
-
// ---
|
|
260
|
+
// --- Legacy synonym groups (deprecated — kept for backward compatibility) ---
|
|
261
|
+
// @deprecated Use analyzeConflict() from semantics.js instead
|
|
255
262
|
const SYNONYM_GROUPS = [
|
|
256
263
|
["remove", "delete", "drop", "eliminate", "destroy", "kill", "purge", "wipe"],
|
|
257
264
|
["add", "create", "introduce", "insert", "new"],
|
|
@@ -270,12 +277,13 @@ const SYNONYM_GROUPS = [
|
|
|
270
277
|
["enable", "activate", "turn-on", "switch-on"],
|
|
271
278
|
];
|
|
272
279
|
|
|
273
|
-
//
|
|
280
|
+
// @deprecated
|
|
274
281
|
const NEGATION_WORDS = ["no", "not", "never", "without", "dont", "don't", "cannot", "can't", "shouldn't", "mustn't", "avoid", "prevent", "prohibit", "forbid", "disallow"];
|
|
275
282
|
|
|
276
|
-
//
|
|
283
|
+
// @deprecated
|
|
277
284
|
const DESTRUCTIVE_WORDS = ["remove", "delete", "drop", "destroy", "kill", "purge", "wipe", "break", "disable", "revert", "rollback", "undo"];
|
|
278
285
|
|
|
286
|
+
// @deprecated — use analyzeConflict() from semantics.js
|
|
279
287
|
function expandWithSynonyms(words) {
|
|
280
288
|
const expanded = new Set(words);
|
|
281
289
|
for (const word of words) {
|
|
@@ -288,17 +296,20 @@ function expandWithSynonyms(words) {
|
|
|
288
296
|
return [...expanded];
|
|
289
297
|
}
|
|
290
298
|
|
|
299
|
+
// @deprecated
|
|
291
300
|
function hasNegation(text) {
|
|
292
301
|
const lower = text.toLowerCase();
|
|
293
302
|
return NEGATION_WORDS.some((neg) => lower.includes(neg));
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
// @deprecated
|
|
296
306
|
function isDestructiveAction(text) {
|
|
297
307
|
const lower = text.toLowerCase();
|
|
298
308
|
return DESTRUCTIVE_WORDS.some((w) => lower.includes(w));
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
// Check if a proposed action conflicts with any active SpecLock
|
|
312
|
+
// v2: Uses the semantic analysis engine from semantics.js
|
|
302
313
|
export function checkConflict(root, proposedAction) {
|
|
303
314
|
const brain = ensureInit(root);
|
|
304
315
|
const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
|
|
@@ -310,61 +321,18 @@ export function checkConflict(root, proposedAction) {
|
|
|
310
321
|
};
|
|
311
322
|
}
|
|
312
323
|
|
|
313
|
-
const actionLower = proposedAction.toLowerCase();
|
|
314
|
-
const actionWords = actionLower.split(/\s+/).filter((w) => w.length > 2);
|
|
315
|
-
const actionExpanded = expandWithSynonyms(actionWords);
|
|
316
|
-
const actionIsDestructive = isDestructiveAction(actionLower);
|
|
317
|
-
|
|
318
324
|
const conflicting = [];
|
|
319
325
|
for (const lock of activeLocks) {
|
|
320
|
-
const
|
|
321
|
-
const lockWords = lockLower.split(/\s+/).filter((w) => w.length > 2);
|
|
322
|
-
const lockExpanded = expandWithSynonyms(lockWords);
|
|
323
|
-
|
|
324
|
-
// Direct keyword overlap
|
|
325
|
-
const directOverlap = actionWords.filter((w) => lockWords.includes(w));
|
|
326
|
-
|
|
327
|
-
// Synonym-expanded overlap
|
|
328
|
-
const synonymOverlap = actionExpanded.filter((w) => lockExpanded.includes(w));
|
|
329
|
-
const uniqueSynonymMatches = synonymOverlap.filter((w) => !directOverlap.includes(w));
|
|
330
|
-
|
|
331
|
-
// Negation analysis: lock says "No X" and action does X
|
|
332
|
-
const lockHasNegation = hasNegation(lockLower);
|
|
333
|
-
const actionHasNegation = hasNegation(actionLower);
|
|
334
|
-
const negationConflict = lockHasNegation && !actionHasNegation && synonymOverlap.length > 0;
|
|
335
|
-
|
|
336
|
-
// Calculate confidence score
|
|
337
|
-
let confidence = 0;
|
|
338
|
-
let reasons = [];
|
|
339
|
-
|
|
340
|
-
if (directOverlap.length > 0) {
|
|
341
|
-
confidence += directOverlap.length * 30;
|
|
342
|
-
reasons.push(`direct keyword match: ${directOverlap.join(", ")}`);
|
|
343
|
-
}
|
|
344
|
-
if (uniqueSynonymMatches.length > 0) {
|
|
345
|
-
confidence += uniqueSynonymMatches.length * 15;
|
|
346
|
-
reasons.push(`synonym match: ${uniqueSynonymMatches.join(", ")}`);
|
|
347
|
-
}
|
|
348
|
-
if (negationConflict) {
|
|
349
|
-
confidence += 40;
|
|
350
|
-
reasons.push("lock prohibits this action (negation detected)");
|
|
351
|
-
}
|
|
352
|
-
if (actionIsDestructive && synonymOverlap.length > 0) {
|
|
353
|
-
confidence += 20;
|
|
354
|
-
reasons.push("destructive action against locked constraint");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
confidence = Math.min(confidence, 100);
|
|
326
|
+
const result = analyzeConflict(proposedAction, lock.text);
|
|
358
327
|
|
|
359
|
-
if (
|
|
360
|
-
const level = confidence >= 70 ? "HIGH" : confidence >= 40 ? "MEDIUM" : "LOW";
|
|
328
|
+
if (result.isConflict) {
|
|
361
329
|
conflicting.push({
|
|
362
330
|
id: lock.id,
|
|
363
331
|
text: lock.text,
|
|
364
|
-
matchedKeywords: [
|
|
365
|
-
confidence,
|
|
366
|
-
level,
|
|
367
|
-
reasons,
|
|
332
|
+
matchedKeywords: [],
|
|
333
|
+
confidence: result.confidence,
|
|
334
|
+
level: result.level,
|
|
335
|
+
reasons: result.reasons,
|
|
368
336
|
});
|
|
369
337
|
}
|
|
370
338
|
}
|
|
@@ -373,7 +341,7 @@ export function checkConflict(root, proposedAction) {
|
|
|
373
341
|
return {
|
|
374
342
|
hasConflict: false,
|
|
375
343
|
conflictingLocks: [],
|
|
376
|
-
analysis: `Checked against ${activeLocks.length} active lock(s). No conflicts detected (
|
|
344
|
+
analysis: `Checked against ${activeLocks.length} active lock(s). No conflicts detected (semantic analysis v2). Proceed with caution.`,
|
|
377
345
|
};
|
|
378
346
|
}
|
|
379
347
|
|
|
@@ -406,6 +374,21 @@ export function checkConflict(root, proposedAction) {
|
|
|
406
374
|
return result;
|
|
407
375
|
}
|
|
408
376
|
|
|
377
|
+
// Async version — uses LLM if available, falls back to heuristic
|
|
378
|
+
export async function checkConflictAsync(root, proposedAction) {
|
|
379
|
+
// Try LLM first (if llm-checker is available)
|
|
380
|
+
try {
|
|
381
|
+
const { llmCheckConflict } = await import("./llm-checker.js");
|
|
382
|
+
const llmResult = await llmCheckConflict(root, proposedAction);
|
|
383
|
+
if (llmResult) return llmResult;
|
|
384
|
+
} catch (_) {
|
|
385
|
+
// LLM checker not available or failed — fall through to heuristic
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Fallback to heuristic
|
|
389
|
+
return checkConflict(root, proposedAction);
|
|
390
|
+
}
|
|
391
|
+
|
|
409
392
|
// --- Auto-lock suggestions ---
|
|
410
393
|
export function suggestLocks(root) {
|
|
411
394
|
const brain = ensureInit(root);
|
|
@@ -478,7 +461,7 @@ export function suggestLocks(root) {
|
|
|
478
461
|
return { suggestions, totalLocks: brain.specLock.items.filter((l) => l.active).length };
|
|
479
462
|
}
|
|
480
463
|
|
|
481
|
-
// --- Drift detection ---
|
|
464
|
+
// --- Drift detection (v2: uses semantic engine) ---
|
|
482
465
|
export function detectDrift(root) {
|
|
483
466
|
const brain = ensureInit(root);
|
|
484
467
|
const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
|
|
@@ -488,29 +471,20 @@ export function detectDrift(root) {
|
|
|
488
471
|
|
|
489
472
|
const drifts = [];
|
|
490
473
|
|
|
491
|
-
// Check recent changes against locks
|
|
474
|
+
// Check recent changes against locks using the semantic engine
|
|
492
475
|
for (const change of brain.state.recentChanges) {
|
|
493
|
-
const changeLower = change.summary.toLowerCase();
|
|
494
|
-
const changeWords = changeLower.split(/\s+/).filter((w) => w.length > 2);
|
|
495
|
-
const changeExpanded = expandWithSynonyms(changeWords);
|
|
496
|
-
|
|
497
476
|
for (const lock of activeLocks) {
|
|
498
|
-
const
|
|
499
|
-
const lockWords = lockLower.split(/\s+/).filter((w) => w.length > 2);
|
|
500
|
-
const lockExpanded = expandWithSynonyms(lockWords);
|
|
501
|
-
|
|
502
|
-
const overlap = changeExpanded.filter((w) => lockExpanded.includes(w));
|
|
503
|
-
const lockHasNegation = hasNegation(lockLower);
|
|
477
|
+
const result = analyzeConflict(change.summary, lock.text);
|
|
504
478
|
|
|
505
|
-
if (
|
|
479
|
+
if (result.isConflict) {
|
|
506
480
|
drifts.push({
|
|
507
481
|
lockId: lock.id,
|
|
508
482
|
lockText: lock.text,
|
|
509
483
|
changeEventId: change.eventId,
|
|
510
484
|
changeSummary: change.summary,
|
|
511
485
|
changeAt: change.at,
|
|
512
|
-
matchedTerms:
|
|
513
|
-
severity:
|
|
486
|
+
matchedTerms: result.reasons,
|
|
487
|
+
severity: result.level === "HIGH" ? "high" : "medium",
|
|
514
488
|
});
|
|
515
489
|
}
|
|
516
490
|
}
|
|
@@ -1246,3 +1220,9 @@ export function auditStagedFiles(root) {
|
|
|
1246
1220
|
message,
|
|
1247
1221
|
};
|
|
1248
1222
|
}
|
|
1223
|
+
|
|
1224
|
+
// --- Enterprise features (v2.1) ---
|
|
1225
|
+
|
|
1226
|
+
export { verifyAuditChain } from "./audit.js";
|
|
1227
|
+
export { exportCompliance } from "./compliance.js";
|
|
1228
|
+
export { checkFeature, checkLimits, getLicenseInfo } from "./license.js";
|