speclock 4.5.6 → 5.0.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.
@@ -0,0 +1,635 @@
1
+ // ===================================================================
2
+ // SpecLock Code Graph — Dependency Analysis & Blast Radius
3
+ // Builds a live dependency graph of the codebase by parsing imports.
4
+ // Enables blast radius calculation, lock-to-file mapping, module
5
+ // detection, and critical path analysis.
6
+ //
7
+ // Developed by Sandeep Roy (https://github.com/sgroy10)
8
+ // ===================================================================
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { readBrain } from "./storage.js";
13
+ import { extractLockSubject } from "./lock-author.js";
14
+
15
+ // --- Constants ---
16
+
17
+ const SUPPORTED_EXTENSIONS = new Set([
18
+ ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
19
+ ".py", ".pyw",
20
+ ]);
21
+
22
+ const SKIP_DIRS = new Set([
23
+ "node_modules", ".git", ".speclock", "__pycache__",
24
+ "dist", "build", ".next", "coverage", ".cache",
25
+ "venv", ".venv", "env", ".env",
26
+ ]);
27
+
28
+ const GRAPH_FILE = "code-graph.json";
29
+ const GRAPH_STALE_MS = 60 * 60 * 1000; // 1 hour
30
+
31
+ // --- Import parsing regexes ---
32
+
33
+ // JS/TS: import ... from "path"
34
+ const JS_IMPORT_FROM = /(?:import|export)\s+(?:[\s\S]*?)\s+from\s+["']([^"']+)["']/g;
35
+ // JS/TS: require("path")
36
+ const JS_REQUIRE = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
37
+ // JS/TS: import("path") — dynamic import
38
+ const JS_DYNAMIC_IMPORT = /import\s*\(\s*["']([^"']+)["']\s*\)/g;
39
+
40
+ // Python: import module
41
+ const PY_IMPORT = /^import\s+([\w.]+)/gm;
42
+ // Python: from module import ...
43
+ const PY_FROM_IMPORT = /^from\s+([\w.]+)\s+import/gm;
44
+
45
+ // --- Core functions ---
46
+
47
+ /**
48
+ * Build the dependency graph for a project.
49
+ * @param {string} root - Project root path
50
+ * @param {{ force?: boolean, extensions?: string[] }} options
51
+ * @returns {Object} The built graph
52
+ */
53
+ export function buildGraph(root, options = {}) {
54
+ const extensions = options.extensions
55
+ ? new Set(options.extensions)
56
+ : SUPPORTED_EXTENSIONS;
57
+
58
+ // Scan all source files
59
+ const sourceFiles = [];
60
+ scanDirectory(root, root, extensions, sourceFiles);
61
+
62
+ // Build adjacency graph
63
+ const files = {};
64
+ for (const filePath of sourceFiles) {
65
+ const relPath = path.relative(root, filePath).replace(/\\/g, "/");
66
+ const ext = path.extname(filePath).toLowerCase();
67
+ const language = getLanguage(ext);
68
+ const content = safeReadFile(filePath);
69
+
70
+ let imports = [];
71
+ if (content) {
72
+ if (language === "js" || language === "ts") {
73
+ imports = parseJsImports(content, filePath, root);
74
+ } else if (language === "py") {
75
+ imports = parsePyImports(content, filePath, root);
76
+ }
77
+ }
78
+
79
+ let size = 0;
80
+ try { size = fs.statSync(filePath).size; } catch (_) {}
81
+
82
+ files[relPath] = {
83
+ imports: imports,
84
+ importedBy: [], // populated in second pass
85
+ size,
86
+ language,
87
+ };
88
+ }
89
+
90
+ // Second pass: populate importedBy
91
+ for (const [filePath, data] of Object.entries(files)) {
92
+ for (const imp of data.imports) {
93
+ if (files[imp]) {
94
+ if (!files[imp].importedBy.includes(filePath)) {
95
+ files[imp].importedBy.push(filePath);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // Compute stats
102
+ const languageCounts = {};
103
+ let totalEdges = 0;
104
+ const entryPoints = [];
105
+
106
+ for (const [filePath, data] of Object.entries(files)) {
107
+ const lang = data.language;
108
+ languageCounts[lang] = (languageCounts[lang] || 0) + 1;
109
+ totalEdges += data.imports.length;
110
+ if (data.importedBy.length === 0 && data.imports.length > 0) {
111
+ entryPoints.push(filePath);
112
+ }
113
+ }
114
+
115
+ const graph = {
116
+ builtAt: new Date().toISOString(),
117
+ root: root.replace(/\\/g, "/"),
118
+ files,
119
+ stats: {
120
+ totalFiles: Object.keys(files).length,
121
+ totalEdges,
122
+ entryPoints,
123
+ languages: languageCounts,
124
+ },
125
+ };
126
+
127
+ // Save to .speclock/code-graph.json
128
+ saveGraph(root, graph);
129
+
130
+ return graph;
131
+ }
132
+
133
+ /**
134
+ * Get or rebuild the graph if stale.
135
+ * @param {string} root - Project root
136
+ * @param {{ force?: boolean }} options
137
+ * @returns {Object} The graph
138
+ */
139
+ export function getOrBuildGraph(root, options = {}) {
140
+ if (!options.force) {
141
+ const existing = loadGraph(root);
142
+ if (existing) {
143
+ const age = Date.now() - new Date(existing.builtAt).getTime();
144
+ if (age < GRAPH_STALE_MS) {
145
+ return existing;
146
+ }
147
+ }
148
+ }
149
+ return buildGraph(root, options);
150
+ }
151
+
152
+ /**
153
+ * Calculate blast radius for a file change.
154
+ * @param {string} root - Project root
155
+ * @param {string} filePath - Relative path of the file being changed
156
+ * @returns {Object} Blast radius analysis
157
+ */
158
+ export function getBlastRadius(root, filePath) {
159
+ const graph = getOrBuildGraph(root);
160
+ const normalizedPath = filePath.replace(/\\/g, "/");
161
+
162
+ if (!graph.files[normalizedPath]) {
163
+ return {
164
+ file: normalizedPath,
165
+ found: false,
166
+ error: `File not found in graph: ${normalizedPath}`,
167
+ directDependents: [],
168
+ transitiveDependents: [],
169
+ depth: 0,
170
+ blastRadius: 0,
171
+ totalFiles: graph.stats.totalFiles,
172
+ impactPercent: 0,
173
+ };
174
+ }
175
+
176
+ // BFS to find all transitive dependents
177
+ const visited = new Set();
178
+ const queue = [{ file: normalizedPath, depth: 0 }];
179
+ const directDependents = [...graph.files[normalizedPath].importedBy];
180
+ let maxDepth = 0;
181
+
182
+ while (queue.length > 0) {
183
+ const { file, depth } = queue.shift();
184
+ if (visited.has(file)) continue;
185
+ visited.add(file);
186
+ if (depth > maxDepth) maxDepth = depth;
187
+
188
+ const node = graph.files[file];
189
+ if (!node) continue;
190
+
191
+ for (const dependent of node.importedBy) {
192
+ if (!visited.has(dependent)) {
193
+ queue.push({ file: dependent, depth: depth + 1 });
194
+ }
195
+ }
196
+ }
197
+
198
+ // Remove the file itself from transitive dependents
199
+ visited.delete(normalizedPath);
200
+ const transitiveDependents = [...visited];
201
+
202
+ const blastRadius = transitiveDependents.length;
203
+ const impactPercent = graph.stats.totalFiles > 0
204
+ ? Math.round((blastRadius / graph.stats.totalFiles) * 1000) / 10
205
+ : 0;
206
+
207
+ return {
208
+ file: normalizedPath,
209
+ found: true,
210
+ directDependents,
211
+ transitiveDependents,
212
+ depth: maxDepth,
213
+ blastRadius,
214
+ totalFiles: graph.stats.totalFiles,
215
+ impactPercent,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Map all active locks to actual code files.
221
+ * @param {string} root - Project root
222
+ * @returns {Array} Lock-to-file mappings
223
+ */
224
+ export function mapLocksToFiles(root) {
225
+ const graph = getOrBuildGraph(root);
226
+ const brain = readBrain(root);
227
+ if (!brain) return [];
228
+
229
+ const activeLocks = (brain.specLock?.items || []).filter(l => l.active !== false);
230
+ const allFiles = Object.keys(graph.files);
231
+
232
+ const mappings = [];
233
+ for (const lock of activeLocks) {
234
+ const subject = extractLockSubject(lock.text).toLowerCase();
235
+ const keywords = extractKeywords(subject);
236
+
237
+ const matchedFiles = [];
238
+ const matchedModules = new Set();
239
+
240
+ for (const file of allFiles) {
241
+ const fileLower = file.toLowerCase();
242
+ const matched = keywords.some(kw => {
243
+ // Match against file path segments
244
+ const segments = fileLower.split("/");
245
+ return segments.some(seg => seg.includes(kw));
246
+ });
247
+
248
+ if (matched) {
249
+ matchedFiles.push(file);
250
+ // Extract module name (first directory under src/ or top-level)
251
+ const parts = file.split("/");
252
+ if (parts.length >= 2) {
253
+ const moduleDir = parts[0] === "src" && parts.length >= 3 ? parts[1] : parts[0];
254
+ matchedModules.add(moduleDir);
255
+ }
256
+ }
257
+ }
258
+
259
+ // Calculate combined blast radius for all matched files
260
+ let totalBlastRadius = 0;
261
+ const allAffected = new Set();
262
+ for (const f of matchedFiles) {
263
+ const br = getBlastRadius(root, f);
264
+ for (const dep of br.transitiveDependents) {
265
+ allAffected.add(dep);
266
+ }
267
+ }
268
+ totalBlastRadius = allAffected.size;
269
+
270
+ mappings.push({
271
+ lockId: lock.id,
272
+ lockText: lock.text,
273
+ matchedFiles,
274
+ matchedModules: [...matchedModules],
275
+ blastRadius: totalBlastRadius,
276
+ });
277
+ }
278
+
279
+ return mappings;
280
+ }
281
+
282
+ /**
283
+ * Identify logical modules in the project.
284
+ * @param {string} root - Project root
285
+ * @returns {Object} Modules with their files and dependencies
286
+ */
287
+ export function getModules(root) {
288
+ const graph = getOrBuildGraph(root);
289
+ const modules = {};
290
+
291
+ for (const [filePath, data] of Object.entries(graph.files)) {
292
+ const parts = filePath.split("/");
293
+ let moduleName;
294
+
295
+ if (parts[0] === "src" && parts.length >= 3) {
296
+ moduleName = parts[1]; // src/<module>/...
297
+ } else if (parts.length >= 2) {
298
+ moduleName = parts[0]; // <module>/...
299
+ } else {
300
+ moduleName = "_root"; // top-level files
301
+ }
302
+
303
+ if (!modules[moduleName]) {
304
+ modules[moduleName] = {
305
+ files: [],
306
+ entryPoint: null,
307
+ dependencies: new Set(),
308
+ dependents: new Set(),
309
+ totalSize: 0,
310
+ };
311
+ }
312
+
313
+ modules[moduleName].files.push(filePath);
314
+ modules[moduleName].totalSize += data.size;
315
+
316
+ // Track inter-module dependencies
317
+ for (const imp of data.imports) {
318
+ const impParts = imp.split("/");
319
+ let impModule;
320
+ if (impParts[0] === "src" && impParts.length >= 3) {
321
+ impModule = impParts[1];
322
+ } else if (impParts.length >= 2) {
323
+ impModule = impParts[0];
324
+ } else {
325
+ impModule = "_root";
326
+ }
327
+
328
+ if (impModule !== moduleName) {
329
+ modules[moduleName].dependencies.add(impModule);
330
+ }
331
+ }
332
+
333
+ for (const dep of data.importedBy) {
334
+ const depParts = dep.split("/");
335
+ let depModule;
336
+ if (depParts[0] === "src" && depParts.length >= 3) {
337
+ depModule = depParts[1];
338
+ } else if (depParts.length >= 2) {
339
+ depModule = depParts[0];
340
+ } else {
341
+ depModule = "_root";
342
+ }
343
+
344
+ if (depModule !== moduleName) {
345
+ modules[moduleName].dependents.add(depModule);
346
+ }
347
+ }
348
+ }
349
+
350
+ // Find entry points per module (most imported file within the module)
351
+ for (const [name, mod] of Object.entries(modules)) {
352
+ let bestEntry = null;
353
+ let maxImportedBy = -1;
354
+
355
+ for (const file of mod.files) {
356
+ const importedByCount = graph.files[file]?.importedBy?.length || 0;
357
+ if (importedByCount > maxImportedBy) {
358
+ maxImportedBy = importedByCount;
359
+ bestEntry = file;
360
+ }
361
+ }
362
+
363
+ mod.entryPoint = bestEntry;
364
+ // Convert Sets to arrays for serialization
365
+ mod.dependencies = [...mod.dependencies];
366
+ mod.dependents = [...mod.dependents];
367
+ }
368
+
369
+ return modules;
370
+ }
371
+
372
+ /**
373
+ * Find critical paths — files with highest risk/impact.
374
+ * @param {string} root - Project root
375
+ * @param {{ limit?: number }} options
376
+ * @returns {Array} Files sorted by risk score
377
+ */
378
+ export function getCriticalPaths(root, options = {}) {
379
+ const limit = options.limit || 10;
380
+ const graph = getOrBuildGraph(root);
381
+
382
+ const scored = [];
383
+ for (const [filePath, data] of Object.entries(graph.files)) {
384
+ const directDependents = data.importedBy.length;
385
+ const imports = data.imports.length;
386
+
387
+ // Simple risk score: weighted by dependents (files that break if this changes)
388
+ const riskScore = directDependents * 3 + imports;
389
+
390
+ scored.push({
391
+ file: filePath,
392
+ directDependents,
393
+ imports,
394
+ riskScore,
395
+ language: data.language,
396
+ size: data.size,
397
+ });
398
+ }
399
+
400
+ // Sort by risk score descending
401
+ scored.sort((a, b) => b.riskScore - a.riskScore);
402
+
403
+ return scored.slice(0, limit);
404
+ }
405
+
406
+ // --- Import parsers ---
407
+
408
+ function parseJsImports(content, filePath, root) {
409
+ const imports = [];
410
+ const dir = path.dirname(filePath);
411
+
412
+ // Reset regex lastIndex
413
+ JS_IMPORT_FROM.lastIndex = 0;
414
+ JS_REQUIRE.lastIndex = 0;
415
+ JS_DYNAMIC_IMPORT.lastIndex = 0;
416
+
417
+ const rawImports = new Set();
418
+
419
+ let match;
420
+ while ((match = JS_IMPORT_FROM.exec(content)) !== null) {
421
+ rawImports.add(match[1]);
422
+ }
423
+ while ((match = JS_REQUIRE.exec(content)) !== null) {
424
+ rawImports.add(match[1]);
425
+ }
426
+ while ((match = JS_DYNAMIC_IMPORT.exec(content)) !== null) {
427
+ rawImports.add(match[1]);
428
+ }
429
+
430
+ for (const raw of rawImports) {
431
+ const resolved = resolveJsImport(raw, dir, root);
432
+ if (resolved) {
433
+ imports.push(resolved);
434
+ }
435
+ }
436
+
437
+ return imports;
438
+ }
439
+
440
+ function resolveJsImport(importPath, fromDir, root) {
441
+ // Skip external/node_modules imports
442
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
443
+ return null;
444
+ }
445
+
446
+ const resolved = path.resolve(fromDir, importPath);
447
+ const relPath = path.relative(root, resolved).replace(/\\/g, "/");
448
+
449
+ // Try exact path first, then with extensions
450
+ const extensions = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"];
451
+
452
+ // Check exact path
453
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
454
+ return relPath;
455
+ }
456
+
457
+ // Try adding extensions
458
+ for (const ext of extensions) {
459
+ const withExt = resolved + ext;
460
+ if (fs.existsSync(withExt)) {
461
+ return path.relative(root, withExt).replace(/\\/g, "/");
462
+ }
463
+ }
464
+
465
+ // Try index files
466
+ for (const ext of extensions) {
467
+ const indexFile = path.join(resolved, "index" + ext);
468
+ if (fs.existsSync(indexFile)) {
469
+ return path.relative(root, indexFile).replace(/\\/g, "/");
470
+ }
471
+ }
472
+
473
+ // Return the normalized path even if file doesn't exist (might be an alias)
474
+ return relPath;
475
+ }
476
+
477
+ function parsePyImports(content, filePath, root) {
478
+ const imports = [];
479
+ const dir = path.dirname(filePath);
480
+
481
+ PY_IMPORT.lastIndex = 0;
482
+ PY_FROM_IMPORT.lastIndex = 0;
483
+
484
+ const rawImports = new Set();
485
+
486
+ let match;
487
+ while ((match = PY_IMPORT.exec(content)) !== null) {
488
+ rawImports.add(match[1]);
489
+ }
490
+ while ((match = PY_FROM_IMPORT.exec(content)) !== null) {
491
+ rawImports.add(match[1]);
492
+ }
493
+
494
+ for (const raw of rawImports) {
495
+ const resolved = resolvePyImport(raw, dir, root);
496
+ if (resolved) {
497
+ imports.push(resolved);
498
+ }
499
+ }
500
+
501
+ return imports;
502
+ }
503
+
504
+ function resolvePyImport(importPath, fromDir, root) {
505
+ // Skip stdlib and installed packages (no dots = likely external)
506
+ if (importPath.startsWith(".")) {
507
+ // Relative import
508
+ const parts = importPath.split(".");
509
+ let upCount = 0;
510
+ for (const p of parts) {
511
+ if (p === "") upCount++;
512
+ else break;
513
+ }
514
+ const remaining = parts.filter(p => p !== "");
515
+ let resolved = fromDir;
516
+ for (let i = 0; i < upCount; i++) {
517
+ resolved = path.dirname(resolved);
518
+ }
519
+ const modulePath = path.join(resolved, ...remaining);
520
+ return tryResolvePyPath(modulePath, root);
521
+ }
522
+
523
+ // Try as project-local import (check if the module exists in root)
524
+ const parts = importPath.split(".");
525
+ const modulePath = path.join(root, ...parts);
526
+ return tryResolvePyPath(modulePath, root);
527
+ }
528
+
529
+ function tryResolvePyPath(modulePath, root) {
530
+ // Try as .py file
531
+ const pyFile = modulePath + ".py";
532
+ if (fs.existsSync(pyFile)) {
533
+ return path.relative(root, pyFile).replace(/\\/g, "/");
534
+ }
535
+
536
+ // Try as package (__init__.py)
537
+ const initFile = path.join(modulePath, "__init__.py");
538
+ if (fs.existsSync(initFile)) {
539
+ return path.relative(root, initFile).replace(/\\/g, "/");
540
+ }
541
+
542
+ return null;
543
+ }
544
+
545
+ // --- File scanning ---
546
+
547
+ function scanDirectory(root, dir, extensions, results) {
548
+ let entries;
549
+ try {
550
+ entries = fs.readdirSync(dir, { withFileTypes: true });
551
+ } catch (_) {
552
+ return;
553
+ }
554
+
555
+ for (const entry of entries) {
556
+ if (entry.name.startsWith(".") && entry.name !== ".") continue;
557
+ if (SKIP_DIRS.has(entry.name)) continue;
558
+
559
+ const fullPath = path.join(dir, entry.name);
560
+
561
+ if (entry.isDirectory()) {
562
+ scanDirectory(root, fullPath, extensions, results);
563
+ } else if (entry.isFile()) {
564
+ const ext = path.extname(entry.name).toLowerCase();
565
+ if (extensions.has(ext)) {
566
+ results.push(fullPath);
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ // --- Utilities ---
573
+
574
+ function safeReadFile(filePath) {
575
+ try {
576
+ return fs.readFileSync(filePath, "utf-8");
577
+ } catch (_) {
578
+ return null;
579
+ }
580
+ }
581
+
582
+ function getLanguage(ext) {
583
+ switch (ext) {
584
+ case ".js": case ".jsx": case ".mjs": case ".cjs": return "js";
585
+ case ".ts": case ".tsx": return "ts";
586
+ case ".py": case ".pyw": return "py";
587
+ default: return "unknown";
588
+ }
589
+ }
590
+
591
+ function saveGraph(root, graph) {
592
+ const speclockDir = path.join(root, ".speclock");
593
+ if (!fs.existsSync(speclockDir)) {
594
+ fs.mkdirSync(speclockDir, { recursive: true });
595
+ }
596
+ const graphPath = path.join(speclockDir, GRAPH_FILE);
597
+ fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2));
598
+ }
599
+
600
+ function loadGraph(root) {
601
+ const graphPath = path.join(root, ".speclock", GRAPH_FILE);
602
+ if (!fs.existsSync(graphPath)) return null;
603
+ try {
604
+ return JSON.parse(fs.readFileSync(graphPath, "utf-8"));
605
+ } catch (_) {
606
+ return null;
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Extract meaningful keywords from lock subject text.
612
+ * @param {string} text - The lock subject
613
+ * @returns {string[]} Keywords to match against file paths
614
+ */
615
+ function extractKeywords(text) {
616
+ // Split on whitespace and common separators, filter short/stop words
617
+ const stopWords = new Set([
618
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
619
+ "and", "or", "but", "in", "on", "at", "to", "for", "of",
620
+ "with", "by", "from", "as", "not", "no", "do", "does",
621
+ "did", "will", "would", "should", "could", "may", "might",
622
+ "must", "shall", "can", "this", "that", "it", "its",
623
+ "any", "all", "each", "every", "some", "system", "module",
624
+ "file", "files", "code", "change", "modify", "touch", "edit",
625
+ "update", "delete", "remove", "add", "create", "never",
626
+ "always", "configuration", "config",
627
+ ]);
628
+
629
+ const words = text
630
+ .split(/[\s\-_./\\]+/)
631
+ .map(w => w.replace(/[^a-z0-9]/g, ""))
632
+ .filter(w => w.length >= 2 && !stopWords.has(w));
633
+
634
+ return [...new Set(words)];
635
+ }
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "4.5.6";
12
+ const VERSION = "5.0.0";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -17,6 +17,7 @@ import {
17
17
  } from "./storage.js";
18
18
  import { getStagedFiles } from "./git.js";
19
19
  import { analyzeConflict } from "./semantics.js";
20
+ import { checkAllTypedConstraints, CONSTRAINT_TYPES } from "./typed-constraints.js";
20
21
  import { ensureInit } from "./memory.js";
21
22
 
22
23
  // --- Legacy helpers (kept for pre-commit audit backward compat) ---
@@ -3,10 +3,11 @@
3
3
  * Re-exports all functionality from focused modules.
4
4
  * This file exists for backward compatibility — all imports from engine.js still work.
5
5
  *
6
- * Module structure (v2.5):
7
- * - memory.js: Goal, lock, decision, note, deploy facts CRUD
6
+ * Module structure (v5.0):
7
+ * - memory.js: Goal, lock, typed lock, decision, note, deploy facts CRUD
8
8
  * - tracking.js: Change logging, file event handling
9
9
  * - conflict.js: Conflict checking, drift detection, suggestions, audit
10
+ * - typed-constraints.js: Numerical, range, state, temporal constraint checking
10
11
  * - sessions.js: Session management (briefing, start, end)
11
12
  * - enforcer.js: Hard/advisory enforcement, overrides, escalation
12
13
  * - pre-commit-semantic.js: Semantic pre-commit analysis
@@ -22,6 +23,8 @@ export {
22
23
  ensureInit,
23
24
  setGoal,
24
25
  addLock,
26
+ addTypedLock,
27
+ updateTypedLockThreshold,
25
28
  removeLock,
26
29
  addDecision,
27
30
  addNote,
@@ -599,6 +602,16 @@ export {
599
602
  listSessions,
600
603
  } from "./sso.js";
601
604
 
605
+ // --- Typed Constraints for Autonomous Systems (v5.0) ---
606
+ export {
607
+ CONSTRAINT_TYPES,
608
+ OPERATORS,
609
+ validateTypedLock,
610
+ checkTypedConstraint,
611
+ checkAllTypedConstraints,
612
+ formatTypedLockText,
613
+ } from "./typed-constraints.js";
614
+
602
615
  // --- Smart Lock Authoring (v4.0) ---
603
616
  export {
604
617
  normalizeLock,
@@ -608,3 +621,20 @@ export {
608
621
  extractLockSubject,
609
622
  rewriteLock,
610
623
  } from "./lock-author.js";
624
+
625
+ // --- Spec Compiler (v5.0) ---
626
+ export {
627
+ compileSpec,
628
+ compileFile,
629
+ compileAndApply,
630
+ } from "./spec-compiler.js";
631
+
632
+ // --- Code Graph (v5.0) ---
633
+ export {
634
+ buildGraph,
635
+ getOrBuildGraph,
636
+ getBlastRadius,
637
+ mapLocksToFiles,
638
+ getModules,
639
+ getCriticalPaths,
640
+ } from "./code-graph.js";