@ipation/specbridge 1.0.6 → 1.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/dist/cli.js CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command12 } from "commander";
5
- import chalk12 from "chalk";
6
- import { readFileSync } from "fs";
7
- import { fileURLToPath } from "url";
8
- import { dirname as dirname3, join as join9 } from "path";
4
+ import { Command as Command16 } from "commander";
5
+ import chalk14 from "chalk";
6
+ import { readFileSync as readFileSync2 } from "fs";
7
+ import { fileURLToPath as fileURLToPath3 } from "url";
8
+ import { dirname as dirname4, join as join10 } from "path";
9
9
 
10
10
  // src/core/errors/index.ts
11
11
  var SpecBridgeError = class extends Error {
12
- constructor(message, code, details) {
12
+ constructor(message, code, details, suggestion) {
13
13
  super(message);
14
14
  this.code = code;
15
15
  this.details = details;
16
+ this.suggestion = suggestion;
16
17
  this.name = "SpecBridgeError";
17
18
  Error.captureStackTrace(this, this.constructor);
18
19
  }
@@ -38,15 +39,15 @@ var DecisionNotFoundError = class extends SpecBridgeError {
38
39
  }
39
40
  };
40
41
  var FileSystemError = class extends SpecBridgeError {
41
- constructor(message, path) {
42
- super(message, "FILE_SYSTEM_ERROR", { path });
43
- this.path = path;
42
+ constructor(message, path5) {
43
+ super(message, "FILE_SYSTEM_ERROR", { path: path5 });
44
+ this.path = path5;
44
45
  this.name = "FileSystemError";
45
46
  }
46
47
  };
47
48
  var AlreadyInitializedError = class extends SpecBridgeError {
48
- constructor(path) {
49
- super(`SpecBridge is already initialized at ${path}`, "ALREADY_INITIALIZED", { path });
49
+ constructor(path5) {
50
+ super(`SpecBridge is already initialized at ${path5}`, "ALREADY_INITIALIZED", { path: path5 });
50
51
  this.name = "AlreadyInitializedError";
51
52
  }
52
53
  };
@@ -54,7 +55,9 @@ var NotInitializedError = class extends SpecBridgeError {
54
55
  constructor() {
55
56
  super(
56
57
  'SpecBridge is not initialized. Run "specbridge init" first.',
57
- "NOT_INITIALIZED"
58
+ "NOT_INITIALIZED",
59
+ void 0,
60
+ "Run `specbridge init` in this directory to create .specbridge/"
58
61
  );
59
62
  this.name = "NotInitializedError";
60
63
  }
@@ -78,6 +81,10 @@ ${detailsStr}`;
78
81
  if (error instanceof DecisionValidationError && error.validationErrors.length > 0) {
79
82
  message += "\nValidation errors:\n" + error.validationErrors.map((e) => ` - ${e}`).join("\n");
80
83
  }
84
+ if (error.suggestion) {
85
+ message += `
86
+ Suggestion: ${error.suggestion}`;
87
+ }
81
88
  return message;
82
89
  }
83
90
  return `Error: ${error.message}`;
@@ -93,23 +100,23 @@ import { join as join2 } from "path";
93
100
  import { readFile, writeFile, mkdir, access, readdir, stat } from "fs/promises";
94
101
  import { join, dirname } from "path";
95
102
  import { constants } from "fs";
96
- async function pathExists(path) {
103
+ async function pathExists(path5) {
97
104
  try {
98
- await access(path, constants.F_OK);
105
+ await access(path5, constants.F_OK);
99
106
  return true;
100
107
  } catch {
101
108
  return false;
102
109
  }
103
110
  }
104
- async function ensureDir(path) {
105
- await mkdir(path, { recursive: true });
111
+ async function ensureDir(path5) {
112
+ await mkdir(path5, { recursive: true });
106
113
  }
107
- async function readTextFile(path) {
108
- return readFile(path, "utf-8");
114
+ async function readTextFile(path5) {
115
+ return readFile(path5, "utf-8");
109
116
  }
110
- async function writeTextFile(path, content) {
111
- await ensureDir(dirname(path));
112
- await writeFile(path, content, "utf-8");
117
+ async function writeTextFile(path5, content) {
118
+ await ensureDir(dirname(path5));
119
+ await writeFile(path5, content, "utf-8");
113
120
  }
114
121
  async function readFilesInDir(dirPath, filter) {
115
122
  try {
@@ -419,8 +426,8 @@ var CodeScanner = class {
419
426
  /**
420
427
  * Get a specific file
421
428
  */
422
- getFile(path) {
423
- return this.scannedFiles.get(path);
429
+ getFile(path5) {
430
+ return this.scannedFiles.get(path5);
424
431
  }
425
432
  /**
426
433
  * Get project instance for advanced analysis
@@ -433,12 +440,12 @@ var CodeScanner = class {
433
440
  */
434
441
  findClasses() {
435
442
  const classes = [];
436
- for (const { path, sourceFile } of this.scannedFiles.values()) {
443
+ for (const { path: path5, sourceFile } of this.scannedFiles.values()) {
437
444
  for (const classDecl of sourceFile.getClasses()) {
438
445
  const name = classDecl.getName();
439
446
  if (name) {
440
447
  classes.push({
441
- file: path,
448
+ file: path5,
442
449
  name,
443
450
  line: classDecl.getStartLineNumber()
444
451
  });
@@ -452,12 +459,12 @@ var CodeScanner = class {
452
459
  */
453
460
  findFunctions() {
454
461
  const functions = [];
455
- for (const { path, sourceFile } of this.scannedFiles.values()) {
462
+ for (const { path: path5, sourceFile } of this.scannedFiles.values()) {
456
463
  for (const funcDecl of sourceFile.getFunctions()) {
457
464
  const name = funcDecl.getName();
458
465
  if (name) {
459
466
  functions.push({
460
- file: path,
467
+ file: path5,
461
468
  name,
462
469
  line: funcDecl.getStartLineNumber(),
463
470
  isExported: funcDecl.isExported()
@@ -468,7 +475,7 @@ var CodeScanner = class {
468
475
  const init = varDecl.getInitializer();
469
476
  if (init && Node.isArrowFunction(init)) {
470
477
  functions.push({
471
- file: path,
478
+ file: path5,
472
479
  name: varDecl.getName(),
473
480
  line: varDecl.getStartLineNumber(),
474
481
  isExported: varDecl.isExported()
@@ -483,12 +490,12 @@ var CodeScanner = class {
483
490
  */
484
491
  findImports() {
485
492
  const imports = [];
486
- for (const { path, sourceFile } of this.scannedFiles.values()) {
493
+ for (const { path: path5, sourceFile } of this.scannedFiles.values()) {
487
494
  for (const importDecl of sourceFile.getImportDeclarations()) {
488
495
  const module = importDecl.getModuleSpecifierValue();
489
496
  const namedImports = importDecl.getNamedImports().map((n) => n.getName());
490
497
  imports.push({
491
- file: path,
498
+ file: path5,
492
499
  module,
493
500
  named: namedImports,
494
501
  line: importDecl.getStartLineNumber()
@@ -502,10 +509,10 @@ var CodeScanner = class {
502
509
  */
503
510
  findInterfaces() {
504
511
  const interfaces = [];
505
- for (const { path, sourceFile } of this.scannedFiles.values()) {
512
+ for (const { path: path5, sourceFile } of this.scannedFiles.values()) {
506
513
  for (const interfaceDecl of sourceFile.getInterfaces()) {
507
514
  interfaces.push({
508
- file: path,
515
+ file: path5,
509
516
  name: interfaceDecl.getName(),
510
517
  line: interfaceDecl.getStartLineNumber()
511
518
  });
@@ -518,10 +525,10 @@ var CodeScanner = class {
518
525
  */
519
526
  findTypeAliases() {
520
527
  const types = [];
521
- for (const { path, sourceFile } of this.scannedFiles.values()) {
528
+ for (const { path: path5, sourceFile } of this.scannedFiles.values()) {
522
529
  for (const typeAlias of sourceFile.getTypeAliases()) {
523
530
  types.push({
524
- file: path,
531
+ file: path5,
525
532
  name: typeAlias.getName(),
526
533
  line: typeAlias.getStartLineNumber()
527
534
  });
@@ -534,13 +541,13 @@ var CodeScanner = class {
534
541
  */
535
542
  findTryCatchBlocks() {
536
543
  const blocks = [];
537
- for (const { path, sourceFile } of this.scannedFiles.values()) {
544
+ for (const { path: path5, sourceFile } of this.scannedFiles.values()) {
538
545
  sourceFile.forEachDescendant((node) => {
539
546
  if (Node.isTryStatement(node)) {
540
547
  const catchClause = node.getCatchClause();
541
548
  const hasThrow = catchClause ? catchClause.getDescendantsOfKind(SyntaxKind.ThrowStatement).length > 0 : false;
542
549
  blocks.push({
543
- file: path,
550
+ file: path5,
544
551
  line: node.getStartLineNumber(),
545
552
  hasThrow
546
553
  });
@@ -1105,7 +1112,7 @@ var ErrorsAnalyzer = class {
1105
1112
  let throwNewError = 0;
1106
1113
  let throwCustom = 0;
1107
1114
  const examples = [];
1108
- for (const { path, sourceFile } of files) {
1115
+ for (const { path: path5, sourceFile } of files) {
1109
1116
  sourceFile.forEachDescendant((node) => {
1110
1117
  if (Node2.isThrowStatement(node)) {
1111
1118
  const expression = node.getExpression();
@@ -1118,7 +1125,7 @@ var ErrorsAnalyzer = class {
1118
1125
  if (examples.length < 3) {
1119
1126
  const snippet = text.length > 50 ? text.slice(0, 50) + "..." : text;
1120
1127
  examples.push({
1121
- file: path,
1128
+ file: path5,
1122
1129
  line: node.getStartLineNumber(),
1123
1130
  snippet: `throw ${snippet}`
1124
1131
  });
@@ -1415,8 +1422,8 @@ function validateDecision(data) {
1415
1422
  }
1416
1423
  function formatValidationErrors(errors) {
1417
1424
  return errors.errors.map((err) => {
1418
- const path = err.path.join(".");
1419
- return `${path}: ${err.message}`;
1425
+ const path5 = err.path.join(".");
1426
+ return `${path5}: ${err.message}`;
1420
1427
  });
1421
1428
  }
1422
1429
 
@@ -1795,6 +1802,7 @@ var NamingVerifier = class {
1795
1802
  };
1796
1803
 
1797
1804
  // src/verification/verifiers/imports.ts
1805
+ import path from "path";
1798
1806
  var ImportsVerifier = class {
1799
1807
  id = "imports";
1800
1808
  name = "Import Pattern Verifier";
@@ -1803,6 +1811,39 @@ var ImportsVerifier = class {
1803
1811
  const violations = [];
1804
1812
  const { sourceFile, constraint, decisionId, filePath } = ctx;
1805
1813
  const rule = constraint.rule.toLowerCase();
1814
+ if (rule.includes(".js") && rule.includes("extension") || rule.includes("esm") || rule.includes("add .js")) {
1815
+ for (const importDecl of sourceFile.getImportDeclarations()) {
1816
+ const moduleSpec = importDecl.getModuleSpecifierValue();
1817
+ if (!moduleSpec.startsWith(".")) continue;
1818
+ const ext = path.posix.extname(moduleSpec);
1819
+ let suggested = null;
1820
+ if (!ext) {
1821
+ suggested = `${moduleSpec}.js`;
1822
+ } else if (ext === ".ts" || ext === ".tsx" || ext === ".jsx") {
1823
+ suggested = moduleSpec.slice(0, -ext.length) + ".js";
1824
+ } else if (ext !== ".js") {
1825
+ continue;
1826
+ }
1827
+ if (!suggested || suggested === moduleSpec) continue;
1828
+ const ms = importDecl.getModuleSpecifier();
1829
+ const start = ms.getStart() + 1;
1830
+ const end = ms.getEnd() - 1;
1831
+ violations.push(createViolation({
1832
+ decisionId,
1833
+ constraintId: constraint.id,
1834
+ type: constraint.type,
1835
+ severity: constraint.severity,
1836
+ message: `Relative import "${moduleSpec}" should include a .js extension`,
1837
+ file: filePath,
1838
+ line: importDecl.getStartLineNumber(),
1839
+ suggestion: `Update to "${suggested}"`,
1840
+ autofix: {
1841
+ description: "Add/normalize .js extension in import specifier",
1842
+ edits: [{ start, end, text: suggested }]
1843
+ }
1844
+ }));
1845
+ }
1846
+ }
1806
1847
  if (rule.includes("barrel") || rule.includes("index")) {
1807
1848
  for (const importDecl of sourceFile.getImportDeclarations()) {
1808
1849
  const moduleSpec = importDecl.getModuleSpecifierValue();
@@ -2041,12 +2082,590 @@ var RegexVerifier = class {
2041
2082
  }
2042
2083
  };
2043
2084
 
2085
+ // src/verification/verifiers/dependencies.ts
2086
+ import path2 from "path";
2087
+ var graphCache = /* @__PURE__ */ new WeakMap();
2088
+ function normalizeFsPath(p) {
2089
+ return p.replaceAll("\\", "/");
2090
+ }
2091
+ function joinLike(fromFilePath, relative2) {
2092
+ const fromNorm = normalizeFsPath(fromFilePath);
2093
+ const relNorm = normalizeFsPath(relative2);
2094
+ if (path2.isAbsolute(fromNorm)) {
2095
+ return normalizeFsPath(path2.resolve(path2.dirname(fromNorm), relNorm));
2096
+ }
2097
+ const dir = path2.posix.dirname(fromNorm);
2098
+ return path2.posix.normalize(path2.posix.join(dir, relNorm));
2099
+ }
2100
+ function resolveToSourceFilePath(project, fromFilePath, moduleSpec) {
2101
+ if (!moduleSpec.startsWith(".")) return null;
2102
+ const candidates = [];
2103
+ const raw = joinLike(fromFilePath, moduleSpec);
2104
+ const addCandidate = (p) => candidates.push(normalizeFsPath(p));
2105
+ const ext = path2.posix.extname(raw);
2106
+ if (ext) {
2107
+ addCandidate(raw);
2108
+ if (ext === ".js") addCandidate(raw.slice(0, -3) + ".ts");
2109
+ if (ext === ".jsx") addCandidate(raw.slice(0, -4) + ".tsx");
2110
+ if (ext === ".ts") addCandidate(raw.slice(0, -3) + ".js");
2111
+ if (ext === ".tsx") addCandidate(raw.slice(0, -4) + ".jsx");
2112
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.ts"));
2113
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.tsx"));
2114
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.js"));
2115
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.jsx"));
2116
+ } else {
2117
+ addCandidate(raw + ".ts");
2118
+ addCandidate(raw + ".tsx");
2119
+ addCandidate(raw + ".js");
2120
+ addCandidate(raw + ".jsx");
2121
+ addCandidate(path2.posix.join(raw, "index.ts"));
2122
+ addCandidate(path2.posix.join(raw, "index.tsx"));
2123
+ addCandidate(path2.posix.join(raw, "index.js"));
2124
+ addCandidate(path2.posix.join(raw, "index.jsx"));
2125
+ }
2126
+ for (const candidate of candidates) {
2127
+ const sf = project.getSourceFile(candidate);
2128
+ if (sf) return sf.getFilePath();
2129
+ }
2130
+ return null;
2131
+ }
2132
+ function buildDependencyGraph(project) {
2133
+ const cached = graphCache.get(project);
2134
+ const sourceFiles = project.getSourceFiles();
2135
+ if (cached && cached.fileCount === sourceFiles.length) {
2136
+ return cached.graph;
2137
+ }
2138
+ const graph = /* @__PURE__ */ new Map();
2139
+ for (const sf of sourceFiles) {
2140
+ const from = normalizeFsPath(sf.getFilePath());
2141
+ if (!graph.has(from)) graph.set(from, /* @__PURE__ */ new Set());
2142
+ for (const importDecl of sf.getImportDeclarations()) {
2143
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2144
+ const resolved = resolveToSourceFilePath(project, from, moduleSpec);
2145
+ if (resolved) {
2146
+ graph.get(from).add(normalizeFsPath(resolved));
2147
+ }
2148
+ }
2149
+ }
2150
+ graphCache.set(project, { fileCount: sourceFiles.length, graph });
2151
+ return graph;
2152
+ }
2153
+ function tarjanScc(graph) {
2154
+ let index = 0;
2155
+ const stack = [];
2156
+ const onStack = /* @__PURE__ */ new Set();
2157
+ const indices = /* @__PURE__ */ new Map();
2158
+ const lowlink = /* @__PURE__ */ new Map();
2159
+ const result = [];
2160
+ const strongConnect = (v) => {
2161
+ indices.set(v, index);
2162
+ lowlink.set(v, index);
2163
+ index++;
2164
+ stack.push(v);
2165
+ onStack.add(v);
2166
+ const edges = graph.get(v) || /* @__PURE__ */ new Set();
2167
+ for (const w of edges) {
2168
+ if (!indices.has(w)) {
2169
+ strongConnect(w);
2170
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
2171
+ } else if (onStack.has(w)) {
2172
+ lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
2173
+ }
2174
+ }
2175
+ if (lowlink.get(v) === indices.get(v)) {
2176
+ const scc = [];
2177
+ while (stack.length > 0) {
2178
+ const w = stack.pop();
2179
+ if (!w) break;
2180
+ onStack.delete(w);
2181
+ scc.push(w);
2182
+ if (w === v) break;
2183
+ }
2184
+ result.push(scc);
2185
+ }
2186
+ };
2187
+ for (const v of graph.keys()) {
2188
+ if (!indices.has(v)) strongConnect(v);
2189
+ }
2190
+ return result;
2191
+ }
2192
+ function parseMaxImportDepth(rule) {
2193
+ const m = rule.match(/maximum\s+import\s+depth\s*[:=]?\s*(\d+)/i);
2194
+ return m ? Number.parseInt(m[1], 10) : null;
2195
+ }
2196
+ function parseBannedDependency(rule) {
2197
+ const m = rule.match(/no\s+dependencies?\s+on\s+(?:package\s+)?(.+?)(?:\.|$)/i);
2198
+ if (!m) return null;
2199
+ const value = m[1].trim();
2200
+ return value.length > 0 ? value : null;
2201
+ }
2202
+ function parseLayerRule(rule) {
2203
+ const m = rule.match(/(\w+)\s+layer\s+cannot\s+depend\s+on\s+(\w+)\s+layer/i);
2204
+ if (!m) return null;
2205
+ return { fromLayer: m[1].toLowerCase(), toLayer: m[2].toLowerCase() };
2206
+ }
2207
+ function fileInLayer(filePath, layer) {
2208
+ const fp = normalizeFsPath(filePath).toLowerCase();
2209
+ return fp.includes(`/${layer}/`) || fp.endsWith(`/${layer}.ts`) || fp.endsWith(`/${layer}.tsx`);
2210
+ }
2211
+ var DependencyVerifier = class {
2212
+ id = "dependencies";
2213
+ name = "Dependency Verifier";
2214
+ description = "Checks dependency constraints, import depth, and circular dependencies";
2215
+ async verify(ctx) {
2216
+ const violations = [];
2217
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2218
+ const rule = constraint.rule;
2219
+ const lowerRule = rule.toLowerCase();
2220
+ const project = sourceFile.getProject();
2221
+ const projectFilePath = normalizeFsPath(sourceFile.getFilePath());
2222
+ if (lowerRule.includes("circular") || lowerRule.includes("cycle")) {
2223
+ const graph = buildDependencyGraph(project);
2224
+ const sccs = tarjanScc(graph);
2225
+ const current = projectFilePath;
2226
+ for (const scc of sccs) {
2227
+ const hasSelfLoop = scc.length === 1 && (graph.get(scc[0])?.has(scc[0]) ?? false);
2228
+ const isCycle = scc.length > 1 || hasSelfLoop;
2229
+ if (!isCycle) continue;
2230
+ if (!scc.includes(current)) continue;
2231
+ const sorted = [...scc].sort();
2232
+ if (sorted[0] !== current) continue;
2233
+ violations.push(createViolation({
2234
+ decisionId,
2235
+ constraintId: constraint.id,
2236
+ type: constraint.type,
2237
+ severity: constraint.severity,
2238
+ message: `Circular dependency detected across: ${sorted.join(" -> ")}`,
2239
+ file: filePath,
2240
+ line: 1,
2241
+ suggestion: "Break the cycle by extracting shared abstractions or reversing the dependency"
2242
+ }));
2243
+ }
2244
+ }
2245
+ const layerRule = parseLayerRule(rule);
2246
+ if (layerRule && fileInLayer(projectFilePath, layerRule.fromLayer)) {
2247
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2248
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2249
+ const resolved = resolveToSourceFilePath(project, projectFilePath, moduleSpec);
2250
+ if (!resolved) continue;
2251
+ if (fileInLayer(resolved, layerRule.toLayer)) {
2252
+ violations.push(createViolation({
2253
+ decisionId,
2254
+ constraintId: constraint.id,
2255
+ type: constraint.type,
2256
+ severity: constraint.severity,
2257
+ message: `Layer violation: ${layerRule.fromLayer} depends on ${layerRule.toLayer} via import "${moduleSpec}"`,
2258
+ file: filePath,
2259
+ line: importDecl.getStartLineNumber(),
2260
+ suggestion: `Refactor to remove dependency from ${layerRule.fromLayer} to ${layerRule.toLayer}`
2261
+ }));
2262
+ }
2263
+ }
2264
+ }
2265
+ const banned = parseBannedDependency(rule);
2266
+ if (banned) {
2267
+ const bannedLower = banned.toLowerCase();
2268
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2269
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2270
+ if (moduleSpec.toLowerCase().includes(bannedLower)) {
2271
+ violations.push(createViolation({
2272
+ decisionId,
2273
+ constraintId: constraint.id,
2274
+ type: constraint.type,
2275
+ severity: constraint.severity,
2276
+ message: `Banned dependency import detected: "${moduleSpec}"`,
2277
+ file: filePath,
2278
+ line: importDecl.getStartLineNumber(),
2279
+ suggestion: `Remove or replace dependency "${banned}"`
2280
+ }));
2281
+ }
2282
+ }
2283
+ }
2284
+ const maxDepth = parseMaxImportDepth(rule);
2285
+ if (maxDepth !== null) {
2286
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2287
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2288
+ if (!moduleSpec.startsWith(".")) continue;
2289
+ const depth = (moduleSpec.match(/\.\.\//g) || []).length;
2290
+ if (depth > maxDepth) {
2291
+ violations.push(createViolation({
2292
+ decisionId,
2293
+ constraintId: constraint.id,
2294
+ type: constraint.type,
2295
+ severity: constraint.severity,
2296
+ message: `Import depth ${depth} exceeds maximum ${maxDepth}: "${moduleSpec}"`,
2297
+ file: filePath,
2298
+ line: importDecl.getStartLineNumber(),
2299
+ suggestion: "Use a shallower module boundary (or introduce a public entrypoint for this dependency)"
2300
+ }));
2301
+ }
2302
+ }
2303
+ }
2304
+ return violations;
2305
+ }
2306
+ };
2307
+
2308
+ // src/verification/verifiers/complexity.ts
2309
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
2310
+ function parseLimit(rule, pattern) {
2311
+ const m = rule.match(pattern);
2312
+ return m ? Number.parseInt(m[1], 10) : null;
2313
+ }
2314
+ function getFileLineCount(text) {
2315
+ if (text.length === 0) return 0;
2316
+ return text.split("\n").length;
2317
+ }
2318
+ function getDecisionPoints(fn) {
2319
+ let points = 0;
2320
+ for (const d of fn.getDescendants()) {
2321
+ switch (d.getKind()) {
2322
+ case SyntaxKind2.IfStatement:
2323
+ case SyntaxKind2.ForStatement:
2324
+ case SyntaxKind2.ForInStatement:
2325
+ case SyntaxKind2.ForOfStatement:
2326
+ case SyntaxKind2.WhileStatement:
2327
+ case SyntaxKind2.DoStatement:
2328
+ case SyntaxKind2.CatchClause:
2329
+ case SyntaxKind2.ConditionalExpression:
2330
+ case SyntaxKind2.CaseClause:
2331
+ points++;
2332
+ break;
2333
+ case SyntaxKind2.BinaryExpression: {
2334
+ const text = d.getText();
2335
+ if (text.includes("&&") || text.includes("||")) points++;
2336
+ break;
2337
+ }
2338
+ default:
2339
+ break;
2340
+ }
2341
+ }
2342
+ return points;
2343
+ }
2344
+ function calculateCyclomaticComplexity(fn) {
2345
+ return 1 + getDecisionPoints(fn);
2346
+ }
2347
+ function getFunctionDisplayName(fn) {
2348
+ if ("getName" in fn && typeof fn.getName === "function") {
2349
+ const name = fn.getName();
2350
+ if (typeof name === "string" && name.length > 0) return name;
2351
+ }
2352
+ const parent = fn.getParent();
2353
+ if (parent?.getKind() === SyntaxKind2.VariableDeclaration) {
2354
+ const vd = parent;
2355
+ if (typeof vd.getName === "function") return vd.getName();
2356
+ }
2357
+ return "<anonymous>";
2358
+ }
2359
+ function maxNestingDepth(node) {
2360
+ let maxDepth = 0;
2361
+ const walk = (n, depth) => {
2362
+ maxDepth = Math.max(maxDepth, depth);
2363
+ const kind = n.getKind();
2364
+ const isNestingNode = kind === SyntaxKind2.IfStatement || kind === SyntaxKind2.ForStatement || kind === SyntaxKind2.ForInStatement || kind === SyntaxKind2.ForOfStatement || kind === SyntaxKind2.WhileStatement || kind === SyntaxKind2.DoStatement || kind === SyntaxKind2.SwitchStatement || kind === SyntaxKind2.TryStatement;
2365
+ for (const child of n.getChildren()) {
2366
+ if (child.getKind() === SyntaxKind2.FunctionDeclaration || child.getKind() === SyntaxKind2.FunctionExpression || child.getKind() === SyntaxKind2.ArrowFunction || child.getKind() === SyntaxKind2.MethodDeclaration) {
2367
+ continue;
2368
+ }
2369
+ walk(child, isNestingNode ? depth + 1 : depth);
2370
+ }
2371
+ };
2372
+ walk(node, 0);
2373
+ return maxDepth;
2374
+ }
2375
+ var ComplexityVerifier = class {
2376
+ id = "complexity";
2377
+ name = "Complexity Verifier";
2378
+ description = "Checks cyclomatic complexity, file size, parameters, and nesting depth";
2379
+ async verify(ctx) {
2380
+ const violations = [];
2381
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2382
+ const rule = constraint.rule;
2383
+ const maxComplexity = parseLimit(rule, /complexity\s+(?:must\s+)?not\s+exceed\s+(\d+)/i);
2384
+ const maxLines = parseLimit(rule, /file\s+size\s+(?:must\s+)?not\s+exceed\s+(\d+)\s+lines?/i);
2385
+ const maxParams = parseLimit(rule, /at\s+most\s+(\d+)\s+parameters?/i) ?? parseLimit(rule, /parameters?\s+(?:must\s+)?not\s+exceed\s+(\d+)/i);
2386
+ const maxNesting = parseLimit(rule, /nesting\s+depth\s+(?:must\s+)?not\s+exceed\s+(\d+)/i);
2387
+ if (maxLines !== null) {
2388
+ const lineCount = getFileLineCount(sourceFile.getFullText());
2389
+ if (lineCount > maxLines) {
2390
+ violations.push(createViolation({
2391
+ decisionId,
2392
+ constraintId: constraint.id,
2393
+ type: constraint.type,
2394
+ severity: constraint.severity,
2395
+ message: `File has ${lineCount} lines which exceeds maximum ${maxLines}`,
2396
+ file: filePath,
2397
+ line: 1,
2398
+ suggestion: "Split the file into smaller modules"
2399
+ }));
2400
+ }
2401
+ }
2402
+ const functionLikes = [
2403
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionDeclaration),
2404
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionExpression),
2405
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.ArrowFunction),
2406
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.MethodDeclaration)
2407
+ ];
2408
+ for (const fn of functionLikes) {
2409
+ const fnName = getFunctionDisplayName(fn);
2410
+ if (maxComplexity !== null) {
2411
+ const complexity = calculateCyclomaticComplexity(fn);
2412
+ if (complexity > maxComplexity) {
2413
+ violations.push(createViolation({
2414
+ decisionId,
2415
+ constraintId: constraint.id,
2416
+ type: constraint.type,
2417
+ severity: constraint.severity,
2418
+ message: `Function ${fnName} has cyclomatic complexity ${complexity} which exceeds maximum ${maxComplexity}`,
2419
+ file: filePath,
2420
+ line: fn.getStartLineNumber(),
2421
+ suggestion: "Refactor to reduce branching or extract smaller functions"
2422
+ }));
2423
+ }
2424
+ }
2425
+ if (maxParams !== null && "getParameters" in fn) {
2426
+ const params = fn.getParameters();
2427
+ const paramCount = Array.isArray(params) ? params.length : 0;
2428
+ if (paramCount > maxParams) {
2429
+ violations.push(createViolation({
2430
+ decisionId,
2431
+ constraintId: constraint.id,
2432
+ type: constraint.type,
2433
+ severity: constraint.severity,
2434
+ message: `Function ${fnName} has ${paramCount} parameters which exceeds maximum ${maxParams}`,
2435
+ file: filePath,
2436
+ line: fn.getStartLineNumber(),
2437
+ suggestion: "Consider grouping parameters into an options object"
2438
+ }));
2439
+ }
2440
+ }
2441
+ if (maxNesting !== null) {
2442
+ const depth = maxNestingDepth(fn);
2443
+ if (depth > maxNesting) {
2444
+ violations.push(createViolation({
2445
+ decisionId,
2446
+ constraintId: constraint.id,
2447
+ type: constraint.type,
2448
+ severity: constraint.severity,
2449
+ message: `Function ${fnName} has nesting depth ${depth} which exceeds maximum ${maxNesting}`,
2450
+ file: filePath,
2451
+ line: fn.getStartLineNumber(),
2452
+ suggestion: "Reduce nesting by using early returns or extracting functions"
2453
+ }));
2454
+ }
2455
+ }
2456
+ }
2457
+ return violations;
2458
+ }
2459
+ };
2460
+
2461
+ // src/verification/verifiers/security.ts
2462
+ import { SyntaxKind as SyntaxKind3 } from "ts-morph";
2463
+ var SECRET_NAME_RE = /(api[_-]?key|password|secret|token)/i;
2464
+ function isStringLiteralLike(node) {
2465
+ const k = node.getKind();
2466
+ return k === SyntaxKind3.StringLiteral || k === SyntaxKind3.NoSubstitutionTemplateLiteral;
2467
+ }
2468
+ var SecurityVerifier = class {
2469
+ id = "security";
2470
+ name = "Security Verifier";
2471
+ description = "Detects common security footguns (secrets, eval, XSS/SQL injection heuristics)";
2472
+ async verify(ctx) {
2473
+ const violations = [];
2474
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2475
+ const rule = constraint.rule.toLowerCase();
2476
+ const checkSecrets = rule.includes("secret") || rule.includes("password") || rule.includes("token") || rule.includes("api key") || rule.includes("hardcoded");
2477
+ const checkEval = rule.includes("eval") || rule.includes("function constructor");
2478
+ const checkXss = rule.includes("xss") || rule.includes("innerhtml") || rule.includes("dangerouslysetinnerhtml");
2479
+ const checkSql = rule.includes("sql") || rule.includes("injection");
2480
+ const checkProto = rule.includes("prototype pollution") || rule.includes("__proto__");
2481
+ if (checkSecrets) {
2482
+ for (const vd of sourceFile.getVariableDeclarations()) {
2483
+ const name = vd.getName();
2484
+ if (!SECRET_NAME_RE.test(name)) continue;
2485
+ const init = vd.getInitializer();
2486
+ if (!init || !isStringLiteralLike(init)) continue;
2487
+ const value = init.getText().slice(1, -1);
2488
+ if (value.length === 0) continue;
2489
+ violations.push(createViolation({
2490
+ decisionId,
2491
+ constraintId: constraint.id,
2492
+ type: constraint.type,
2493
+ severity: constraint.severity,
2494
+ message: `Possible hardcoded secret in variable "${name}"`,
2495
+ file: filePath,
2496
+ line: vd.getStartLineNumber(),
2497
+ suggestion: "Move secrets to environment variables or a secret manager"
2498
+ }));
2499
+ }
2500
+ for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
2501
+ const nameNode = pa.getNameNode?.();
2502
+ const propName = nameNode?.getText?.() ?? "";
2503
+ if (!SECRET_NAME_RE.test(propName)) continue;
2504
+ const init = pa.getInitializer();
2505
+ if (!init || !isStringLiteralLike(init)) continue;
2506
+ violations.push(createViolation({
2507
+ decisionId,
2508
+ constraintId: constraint.id,
2509
+ type: constraint.type,
2510
+ severity: constraint.severity,
2511
+ message: `Possible hardcoded secret in object property ${propName}`,
2512
+ file: filePath,
2513
+ line: pa.getStartLineNumber(),
2514
+ suggestion: "Move secrets to environment variables or a secret manager"
2515
+ }));
2516
+ }
2517
+ }
2518
+ if (checkEval) {
2519
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2520
+ const exprText = call.getExpression().getText();
2521
+ if (exprText === "eval" || exprText === "Function") {
2522
+ violations.push(createViolation({
2523
+ decisionId,
2524
+ constraintId: constraint.id,
2525
+ type: constraint.type,
2526
+ severity: constraint.severity,
2527
+ message: `Unsafe dynamic code execution via ${exprText}()`,
2528
+ file: filePath,
2529
+ line: call.getStartLineNumber(),
2530
+ suggestion: "Avoid eval/Function; use safer alternatives"
2531
+ }));
2532
+ }
2533
+ }
2534
+ }
2535
+ if (checkXss) {
2536
+ for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
2537
+ const left = bin.getLeft();
2538
+ if (left.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2539
+ const pa = left;
2540
+ if (pa.getName?.() === "innerHTML") {
2541
+ violations.push(createViolation({
2542
+ decisionId,
2543
+ constraintId: constraint.id,
2544
+ type: constraint.type,
2545
+ severity: constraint.severity,
2546
+ message: "Potential XSS: assignment to innerHTML",
2547
+ file: filePath,
2548
+ line: bin.getStartLineNumber(),
2549
+ suggestion: "Prefer textContent or a safe templating/escaping strategy"
2550
+ }));
2551
+ }
2552
+ }
2553
+ if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
2554
+ violations.push(createViolation({
2555
+ decisionId,
2556
+ constraintId: constraint.id,
2557
+ type: constraint.type,
2558
+ severity: constraint.severity,
2559
+ message: "Potential XSS: usage of dangerouslySetInnerHTML",
2560
+ file: filePath,
2561
+ line: 1,
2562
+ suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
2563
+ }));
2564
+ }
2565
+ }
2566
+ if (checkSql) {
2567
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2568
+ const expr = call.getExpression();
2569
+ if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2570
+ const name = expr.getName?.();
2571
+ if (name !== "query" && name !== "execute") continue;
2572
+ const arg = call.getArguments()[0];
2573
+ if (!arg) continue;
2574
+ const isTemplate = arg.getKind() === SyntaxKind3.TemplateExpression;
2575
+ const isConcat = arg.getKind() === SyntaxKind3.BinaryExpression && arg.getText().includes("+");
2576
+ if (!isTemplate && !isConcat) continue;
2577
+ const text = arg.getText().toLowerCase();
2578
+ if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
2579
+ continue;
2580
+ }
2581
+ violations.push(createViolation({
2582
+ decisionId,
2583
+ constraintId: constraint.id,
2584
+ type: constraint.type,
2585
+ severity: constraint.severity,
2586
+ message: "Potential SQL injection: dynamically constructed SQL query",
2587
+ file: filePath,
2588
+ line: call.getStartLineNumber(),
2589
+ suggestion: "Use parameterized queries / prepared statements"
2590
+ }));
2591
+ }
2592
+ }
2593
+ if (checkProto) {
2594
+ const text = sourceFile.getFullText();
2595
+ if (text.includes("__proto__") || text.includes("constructor.prototype")) {
2596
+ violations.push(createViolation({
2597
+ decisionId,
2598
+ constraintId: constraint.id,
2599
+ type: constraint.type,
2600
+ severity: constraint.severity,
2601
+ message: "Potential prototype pollution pattern detected",
2602
+ file: filePath,
2603
+ line: 1,
2604
+ suggestion: "Avoid writing to __proto__/prototype; validate object keys"
2605
+ }));
2606
+ }
2607
+ }
2608
+ return violations;
2609
+ }
2610
+ };
2611
+
2612
+ // src/verification/verifiers/api.ts
2613
+ import { SyntaxKind as SyntaxKind4 } from "ts-morph";
2614
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
2615
+ function isKebabPath(pathValue) {
2616
+ const parts = pathValue.split("/").filter(Boolean);
2617
+ for (const part of parts) {
2618
+ if (part.startsWith(":")) continue;
2619
+ if (!/^[a-z0-9-]+$/.test(part)) return false;
2620
+ }
2621
+ return true;
2622
+ }
2623
+ var ApiVerifier = class {
2624
+ id = "api";
2625
+ name = "API Consistency Verifier";
2626
+ description = "Checks basic REST endpoint naming conventions in common frameworks";
2627
+ async verify(ctx) {
2628
+ const violations = [];
2629
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2630
+ const rule = constraint.rule.toLowerCase();
2631
+ const enforceKebab = rule.includes("kebab") || rule.includes("kebab-case");
2632
+ if (!enforceKebab) return violations;
2633
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
2634
+ const expr = call.getExpression();
2635
+ if (expr.getKind() !== SyntaxKind4.PropertyAccessExpression) continue;
2636
+ const method = expr.getName?.();
2637
+ if (!method || !HTTP_METHODS.has(String(method))) continue;
2638
+ const firstArg = call.getArguments()[0];
2639
+ if (!firstArg || firstArg.getKind() !== SyntaxKind4.StringLiteral) continue;
2640
+ const pathValue = firstArg.getLiteralValue?.() ?? firstArg.getText().slice(1, -1);
2641
+ if (typeof pathValue !== "string") continue;
2642
+ if (!isKebabPath(pathValue)) {
2643
+ violations.push(createViolation({
2644
+ decisionId,
2645
+ constraintId: constraint.id,
2646
+ type: constraint.type,
2647
+ severity: constraint.severity,
2648
+ message: `Endpoint path "${pathValue}" is not kebab-case`,
2649
+ file: filePath,
2650
+ line: call.getStartLineNumber(),
2651
+ suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
2652
+ }));
2653
+ }
2654
+ }
2655
+ return violations;
2656
+ }
2657
+ };
2658
+
2044
2659
  // src/verification/verifiers/index.ts
2045
2660
  var builtinVerifiers = {
2046
2661
  naming: () => new NamingVerifier(),
2047
2662
  imports: () => new ImportsVerifier(),
2048
2663
  errors: () => new ErrorsVerifier(),
2049
- regex: () => new RegexVerifier()
2664
+ regex: () => new RegexVerifier(),
2665
+ dependencies: () => new DependencyVerifier(),
2666
+ complexity: () => new ComplexityVerifier(),
2667
+ security: () => new SecurityVerifier(),
2668
+ api: () => new ApiVerifier()
2050
2669
  };
2051
2670
  function getVerifier(id) {
2052
2671
  const factory = builtinVerifiers[id];
@@ -2057,6 +2676,18 @@ function selectVerifierForConstraint(rule, specifiedVerifier) {
2057
2676
  return getVerifier(specifiedVerifier);
2058
2677
  }
2059
2678
  const lowerRule = rule.toLowerCase();
2679
+ if (lowerRule.includes("dependency") || lowerRule.includes("circular dependenc") || lowerRule.includes("import depth") || lowerRule.includes("layer") && lowerRule.includes("depend on")) {
2680
+ return getVerifier("dependencies");
2681
+ }
2682
+ if (lowerRule.includes("cyclomatic") || lowerRule.includes("complexity") || lowerRule.includes("nesting") || lowerRule.includes("parameters") || lowerRule.includes("file size")) {
2683
+ return getVerifier("complexity");
2684
+ }
2685
+ if (lowerRule.includes("security") || lowerRule.includes("secret") || lowerRule.includes("password") || lowerRule.includes("token") || lowerRule.includes("xss") || lowerRule.includes("sql") || lowerRule.includes("eval")) {
2686
+ return getVerifier("security");
2687
+ }
2688
+ if (lowerRule.includes("endpoint") || lowerRule.includes("rest") || lowerRule.includes("api") && lowerRule.includes("path")) {
2689
+ return getVerifier("api");
2690
+ }
2060
2691
  if (lowerRule.includes("naming") || lowerRule.includes("case")) {
2061
2692
  return getVerifier("naming");
2062
2693
  }
@@ -2072,10 +2703,60 @@ function selectVerifierForConstraint(rule, specifiedVerifier) {
2072
2703
  return getVerifier("regex");
2073
2704
  }
2074
2705
 
2706
+ // src/verification/cache.ts
2707
+ import { stat as stat2 } from "fs/promises";
2708
+ var AstCache = class {
2709
+ cache = /* @__PURE__ */ new Map();
2710
+ async get(filePath, project) {
2711
+ try {
2712
+ const info = await stat2(filePath);
2713
+ const cached = this.cache.get(filePath);
2714
+ if (cached && cached.mtimeMs >= info.mtimeMs) {
2715
+ return cached.sourceFile;
2716
+ }
2717
+ let sourceFile = project.getSourceFile(filePath);
2718
+ if (!sourceFile) {
2719
+ sourceFile = project.addSourceFileAtPath(filePath);
2720
+ } else {
2721
+ sourceFile.refreshFromFileSystemSync();
2722
+ }
2723
+ this.cache.set(filePath, { sourceFile, mtimeMs: info.mtimeMs });
2724
+ return sourceFile;
2725
+ } catch {
2726
+ return null;
2727
+ }
2728
+ }
2729
+ clear() {
2730
+ this.cache.clear();
2731
+ }
2732
+ };
2733
+
2734
+ // src/verification/applicability.ts
2735
+ function isConstraintExcepted(filePath, constraint, cwd) {
2736
+ if (!constraint.exceptions) return false;
2737
+ return constraint.exceptions.some((exception) => {
2738
+ if (exception.expiresAt) {
2739
+ const expiryDate = new Date(exception.expiresAt);
2740
+ if (expiryDate < /* @__PURE__ */ new Date()) {
2741
+ return false;
2742
+ }
2743
+ }
2744
+ return matchesPattern(filePath, exception.pattern, { cwd });
2745
+ });
2746
+ }
2747
+ function shouldApplyConstraintToFile(params) {
2748
+ const { filePath, constraint, cwd, severityFilter } = params;
2749
+ if (!matchesPattern(filePath, constraint.scope, { cwd })) return false;
2750
+ if (severityFilter && !severityFilter.includes(constraint.severity)) return false;
2751
+ if (isConstraintExcepted(filePath, constraint, cwd)) return false;
2752
+ return true;
2753
+ }
2754
+
2075
2755
  // src/verification/engine.ts
2076
2756
  var VerificationEngine = class {
2077
2757
  registry;
2078
2758
  project;
2759
+ astCache;
2079
2760
  constructor(registry) {
2080
2761
  this.registry = registry || createRegistry();
2081
2762
  this.project = new Project2({
@@ -2087,6 +2768,7 @@ var VerificationEngine = class {
2087
2768
  },
2088
2769
  skipAddingFilesFromTsConfig: true
2089
2770
  });
2771
+ this.astCache = new AstCache();
2090
2772
  }
2091
2773
  /**
2092
2774
  * Run verification
@@ -2129,8 +2811,8 @@ var VerificationEngine = class {
2129
2811
  let failed = 0;
2130
2812
  const skipped = 0;
2131
2813
  let timeoutHandle = null;
2132
- const timeoutPromise = new Promise((resolve) => {
2133
- timeoutHandle = setTimeout(() => resolve("timeout"), timeout);
2814
+ const timeoutPromise = new Promise((resolve2) => {
2815
+ timeoutHandle = setTimeout(() => resolve2("timeout"), timeout);
2134
2816
  timeoutHandle.unref();
2135
2817
  });
2136
2818
  const verificationPromise = this.verifyFiles(
@@ -2192,23 +2874,11 @@ var VerificationEngine = class {
2192
2874
  */
2193
2875
  async verifyFile(filePath, decisions, severityFilter, cwd = process.cwd()) {
2194
2876
  const violations = [];
2195
- let sourceFile = this.project.getSourceFile(filePath);
2196
- if (!sourceFile) {
2197
- try {
2198
- sourceFile = this.project.addSourceFileAtPath(filePath);
2199
- } catch {
2200
- return violations;
2201
- }
2202
- }
2877
+ const sourceFile = await this.astCache.get(filePath, this.project);
2878
+ if (!sourceFile) return violations;
2203
2879
  for (const decision of decisions) {
2204
2880
  for (const constraint of decision.constraints) {
2205
- if (!matchesPattern(filePath, constraint.scope, { cwd })) {
2206
- continue;
2207
- }
2208
- if (severityFilter && !severityFilter.includes(constraint.severity)) {
2209
- continue;
2210
- }
2211
- if (this.isExcepted(filePath, constraint, cwd)) {
2881
+ if (!shouldApplyConstraintToFile({ filePath, constraint, cwd, severityFilter })) {
2212
2882
  continue;
2213
2883
  }
2214
2884
  const verifier = selectVerifierForConstraint(constraint.rule, constraint.verifier);
@@ -2234,25 +2904,16 @@ var VerificationEngine = class {
2234
2904
  * Verify multiple files
2235
2905
  */
2236
2906
  async verifyFiles(files, decisions, severityFilter, cwd, onFileVerified) {
2237
- for (const file of files) {
2238
- const violations = await this.verifyFile(file, decisions, severityFilter, cwd);
2239
- onFileVerified(violations);
2240
- }
2241
- }
2242
- /**
2243
- * Check if file is excepted from constraint
2244
- */
2245
- isExcepted(filePath, constraint, cwd) {
2246
- if (!constraint.exceptions) return false;
2247
- return constraint.exceptions.some((exception) => {
2248
- if (exception.expiresAt) {
2249
- const expiryDate = new Date(exception.expiresAt);
2250
- if (expiryDate < /* @__PURE__ */ new Date()) {
2251
- return false;
2252
- }
2907
+ const BATCH_SIZE = 10;
2908
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
2909
+ const batch = files.slice(i, i + BATCH_SIZE);
2910
+ const results = await Promise.all(
2911
+ batch.map((file) => this.verifyFile(file, decisions, severityFilter, cwd))
2912
+ );
2913
+ for (const violations of results) {
2914
+ onFileVerified(violations);
2253
2915
  }
2254
- return matchesPattern(filePath, exception.pattern, { cwd });
2255
- });
2916
+ }
2256
2917
  }
2257
2918
  /**
2258
2919
  * Get registry
@@ -2265,8 +2926,111 @@ function createVerificationEngine(registry) {
2265
2926
  return new VerificationEngine(registry);
2266
2927
  }
2267
2928
 
2929
+ // src/verification/autofix/engine.ts
2930
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2931
+ import readline from "readline/promises";
2932
+ import { stdin, stdout } from "process";
2933
+ function applyEdits(content, edits) {
2934
+ const sorted = [...edits].sort((a, b) => b.start - a.start);
2935
+ let next = content;
2936
+ const patches = [];
2937
+ let skippedEdits = 0;
2938
+ let lastStart = Number.POSITIVE_INFINITY;
2939
+ for (const edit of sorted) {
2940
+ if (edit.start < 0 || edit.end < edit.start || edit.end > next.length) {
2941
+ skippedEdits++;
2942
+ continue;
2943
+ }
2944
+ if (edit.end > lastStart) {
2945
+ skippedEdits++;
2946
+ continue;
2947
+ }
2948
+ lastStart = edit.start;
2949
+ const originalText = next.slice(edit.start, edit.end);
2950
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
2951
+ patches.push({
2952
+ filePath: "",
2953
+ description: edit.description,
2954
+ start: edit.start,
2955
+ end: edit.end,
2956
+ originalText,
2957
+ fixedText: edit.text
2958
+ });
2959
+ }
2960
+ return { next, patches, skippedEdits };
2961
+ }
2962
+ async function confirmFix(prompt) {
2963
+ const rl = readline.createInterface({ input: stdin, output: stdout });
2964
+ try {
2965
+ const answer = await rl.question(`${prompt} (y/N) `);
2966
+ return answer.trim().toLowerCase() === "y";
2967
+ } finally {
2968
+ rl.close();
2969
+ }
2970
+ }
2971
+ var AutofixEngine = class {
2972
+ async applyFixes(violations, options = {}) {
2973
+ const fixable = violations.filter((v) => v.autofix && v.autofix.edits.length > 0);
2974
+ const byFile = /* @__PURE__ */ new Map();
2975
+ for (const v of fixable) {
2976
+ const list = byFile.get(v.file) ?? [];
2977
+ list.push(v);
2978
+ byFile.set(v.file, list);
2979
+ }
2980
+ const applied = [];
2981
+ let skippedViolations = 0;
2982
+ for (const [filePath, fileViolations] of byFile) {
2983
+ const original = await readFile2(filePath, "utf-8");
2984
+ const edits = [];
2985
+ for (const violation of fileViolations) {
2986
+ const fix = violation.autofix;
2987
+ if (options.interactive) {
2988
+ const ok = await confirmFix(`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`);
2989
+ if (!ok) {
2990
+ skippedViolations++;
2991
+ continue;
2992
+ }
2993
+ }
2994
+ for (const edit of fix.edits) {
2995
+ edits.push({ ...edit, description: fix.description });
2996
+ }
2997
+ }
2998
+ if (edits.length === 0) continue;
2999
+ const { next, patches, skippedEdits } = applyEdits(original, edits);
3000
+ skippedViolations += skippedEdits;
3001
+ if (!options.dryRun) {
3002
+ await writeFile2(filePath, next, "utf-8");
3003
+ }
3004
+ for (const patch of patches) {
3005
+ applied.push({ ...patch, filePath });
3006
+ }
3007
+ }
3008
+ return { applied, skipped: skippedViolations };
3009
+ }
3010
+ };
3011
+
3012
+ // src/verification/incremental.ts
3013
+ import { execFile } from "child_process";
3014
+ import { promisify } from "util";
3015
+ import { resolve } from "path";
3016
+ var execFileAsync = promisify(execFile);
3017
+ async function getChangedFiles(cwd) {
3018
+ try {
3019
+ const { stdout: stdout2 } = await execFileAsync("git", ["diff", "--name-only", "--diff-filter=AM", "HEAD"], { cwd });
3020
+ const rel = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean);
3021
+ const abs = [];
3022
+ for (const file of rel) {
3023
+ const full = resolve(cwd, file);
3024
+ if (await pathExists(full)) abs.push(full);
3025
+ }
3026
+ return abs;
3027
+ } catch {
3028
+ return [];
3029
+ }
3030
+ }
3031
+
2268
3032
  // src/cli/commands/verify.ts
2269
- var verifyCommand = new Command3("verify").description("Verify code compliance against decisions").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("-f, --files <patterns>", "Comma-separated file patterns to check").option("-d, --decisions <ids>", "Comma-separated decision IDs to check").option("-s, --severity <levels>", "Comma-separated severity levels (critical, high, medium, low)").option("--json", "Output as JSON").option("--fix", "Attempt to auto-fix violations (not yet implemented)").action(async (options) => {
3033
+ var verifyCommand = new Command3("verify").description("Verify code compliance against decisions").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("-f, --files <patterns>", "Comma-separated file patterns to check").option("-d, --decisions <ids>", "Comma-separated decision IDs to check").option("-s, --severity <levels>", "Comma-separated severity levels (critical, high, medium, low)").option("--json", "Output as JSON").option("--incremental", "Only verify changed files (git diff --name-only --diff-filter=AM HEAD)").option("--fix", "Apply auto-fixes for supported violations").option("--dry-run", "Show what would be fixed without applying (requires --fix)").option("--interactive", "Confirm each fix interactively (requires --fix)").action(async (options) => {
2270
3034
  const cwd = process.cwd();
2271
3035
  if (!await pathExists(getSpecBridgeDir(cwd))) {
2272
3036
  throw new NotInitializedError();
@@ -2275,23 +3039,60 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
2275
3039
  try {
2276
3040
  const config = await loadConfig(cwd);
2277
3041
  const level = options.level || "full";
2278
- const files = options.files?.split(",").map((f) => f.trim());
3042
+ let files = options.files?.split(",").map((f) => f.trim());
2279
3043
  const decisions = options.decisions?.split(",").map((d) => d.trim());
2280
3044
  const severity = options.severity?.split(",").map((s) => s.trim());
3045
+ if (options.incremental) {
3046
+ const changed = await getChangedFiles(cwd);
3047
+ files = changed.length > 0 ? changed : [];
3048
+ }
2281
3049
  spinner.text = `Running ${level}-level verification...`;
2282
3050
  const engine = createVerificationEngine();
2283
- const result = await engine.verify(config, {
3051
+ let result = await engine.verify(config, {
2284
3052
  level,
2285
3053
  files,
2286
3054
  decisions,
2287
3055
  severity,
2288
3056
  cwd
2289
3057
  });
3058
+ let fixResult;
3059
+ if (options.fix && result.violations.length > 0) {
3060
+ const fixableCount = result.violations.filter((v) => v.autofix).length;
3061
+ if (fixableCount === 0) {
3062
+ spinner.stop();
3063
+ if (!options.json) {
3064
+ console.log(chalk3.yellow("No auto-fixable violations found"));
3065
+ }
3066
+ } else {
3067
+ spinner.text = `Applying ${fixableCount} auto-fix(es)...`;
3068
+ const fixer = new AutofixEngine();
3069
+ fixResult = await fixer.applyFixes(result.violations, {
3070
+ dryRun: options.dryRun,
3071
+ interactive: options.interactive
3072
+ });
3073
+ if (!options.dryRun && fixResult.applied.length > 0) {
3074
+ result = await engine.verify(config, {
3075
+ level,
3076
+ files,
3077
+ decisions,
3078
+ severity,
3079
+ cwd
3080
+ });
3081
+ }
3082
+ }
3083
+ }
2290
3084
  spinner.stop();
2291
3085
  if (options.json) {
2292
- console.log(JSON.stringify(result, null, 2));
3086
+ console.log(JSON.stringify({ ...result, autofix: fixResult }, null, 2));
2293
3087
  } else {
2294
3088
  printResult(result, level);
3089
+ if (options.fix && fixResult) {
3090
+ console.log(chalk3.green(`\u2713 Applied ${fixResult.applied.length} fix(es)`));
3091
+ if (fixResult.skipped > 0) {
3092
+ console.log(chalk3.yellow(`\u2298 Skipped ${fixResult.skipped} fix(es)`));
3093
+ }
3094
+ console.log("");
3095
+ }
2295
3096
  }
2296
3097
  if (!result.success) {
2297
3098
  process.exit(1);
@@ -2748,17 +3549,10 @@ var HOOK_SCRIPT = `#!/bin/sh
2748
3549
  # SpecBridge pre-commit hook
2749
3550
  # Runs verification on staged files
2750
3551
 
2751
- # Get list of staged TypeScript files
2752
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(ts|tsx)$')
2753
-
2754
- if [ -z "$STAGED_FILES" ]; then
2755
- exit 0
2756
- fi
2757
-
2758
3552
  echo "Running SpecBridge verification..."
2759
3553
 
2760
- # Run specbridge verify on staged files
2761
- npx specbridge hook run --level commit --files "$STAGED_FILES"
3554
+ # Run specbridge hook (it will detect staged files automatically)
3555
+ npx specbridge hook run --level commit
2762
3556
 
2763
3557
  exit $?
2764
3558
  `;
@@ -2840,7 +3634,18 @@ function createHookCommand() {
2840
3634
  try {
2841
3635
  const config = await loadConfig(cwd);
2842
3636
  const level = options.level || "commit";
2843
- const files = options.files ? options.files.split(/[\s,]+/).filter((f) => f.length > 0) : void 0;
3637
+ let files = options.files ? options.files.split(/[\s,]+/).filter((f) => f.length > 0) : void 0;
3638
+ if (!files || files.length === 0) {
3639
+ const { execFile: execFile2 } = await import("child_process");
3640
+ const { promisify: promisify2 } = await import("util");
3641
+ const execFileAsync2 = promisify2(execFile2);
3642
+ try {
3643
+ const { stdout: stdout2 } = await execFileAsync2("git", ["diff", "--cached", "--name-only", "--diff-filter=AM"], { cwd });
3644
+ files = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean).filter((f) => /\.(ts|tsx|js|jsx)$/.test(f));
3645
+ } catch {
3646
+ files = [];
3647
+ }
3648
+ }
2844
3649
  if (!files || files.length === 0) {
2845
3650
  process.exit(0);
2846
3651
  }
@@ -3322,11 +4127,547 @@ var contextCommand = new Command11("context").description("Generate architectura
3322
4127
  }
3323
4128
  });
3324
4129
 
4130
+ // src/cli/commands/lsp.ts
4131
+ import { Command as Command12 } from "commander";
4132
+
4133
+ // src/lsp/server.ts
4134
+ import { createConnection, ProposedFeatures, TextDocuments, TextDocumentSyncKind, DiagnosticSeverity, CodeActionKind } from "vscode-languageserver/node.js";
4135
+ import { TextDocument } from "vscode-languageserver-textdocument";
4136
+ import { fileURLToPath } from "url";
4137
+ import path3 from "path";
4138
+ import { Project as Project3 } from "ts-morph";
4139
+ import chalk12 from "chalk";
4140
+ function severityToDiagnostic(severity) {
4141
+ switch (severity) {
4142
+ case "critical":
4143
+ return DiagnosticSeverity.Error;
4144
+ case "high":
4145
+ return DiagnosticSeverity.Warning;
4146
+ case "medium":
4147
+ return DiagnosticSeverity.Information;
4148
+ case "low":
4149
+ return DiagnosticSeverity.Hint;
4150
+ default:
4151
+ return DiagnosticSeverity.Information;
4152
+ }
4153
+ }
4154
+ function uriToFilePath(uri) {
4155
+ if (uri.startsWith("file://")) return fileURLToPath(uri);
4156
+ return uri;
4157
+ }
4158
+ function violationToRange(doc, v) {
4159
+ const edit = v.autofix?.edits?.[0];
4160
+ if (edit) {
4161
+ return {
4162
+ start: doc.positionAt(edit.start),
4163
+ end: doc.positionAt(edit.end)
4164
+ };
4165
+ }
4166
+ const line = Math.max(0, (v.line ?? 1) - 1);
4167
+ const char = Math.max(0, (v.column ?? 1) - 1);
4168
+ return {
4169
+ start: { line, character: char },
4170
+ end: { line, character: char + 1 }
4171
+ };
4172
+ }
4173
+ var SpecBridgeLspServer = class {
4174
+ connection = createConnection(ProposedFeatures.all);
4175
+ documents = new TextDocuments(TextDocument);
4176
+ options;
4177
+ registry = null;
4178
+ decisions = [];
4179
+ cwd;
4180
+ project;
4181
+ cache = /* @__PURE__ */ new Map();
4182
+ initError = null;
4183
+ constructor(options) {
4184
+ this.options = options;
4185
+ this.cwd = options.cwd;
4186
+ this.project = new Project3({
4187
+ compilerOptions: {
4188
+ allowJs: true,
4189
+ checkJs: false,
4190
+ noEmit: true,
4191
+ skipLibCheck: true
4192
+ },
4193
+ skipAddingFilesFromTsConfig: true
4194
+ });
4195
+ }
4196
+ async initialize() {
4197
+ this.connection.onInitialize(async () => {
4198
+ await this.initializeWorkspace();
4199
+ return {
4200
+ capabilities: {
4201
+ textDocumentSync: TextDocumentSyncKind.Incremental,
4202
+ codeActionProvider: true
4203
+ }
4204
+ };
4205
+ });
4206
+ this.documents.onDidOpen((e) => {
4207
+ void this.validateDocument(e.document);
4208
+ });
4209
+ this.documents.onDidChangeContent((change) => {
4210
+ void this.validateDocument(change.document);
4211
+ });
4212
+ this.documents.onDidClose((e) => {
4213
+ this.cache.delete(e.document.uri);
4214
+ this.connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
4215
+ });
4216
+ this.connection.onCodeAction((params) => {
4217
+ const violations = this.cache.get(params.textDocument.uri) || [];
4218
+ const doc = this.documents.get(params.textDocument.uri);
4219
+ if (!doc) return [];
4220
+ return violations.filter((v) => v.autofix && v.autofix.edits.length > 0).map((v) => {
4221
+ const edits = v.autofix.edits.map((edit) => ({
4222
+ range: {
4223
+ start: doc.positionAt(edit.start),
4224
+ end: doc.positionAt(edit.end)
4225
+ },
4226
+ newText: edit.text
4227
+ }));
4228
+ return {
4229
+ title: v.autofix.description,
4230
+ kind: CodeActionKind.QuickFix,
4231
+ edit: {
4232
+ changes: {
4233
+ [params.textDocument.uri]: edits
4234
+ }
4235
+ }
4236
+ };
4237
+ });
4238
+ });
4239
+ this.documents.listen(this.connection);
4240
+ this.connection.listen();
4241
+ }
4242
+ async initializeWorkspace() {
4243
+ if (!await pathExists(getSpecBridgeDir(this.cwd))) {
4244
+ const err = new NotInitializedError();
4245
+ this.initError = err.message;
4246
+ if (this.options.verbose) this.connection.console.error(chalk12.red(this.initError));
4247
+ return;
4248
+ }
4249
+ try {
4250
+ const config = await loadConfig(this.cwd);
4251
+ this.registry = createRegistry({ basePath: this.cwd });
4252
+ await this.registry.load();
4253
+ this.decisions = this.registry.getActive();
4254
+ for (const root of config.project.sourceRoots) {
4255
+ const rootPath = path3.isAbsolute(root) ? root : path3.join(this.cwd, root);
4256
+ const dir = rootPath.includes("*") ? rootPath.split("*")[0] : rootPath;
4257
+ if (dir && await pathExists(dir)) {
4258
+ this.project.addSourceFilesAtPaths(path3.join(dir, "**/*.{ts,tsx,js,jsx}"));
4259
+ }
4260
+ }
4261
+ if (this.options.verbose) {
4262
+ this.connection.console.log(chalk12.dim(`Loaded ${this.decisions.length} active decision(s)`));
4263
+ }
4264
+ } catch (error) {
4265
+ this.initError = error instanceof Error ? error.message : String(error);
4266
+ if (this.options.verbose) this.connection.console.error(chalk12.red(this.initError));
4267
+ }
4268
+ }
4269
+ async verifyTextDocument(doc) {
4270
+ if (this.initError) {
4271
+ throw new Error(this.initError);
4272
+ }
4273
+ if (!this.registry) return [];
4274
+ const filePath = uriToFilePath(doc.uri);
4275
+ const sourceFile = this.project.createSourceFile(filePath, doc.getText(), { overwrite: true });
4276
+ const violations = [];
4277
+ for (const decision of this.decisions) {
4278
+ for (const constraint of decision.constraints) {
4279
+ if (!shouldApplyConstraintToFile({ filePath, constraint, cwd: this.cwd })) continue;
4280
+ const verifier = selectVerifierForConstraint(constraint.rule, constraint.verifier);
4281
+ if (!verifier) continue;
4282
+ const ctx = {
4283
+ filePath,
4284
+ sourceFile,
4285
+ constraint,
4286
+ decisionId: decision.metadata.id
4287
+ };
4288
+ try {
4289
+ const constraintViolations = await verifier.verify(ctx);
4290
+ violations.push(...constraintViolations);
4291
+ } catch {
4292
+ }
4293
+ }
4294
+ }
4295
+ return violations;
4296
+ }
4297
+ async validateDocument(doc) {
4298
+ try {
4299
+ const violations = await this.verifyTextDocument(doc);
4300
+ this.cache.set(doc.uri, violations);
4301
+ const diagnostics = violations.map((v) => ({
4302
+ range: violationToRange(doc, v),
4303
+ severity: severityToDiagnostic(v.severity),
4304
+ message: v.message,
4305
+ source: "specbridge"
4306
+ }));
4307
+ this.connection.sendDiagnostics({ uri: doc.uri, diagnostics });
4308
+ } catch (error) {
4309
+ const msg = error instanceof Error ? error.message : String(error);
4310
+ this.connection.sendDiagnostics({
4311
+ uri: doc.uri,
4312
+ diagnostics: [
4313
+ {
4314
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
4315
+ severity: DiagnosticSeverity.Error,
4316
+ message: msg,
4317
+ source: "specbridge"
4318
+ }
4319
+ ]
4320
+ });
4321
+ }
4322
+ }
4323
+ };
4324
+
4325
+ // src/lsp/index.ts
4326
+ async function startLspServer(options) {
4327
+ const server = new SpecBridgeLspServer(options);
4328
+ await server.initialize();
4329
+ }
4330
+
4331
+ // src/cli/commands/lsp.ts
4332
+ var lspCommand = new Command12("lsp").description("Start SpecBridge language server (stdio)").option("--verbose", "Enable verbose server logging", false).action(async (options) => {
4333
+ await startLspServer({ cwd: process.cwd(), verbose: Boolean(options.verbose) });
4334
+ });
4335
+
4336
+ // src/cli/commands/watch.ts
4337
+ import { Command as Command13 } from "commander";
4338
+ import chalk13 from "chalk";
4339
+ import chokidar from "chokidar";
4340
+ import path4 from "path";
4341
+ var watchCommand = new Command13("watch").description("Watch for changes and verify files continuously").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("--debounce <ms>", "Debounce verify on rapid changes", "150").action(async (options) => {
4342
+ const cwd = process.cwd();
4343
+ if (!await pathExists(getSpecBridgeDir(cwd))) {
4344
+ throw new NotInitializedError();
4345
+ }
4346
+ const config = await loadConfig(cwd);
4347
+ const engine = createVerificationEngine();
4348
+ const level = options.level || "full";
4349
+ const debounceMs = Number.parseInt(options.debounce || "150", 10);
4350
+ let timer = null;
4351
+ let pendingPath = null;
4352
+ const run = async (changedPath) => {
4353
+ const absolutePath = path4.isAbsolute(changedPath) ? changedPath : path4.join(cwd, changedPath);
4354
+ const result = await engine.verify(config, {
4355
+ level,
4356
+ files: [absolutePath],
4357
+ cwd
4358
+ });
4359
+ const prefix = result.success ? chalk13.green("\u2713") : chalk13.red("\u2717");
4360
+ const summary = `${prefix} ${path4.relative(cwd, absolutePath)}: ${result.violations.length} violation(s)`;
4361
+ console.log(summary);
4362
+ for (const v of result.violations.slice(0, 20)) {
4363
+ const loc = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
4364
+ console.log(chalk13.dim(` - ${v.file}${loc}: ${v.message} [${v.severity}]`));
4365
+ }
4366
+ if (result.violations.length > 20) {
4367
+ console.log(chalk13.dim(` \u2026 ${result.violations.length - 20} more`));
4368
+ }
4369
+ };
4370
+ const watcher = chokidar.watch(config.project.sourceRoots, {
4371
+ cwd,
4372
+ ignored: config.project.exclude,
4373
+ ignoreInitial: true,
4374
+ persistent: true
4375
+ });
4376
+ console.log(chalk13.blue("Watching for changes..."));
4377
+ watcher.on("change", (changedPath) => {
4378
+ pendingPath = changedPath;
4379
+ if (timer) clearTimeout(timer);
4380
+ timer = setTimeout(() => {
4381
+ if (!pendingPath) return;
4382
+ void run(pendingPath);
4383
+ pendingPath = null;
4384
+ }, debounceMs);
4385
+ });
4386
+ });
4387
+
4388
+ // src/cli/commands/mcp-server.ts
4389
+ import { Command as Command14 } from "commander";
4390
+ import { readFileSync } from "fs";
4391
+ import { fileURLToPath as fileURLToPath2 } from "url";
4392
+ import { dirname as dirname3, join as join9 } from "path";
4393
+
4394
+ // src/mcp/server.ts
4395
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4396
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4397
+ import { z as z3 } from "zod";
4398
+ var SpecBridgeMcpServer = class {
4399
+ server;
4400
+ cwd;
4401
+ config = null;
4402
+ registry = null;
4403
+ constructor(options) {
4404
+ this.cwd = options.cwd;
4405
+ this.server = new McpServer({ name: "specbridge", version: options.version });
4406
+ }
4407
+ async initialize() {
4408
+ this.config = await loadConfig(this.cwd);
4409
+ this.registry = createRegistry({ basePath: this.cwd });
4410
+ await this.registry.load();
4411
+ this.registerResources();
4412
+ this.registerTools();
4413
+ }
4414
+ async startStdio() {
4415
+ const transport = new StdioServerTransport();
4416
+ await this.server.connect(transport);
4417
+ }
4418
+ getReady() {
4419
+ if (!this.config || !this.registry) {
4420
+ throw new Error("SpecBridge MCP server not initialized. Call initialize() first.");
4421
+ }
4422
+ return { config: this.config, registry: this.registry };
4423
+ }
4424
+ registerResources() {
4425
+ const { config, registry } = this.getReady();
4426
+ this.server.registerResource(
4427
+ "decisions",
4428
+ "decision:///",
4429
+ {
4430
+ title: "Architectural Decisions",
4431
+ description: "List of all architectural decisions",
4432
+ mimeType: "application/json"
4433
+ },
4434
+ async (uri) => {
4435
+ const decisions = registry.getAll();
4436
+ return {
4437
+ contents: [
4438
+ {
4439
+ uri: uri.href,
4440
+ mimeType: "application/json",
4441
+ text: JSON.stringify(decisions, null, 2)
4442
+ }
4443
+ ]
4444
+ };
4445
+ }
4446
+ );
4447
+ this.server.registerResource(
4448
+ "decision",
4449
+ new ResourceTemplate("decision://{id}", { list: void 0 }),
4450
+ {
4451
+ title: "Architectural Decision",
4452
+ description: "A specific architectural decision by id",
4453
+ mimeType: "application/json"
4454
+ },
4455
+ async (uri, variables) => {
4456
+ const raw = variables.id;
4457
+ const decisionId = Array.isArray(raw) ? raw[0] ?? "" : raw ?? "";
4458
+ const decision = registry.get(String(decisionId));
4459
+ return {
4460
+ contents: [
4461
+ {
4462
+ uri: uri.href,
4463
+ mimeType: "application/json",
4464
+ text: JSON.stringify(decision, null, 2)
4465
+ }
4466
+ ]
4467
+ };
4468
+ }
4469
+ );
4470
+ this.server.registerResource(
4471
+ "latest_report",
4472
+ "report:///latest",
4473
+ {
4474
+ title: "Latest Compliance Report",
4475
+ description: "Most recent compliance report (generated on demand)",
4476
+ mimeType: "application/json"
4477
+ },
4478
+ async (uri) => {
4479
+ const report = await generateReport(config, { cwd: this.cwd });
4480
+ return {
4481
+ contents: [
4482
+ {
4483
+ uri: uri.href,
4484
+ mimeType: "application/json",
4485
+ text: JSON.stringify(report, null, 2)
4486
+ }
4487
+ ]
4488
+ };
4489
+ }
4490
+ );
4491
+ }
4492
+ registerTools() {
4493
+ const { config } = this.getReady();
4494
+ this.server.registerTool(
4495
+ "generate_context",
4496
+ {
4497
+ title: "Generate architectural context",
4498
+ description: "Generate architectural context for a file from applicable decisions",
4499
+ inputSchema: {
4500
+ filePath: z3.string().describe("Path to the file to analyze"),
4501
+ includeRationale: z3.boolean().optional().default(true),
4502
+ format: z3.enum(["markdown", "json", "mcp"]).optional().default("markdown")
4503
+ }
4504
+ },
4505
+ async (args) => {
4506
+ const text = await generateFormattedContext(args.filePath, config, {
4507
+ includeRationale: args.includeRationale,
4508
+ format: args.format,
4509
+ cwd: this.cwd
4510
+ });
4511
+ return { content: [{ type: "text", text }] };
4512
+ }
4513
+ );
4514
+ this.server.registerTool(
4515
+ "verify_compliance",
4516
+ {
4517
+ title: "Verify compliance",
4518
+ description: "Verify code compliance against constraints",
4519
+ inputSchema: {
4520
+ level: z3.enum(["commit", "pr", "full"]).optional().default("full"),
4521
+ files: z3.array(z3.string()).optional(),
4522
+ decisions: z3.array(z3.string()).optional(),
4523
+ severity: z3.array(z3.enum(["critical", "high", "medium", "low"])).optional()
4524
+ }
4525
+ },
4526
+ async (args) => {
4527
+ const engine = createVerificationEngine();
4528
+ const result = await engine.verify(config, {
4529
+ level: args.level,
4530
+ files: args.files,
4531
+ decisions: args.decisions,
4532
+ severity: args.severity,
4533
+ cwd: this.cwd
4534
+ });
4535
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
4536
+ }
4537
+ );
4538
+ this.server.registerTool(
4539
+ "get_report",
4540
+ {
4541
+ title: "Get compliance report",
4542
+ description: "Generate a compliance report for the current workspace",
4543
+ inputSchema: {
4544
+ format: z3.enum(["summary", "detailed", "json", "markdown"]).optional().default("summary"),
4545
+ includeAll: z3.boolean().optional().default(false)
4546
+ }
4547
+ },
4548
+ async (args) => {
4549
+ const report = await generateReport(config, { cwd: this.cwd, includeAll: args.includeAll });
4550
+ if (args.format === "json") {
4551
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
4552
+ }
4553
+ if (args.format === "markdown") {
4554
+ return { content: [{ type: "text", text: formatMarkdownReport(report) }] };
4555
+ }
4556
+ if (args.format === "detailed") {
4557
+ return { content: [{ type: "text", text: formatConsoleReport(report) }] };
4558
+ }
4559
+ const summary = {
4560
+ timestamp: report.timestamp,
4561
+ project: report.project,
4562
+ compliance: report.summary.compliance,
4563
+ violations: report.summary.violations,
4564
+ decisions: report.summary.activeDecisions
4565
+ };
4566
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
4567
+ }
4568
+ );
4569
+ }
4570
+ };
4571
+
4572
+ // src/cli/commands/mcp-server.ts
4573
+ function getCliVersion() {
4574
+ try {
4575
+ const __dirname2 = dirname3(fileURLToPath2(import.meta.url));
4576
+ const packageJsonPath2 = join9(__dirname2, "../package.json");
4577
+ const pkg = JSON.parse(readFileSync(packageJsonPath2, "utf-8"));
4578
+ return String(pkg.version || "0.0.0");
4579
+ } catch {
4580
+ return "0.0.0";
4581
+ }
4582
+ }
4583
+ var mcpServerCommand = new Command14("mcp-server").description("Start SpecBridge MCP server (stdio)").action(async () => {
4584
+ const server = new SpecBridgeMcpServer({
4585
+ cwd: process.cwd(),
4586
+ version: getCliVersion()
4587
+ });
4588
+ await server.initialize();
4589
+ console.error("SpecBridge MCP server starting (stdio)...");
4590
+ await server.startStdio();
4591
+ });
4592
+
4593
+ // src/cli/commands/prompt.ts
4594
+ import { Command as Command15 } from "commander";
4595
+
4596
+ // src/agent/templates.ts
4597
+ var templates = {
4598
+ "code-review": {
4599
+ name: "Code Review",
4600
+ description: "Review code for architectural compliance",
4601
+ generate: (context) => {
4602
+ return [
4603
+ "You are reviewing code for architectural compliance.",
4604
+ "",
4605
+ formatContextAsMarkdown(context),
4606
+ "",
4607
+ "Task:",
4608
+ "- Identify violations of the constraints above.",
4609
+ "- Suggest concrete changes to achieve compliance."
4610
+ ].join("\n");
4611
+ }
4612
+ },
4613
+ refactoring: {
4614
+ name: "Refactoring Guidance",
4615
+ description: "Guide refactoring to meet constraints",
4616
+ generate: (context) => {
4617
+ return [
4618
+ "You are helping refactor code to meet architectural constraints.",
4619
+ "",
4620
+ formatContextAsMarkdown(context),
4621
+ "",
4622
+ "Task:",
4623
+ "- Propose a step-by-step refactoring plan to satisfy all invariants first, then conventions/guidelines.",
4624
+ "- Highlight risky changes and suggest safe incremental steps."
4625
+ ].join("\n");
4626
+ }
4627
+ },
4628
+ migration: {
4629
+ name: "Migration Plan",
4630
+ description: "Generate a migration plan for a new/changed decision",
4631
+ generate: (context, options) => {
4632
+ const decisionId = String(options?.decisionId ?? "");
4633
+ return [
4634
+ `A new architectural decision has been introduced: ${decisionId || "<decision-id>"}`,
4635
+ "",
4636
+ formatContextAsMarkdown(context),
4637
+ "",
4638
+ "Task:",
4639
+ "- Provide an impact analysis.",
4640
+ "- Produce a step-by-step migration plan.",
4641
+ "- Include a checklist for completion."
4642
+ ].join("\n");
4643
+ }
4644
+ }
4645
+ };
4646
+
4647
+ // src/cli/commands/prompt.ts
4648
+ var promptCommand = new Command15("prompt").description("Generate AI agent prompt templates").argument("<template>", "Template name (code-review|refactoring|migration)").argument("<file>", "File path (used to select applicable decisions)").option("--decision <id>", "Decision id (required for migration)").action(async (templateName, file, options) => {
4649
+ const cwd = process.cwd();
4650
+ if (!await pathExists(getSpecBridgeDir(cwd))) {
4651
+ throw new NotInitializedError();
4652
+ }
4653
+ const tpl = templates[templateName];
4654
+ if (!tpl) {
4655
+ throw new Error(`Unknown template: ${templateName}`);
4656
+ }
4657
+ if (templateName === "migration" && !options.decision) {
4658
+ throw new Error("Missing --decision <id> for migration template");
4659
+ }
4660
+ const config = await loadConfig(cwd);
4661
+ const context = await generateContext(file, config, { cwd, includeRationale: true });
4662
+ const prompt = tpl.generate(context, { decisionId: options.decision });
4663
+ console.log(prompt);
4664
+ });
4665
+
3325
4666
  // src/cli/index.ts
3326
- var __dirname = dirname3(fileURLToPath(import.meta.url));
3327
- var packageJsonPath = join9(__dirname, "../package.json");
3328
- var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
3329
- var program = new Command12();
4667
+ var __dirname = dirname4(fileURLToPath3(import.meta.url));
4668
+ var packageJsonPath = join10(__dirname, "../package.json");
4669
+ var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
4670
+ var program = new Command16();
3330
4671
  program.name("specbridge").description("Architecture Decision Runtime - Transform architectural decisions into executable, verifiable constraints").version(packageJson.version);
3331
4672
  program.addCommand(initCommand);
3332
4673
  program.addCommand(inferCommand);
@@ -3335,6 +4676,10 @@ program.addCommand(decisionCommand);
3335
4676
  program.addCommand(hookCommand);
3336
4677
  program.addCommand(reportCommand);
3337
4678
  program.addCommand(contextCommand);
4679
+ program.addCommand(lspCommand);
4680
+ program.addCommand(watchCommand);
4681
+ program.addCommand(mcpServerCommand);
4682
+ program.addCommand(promptCommand);
3338
4683
  program.exitOverride((err) => {
3339
4684
  if (err.code === "commander.help" || err.code === "commander.helpDisplayed") {
3340
4685
  process.exit(0);
@@ -3342,11 +4687,11 @@ program.exitOverride((err) => {
3342
4687
  if (err.code === "commander.version") {
3343
4688
  process.exit(0);
3344
4689
  }
3345
- console.error(chalk12.red(formatError(err)));
4690
+ console.error(chalk14.red(formatError(err)));
3346
4691
  process.exit(1);
3347
4692
  });
3348
4693
  program.parseAsync(process.argv).catch((error) => {
3349
- console.error(chalk12.red(formatError(error)));
4694
+ console.error(chalk14.red(formatError(error)));
3350
4695
  process.exit(1);
3351
4696
  });
3352
4697
  //# sourceMappingURL=cli.js.map