@ipation/specbridge 1.0.6 → 1.1.1

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/index.js CHANGED
@@ -66,8 +66,8 @@ function validateDecision(data) {
66
66
  }
67
67
  function formatValidationErrors(errors) {
68
68
  return errors.errors.map((err) => {
69
- const path = err.path.join(".");
70
- return `${path}: ${err.message}`;
69
+ const path3 = err.path.join(".");
70
+ return `${path3}: ${err.message}`;
71
71
  });
72
72
  }
73
73
 
@@ -147,10 +147,11 @@ var defaultConfig = {
147
147
 
148
148
  // src/core/errors/index.ts
149
149
  var SpecBridgeError = class extends Error {
150
- constructor(message, code, details) {
150
+ constructor(message, code, details, suggestion) {
151
151
  super(message);
152
152
  this.code = code;
153
153
  this.details = details;
154
+ this.suggestion = suggestion;
154
155
  this.name = "SpecBridgeError";
155
156
  Error.captureStackTrace(this, this.constructor);
156
157
  }
@@ -194,15 +195,15 @@ var InferenceError = class extends SpecBridgeError {
194
195
  }
195
196
  };
196
197
  var FileSystemError = class extends SpecBridgeError {
197
- constructor(message, path) {
198
- super(message, "FILE_SYSTEM_ERROR", { path });
199
- this.path = path;
198
+ constructor(message, path3) {
199
+ super(message, "FILE_SYSTEM_ERROR", { path: path3 });
200
+ this.path = path3;
200
201
  this.name = "FileSystemError";
201
202
  }
202
203
  };
203
204
  var AlreadyInitializedError = class extends SpecBridgeError {
204
- constructor(path) {
205
- super(`SpecBridge is already initialized at ${path}`, "ALREADY_INITIALIZED", { path });
205
+ constructor(path3) {
206
+ super(`SpecBridge is already initialized at ${path3}`, "ALREADY_INITIALIZED", { path: path3 });
206
207
  this.name = "AlreadyInitializedError";
207
208
  }
208
209
  };
@@ -210,7 +211,9 @@ var NotInitializedError = class extends SpecBridgeError {
210
211
  constructor() {
211
212
  super(
212
213
  'SpecBridge is not initialized. Run "specbridge init" first.',
213
- "NOT_INITIALIZED"
214
+ "NOT_INITIALIZED",
215
+ void 0,
216
+ "Run `specbridge init` in this directory to create .specbridge/"
214
217
  );
215
218
  this.name = "NotInitializedError";
216
219
  }
@@ -246,6 +249,10 @@ ${detailsStr}`;
246
249
  if (error instanceof DecisionValidationError && error.validationErrors.length > 0) {
247
250
  message += "\nValidation errors:\n" + error.validationErrors.map((e) => ` - ${e}`).join("\n");
248
251
  }
252
+ if (error.suggestion) {
253
+ message += `
254
+ Suggestion: ${error.suggestion}`;
255
+ }
249
256
  return message;
250
257
  }
251
258
  return `Error: ${error.message}`;
@@ -255,31 +262,31 @@ ${detailsStr}`;
255
262
  import { readFile, writeFile, mkdir, access, readdir, stat } from "fs/promises";
256
263
  import { join, dirname } from "path";
257
264
  import { constants } from "fs";
258
- async function pathExists(path) {
265
+ async function pathExists(path3) {
259
266
  try {
260
- await access(path, constants.F_OK);
267
+ await access(path3, constants.F_OK);
261
268
  return true;
262
269
  } catch {
263
270
  return false;
264
271
  }
265
272
  }
266
- async function isDirectory(path) {
273
+ async function isDirectory(path3) {
267
274
  try {
268
- const stats = await stat(path);
275
+ const stats = await stat(path3);
269
276
  return stats.isDirectory();
270
277
  } catch {
271
278
  return false;
272
279
  }
273
280
  }
274
- async function ensureDir(path) {
275
- await mkdir(path, { recursive: true });
281
+ async function ensureDir(path3) {
282
+ await mkdir(path3, { recursive: true });
276
283
  }
277
- async function readTextFile(path) {
278
- return readFile(path, "utf-8");
284
+ async function readTextFile(path3) {
285
+ return readFile(path3, "utf-8");
279
286
  }
280
- async function writeTextFile(path, content) {
281
- await ensureDir(dirname(path));
282
- await writeFile(path, content, "utf-8");
287
+ async function writeTextFile(path3, content) {
288
+ await ensureDir(dirname(path3));
289
+ await writeFile(path3, content, "utf-8");
283
290
  }
284
291
  async function readFilesInDir(dirPath, filter) {
285
292
  try {
@@ -327,15 +334,15 @@ function stringifyYaml(data, options) {
327
334
  function parseYamlDocument(content) {
328
335
  return parseDocument(content);
329
336
  }
330
- function updateYamlDocument(doc, path, value) {
337
+ function updateYamlDocument(doc, path3, value) {
331
338
  let current = doc.contents;
332
- for (let i = 0; i < path.length - 1; i++) {
333
- const key = path[i];
339
+ for (let i = 0; i < path3.length - 1; i++) {
340
+ const key = path3[i];
334
341
  if (key && current && typeof current === "object" && "get" in current) {
335
342
  current = current.get(key);
336
343
  }
337
344
  }
338
- const lastKey = path[path.length - 1];
345
+ const lastKey = path3[path3.length - 1];
339
346
  if (lastKey && current && typeof current === "object" && "set" in current) {
340
347
  current.set(lastKey, value);
341
348
  }
@@ -728,8 +735,8 @@ var CodeScanner = class {
728
735
  /**
729
736
  * Get a specific file
730
737
  */
731
- getFile(path) {
732
- return this.scannedFiles.get(path);
738
+ getFile(path3) {
739
+ return this.scannedFiles.get(path3);
733
740
  }
734
741
  /**
735
742
  * Get project instance for advanced analysis
@@ -742,12 +749,12 @@ var CodeScanner = class {
742
749
  */
743
750
  findClasses() {
744
751
  const classes = [];
745
- for (const { path, sourceFile } of this.scannedFiles.values()) {
752
+ for (const { path: path3, sourceFile } of this.scannedFiles.values()) {
746
753
  for (const classDecl of sourceFile.getClasses()) {
747
754
  const name = classDecl.getName();
748
755
  if (name) {
749
756
  classes.push({
750
- file: path,
757
+ file: path3,
751
758
  name,
752
759
  line: classDecl.getStartLineNumber()
753
760
  });
@@ -761,12 +768,12 @@ var CodeScanner = class {
761
768
  */
762
769
  findFunctions() {
763
770
  const functions = [];
764
- for (const { path, sourceFile } of this.scannedFiles.values()) {
771
+ for (const { path: path3, sourceFile } of this.scannedFiles.values()) {
765
772
  for (const funcDecl of sourceFile.getFunctions()) {
766
773
  const name = funcDecl.getName();
767
774
  if (name) {
768
775
  functions.push({
769
- file: path,
776
+ file: path3,
770
777
  name,
771
778
  line: funcDecl.getStartLineNumber(),
772
779
  isExported: funcDecl.isExported()
@@ -777,7 +784,7 @@ var CodeScanner = class {
777
784
  const init = varDecl.getInitializer();
778
785
  if (init && Node.isArrowFunction(init)) {
779
786
  functions.push({
780
- file: path,
787
+ file: path3,
781
788
  name: varDecl.getName(),
782
789
  line: varDecl.getStartLineNumber(),
783
790
  isExported: varDecl.isExported()
@@ -792,12 +799,12 @@ var CodeScanner = class {
792
799
  */
793
800
  findImports() {
794
801
  const imports = [];
795
- for (const { path, sourceFile } of this.scannedFiles.values()) {
802
+ for (const { path: path3, sourceFile } of this.scannedFiles.values()) {
796
803
  for (const importDecl of sourceFile.getImportDeclarations()) {
797
804
  const module = importDecl.getModuleSpecifierValue();
798
805
  const namedImports = importDecl.getNamedImports().map((n) => n.getName());
799
806
  imports.push({
800
- file: path,
807
+ file: path3,
801
808
  module,
802
809
  named: namedImports,
803
810
  line: importDecl.getStartLineNumber()
@@ -811,10 +818,10 @@ var CodeScanner = class {
811
818
  */
812
819
  findInterfaces() {
813
820
  const interfaces = [];
814
- for (const { path, sourceFile } of this.scannedFiles.values()) {
821
+ for (const { path: path3, sourceFile } of this.scannedFiles.values()) {
815
822
  for (const interfaceDecl of sourceFile.getInterfaces()) {
816
823
  interfaces.push({
817
- file: path,
824
+ file: path3,
818
825
  name: interfaceDecl.getName(),
819
826
  line: interfaceDecl.getStartLineNumber()
820
827
  });
@@ -827,10 +834,10 @@ var CodeScanner = class {
827
834
  */
828
835
  findTypeAliases() {
829
836
  const types = [];
830
- for (const { path, sourceFile } of this.scannedFiles.values()) {
837
+ for (const { path: path3, sourceFile } of this.scannedFiles.values()) {
831
838
  for (const typeAlias of sourceFile.getTypeAliases()) {
832
839
  types.push({
833
- file: path,
840
+ file: path3,
834
841
  name: typeAlias.getName(),
835
842
  line: typeAlias.getStartLineNumber()
836
843
  });
@@ -843,13 +850,13 @@ var CodeScanner = class {
843
850
  */
844
851
  findTryCatchBlocks() {
845
852
  const blocks = [];
846
- for (const { path, sourceFile } of this.scannedFiles.values()) {
853
+ for (const { path: path3, sourceFile } of this.scannedFiles.values()) {
847
854
  sourceFile.forEachDescendant((node) => {
848
855
  if (Node.isTryStatement(node)) {
849
856
  const catchClause = node.getCatchClause();
850
857
  const hasThrow = catchClause ? catchClause.getDescendantsOfKind(SyntaxKind.ThrowStatement).length > 0 : false;
851
858
  blocks.push({
852
- file: path,
859
+ file: path3,
853
860
  line: node.getStartLineNumber(),
854
861
  hasThrow
855
862
  });
@@ -1423,7 +1430,7 @@ var ErrorsAnalyzer = class {
1423
1430
  let throwNewError = 0;
1424
1431
  let throwCustom = 0;
1425
1432
  const examples = [];
1426
- for (const { path, sourceFile } of files) {
1433
+ for (const { path: path3, sourceFile } of files) {
1427
1434
  sourceFile.forEachDescendant((node) => {
1428
1435
  if (Node2.isThrowStatement(node)) {
1429
1436
  const expression = node.getExpression();
@@ -1436,7 +1443,7 @@ var ErrorsAnalyzer = class {
1436
1443
  if (examples.length < 3) {
1437
1444
  const snippet = text.length > 50 ? text.slice(0, 50) + "..." : text;
1438
1445
  examples.push({
1439
- file: path,
1446
+ file: path3,
1440
1447
  line: node.getStartLineNumber(),
1441
1448
  snippet: `throw ${snippet}`
1442
1449
  });
@@ -1698,6 +1705,7 @@ var NamingVerifier = class {
1698
1705
  };
1699
1706
 
1700
1707
  // src/verification/verifiers/imports.ts
1708
+ import path from "path";
1701
1709
  var ImportsVerifier = class {
1702
1710
  id = "imports";
1703
1711
  name = "Import Pattern Verifier";
@@ -1706,6 +1714,39 @@ var ImportsVerifier = class {
1706
1714
  const violations = [];
1707
1715
  const { sourceFile, constraint, decisionId, filePath } = ctx;
1708
1716
  const rule = constraint.rule.toLowerCase();
1717
+ if (rule.includes(".js") && rule.includes("extension") || rule.includes("esm") || rule.includes("add .js")) {
1718
+ for (const importDecl of sourceFile.getImportDeclarations()) {
1719
+ const moduleSpec = importDecl.getModuleSpecifierValue();
1720
+ if (!moduleSpec.startsWith(".")) continue;
1721
+ const ext = path.posix.extname(moduleSpec);
1722
+ let suggested = null;
1723
+ if (!ext) {
1724
+ suggested = `${moduleSpec}.js`;
1725
+ } else if (ext === ".ts" || ext === ".tsx" || ext === ".jsx") {
1726
+ suggested = moduleSpec.slice(0, -ext.length) + ".js";
1727
+ } else if (ext !== ".js") {
1728
+ continue;
1729
+ }
1730
+ if (!suggested || suggested === moduleSpec) continue;
1731
+ const ms = importDecl.getModuleSpecifier();
1732
+ const start = ms.getStart() + 1;
1733
+ const end = ms.getEnd() - 1;
1734
+ violations.push(createViolation({
1735
+ decisionId,
1736
+ constraintId: constraint.id,
1737
+ type: constraint.type,
1738
+ severity: constraint.severity,
1739
+ message: `Relative import "${moduleSpec}" should include a .js extension`,
1740
+ file: filePath,
1741
+ line: importDecl.getStartLineNumber(),
1742
+ suggestion: `Update to "${suggested}"`,
1743
+ autofix: {
1744
+ description: "Add/normalize .js extension in import specifier",
1745
+ edits: [{ start, end, text: suggested }]
1746
+ }
1747
+ }));
1748
+ }
1749
+ }
1709
1750
  if (rule.includes("barrel") || rule.includes("index")) {
1710
1751
  for (const importDecl of sourceFile.getImportDeclarations()) {
1711
1752
  const moduleSpec = importDecl.getModuleSpecifierValue();
@@ -1944,12 +1985,590 @@ var RegexVerifier = class {
1944
1985
  }
1945
1986
  };
1946
1987
 
1988
+ // src/verification/verifiers/dependencies.ts
1989
+ import path2 from "path";
1990
+ var graphCache = /* @__PURE__ */ new WeakMap();
1991
+ function normalizeFsPath(p) {
1992
+ return p.replaceAll("\\", "/");
1993
+ }
1994
+ function joinLike(fromFilePath, relative2) {
1995
+ const fromNorm = normalizeFsPath(fromFilePath);
1996
+ const relNorm = normalizeFsPath(relative2);
1997
+ if (path2.isAbsolute(fromNorm)) {
1998
+ return normalizeFsPath(path2.resolve(path2.dirname(fromNorm), relNorm));
1999
+ }
2000
+ const dir = path2.posix.dirname(fromNorm);
2001
+ return path2.posix.normalize(path2.posix.join(dir, relNorm));
2002
+ }
2003
+ function resolveToSourceFilePath(project, fromFilePath, moduleSpec) {
2004
+ if (!moduleSpec.startsWith(".")) return null;
2005
+ const candidates = [];
2006
+ const raw = joinLike(fromFilePath, moduleSpec);
2007
+ const addCandidate = (p) => candidates.push(normalizeFsPath(p));
2008
+ const ext = path2.posix.extname(raw);
2009
+ if (ext) {
2010
+ addCandidate(raw);
2011
+ if (ext === ".js") addCandidate(raw.slice(0, -3) + ".ts");
2012
+ if (ext === ".jsx") addCandidate(raw.slice(0, -4) + ".tsx");
2013
+ if (ext === ".ts") addCandidate(raw.slice(0, -3) + ".js");
2014
+ if (ext === ".tsx") addCandidate(raw.slice(0, -4) + ".jsx");
2015
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.ts"));
2016
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.tsx"));
2017
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.js"));
2018
+ addCandidate(path2.posix.join(raw.replace(/\/$/, ""), "index.jsx"));
2019
+ } else {
2020
+ addCandidate(raw + ".ts");
2021
+ addCandidate(raw + ".tsx");
2022
+ addCandidate(raw + ".js");
2023
+ addCandidate(raw + ".jsx");
2024
+ addCandidate(path2.posix.join(raw, "index.ts"));
2025
+ addCandidate(path2.posix.join(raw, "index.tsx"));
2026
+ addCandidate(path2.posix.join(raw, "index.js"));
2027
+ addCandidate(path2.posix.join(raw, "index.jsx"));
2028
+ }
2029
+ for (const candidate of candidates) {
2030
+ const sf = project.getSourceFile(candidate);
2031
+ if (sf) return sf.getFilePath();
2032
+ }
2033
+ return null;
2034
+ }
2035
+ function buildDependencyGraph(project) {
2036
+ const cached = graphCache.get(project);
2037
+ const sourceFiles = project.getSourceFiles();
2038
+ if (cached && cached.fileCount === sourceFiles.length) {
2039
+ return cached.graph;
2040
+ }
2041
+ const graph = /* @__PURE__ */ new Map();
2042
+ for (const sf of sourceFiles) {
2043
+ const from = normalizeFsPath(sf.getFilePath());
2044
+ if (!graph.has(from)) graph.set(from, /* @__PURE__ */ new Set());
2045
+ for (const importDecl of sf.getImportDeclarations()) {
2046
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2047
+ const resolved = resolveToSourceFilePath(project, from, moduleSpec);
2048
+ if (resolved) {
2049
+ graph.get(from).add(normalizeFsPath(resolved));
2050
+ }
2051
+ }
2052
+ }
2053
+ graphCache.set(project, { fileCount: sourceFiles.length, graph });
2054
+ return graph;
2055
+ }
2056
+ function tarjanScc(graph) {
2057
+ let index = 0;
2058
+ const stack = [];
2059
+ const onStack = /* @__PURE__ */ new Set();
2060
+ const indices = /* @__PURE__ */ new Map();
2061
+ const lowlink = /* @__PURE__ */ new Map();
2062
+ const result = [];
2063
+ const strongConnect = (v) => {
2064
+ indices.set(v, index);
2065
+ lowlink.set(v, index);
2066
+ index++;
2067
+ stack.push(v);
2068
+ onStack.add(v);
2069
+ const edges = graph.get(v) || /* @__PURE__ */ new Set();
2070
+ for (const w of edges) {
2071
+ if (!indices.has(w)) {
2072
+ strongConnect(w);
2073
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
2074
+ } else if (onStack.has(w)) {
2075
+ lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
2076
+ }
2077
+ }
2078
+ if (lowlink.get(v) === indices.get(v)) {
2079
+ const scc = [];
2080
+ while (stack.length > 0) {
2081
+ const w = stack.pop();
2082
+ if (!w) break;
2083
+ onStack.delete(w);
2084
+ scc.push(w);
2085
+ if (w === v) break;
2086
+ }
2087
+ result.push(scc);
2088
+ }
2089
+ };
2090
+ for (const v of graph.keys()) {
2091
+ if (!indices.has(v)) strongConnect(v);
2092
+ }
2093
+ return result;
2094
+ }
2095
+ function parseMaxImportDepth(rule) {
2096
+ const m = rule.match(/maximum\s{1,5}import\s{1,5}depth\s{0,5}[:=]?\s{0,5}(\d+)/i);
2097
+ return m ? Number.parseInt(m[1], 10) : null;
2098
+ }
2099
+ function parseBannedDependency(rule) {
2100
+ const m = rule.match(/no\s{1,5}dependencies?\s{1,5}on\s{1,5}(?:package\s{1,5})?(.+?)(?:\.|$)/i);
2101
+ if (!m) return null;
2102
+ const value = m[1].trim();
2103
+ return value.length > 0 ? value : null;
2104
+ }
2105
+ function parseLayerRule(rule) {
2106
+ const m = rule.match(/(\w+)\s{1,5}layer\s{1,5}cannot\s{1,5}depend\s{1,5}on\s{1,5}(\w+)\s{1,5}layer/i);
2107
+ if (!m) return null;
2108
+ return { fromLayer: m[1].toLowerCase(), toLayer: m[2].toLowerCase() };
2109
+ }
2110
+ function fileInLayer(filePath, layer) {
2111
+ const fp = normalizeFsPath(filePath).toLowerCase();
2112
+ return fp.includes(`/${layer}/`) || fp.endsWith(`/${layer}.ts`) || fp.endsWith(`/${layer}.tsx`);
2113
+ }
2114
+ var DependencyVerifier = class {
2115
+ id = "dependencies";
2116
+ name = "Dependency Verifier";
2117
+ description = "Checks dependency constraints, import depth, and circular dependencies";
2118
+ async verify(ctx) {
2119
+ const violations = [];
2120
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2121
+ const rule = constraint.rule;
2122
+ const lowerRule = rule.toLowerCase();
2123
+ const project = sourceFile.getProject();
2124
+ const projectFilePath = normalizeFsPath(sourceFile.getFilePath());
2125
+ if (lowerRule.includes("circular") || lowerRule.includes("cycle")) {
2126
+ const graph = buildDependencyGraph(project);
2127
+ const sccs = tarjanScc(graph);
2128
+ const current = projectFilePath;
2129
+ for (const scc of sccs) {
2130
+ const hasSelfLoop = scc.length === 1 && (graph.get(scc[0])?.has(scc[0]) ?? false);
2131
+ const isCycle = scc.length > 1 || hasSelfLoop;
2132
+ if (!isCycle) continue;
2133
+ if (!scc.includes(current)) continue;
2134
+ const sorted = [...scc].sort();
2135
+ if (sorted[0] !== current) continue;
2136
+ violations.push(createViolation({
2137
+ decisionId,
2138
+ constraintId: constraint.id,
2139
+ type: constraint.type,
2140
+ severity: constraint.severity,
2141
+ message: `Circular dependency detected across: ${sorted.join(" -> ")}`,
2142
+ file: filePath,
2143
+ line: 1,
2144
+ suggestion: "Break the cycle by extracting shared abstractions or reversing the dependency"
2145
+ }));
2146
+ }
2147
+ }
2148
+ const layerRule = parseLayerRule(rule);
2149
+ if (layerRule && fileInLayer(projectFilePath, layerRule.fromLayer)) {
2150
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2151
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2152
+ const resolved = resolveToSourceFilePath(project, projectFilePath, moduleSpec);
2153
+ if (!resolved) continue;
2154
+ if (fileInLayer(resolved, layerRule.toLayer)) {
2155
+ violations.push(createViolation({
2156
+ decisionId,
2157
+ constraintId: constraint.id,
2158
+ type: constraint.type,
2159
+ severity: constraint.severity,
2160
+ message: `Layer violation: ${layerRule.fromLayer} depends on ${layerRule.toLayer} via import "${moduleSpec}"`,
2161
+ file: filePath,
2162
+ line: importDecl.getStartLineNumber(),
2163
+ suggestion: `Refactor to remove dependency from ${layerRule.fromLayer} to ${layerRule.toLayer}`
2164
+ }));
2165
+ }
2166
+ }
2167
+ }
2168
+ const banned = parseBannedDependency(rule);
2169
+ if (banned) {
2170
+ const bannedLower = banned.toLowerCase();
2171
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2172
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2173
+ if (moduleSpec.toLowerCase().includes(bannedLower)) {
2174
+ violations.push(createViolation({
2175
+ decisionId,
2176
+ constraintId: constraint.id,
2177
+ type: constraint.type,
2178
+ severity: constraint.severity,
2179
+ message: `Banned dependency import detected: "${moduleSpec}"`,
2180
+ file: filePath,
2181
+ line: importDecl.getStartLineNumber(),
2182
+ suggestion: `Remove or replace dependency "${banned}"`
2183
+ }));
2184
+ }
2185
+ }
2186
+ }
2187
+ const maxDepth = parseMaxImportDepth(rule);
2188
+ if (maxDepth !== null) {
2189
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2190
+ const moduleSpec = importDecl.getModuleSpecifierValue();
2191
+ if (!moduleSpec.startsWith(".")) continue;
2192
+ const depth = (moduleSpec.match(/\.\.\//g) || []).length;
2193
+ if (depth > maxDepth) {
2194
+ violations.push(createViolation({
2195
+ decisionId,
2196
+ constraintId: constraint.id,
2197
+ type: constraint.type,
2198
+ severity: constraint.severity,
2199
+ message: `Import depth ${depth} exceeds maximum ${maxDepth}: "${moduleSpec}"`,
2200
+ file: filePath,
2201
+ line: importDecl.getStartLineNumber(),
2202
+ suggestion: "Use a shallower module boundary (or introduce a public entrypoint for this dependency)"
2203
+ }));
2204
+ }
2205
+ }
2206
+ }
2207
+ return violations;
2208
+ }
2209
+ };
2210
+
2211
+ // src/verification/verifiers/complexity.ts
2212
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
2213
+ function parseLimit(rule, pattern) {
2214
+ const m = rule.match(pattern);
2215
+ return m ? Number.parseInt(m[1], 10) : null;
2216
+ }
2217
+ function getFileLineCount(text) {
2218
+ if (text.length === 0) return 0;
2219
+ return text.split("\n").length;
2220
+ }
2221
+ function getDecisionPoints(fn) {
2222
+ let points = 0;
2223
+ for (const d of fn.getDescendants()) {
2224
+ switch (d.getKind()) {
2225
+ case SyntaxKind2.IfStatement:
2226
+ case SyntaxKind2.ForStatement:
2227
+ case SyntaxKind2.ForInStatement:
2228
+ case SyntaxKind2.ForOfStatement:
2229
+ case SyntaxKind2.WhileStatement:
2230
+ case SyntaxKind2.DoStatement:
2231
+ case SyntaxKind2.CatchClause:
2232
+ case SyntaxKind2.ConditionalExpression:
2233
+ case SyntaxKind2.CaseClause:
2234
+ points++;
2235
+ break;
2236
+ case SyntaxKind2.BinaryExpression: {
2237
+ const text = d.getText();
2238
+ if (text.includes("&&") || text.includes("||")) points++;
2239
+ break;
2240
+ }
2241
+ default:
2242
+ break;
2243
+ }
2244
+ }
2245
+ return points;
2246
+ }
2247
+ function calculateCyclomaticComplexity(fn) {
2248
+ return 1 + getDecisionPoints(fn);
2249
+ }
2250
+ function getFunctionDisplayName(fn) {
2251
+ if ("getName" in fn && typeof fn.getName === "function") {
2252
+ const name = fn.getName();
2253
+ if (typeof name === "string" && name.length > 0) return name;
2254
+ }
2255
+ const parent = fn.getParent();
2256
+ if (parent?.getKind() === SyntaxKind2.VariableDeclaration) {
2257
+ const vd = parent;
2258
+ if (typeof vd.getName === "function") return vd.getName();
2259
+ }
2260
+ return "<anonymous>";
2261
+ }
2262
+ function maxNestingDepth(node) {
2263
+ let maxDepth = 0;
2264
+ const walk = (n, depth) => {
2265
+ maxDepth = Math.max(maxDepth, depth);
2266
+ const kind = n.getKind();
2267
+ 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;
2268
+ for (const child of n.getChildren()) {
2269
+ if (child.getKind() === SyntaxKind2.FunctionDeclaration || child.getKind() === SyntaxKind2.FunctionExpression || child.getKind() === SyntaxKind2.ArrowFunction || child.getKind() === SyntaxKind2.MethodDeclaration) {
2270
+ continue;
2271
+ }
2272
+ walk(child, isNestingNode ? depth + 1 : depth);
2273
+ }
2274
+ };
2275
+ walk(node, 0);
2276
+ return maxDepth;
2277
+ }
2278
+ var ComplexityVerifier = class {
2279
+ id = "complexity";
2280
+ name = "Complexity Verifier";
2281
+ description = "Checks cyclomatic complexity, file size, parameters, and nesting depth";
2282
+ async verify(ctx) {
2283
+ const violations = [];
2284
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2285
+ const rule = constraint.rule;
2286
+ const maxComplexity = parseLimit(rule, /complexity\s+(?:must\s+)?not\s+exceed\s+(\d+)/i);
2287
+ const maxLines = parseLimit(rule, /file\s+size\s+(?:must\s+)?not\s+exceed\s+(\d+)\s+lines?/i);
2288
+ const maxParams = parseLimit(rule, /at\s+most\s+(\d+)\s+parameters?/i) ?? parseLimit(rule, /parameters?\s+(?:must\s+)?not\s+exceed\s+(\d+)/i);
2289
+ const maxNesting = parseLimit(rule, /nesting\s+depth\s+(?:must\s+)?not\s+exceed\s+(\d+)/i);
2290
+ if (maxLines !== null) {
2291
+ const lineCount = getFileLineCount(sourceFile.getFullText());
2292
+ if (lineCount > maxLines) {
2293
+ violations.push(createViolation({
2294
+ decisionId,
2295
+ constraintId: constraint.id,
2296
+ type: constraint.type,
2297
+ severity: constraint.severity,
2298
+ message: `File has ${lineCount} lines which exceeds maximum ${maxLines}`,
2299
+ file: filePath,
2300
+ line: 1,
2301
+ suggestion: "Split the file into smaller modules"
2302
+ }));
2303
+ }
2304
+ }
2305
+ const functionLikes = [
2306
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionDeclaration),
2307
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionExpression),
2308
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.ArrowFunction),
2309
+ ...sourceFile.getDescendantsOfKind(SyntaxKind2.MethodDeclaration)
2310
+ ];
2311
+ for (const fn of functionLikes) {
2312
+ const fnName = getFunctionDisplayName(fn);
2313
+ if (maxComplexity !== null) {
2314
+ const complexity = calculateCyclomaticComplexity(fn);
2315
+ if (complexity > maxComplexity) {
2316
+ violations.push(createViolation({
2317
+ decisionId,
2318
+ constraintId: constraint.id,
2319
+ type: constraint.type,
2320
+ severity: constraint.severity,
2321
+ message: `Function ${fnName} has cyclomatic complexity ${complexity} which exceeds maximum ${maxComplexity}`,
2322
+ file: filePath,
2323
+ line: fn.getStartLineNumber(),
2324
+ suggestion: "Refactor to reduce branching or extract smaller functions"
2325
+ }));
2326
+ }
2327
+ }
2328
+ if (maxParams !== null && "getParameters" in fn) {
2329
+ const params = fn.getParameters();
2330
+ const paramCount = Array.isArray(params) ? params.length : 0;
2331
+ if (paramCount > maxParams) {
2332
+ violations.push(createViolation({
2333
+ decisionId,
2334
+ constraintId: constraint.id,
2335
+ type: constraint.type,
2336
+ severity: constraint.severity,
2337
+ message: `Function ${fnName} has ${paramCount} parameters which exceeds maximum ${maxParams}`,
2338
+ file: filePath,
2339
+ line: fn.getStartLineNumber(),
2340
+ suggestion: "Consider grouping parameters into an options object"
2341
+ }));
2342
+ }
2343
+ }
2344
+ if (maxNesting !== null) {
2345
+ const depth = maxNestingDepth(fn);
2346
+ if (depth > maxNesting) {
2347
+ violations.push(createViolation({
2348
+ decisionId,
2349
+ constraintId: constraint.id,
2350
+ type: constraint.type,
2351
+ severity: constraint.severity,
2352
+ message: `Function ${fnName} has nesting depth ${depth} which exceeds maximum ${maxNesting}`,
2353
+ file: filePath,
2354
+ line: fn.getStartLineNumber(),
2355
+ suggestion: "Reduce nesting by using early returns or extracting functions"
2356
+ }));
2357
+ }
2358
+ }
2359
+ }
2360
+ return violations;
2361
+ }
2362
+ };
2363
+
2364
+ // src/verification/verifiers/security.ts
2365
+ import { SyntaxKind as SyntaxKind3 } from "ts-morph";
2366
+ var SECRET_NAME_RE = /(api[_-]?key|password|secret|token)/i;
2367
+ function isStringLiteralLike(node) {
2368
+ const k = node.getKind();
2369
+ return k === SyntaxKind3.StringLiteral || k === SyntaxKind3.NoSubstitutionTemplateLiteral;
2370
+ }
2371
+ var SecurityVerifier = class {
2372
+ id = "security";
2373
+ name = "Security Verifier";
2374
+ description = "Detects common security footguns (secrets, eval, XSS/SQL injection heuristics)";
2375
+ async verify(ctx) {
2376
+ const violations = [];
2377
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2378
+ const rule = constraint.rule.toLowerCase();
2379
+ const checkSecrets = rule.includes("secret") || rule.includes("password") || rule.includes("token") || rule.includes("api key") || rule.includes("hardcoded");
2380
+ const checkEval = rule.includes("eval") || rule.includes("function constructor");
2381
+ const checkXss = rule.includes("xss") || rule.includes("innerhtml") || rule.includes("dangerouslysetinnerhtml");
2382
+ const checkSql = rule.includes("sql") || rule.includes("injection");
2383
+ const checkProto = rule.includes("prototype pollution") || rule.includes("__proto__");
2384
+ if (checkSecrets) {
2385
+ for (const vd of sourceFile.getVariableDeclarations()) {
2386
+ const name = vd.getName();
2387
+ if (!SECRET_NAME_RE.test(name)) continue;
2388
+ const init = vd.getInitializer();
2389
+ if (!init || !isStringLiteralLike(init)) continue;
2390
+ const value = init.getText().slice(1, -1);
2391
+ if (value.length === 0) continue;
2392
+ violations.push(createViolation({
2393
+ decisionId,
2394
+ constraintId: constraint.id,
2395
+ type: constraint.type,
2396
+ severity: constraint.severity,
2397
+ message: `Possible hardcoded secret in variable "${name}"`,
2398
+ file: filePath,
2399
+ line: vd.getStartLineNumber(),
2400
+ suggestion: "Move secrets to environment variables or a secret manager"
2401
+ }));
2402
+ }
2403
+ for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
2404
+ const nameNode = pa.getNameNode?.();
2405
+ const propName = nameNode?.getText?.() ?? "";
2406
+ if (!SECRET_NAME_RE.test(propName)) continue;
2407
+ const init = pa.getInitializer();
2408
+ if (!init || !isStringLiteralLike(init)) continue;
2409
+ violations.push(createViolation({
2410
+ decisionId,
2411
+ constraintId: constraint.id,
2412
+ type: constraint.type,
2413
+ severity: constraint.severity,
2414
+ message: `Possible hardcoded secret in object property ${propName}`,
2415
+ file: filePath,
2416
+ line: pa.getStartLineNumber(),
2417
+ suggestion: "Move secrets to environment variables or a secret manager"
2418
+ }));
2419
+ }
2420
+ }
2421
+ if (checkEval) {
2422
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2423
+ const exprText = call.getExpression().getText();
2424
+ if (exprText === "eval" || exprText === "Function") {
2425
+ violations.push(createViolation({
2426
+ decisionId,
2427
+ constraintId: constraint.id,
2428
+ type: constraint.type,
2429
+ severity: constraint.severity,
2430
+ message: `Unsafe dynamic code execution via ${exprText}()`,
2431
+ file: filePath,
2432
+ line: call.getStartLineNumber(),
2433
+ suggestion: "Avoid eval/Function; use safer alternatives"
2434
+ }));
2435
+ }
2436
+ }
2437
+ }
2438
+ if (checkXss) {
2439
+ for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
2440
+ const left = bin.getLeft();
2441
+ if (left.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2442
+ const pa = left;
2443
+ if (pa.getName?.() === "innerHTML") {
2444
+ violations.push(createViolation({
2445
+ decisionId,
2446
+ constraintId: constraint.id,
2447
+ type: constraint.type,
2448
+ severity: constraint.severity,
2449
+ message: "Potential XSS: assignment to innerHTML",
2450
+ file: filePath,
2451
+ line: bin.getStartLineNumber(),
2452
+ suggestion: "Prefer textContent or a safe templating/escaping strategy"
2453
+ }));
2454
+ }
2455
+ }
2456
+ if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
2457
+ violations.push(createViolation({
2458
+ decisionId,
2459
+ constraintId: constraint.id,
2460
+ type: constraint.type,
2461
+ severity: constraint.severity,
2462
+ message: "Potential XSS: usage of dangerouslySetInnerHTML",
2463
+ file: filePath,
2464
+ line: 1,
2465
+ suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
2466
+ }));
2467
+ }
2468
+ }
2469
+ if (checkSql) {
2470
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2471
+ const expr = call.getExpression();
2472
+ if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2473
+ const name = expr.getName?.();
2474
+ if (name !== "query" && name !== "execute") continue;
2475
+ const arg = call.getArguments()[0];
2476
+ if (!arg) continue;
2477
+ const isTemplate = arg.getKind() === SyntaxKind3.TemplateExpression;
2478
+ const isConcat = arg.getKind() === SyntaxKind3.BinaryExpression && arg.getText().includes("+");
2479
+ if (!isTemplate && !isConcat) continue;
2480
+ const text = arg.getText().toLowerCase();
2481
+ if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
2482
+ continue;
2483
+ }
2484
+ violations.push(createViolation({
2485
+ decisionId,
2486
+ constraintId: constraint.id,
2487
+ type: constraint.type,
2488
+ severity: constraint.severity,
2489
+ message: "Potential SQL injection: dynamically constructed SQL query",
2490
+ file: filePath,
2491
+ line: call.getStartLineNumber(),
2492
+ suggestion: "Use parameterized queries / prepared statements"
2493
+ }));
2494
+ }
2495
+ }
2496
+ if (checkProto) {
2497
+ const text = sourceFile.getFullText();
2498
+ if (text.includes("__proto__") || text.includes("constructor.prototype")) {
2499
+ violations.push(createViolation({
2500
+ decisionId,
2501
+ constraintId: constraint.id,
2502
+ type: constraint.type,
2503
+ severity: constraint.severity,
2504
+ message: "Potential prototype pollution pattern detected",
2505
+ file: filePath,
2506
+ line: 1,
2507
+ suggestion: "Avoid writing to __proto__/prototype; validate object keys"
2508
+ }));
2509
+ }
2510
+ }
2511
+ return violations;
2512
+ }
2513
+ };
2514
+
2515
+ // src/verification/verifiers/api.ts
2516
+ import { SyntaxKind as SyntaxKind4 } from "ts-morph";
2517
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
2518
+ function isKebabPath(pathValue) {
2519
+ const parts = pathValue.split("/").filter(Boolean);
2520
+ for (const part of parts) {
2521
+ if (part.startsWith(":")) continue;
2522
+ if (!/^[a-z0-9-]+$/.test(part)) return false;
2523
+ }
2524
+ return true;
2525
+ }
2526
+ var ApiVerifier = class {
2527
+ id = "api";
2528
+ name = "API Consistency Verifier";
2529
+ description = "Checks basic REST endpoint naming conventions in common frameworks";
2530
+ async verify(ctx) {
2531
+ const violations = [];
2532
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2533
+ const rule = constraint.rule.toLowerCase();
2534
+ const enforceKebab = rule.includes("kebab") || rule.includes("kebab-case");
2535
+ if (!enforceKebab) return violations;
2536
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
2537
+ const expr = call.getExpression();
2538
+ if (expr.getKind() !== SyntaxKind4.PropertyAccessExpression) continue;
2539
+ const method = expr.getName?.();
2540
+ if (!method || !HTTP_METHODS.has(String(method))) continue;
2541
+ const firstArg = call.getArguments()[0];
2542
+ if (!firstArg || firstArg.getKind() !== SyntaxKind4.StringLiteral) continue;
2543
+ const pathValue = firstArg.getLiteralValue?.() ?? firstArg.getText().slice(1, -1);
2544
+ if (typeof pathValue !== "string") continue;
2545
+ if (!isKebabPath(pathValue)) {
2546
+ violations.push(createViolation({
2547
+ decisionId,
2548
+ constraintId: constraint.id,
2549
+ type: constraint.type,
2550
+ severity: constraint.severity,
2551
+ message: `Endpoint path "${pathValue}" is not kebab-case`,
2552
+ file: filePath,
2553
+ line: call.getStartLineNumber(),
2554
+ suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
2555
+ }));
2556
+ }
2557
+ }
2558
+ return violations;
2559
+ }
2560
+ };
2561
+
1947
2562
  // src/verification/verifiers/index.ts
1948
2563
  var builtinVerifiers = {
1949
2564
  naming: () => new NamingVerifier(),
1950
2565
  imports: () => new ImportsVerifier(),
1951
2566
  errors: () => new ErrorsVerifier(),
1952
- regex: () => new RegexVerifier()
2567
+ regex: () => new RegexVerifier(),
2568
+ dependencies: () => new DependencyVerifier(),
2569
+ complexity: () => new ComplexityVerifier(),
2570
+ security: () => new SecurityVerifier(),
2571
+ api: () => new ApiVerifier()
1953
2572
  };
1954
2573
  function getVerifier(id) {
1955
2574
  const factory = builtinVerifiers[id];
@@ -1963,6 +2582,18 @@ function selectVerifierForConstraint(rule, specifiedVerifier) {
1963
2582
  return getVerifier(specifiedVerifier);
1964
2583
  }
1965
2584
  const lowerRule = rule.toLowerCase();
2585
+ if (lowerRule.includes("dependency") || lowerRule.includes("circular dependenc") || lowerRule.includes("import depth") || lowerRule.includes("layer") && lowerRule.includes("depend on")) {
2586
+ return getVerifier("dependencies");
2587
+ }
2588
+ if (lowerRule.includes("cyclomatic") || lowerRule.includes("complexity") || lowerRule.includes("nesting") || lowerRule.includes("parameters") || lowerRule.includes("file size")) {
2589
+ return getVerifier("complexity");
2590
+ }
2591
+ if (lowerRule.includes("security") || lowerRule.includes("secret") || lowerRule.includes("password") || lowerRule.includes("token") || lowerRule.includes("xss") || lowerRule.includes("sql") || lowerRule.includes("eval")) {
2592
+ return getVerifier("security");
2593
+ }
2594
+ if (lowerRule.includes("endpoint") || lowerRule.includes("rest") || lowerRule.includes("api") && lowerRule.includes("path")) {
2595
+ return getVerifier("api");
2596
+ }
1966
2597
  if (lowerRule.includes("naming") || lowerRule.includes("case")) {
1967
2598
  return getVerifier("naming");
1968
2599
  }
@@ -1978,10 +2609,60 @@ function selectVerifierForConstraint(rule, specifiedVerifier) {
1978
2609
  return getVerifier("regex");
1979
2610
  }
1980
2611
 
2612
+ // src/verification/cache.ts
2613
+ import { stat as stat2 } from "fs/promises";
2614
+ var AstCache = class {
2615
+ cache = /* @__PURE__ */ new Map();
2616
+ async get(filePath, project) {
2617
+ try {
2618
+ const info = await stat2(filePath);
2619
+ const cached = this.cache.get(filePath);
2620
+ if (cached && cached.mtimeMs >= info.mtimeMs) {
2621
+ return cached.sourceFile;
2622
+ }
2623
+ let sourceFile = project.getSourceFile(filePath);
2624
+ if (!sourceFile) {
2625
+ sourceFile = project.addSourceFileAtPath(filePath);
2626
+ } else {
2627
+ sourceFile.refreshFromFileSystemSync();
2628
+ }
2629
+ this.cache.set(filePath, { sourceFile, mtimeMs: info.mtimeMs });
2630
+ return sourceFile;
2631
+ } catch {
2632
+ return null;
2633
+ }
2634
+ }
2635
+ clear() {
2636
+ this.cache.clear();
2637
+ }
2638
+ };
2639
+
2640
+ // src/verification/applicability.ts
2641
+ function isConstraintExcepted(filePath, constraint, cwd) {
2642
+ if (!constraint.exceptions) return false;
2643
+ return constraint.exceptions.some((exception) => {
2644
+ if (exception.expiresAt) {
2645
+ const expiryDate = new Date(exception.expiresAt);
2646
+ if (expiryDate < /* @__PURE__ */ new Date()) {
2647
+ return false;
2648
+ }
2649
+ }
2650
+ return matchesPattern(filePath, exception.pattern, { cwd });
2651
+ });
2652
+ }
2653
+ function shouldApplyConstraintToFile(params) {
2654
+ const { filePath, constraint, cwd, severityFilter } = params;
2655
+ if (!matchesPattern(filePath, constraint.scope, { cwd })) return false;
2656
+ if (severityFilter && !severityFilter.includes(constraint.severity)) return false;
2657
+ if (isConstraintExcepted(filePath, constraint, cwd)) return false;
2658
+ return true;
2659
+ }
2660
+
1981
2661
  // src/verification/engine.ts
1982
2662
  var VerificationEngine = class {
1983
2663
  registry;
1984
2664
  project;
2665
+ astCache;
1985
2666
  constructor(registry) {
1986
2667
  this.registry = registry || createRegistry();
1987
2668
  this.project = new Project2({
@@ -1993,6 +2674,7 @@ var VerificationEngine = class {
1993
2674
  },
1994
2675
  skipAddingFilesFromTsConfig: true
1995
2676
  });
2677
+ this.astCache = new AstCache();
1996
2678
  }
1997
2679
  /**
1998
2680
  * Run verification
@@ -2035,8 +2717,8 @@ var VerificationEngine = class {
2035
2717
  let failed = 0;
2036
2718
  const skipped = 0;
2037
2719
  let timeoutHandle = null;
2038
- const timeoutPromise = new Promise((resolve) => {
2039
- timeoutHandle = setTimeout(() => resolve("timeout"), timeout);
2720
+ const timeoutPromise = new Promise((resolve2) => {
2721
+ timeoutHandle = setTimeout(() => resolve2("timeout"), timeout);
2040
2722
  timeoutHandle.unref();
2041
2723
  });
2042
2724
  const verificationPromise = this.verifyFiles(
@@ -2098,23 +2780,11 @@ var VerificationEngine = class {
2098
2780
  */
2099
2781
  async verifyFile(filePath, decisions, severityFilter, cwd = process.cwd()) {
2100
2782
  const violations = [];
2101
- let sourceFile = this.project.getSourceFile(filePath);
2102
- if (!sourceFile) {
2103
- try {
2104
- sourceFile = this.project.addSourceFileAtPath(filePath);
2105
- } catch {
2106
- return violations;
2107
- }
2108
- }
2783
+ const sourceFile = await this.astCache.get(filePath, this.project);
2784
+ if (!sourceFile) return violations;
2109
2785
  for (const decision of decisions) {
2110
2786
  for (const constraint of decision.constraints) {
2111
- if (!matchesPattern(filePath, constraint.scope, { cwd })) {
2112
- continue;
2113
- }
2114
- if (severityFilter && !severityFilter.includes(constraint.severity)) {
2115
- continue;
2116
- }
2117
- if (this.isExcepted(filePath, constraint, cwd)) {
2787
+ if (!shouldApplyConstraintToFile({ filePath, constraint, cwd, severityFilter })) {
2118
2788
  continue;
2119
2789
  }
2120
2790
  const verifier = selectVerifierForConstraint(constraint.rule, constraint.verifier);
@@ -2140,25 +2810,16 @@ var VerificationEngine = class {
2140
2810
  * Verify multiple files
2141
2811
  */
2142
2812
  async verifyFiles(files, decisions, severityFilter, cwd, onFileVerified) {
2143
- for (const file of files) {
2144
- const violations = await this.verifyFile(file, decisions, severityFilter, cwd);
2145
- onFileVerified(violations);
2146
- }
2147
- }
2148
- /**
2149
- * Check if file is excepted from constraint
2150
- */
2151
- isExcepted(filePath, constraint, cwd) {
2152
- if (!constraint.exceptions) return false;
2153
- return constraint.exceptions.some((exception) => {
2154
- if (exception.expiresAt) {
2155
- const expiryDate = new Date(exception.expiresAt);
2156
- if (expiryDate < /* @__PURE__ */ new Date()) {
2157
- return false;
2158
- }
2813
+ const BATCH_SIZE = 10;
2814
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
2815
+ const batch = files.slice(i, i + BATCH_SIZE);
2816
+ const results = await Promise.all(
2817
+ batch.map((file) => this.verifyFile(file, decisions, severityFilter, cwd))
2818
+ );
2819
+ for (const violations of results) {
2820
+ onFileVerified(violations);
2159
2821
  }
2160
- return matchesPattern(filePath, exception.pattern, { cwd });
2161
- });
2822
+ }
2162
2823
  }
2163
2824
  /**
2164
2825
  * Get registry
@@ -2171,8 +2832,111 @@ function createVerificationEngine(registry) {
2171
2832
  return new VerificationEngine(registry);
2172
2833
  }
2173
2834
 
2835
+ // src/verification/incremental.ts
2836
+ import { execFile } from "child_process";
2837
+ import { promisify } from "util";
2838
+ import { resolve } from "path";
2839
+ var execFileAsync = promisify(execFile);
2840
+ async function getChangedFiles(cwd) {
2841
+ try {
2842
+ const { stdout: stdout2 } = await execFileAsync("git", ["diff", "--name-only", "--diff-filter=AM", "HEAD"], { cwd });
2843
+ const rel = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean);
2844
+ const abs = [];
2845
+ for (const file of rel) {
2846
+ const full = resolve(cwd, file);
2847
+ if (await pathExists(full)) abs.push(full);
2848
+ }
2849
+ return abs;
2850
+ } catch {
2851
+ return [];
2852
+ }
2853
+ }
2854
+
2855
+ // src/verification/autofix/engine.ts
2856
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2857
+ import readline from "readline/promises";
2858
+ import { stdin, stdout } from "process";
2859
+ function applyEdits(content, edits) {
2860
+ const sorted = [...edits].sort((a, b) => b.start - a.start);
2861
+ let next = content;
2862
+ const patches = [];
2863
+ let skippedEdits = 0;
2864
+ let lastStart = Number.POSITIVE_INFINITY;
2865
+ for (const edit of sorted) {
2866
+ if (edit.start < 0 || edit.end < edit.start || edit.end > next.length) {
2867
+ skippedEdits++;
2868
+ continue;
2869
+ }
2870
+ if (edit.end > lastStart) {
2871
+ skippedEdits++;
2872
+ continue;
2873
+ }
2874
+ lastStart = edit.start;
2875
+ const originalText = next.slice(edit.start, edit.end);
2876
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
2877
+ patches.push({
2878
+ filePath: "",
2879
+ description: edit.description,
2880
+ start: edit.start,
2881
+ end: edit.end,
2882
+ originalText,
2883
+ fixedText: edit.text
2884
+ });
2885
+ }
2886
+ return { next, patches, skippedEdits };
2887
+ }
2888
+ async function confirmFix(prompt) {
2889
+ const rl = readline.createInterface({ input: stdin, output: stdout });
2890
+ try {
2891
+ const answer = await rl.question(`${prompt} (y/N) `);
2892
+ return answer.trim().toLowerCase() === "y";
2893
+ } finally {
2894
+ rl.close();
2895
+ }
2896
+ }
2897
+ var AutofixEngine = class {
2898
+ async applyFixes(violations, options = {}) {
2899
+ const fixable = violations.filter((v) => v.autofix && v.autofix.edits.length > 0);
2900
+ const byFile = /* @__PURE__ */ new Map();
2901
+ for (const v of fixable) {
2902
+ const list = byFile.get(v.file) ?? [];
2903
+ list.push(v);
2904
+ byFile.set(v.file, list);
2905
+ }
2906
+ const applied = [];
2907
+ let skippedViolations = 0;
2908
+ for (const [filePath, fileViolations] of byFile) {
2909
+ const original = await readFile2(filePath, "utf-8");
2910
+ const edits = [];
2911
+ for (const violation of fileViolations) {
2912
+ const fix = violation.autofix;
2913
+ if (options.interactive) {
2914
+ const ok = await confirmFix(`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`);
2915
+ if (!ok) {
2916
+ skippedViolations++;
2917
+ continue;
2918
+ }
2919
+ }
2920
+ for (const edit of fix.edits) {
2921
+ edits.push({ ...edit, description: fix.description });
2922
+ }
2923
+ }
2924
+ if (edits.length === 0) continue;
2925
+ const { next, patches, skippedEdits } = applyEdits(original, edits);
2926
+ skippedViolations += skippedEdits;
2927
+ if (!options.dryRun) {
2928
+ await writeFile2(filePath, next, "utf-8");
2929
+ }
2930
+ for (const patch of patches) {
2931
+ applied.push({ ...patch, filePath });
2932
+ }
2933
+ }
2934
+ return { applied, skipped: skippedViolations };
2935
+ }
2936
+ };
2937
+
2174
2938
  // src/propagation/graph.ts
2175
- async function buildDependencyGraph(decisions, files) {
2939
+ async function buildDependencyGraph2(decisions, files) {
2176
2940
  const nodes = /* @__PURE__ */ new Map();
2177
2941
  const decisionToFiles = /* @__PURE__ */ new Map();
2178
2942
  const fileToDecisions = /* @__PURE__ */ new Map();
@@ -2263,7 +3027,7 @@ var PropagationEngine = class {
2263
3027
  absolute: true
2264
3028
  });
2265
3029
  const decisions = this.registry.getActive();
2266
- this.graph = await buildDependencyGraph(decisions, files);
3030
+ this.graph = await buildDependencyGraph2(decisions, files);
2267
3031
  }
2268
3032
  /**
2269
3033
  * Analyze impact of changing a decision
@@ -2289,10 +3053,10 @@ var PropagationEngine = class {
2289
3053
  }
2290
3054
  fileViolations.set(violation.file, existing);
2291
3055
  }
2292
- const affectedFiles = affectedFilePaths.map((path) => ({
2293
- path,
2294
- violations: fileViolations.get(path)?.total || 0,
2295
- autoFixable: fileViolations.get(path)?.autoFixable || 0
3056
+ const affectedFiles = affectedFilePaths.map((path3) => ({
3057
+ path: path3,
3058
+ violations: fileViolations.get(path3)?.total || 0,
3059
+ autoFixable: fileViolations.get(path3)?.autoFixable || 0
2296
3060
  }));
2297
3061
  affectedFiles.sort((a, b) => b.violations - a.violations);
2298
3062
  const totalViolations = result.violations.length;
@@ -2981,11 +3745,244 @@ ${decision.decision.summary}
2981
3745
  );
2982
3746
  }
2983
3747
  };
3748
+
3749
+ // src/agent/templates.ts
3750
+ var templates = {
3751
+ "code-review": {
3752
+ name: "Code Review",
3753
+ description: "Review code for architectural compliance",
3754
+ generate: (context) => {
3755
+ return [
3756
+ "You are reviewing code for architectural compliance.",
3757
+ "",
3758
+ formatContextAsMarkdown(context),
3759
+ "",
3760
+ "Task:",
3761
+ "- Identify violations of the constraints above.",
3762
+ "- Suggest concrete changes to achieve compliance."
3763
+ ].join("\n");
3764
+ }
3765
+ },
3766
+ refactoring: {
3767
+ name: "Refactoring Guidance",
3768
+ description: "Guide refactoring to meet constraints",
3769
+ generate: (context) => {
3770
+ return [
3771
+ "You are helping refactor code to meet architectural constraints.",
3772
+ "",
3773
+ formatContextAsMarkdown(context),
3774
+ "",
3775
+ "Task:",
3776
+ "- Propose a step-by-step refactoring plan to satisfy all invariants first, then conventions/guidelines.",
3777
+ "- Highlight risky changes and suggest safe incremental steps."
3778
+ ].join("\n");
3779
+ }
3780
+ },
3781
+ migration: {
3782
+ name: "Migration Plan",
3783
+ description: "Generate a migration plan for a new/changed decision",
3784
+ generate: (context, options) => {
3785
+ const decisionId = String(options?.decisionId ?? "");
3786
+ return [
3787
+ `A new architectural decision has been introduced: ${decisionId || "<decision-id>"}`,
3788
+ "",
3789
+ formatContextAsMarkdown(context),
3790
+ "",
3791
+ "Task:",
3792
+ "- Provide an impact analysis.",
3793
+ "- Produce a step-by-step migration plan.",
3794
+ "- Include a checklist for completion."
3795
+ ].join("\n");
3796
+ }
3797
+ }
3798
+ };
3799
+
3800
+ // src/mcp/server.ts
3801
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3802
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3803
+ import { z as z3 } from "zod";
3804
+ var SpecBridgeMcpServer = class {
3805
+ server;
3806
+ cwd;
3807
+ config = null;
3808
+ registry = null;
3809
+ constructor(options) {
3810
+ this.cwd = options.cwd;
3811
+ this.server = new McpServer({ name: "specbridge", version: options.version });
3812
+ }
3813
+ async initialize() {
3814
+ this.config = await loadConfig(this.cwd);
3815
+ this.registry = createRegistry({ basePath: this.cwd });
3816
+ await this.registry.load();
3817
+ this.registerResources();
3818
+ this.registerTools();
3819
+ }
3820
+ async startStdio() {
3821
+ const transport = new StdioServerTransport();
3822
+ await this.server.connect(transport);
3823
+ }
3824
+ getReady() {
3825
+ if (!this.config || !this.registry) {
3826
+ throw new Error("SpecBridge MCP server not initialized. Call initialize() first.");
3827
+ }
3828
+ return { config: this.config, registry: this.registry };
3829
+ }
3830
+ registerResources() {
3831
+ const { config, registry } = this.getReady();
3832
+ this.server.registerResource(
3833
+ "decisions",
3834
+ "decision:///",
3835
+ {
3836
+ title: "Architectural Decisions",
3837
+ description: "List of all architectural decisions",
3838
+ mimeType: "application/json"
3839
+ },
3840
+ async (uri) => {
3841
+ const decisions = registry.getAll();
3842
+ return {
3843
+ contents: [
3844
+ {
3845
+ uri: uri.href,
3846
+ mimeType: "application/json",
3847
+ text: JSON.stringify(decisions, null, 2)
3848
+ }
3849
+ ]
3850
+ };
3851
+ }
3852
+ );
3853
+ this.server.registerResource(
3854
+ "decision",
3855
+ new ResourceTemplate("decision://{id}", { list: void 0 }),
3856
+ {
3857
+ title: "Architectural Decision",
3858
+ description: "A specific architectural decision by id",
3859
+ mimeType: "application/json"
3860
+ },
3861
+ async (uri, variables) => {
3862
+ const raw = variables.id;
3863
+ const decisionId = Array.isArray(raw) ? raw[0] ?? "" : raw ?? "";
3864
+ const decision = registry.get(String(decisionId));
3865
+ return {
3866
+ contents: [
3867
+ {
3868
+ uri: uri.href,
3869
+ mimeType: "application/json",
3870
+ text: JSON.stringify(decision, null, 2)
3871
+ }
3872
+ ]
3873
+ };
3874
+ }
3875
+ );
3876
+ this.server.registerResource(
3877
+ "latest_report",
3878
+ "report:///latest",
3879
+ {
3880
+ title: "Latest Compliance Report",
3881
+ description: "Most recent compliance report (generated on demand)",
3882
+ mimeType: "application/json"
3883
+ },
3884
+ async (uri) => {
3885
+ const report = await generateReport(config, { cwd: this.cwd });
3886
+ return {
3887
+ contents: [
3888
+ {
3889
+ uri: uri.href,
3890
+ mimeType: "application/json",
3891
+ text: JSON.stringify(report, null, 2)
3892
+ }
3893
+ ]
3894
+ };
3895
+ }
3896
+ );
3897
+ }
3898
+ registerTools() {
3899
+ const { config } = this.getReady();
3900
+ this.server.registerTool(
3901
+ "generate_context",
3902
+ {
3903
+ title: "Generate architectural context",
3904
+ description: "Generate architectural context for a file from applicable decisions",
3905
+ inputSchema: {
3906
+ filePath: z3.string().describe("Path to the file to analyze"),
3907
+ includeRationale: z3.boolean().optional().default(true),
3908
+ format: z3.enum(["markdown", "json", "mcp"]).optional().default("markdown")
3909
+ }
3910
+ },
3911
+ async (args) => {
3912
+ const text = await generateFormattedContext(args.filePath, config, {
3913
+ includeRationale: args.includeRationale,
3914
+ format: args.format,
3915
+ cwd: this.cwd
3916
+ });
3917
+ return { content: [{ type: "text", text }] };
3918
+ }
3919
+ );
3920
+ this.server.registerTool(
3921
+ "verify_compliance",
3922
+ {
3923
+ title: "Verify compliance",
3924
+ description: "Verify code compliance against constraints",
3925
+ inputSchema: {
3926
+ level: z3.enum(["commit", "pr", "full"]).optional().default("full"),
3927
+ files: z3.array(z3.string()).optional(),
3928
+ decisions: z3.array(z3.string()).optional(),
3929
+ severity: z3.array(z3.enum(["critical", "high", "medium", "low"])).optional()
3930
+ }
3931
+ },
3932
+ async (args) => {
3933
+ const engine = createVerificationEngine();
3934
+ const result = await engine.verify(config, {
3935
+ level: args.level,
3936
+ files: args.files,
3937
+ decisions: args.decisions,
3938
+ severity: args.severity,
3939
+ cwd: this.cwd
3940
+ });
3941
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
3942
+ }
3943
+ );
3944
+ this.server.registerTool(
3945
+ "get_report",
3946
+ {
3947
+ title: "Get compliance report",
3948
+ description: "Generate a compliance report for the current workspace",
3949
+ inputSchema: {
3950
+ format: z3.enum(["summary", "detailed", "json", "markdown"]).optional().default("summary"),
3951
+ includeAll: z3.boolean().optional().default(false)
3952
+ }
3953
+ },
3954
+ async (args) => {
3955
+ const report = await generateReport(config, { cwd: this.cwd, includeAll: args.includeAll });
3956
+ if (args.format === "json") {
3957
+ return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
3958
+ }
3959
+ if (args.format === "markdown") {
3960
+ return { content: [{ type: "text", text: formatMarkdownReport(report) }] };
3961
+ }
3962
+ if (args.format === "detailed") {
3963
+ return { content: [{ type: "text", text: formatConsoleReport(report) }] };
3964
+ }
3965
+ const summary = {
3966
+ timestamp: report.timestamp,
3967
+ project: report.project,
3968
+ compliance: report.summary.compliance,
3969
+ violations: report.summary.violations,
3970
+ decisions: report.summary.activeDecisions
3971
+ };
3972
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
3973
+ }
3974
+ );
3975
+ }
3976
+ };
2984
3977
  export {
2985
3978
  AgentContextGenerator,
2986
3979
  AlreadyInitializedError,
2987
3980
  AnalyzerNotFoundError,
3981
+ ApiVerifier,
3982
+ AstCache,
3983
+ AutofixEngine,
2988
3984
  CodeScanner,
3985
+ ComplexityVerifier,
2989
3986
  ConfigError,
2990
3987
  ConstraintExceptionSchema,
2991
3988
  ConstraintSchema,
@@ -2996,6 +3993,7 @@ export {
2996
3993
  DecisionSchema,
2997
3994
  DecisionStatusSchema,
2998
3995
  DecisionValidationError,
3996
+ DependencyVerifier,
2999
3997
  ErrorsAnalyzer,
3000
3998
  ErrorsVerifier,
3001
3999
  FileSystemError,
@@ -3013,16 +4011,18 @@ export {
3013
4011
  Registry,
3014
4012
  RegistryError,
3015
4013
  Reporter,
4014
+ SecurityVerifier,
3016
4015
  SeveritySchema,
3017
4016
  SpecBridgeConfigSchema,
3018
4017
  SpecBridgeError,
4018
+ SpecBridgeMcpServer,
3019
4019
  StructureAnalyzer,
3020
4020
  VerificationConfigSchema,
3021
4021
  VerificationEngine,
3022
4022
  VerificationError,
3023
4023
  VerificationFrequencySchema,
3024
4024
  VerifierNotFoundError,
3025
- buildDependencyGraph,
4025
+ buildDependencyGraph2 as buildDependencyGraph,
3026
4026
  builtinAnalyzers,
3027
4027
  builtinVerifiers,
3028
4028
  calculateConfidence,
@@ -3051,6 +4051,7 @@ export {
3051
4051
  getAffectingDecisions,
3052
4052
  getAnalyzer,
3053
4053
  getAnalyzerIds,
4054
+ getChangedFiles,
3054
4055
  getConfigPath,
3055
4056
  getDecisionsDir,
3056
4057
  getInferredDir,
@@ -3061,6 +4062,7 @@ export {
3061
4062
  getVerifierIds,
3062
4063
  getVerifiersDir,
3063
4064
  glob,
4065
+ isConstraintExcepted,
3064
4066
  isDirectory,
3065
4067
  loadConfig,
3066
4068
  loadDecisionFile,
@@ -3077,7 +4079,9 @@ export {
3077
4079
  readTextFile,
3078
4080
  runInference,
3079
4081
  selectVerifierForConstraint,
4082
+ shouldApplyConstraintToFile,
3080
4083
  stringifyYaml,
4084
+ templates,
3081
4085
  updateYamlDocument,
3082
4086
  validateConfig,
3083
4087
  validateDecision,