@ipation/specbridge 2.4.7 → 2.4.9

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
@@ -1774,6 +1774,283 @@ function createRegistry(options) {
1774
1774
  return new Registry(options);
1775
1775
  }
1776
1776
 
1777
+ // src/verification/cache.ts
1778
+ import { stat as stat2, readFile as readFile2 } from "fs/promises";
1779
+ import { createHash } from "crypto";
1780
+ var AstCache = class {
1781
+ cache = /* @__PURE__ */ new Map();
1782
+ async get(filePath, project) {
1783
+ try {
1784
+ const stats = await stat2(filePath);
1785
+ const cached = this.cache.get(filePath);
1786
+ if (cached && cached.mtimeMs >= stats.mtimeMs) {
1787
+ return cached.sourceFile;
1788
+ }
1789
+ const content = await readFile2(filePath, "utf-8");
1790
+ const hash = createHash("sha256").update(content).digest("hex");
1791
+ if (cached && cached.hash === hash) {
1792
+ cached.mtimeMs = stats.mtimeMs;
1793
+ return cached.sourceFile;
1794
+ }
1795
+ let sourceFile = project.getSourceFile(filePath);
1796
+ if (!sourceFile) {
1797
+ sourceFile = project.addSourceFileAtPath(filePath);
1798
+ } else {
1799
+ sourceFile.refreshFromFileSystemSync();
1800
+ }
1801
+ this.cache.set(filePath, {
1802
+ sourceFile,
1803
+ hash,
1804
+ mtimeMs: stats.mtimeMs
1805
+ });
1806
+ return sourceFile;
1807
+ } catch {
1808
+ return null;
1809
+ }
1810
+ }
1811
+ clear() {
1812
+ this.cache.clear();
1813
+ }
1814
+ getStats() {
1815
+ return {
1816
+ entries: this.cache.size,
1817
+ memoryEstimate: this.cache.size * 5e4
1818
+ // Rough estimate: 50KB per AST
1819
+ };
1820
+ }
1821
+ };
1822
+
1823
+ // src/verification/file-verifier.ts
1824
+ import { createHash as createHash2 } from "crypto";
1825
+ import { readFile as readFile3 } from "fs/promises";
1826
+
1827
+ // src/verification/applicability.ts
1828
+ function isConstraintExcepted(filePath, constraint, cwd) {
1829
+ if (!constraint.exceptions) return false;
1830
+ return constraint.exceptions.some((exception) => {
1831
+ if (exception.expiresAt) {
1832
+ const expiryDate = new Date(exception.expiresAt);
1833
+ if (expiryDate < /* @__PURE__ */ new Date()) {
1834
+ return false;
1835
+ }
1836
+ }
1837
+ return matchesPattern(filePath, exception.pattern, { cwd });
1838
+ });
1839
+ }
1840
+ function shouldApplyConstraintToFile(params) {
1841
+ const { filePath, constraint, cwd, severityFilter } = params;
1842
+ if (!matchesPattern(filePath, constraint.scope, { cwd })) return false;
1843
+ if (severityFilter && !severityFilter.includes(constraint.severity)) return false;
1844
+ if (isConstraintExcepted(filePath, constraint, cwd)) return false;
1845
+ return true;
1846
+ }
1847
+
1848
+ // src/verification/plugins/loader.ts
1849
+ import { existsSync } from "fs";
1850
+ import { join as join5 } from "path";
1851
+ import { pathToFileURL } from "url";
1852
+ import fg2 from "fast-glob";
1853
+ var PluginLoader = class {
1854
+ plugins = /* @__PURE__ */ new Map();
1855
+ loaded = false;
1856
+ loadErrors = [];
1857
+ logger = getLogger({ module: "verification.plugins.loader" });
1858
+ /**
1859
+ * Load all plugins from the specified base path
1860
+ *
1861
+ * @param basePath - Project root directory (usually cwd)
1862
+ */
1863
+ async loadPlugins(basePath) {
1864
+ const verifiersDir = join5(basePath, ".specbridge", "verifiers");
1865
+ if (!existsSync(verifiersDir)) {
1866
+ this.loaded = true;
1867
+ return;
1868
+ }
1869
+ const files = await fg2("**/*.{ts,js}", {
1870
+ cwd: verifiersDir,
1871
+ absolute: true,
1872
+ ignore: ["**/*.test.{ts,js}", "**/*.d.ts"]
1873
+ });
1874
+ for (const file of files) {
1875
+ try {
1876
+ await this.loadPlugin(file);
1877
+ } catch (error) {
1878
+ const message = error instanceof Error ? error.message : String(error);
1879
+ this.loadErrors.push({ file, error: message });
1880
+ this.logger.warn({ file, error: message }, "Failed to load plugin");
1881
+ }
1882
+ }
1883
+ this.loaded = true;
1884
+ if (this.plugins.size > 0) {
1885
+ this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
1886
+ }
1887
+ if (this.loadErrors.length > 0) {
1888
+ this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
1889
+ }
1890
+ }
1891
+ /**
1892
+ * Load a single plugin file
1893
+ */
1894
+ async loadPlugin(filePath) {
1895
+ const fileUrl = pathToFileURL(filePath).href;
1896
+ const module = await import(fileUrl);
1897
+ const plugin = module.default || module.plugin;
1898
+ if (!plugin) {
1899
+ throw new Error('Plugin must export a default or named "plugin" export');
1900
+ }
1901
+ this.validatePlugin(plugin, filePath);
1902
+ if (this.plugins.has(plugin.metadata.id)) {
1903
+ throw new Error(
1904
+ `Plugin ID "${plugin.metadata.id}" is already registered. Each plugin must have a unique ID.`
1905
+ );
1906
+ }
1907
+ this.plugins.set(plugin.metadata.id, plugin);
1908
+ }
1909
+ /**
1910
+ * Validate plugin structure and metadata
1911
+ */
1912
+ validatePlugin(plugin, _filePath) {
1913
+ if (!plugin.metadata) {
1914
+ throw new Error('Plugin must have a "metadata" property');
1915
+ }
1916
+ if (!plugin.metadata.id || typeof plugin.metadata.id !== "string") {
1917
+ throw new Error('Plugin metadata must have a string "id" property');
1918
+ }
1919
+ if (!plugin.metadata.version || typeof plugin.metadata.version !== "string") {
1920
+ throw new Error('Plugin metadata must have a string "version" property');
1921
+ }
1922
+ const idPattern = /^[a-z][a-z0-9-]*$/;
1923
+ if (!idPattern.test(plugin.metadata.id)) {
1924
+ throw new Error(
1925
+ `Plugin ID "${plugin.metadata.id}" is invalid. ID must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`
1926
+ );
1927
+ }
1928
+ if (typeof plugin.createVerifier !== "function") {
1929
+ throw new Error('Plugin must have a "createVerifier" function');
1930
+ }
1931
+ let verifier;
1932
+ try {
1933
+ verifier = plugin.createVerifier();
1934
+ } catch (error) {
1935
+ const message = error instanceof Error ? error.message : String(error);
1936
+ throw new Error(`createVerifier() threw an error: ${message}`);
1937
+ }
1938
+ if (!verifier || typeof verifier !== "object") {
1939
+ throw new Error("createVerifier() must return an object");
1940
+ }
1941
+ if (!verifier.id || typeof verifier.id !== "string") {
1942
+ throw new Error('Verifier must have a string "id" property');
1943
+ }
1944
+ if (!verifier.name || typeof verifier.name !== "string") {
1945
+ throw new Error('Verifier must have a string "name" property');
1946
+ }
1947
+ if (!verifier.description || typeof verifier.description !== "string") {
1948
+ throw new Error('Verifier must have a string "description" property');
1949
+ }
1950
+ if (typeof verifier.verify !== "function") {
1951
+ throw new Error('Verifier must have a "verify" method');
1952
+ }
1953
+ if (verifier.id !== plugin.metadata.id) {
1954
+ throw new Error(
1955
+ `Verifier ID "${verifier.id}" does not match plugin metadata ID "${plugin.metadata.id}". These must be identical.`
1956
+ );
1957
+ }
1958
+ }
1959
+ /**
1960
+ * Get a verifier instance by ID
1961
+ *
1962
+ * @param id - Verifier ID
1963
+ * @returns Verifier instance or null if not found
1964
+ */
1965
+ getVerifier(id) {
1966
+ const plugin = this.plugins.get(id);
1967
+ return plugin ? plugin.createVerifier() : null;
1968
+ }
1969
+ /**
1970
+ * Get a plugin by ID (includes paramsSchema)
1971
+ *
1972
+ * @param id - Plugin ID
1973
+ * @returns Plugin or null if not found
1974
+ */
1975
+ getPlugin(id) {
1976
+ return this.plugins.get(id) || null;
1977
+ }
1978
+ /**
1979
+ * Validate params against a plugin's paramsSchema
1980
+ *
1981
+ * @param id - Plugin ID
1982
+ * @param params - Parameters to validate
1983
+ * @returns Validation result with success flag and error message if failed
1984
+ */
1985
+ validateParams(id, params) {
1986
+ const plugin = this.plugins.get(id);
1987
+ if (!plugin) {
1988
+ return { success: false, error: `Plugin ${id} not found` };
1989
+ }
1990
+ if (!plugin.paramsSchema) {
1991
+ return { success: true };
1992
+ }
1993
+ if (!params) {
1994
+ return { success: false, error: `Plugin ${id} requires params but none were provided` };
1995
+ }
1996
+ if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
1997
+ return {
1998
+ success: false,
1999
+ error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
2000
+ };
2001
+ }
2002
+ const schema = plugin.paramsSchema;
2003
+ if (schema.safeParse) {
2004
+ const result = schema.safeParse(params);
2005
+ if (!result.success) {
2006
+ const errors = result.error?.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ") || "Validation failed";
2007
+ return { success: false, error: `Invalid params for ${id}: ${errors}` };
2008
+ }
2009
+ } else {
2010
+ try {
2011
+ schema.parse(params);
2012
+ } catch (error) {
2013
+ const message = error instanceof Error ? error.message : String(error);
2014
+ return { success: false, error: `Invalid params for ${id}: ${message}` };
2015
+ }
2016
+ }
2017
+ return { success: true };
2018
+ }
2019
+ /**
2020
+ * Get all registered plugin IDs
2021
+ */
2022
+ getPluginIds() {
2023
+ return Array.from(this.plugins.keys());
2024
+ }
2025
+ /**
2026
+ * Check if plugins have been loaded
2027
+ */
2028
+ isLoaded() {
2029
+ return this.loaded;
2030
+ }
2031
+ /**
2032
+ * Get load errors (for diagnostics)
2033
+ */
2034
+ getLoadErrors() {
2035
+ return [...this.loadErrors];
2036
+ }
2037
+ /**
2038
+ * Reset the loader (useful for testing)
2039
+ */
2040
+ reset() {
2041
+ this.plugins.clear();
2042
+ this.loaded = false;
2043
+ this.loadErrors = [];
2044
+ }
2045
+ };
2046
+ var pluginLoader = null;
2047
+ function getPluginLoader() {
2048
+ if (!pluginLoader) {
2049
+ pluginLoader = new PluginLoader();
2050
+ }
2051
+ return pluginLoader;
2052
+ }
2053
+
1777
2054
  // src/verification/verifiers/base.ts
1778
2055
  function createViolation(params) {
1779
2056
  return params;
@@ -2633,418 +2910,212 @@ function isStringLiteralLike(node) {
2633
2910
  }
2634
2911
  var SecurityVerifier = class {
2635
2912
  id = "security";
2636
- name = "Security Verifier";
2637
- description = "Detects common security footguns (secrets, eval, XSS/SQL injection heuristics)";
2638
- async verify(ctx) {
2639
- const violations = [];
2640
- const { sourceFile, constraint, decisionId, filePath } = ctx;
2641
- const rule = constraint.rule.toLowerCase();
2642
- const checkSecrets = rule.includes("secret") || rule.includes("password") || rule.includes("token") || rule.includes("api key") || rule.includes("hardcoded");
2643
- const checkEval = rule.includes("eval") || rule.includes("function constructor");
2644
- const checkXss = rule.includes("xss") || rule.includes("innerhtml") || rule.includes("dangerouslysetinnerhtml");
2645
- const checkSql = rule.includes("sql") || rule.includes("injection");
2646
- const checkProto = rule.includes("prototype pollution") || rule.includes("__proto__");
2647
- if (checkSecrets) {
2648
- for (const vd of sourceFile.getVariableDeclarations()) {
2649
- const name = vd.getName();
2650
- if (!SECRET_NAME_RE.test(name)) continue;
2651
- const init = vd.getInitializer();
2652
- if (!init || !isStringLiteralLike(init)) continue;
2653
- const value = init.getText().slice(1, -1);
2654
- if (value.length === 0) continue;
2655
- violations.push(
2656
- createViolation({
2657
- decisionId,
2658
- constraintId: constraint.id,
2659
- type: constraint.type,
2660
- severity: constraint.severity,
2661
- message: `Possible hardcoded secret in variable "${name}"`,
2662
- file: filePath,
2663
- line: vd.getStartLineNumber(),
2664
- suggestion: "Move secrets to environment variables or a secret manager"
2665
- })
2666
- );
2667
- }
2668
- for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
2669
- const propName = pa.getNameNode().getText();
2670
- if (!SECRET_NAME_RE.test(propName)) continue;
2671
- const init = pa.getInitializer();
2672
- if (!init || !isStringLiteralLike(init)) continue;
2673
- violations.push(
2674
- createViolation({
2675
- decisionId,
2676
- constraintId: constraint.id,
2677
- type: constraint.type,
2678
- severity: constraint.severity,
2679
- message: `Possible hardcoded secret in object property ${propName}`,
2680
- file: filePath,
2681
- line: pa.getStartLineNumber(),
2682
- suggestion: "Move secrets to environment variables or a secret manager"
2683
- })
2684
- );
2685
- }
2686
- }
2687
- if (checkEval) {
2688
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2689
- const exprText = call.getExpression().getText();
2690
- if (exprText === "eval" || exprText === "Function") {
2691
- violations.push(
2692
- createViolation({
2693
- decisionId,
2694
- constraintId: constraint.id,
2695
- type: constraint.type,
2696
- severity: constraint.severity,
2697
- message: `Unsafe dynamic code execution via ${exprText}()`,
2698
- file: filePath,
2699
- line: call.getStartLineNumber(),
2700
- suggestion: "Avoid eval/Function; use safer alternatives"
2701
- })
2702
- );
2703
- }
2704
- }
2705
- }
2706
- if (checkXss) {
2707
- for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
2708
- const left = bin.getLeft();
2709
- const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
2710
- if (!propertyAccess) continue;
2711
- if (propertyAccess.getName() === "innerHTML") {
2712
- violations.push(
2713
- createViolation({
2714
- decisionId,
2715
- constraintId: constraint.id,
2716
- type: constraint.type,
2717
- severity: constraint.severity,
2718
- message: "Potential XSS: assignment to innerHTML",
2719
- file: filePath,
2720
- line: bin.getStartLineNumber(),
2721
- suggestion: "Prefer textContent or a safe templating/escaping strategy"
2722
- })
2723
- );
2724
- }
2725
- }
2726
- if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
2727
- violations.push(
2728
- createViolation({
2729
- decisionId,
2730
- constraintId: constraint.id,
2731
- type: constraint.type,
2732
- severity: constraint.severity,
2733
- message: "Potential XSS: usage of dangerouslySetInnerHTML",
2734
- file: filePath,
2735
- line: 1,
2736
- suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
2737
- })
2738
- );
2739
- }
2740
- }
2741
- if (checkSql) {
2742
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2743
- const expr = call.getExpression();
2744
- const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
2745
- if (!propertyAccess) continue;
2746
- const name = propertyAccess.getName();
2747
- if (name !== "query" && name !== "execute") continue;
2748
- const arg = call.getArguments()[0];
2749
- if (!arg) continue;
2750
- const isTemplate = arg.getKind() === SyntaxKind3.TemplateExpression;
2751
- const isConcat = arg.getKind() === SyntaxKind3.BinaryExpression && arg.getText().includes("+");
2752
- if (!isTemplate && !isConcat) continue;
2753
- const text = arg.getText().toLowerCase();
2754
- if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
2755
- continue;
2756
- }
2757
- violations.push(
2758
- createViolation({
2759
- decisionId,
2760
- constraintId: constraint.id,
2761
- type: constraint.type,
2762
- severity: constraint.severity,
2763
- message: "Potential SQL injection: dynamically constructed SQL query",
2764
- file: filePath,
2765
- line: call.getStartLineNumber(),
2766
- suggestion: "Use parameterized queries / prepared statements"
2767
- })
2768
- );
2769
- }
2770
- }
2771
- if (checkProto) {
2772
- const text = sourceFile.getFullText();
2773
- if (text.includes("__proto__") || text.includes("constructor.prototype")) {
2913
+ name = "Security Verifier";
2914
+ description = "Detects common security footguns (secrets, eval, XSS/SQL injection heuristics)";
2915
+ async verify(ctx) {
2916
+ const violations = [];
2917
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
2918
+ const rule = constraint.rule.toLowerCase();
2919
+ const checkSecrets = rule.includes("secret") || rule.includes("password") || rule.includes("token") || rule.includes("api key") || rule.includes("hardcoded");
2920
+ const checkEval = rule.includes("eval") || rule.includes("function constructor");
2921
+ const checkXss = rule.includes("xss") || rule.includes("innerhtml") || rule.includes("dangerouslysetinnerhtml");
2922
+ const checkSql = rule.includes("sql") || rule.includes("injection");
2923
+ const checkProto = rule.includes("prototype pollution") || rule.includes("__proto__");
2924
+ if (checkSecrets) {
2925
+ for (const vd of sourceFile.getVariableDeclarations()) {
2926
+ const name = vd.getName();
2927
+ if (!SECRET_NAME_RE.test(name)) continue;
2928
+ const init = vd.getInitializer();
2929
+ if (!init || !isStringLiteralLike(init)) continue;
2930
+ const value = init.getText().slice(1, -1);
2931
+ if (value.length === 0) continue;
2774
2932
  violations.push(
2775
2933
  createViolation({
2776
2934
  decisionId,
2777
2935
  constraintId: constraint.id,
2778
2936
  type: constraint.type,
2779
2937
  severity: constraint.severity,
2780
- message: "Potential prototype pollution pattern detected",
2938
+ message: `Possible hardcoded secret in variable "${name}"`,
2781
2939
  file: filePath,
2782
- line: 1,
2783
- suggestion: "Avoid writing to __proto__/prototype; validate object keys"
2940
+ line: vd.getStartLineNumber(),
2941
+ suggestion: "Move secrets to environment variables or a secret manager"
2784
2942
  })
2785
2943
  );
2786
2944
  }
2787
- }
2788
- return violations;
2789
- }
2790
- };
2791
-
2792
- // src/verification/verifiers/api.ts
2793
- import { SyntaxKind as SyntaxKind4 } from "ts-morph";
2794
- var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
2795
- function isKebabPath(pathValue) {
2796
- const parts = pathValue.split("/").filter(Boolean);
2797
- for (const part of parts) {
2798
- if (part.startsWith(":")) continue;
2799
- if (!/^[a-z0-9-]+$/.test(part)) return false;
2800
- }
2801
- return true;
2802
- }
2803
- var ApiVerifier = class {
2804
- id = "api";
2805
- name = "API Consistency Verifier";
2806
- description = "Checks basic REST endpoint naming conventions in common frameworks";
2807
- async verify(ctx) {
2808
- const violations = [];
2809
- const { sourceFile, constraint, decisionId, filePath } = ctx;
2810
- const rule = constraint.rule.toLowerCase();
2811
- const enforceKebab = rule.includes("kebab") || rule.includes("kebab-case");
2812
- if (!enforceKebab) return violations;
2813
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
2814
- const expr = call.getExpression();
2815
- const propertyAccess = expr.asKind(SyntaxKind4.PropertyAccessExpression);
2816
- if (!propertyAccess) continue;
2817
- const method = propertyAccess.getName();
2818
- if (!method || !HTTP_METHODS.has(String(method))) continue;
2819
- const firstArg = call.getArguments()[0];
2820
- const stringLiteral = firstArg?.asKind(SyntaxKind4.StringLiteral);
2821
- if (!stringLiteral) continue;
2822
- const pathValue = stringLiteral.getLiteralValue();
2823
- if (typeof pathValue !== "string") continue;
2824
- if (!isKebabPath(pathValue)) {
2945
+ for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
2946
+ const propName = pa.getNameNode().getText();
2947
+ if (!SECRET_NAME_RE.test(propName)) continue;
2948
+ const init = pa.getInitializer();
2949
+ if (!init || !isStringLiteralLike(init)) continue;
2825
2950
  violations.push(
2826
2951
  createViolation({
2827
2952
  decisionId,
2828
2953
  constraintId: constraint.id,
2829
2954
  type: constraint.type,
2830
2955
  severity: constraint.severity,
2831
- message: `Endpoint path "${pathValue}" is not kebab-case`,
2956
+ message: `Possible hardcoded secret in object property ${propName}`,
2832
2957
  file: filePath,
2833
- line: call.getStartLineNumber(),
2834
- suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
2958
+ line: pa.getStartLineNumber(),
2959
+ suggestion: "Move secrets to environment variables or a secret manager"
2835
2960
  })
2836
2961
  );
2837
2962
  }
2838
2963
  }
2839
- return violations;
2840
- }
2841
- };
2842
-
2843
- // src/verification/plugins/loader.ts
2844
- import { existsSync } from "fs";
2845
- import { join as join5 } from "path";
2846
- import { pathToFileURL } from "url";
2847
- import fg2 from "fast-glob";
2848
- var PluginLoader = class {
2849
- plugins = /* @__PURE__ */ new Map();
2850
- loaded = false;
2851
- loadErrors = [];
2852
- logger = getLogger({ module: "verification.plugins.loader" });
2853
- /**
2854
- * Load all plugins from the specified base path
2855
- *
2856
- * @param basePath - Project root directory (usually cwd)
2857
- */
2858
- async loadPlugins(basePath) {
2859
- const verifiersDir = join5(basePath, ".specbridge", "verifiers");
2860
- if (!existsSync(verifiersDir)) {
2861
- this.loaded = true;
2862
- return;
2863
- }
2864
- const files = await fg2("**/*.{ts,js}", {
2865
- cwd: verifiersDir,
2866
- absolute: true,
2867
- ignore: ["**/*.test.{ts,js}", "**/*.d.ts"]
2868
- });
2869
- for (const file of files) {
2870
- try {
2871
- await this.loadPlugin(file);
2872
- } catch (error) {
2873
- const message = error instanceof Error ? error.message : String(error);
2874
- this.loadErrors.push({ file, error: message });
2875
- this.logger.warn({ file, error: message }, "Failed to load plugin");
2964
+ if (checkEval) {
2965
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2966
+ const exprText = call.getExpression().getText();
2967
+ if (exprText === "eval" || exprText === "Function") {
2968
+ violations.push(
2969
+ createViolation({
2970
+ decisionId,
2971
+ constraintId: constraint.id,
2972
+ type: constraint.type,
2973
+ severity: constraint.severity,
2974
+ message: `Unsafe dynamic code execution via ${exprText}()`,
2975
+ file: filePath,
2976
+ line: call.getStartLineNumber(),
2977
+ suggestion: "Avoid eval/Function; use safer alternatives"
2978
+ })
2979
+ );
2980
+ }
2876
2981
  }
2877
2982
  }
2878
- this.loaded = true;
2879
- if (this.plugins.size > 0) {
2880
- this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
2881
- }
2882
- if (this.loadErrors.length > 0) {
2883
- this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
2884
- }
2885
- }
2886
- /**
2887
- * Load a single plugin file
2888
- */
2889
- async loadPlugin(filePath) {
2890
- const fileUrl = pathToFileURL(filePath).href;
2891
- const module = await import(fileUrl);
2892
- const plugin = module.default || module.plugin;
2893
- if (!plugin) {
2894
- throw new Error('Plugin must export a default or named "plugin" export');
2895
- }
2896
- this.validatePlugin(plugin, filePath);
2897
- if (this.plugins.has(plugin.metadata.id)) {
2898
- throw new Error(
2899
- `Plugin ID "${plugin.metadata.id}" is already registered. Each plugin must have a unique ID.`
2900
- );
2901
- }
2902
- this.plugins.set(plugin.metadata.id, plugin);
2903
- }
2904
- /**
2905
- * Validate plugin structure and metadata
2906
- */
2907
- validatePlugin(plugin, _filePath) {
2908
- if (!plugin.metadata) {
2909
- throw new Error('Plugin must have a "metadata" property');
2910
- }
2911
- if (!plugin.metadata.id || typeof plugin.metadata.id !== "string") {
2912
- throw new Error('Plugin metadata must have a string "id" property');
2913
- }
2914
- if (!plugin.metadata.version || typeof plugin.metadata.version !== "string") {
2915
- throw new Error('Plugin metadata must have a string "version" property');
2916
- }
2917
- const idPattern = /^[a-z][a-z0-9-]*$/;
2918
- if (!idPattern.test(plugin.metadata.id)) {
2919
- throw new Error(
2920
- `Plugin ID "${plugin.metadata.id}" is invalid. ID must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`
2921
- );
2922
- }
2923
- if (typeof plugin.createVerifier !== "function") {
2924
- throw new Error('Plugin must have a "createVerifier" function');
2925
- }
2926
- let verifier;
2927
- try {
2928
- verifier = plugin.createVerifier();
2929
- } catch (error) {
2930
- const message = error instanceof Error ? error.message : String(error);
2931
- throw new Error(`createVerifier() threw an error: ${message}`);
2932
- }
2933
- if (!verifier || typeof verifier !== "object") {
2934
- throw new Error("createVerifier() must return an object");
2935
- }
2936
- if (!verifier.id || typeof verifier.id !== "string") {
2937
- throw new Error('Verifier must have a string "id" property');
2938
- }
2939
- if (!verifier.name || typeof verifier.name !== "string") {
2940
- throw new Error('Verifier must have a string "name" property');
2941
- }
2942
- if (!verifier.description || typeof verifier.description !== "string") {
2943
- throw new Error('Verifier must have a string "description" property');
2944
- }
2945
- if (typeof verifier.verify !== "function") {
2946
- throw new Error('Verifier must have a "verify" method');
2947
- }
2948
- if (verifier.id !== plugin.metadata.id) {
2949
- throw new Error(
2950
- `Verifier ID "${verifier.id}" does not match plugin metadata ID "${plugin.metadata.id}". These must be identical.`
2951
- );
2952
- }
2953
- }
2954
- /**
2955
- * Get a verifier instance by ID
2956
- *
2957
- * @param id - Verifier ID
2958
- * @returns Verifier instance or null if not found
2959
- */
2960
- getVerifier(id) {
2961
- const plugin = this.plugins.get(id);
2962
- return plugin ? plugin.createVerifier() : null;
2963
- }
2964
- /**
2965
- * Get a plugin by ID (includes paramsSchema)
2966
- *
2967
- * @param id - Plugin ID
2968
- * @returns Plugin or null if not found
2969
- */
2970
- getPlugin(id) {
2971
- return this.plugins.get(id) || null;
2972
- }
2973
- /**
2974
- * Validate params against a plugin's paramsSchema
2975
- *
2976
- * @param id - Plugin ID
2977
- * @param params - Parameters to validate
2978
- * @returns Validation result with success flag and error message if failed
2979
- */
2980
- validateParams(id, params) {
2981
- const plugin = this.plugins.get(id);
2982
- if (!plugin) {
2983
- return { success: false, error: `Plugin ${id} not found` };
2984
- }
2985
- if (!plugin.paramsSchema) {
2986
- return { success: true };
2987
- }
2988
- if (!params) {
2989
- return { success: false, error: `Plugin ${id} requires params but none were provided` };
2990
- }
2991
- if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
2992
- return {
2993
- success: false,
2994
- error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
2995
- };
2983
+ if (checkXss) {
2984
+ for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
2985
+ const left = bin.getLeft();
2986
+ const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
2987
+ if (!propertyAccess) continue;
2988
+ if (propertyAccess.getName() === "innerHTML") {
2989
+ violations.push(
2990
+ createViolation({
2991
+ decisionId,
2992
+ constraintId: constraint.id,
2993
+ type: constraint.type,
2994
+ severity: constraint.severity,
2995
+ message: "Potential XSS: assignment to innerHTML",
2996
+ file: filePath,
2997
+ line: bin.getStartLineNumber(),
2998
+ suggestion: "Prefer textContent or a safe templating/escaping strategy"
2999
+ })
3000
+ );
3001
+ }
3002
+ }
3003
+ if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
3004
+ violations.push(
3005
+ createViolation({
3006
+ decisionId,
3007
+ constraintId: constraint.id,
3008
+ type: constraint.type,
3009
+ severity: constraint.severity,
3010
+ message: "Potential XSS: usage of dangerouslySetInnerHTML",
3011
+ file: filePath,
3012
+ line: 1,
3013
+ suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
3014
+ })
3015
+ );
3016
+ }
2996
3017
  }
2997
- const schema = plugin.paramsSchema;
2998
- if (schema.safeParse) {
2999
- const result = schema.safeParse(params);
3000
- if (!result.success) {
3001
- const errors = result.error?.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ") || "Validation failed";
3002
- return { success: false, error: `Invalid params for ${id}: ${errors}` };
3018
+ if (checkSql) {
3019
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
3020
+ const expr = call.getExpression();
3021
+ const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
3022
+ if (!propertyAccess) continue;
3023
+ const name = propertyAccess.getName();
3024
+ if (name !== "query" && name !== "execute") continue;
3025
+ const arg = call.getArguments()[0];
3026
+ if (!arg) continue;
3027
+ const isTemplate = arg.getKind() === SyntaxKind3.TemplateExpression;
3028
+ const isConcat = arg.getKind() === SyntaxKind3.BinaryExpression && arg.getText().includes("+");
3029
+ if (!isTemplate && !isConcat) continue;
3030
+ const text = arg.getText().toLowerCase();
3031
+ if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
3032
+ continue;
3033
+ }
3034
+ violations.push(
3035
+ createViolation({
3036
+ decisionId,
3037
+ constraintId: constraint.id,
3038
+ type: constraint.type,
3039
+ severity: constraint.severity,
3040
+ message: "Potential SQL injection: dynamically constructed SQL query",
3041
+ file: filePath,
3042
+ line: call.getStartLineNumber(),
3043
+ suggestion: "Use parameterized queries / prepared statements"
3044
+ })
3045
+ );
3003
3046
  }
3004
- } else {
3005
- try {
3006
- schema.parse(params);
3007
- } catch (error) {
3008
- const message = error instanceof Error ? error.message : String(error);
3009
- return { success: false, error: `Invalid params for ${id}: ${message}` };
3047
+ }
3048
+ if (checkProto) {
3049
+ const text = sourceFile.getFullText();
3050
+ if (text.includes("__proto__") || text.includes("constructor.prototype")) {
3051
+ violations.push(
3052
+ createViolation({
3053
+ decisionId,
3054
+ constraintId: constraint.id,
3055
+ type: constraint.type,
3056
+ severity: constraint.severity,
3057
+ message: "Potential prototype pollution pattern detected",
3058
+ file: filePath,
3059
+ line: 1,
3060
+ suggestion: "Avoid writing to __proto__/prototype; validate object keys"
3061
+ })
3062
+ );
3010
3063
  }
3011
3064
  }
3012
- return { success: true };
3013
- }
3014
- /**
3015
- * Get all registered plugin IDs
3016
- */
3017
- getPluginIds() {
3018
- return Array.from(this.plugins.keys());
3019
- }
3020
- /**
3021
- * Check if plugins have been loaded
3022
- */
3023
- isLoaded() {
3024
- return this.loaded;
3025
- }
3026
- /**
3027
- * Get load errors (for diagnostics)
3028
- */
3029
- getLoadErrors() {
3030
- return [...this.loadErrors];
3031
- }
3032
- /**
3033
- * Reset the loader (useful for testing)
3034
- */
3035
- reset() {
3036
- this.plugins.clear();
3037
- this.loaded = false;
3038
- this.loadErrors = [];
3065
+ return violations;
3039
3066
  }
3040
3067
  };
3041
- var pluginLoader = null;
3042
- function getPluginLoader() {
3043
- if (!pluginLoader) {
3044
- pluginLoader = new PluginLoader();
3068
+
3069
+ // src/verification/verifiers/api.ts
3070
+ import { SyntaxKind as SyntaxKind4 } from "ts-morph";
3071
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
3072
+ function isKebabPath(pathValue) {
3073
+ const parts = pathValue.split("/").filter(Boolean);
3074
+ for (const part of parts) {
3075
+ if (part.startsWith(":")) continue;
3076
+ if (!/^[a-z0-9-]+$/.test(part)) return false;
3045
3077
  }
3046
- return pluginLoader;
3078
+ return true;
3047
3079
  }
3080
+ var ApiVerifier = class {
3081
+ id = "api";
3082
+ name = "API Consistency Verifier";
3083
+ description = "Checks basic REST endpoint naming conventions in common frameworks";
3084
+ async verify(ctx) {
3085
+ const violations = [];
3086
+ const { sourceFile, constraint, decisionId, filePath } = ctx;
3087
+ const rule = constraint.rule.toLowerCase();
3088
+ const enforceKebab = rule.includes("kebab") || rule.includes("kebab-case");
3089
+ if (!enforceKebab) return violations;
3090
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
3091
+ const expr = call.getExpression();
3092
+ const propertyAccess = expr.asKind(SyntaxKind4.PropertyAccessExpression);
3093
+ if (!propertyAccess) continue;
3094
+ const method = propertyAccess.getName();
3095
+ if (!method || !HTTP_METHODS.has(String(method))) continue;
3096
+ const firstArg = call.getArguments()[0];
3097
+ const stringLiteral = firstArg?.asKind(SyntaxKind4.StringLiteral);
3098
+ if (!stringLiteral) continue;
3099
+ const pathValue = stringLiteral.getLiteralValue();
3100
+ if (typeof pathValue !== "string") continue;
3101
+ if (!isKebabPath(pathValue)) {
3102
+ violations.push(
3103
+ createViolation({
3104
+ decisionId,
3105
+ constraintId: constraint.id,
3106
+ type: constraint.type,
3107
+ severity: constraint.severity,
3108
+ message: `Endpoint path "${pathValue}" is not kebab-case`,
3109
+ file: filePath,
3110
+ line: call.getStartLineNumber(),
3111
+ suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
3112
+ })
3113
+ );
3114
+ }
3115
+ }
3116
+ return violations;
3117
+ }
3118
+ };
3048
3119
 
3049
3120
  // src/verification/verifiers/index.ts
3050
3121
  var builtinVerifiers = {
@@ -3117,51 +3188,216 @@ function selectVerifierForConstraint(rule, specifiedVerifier, check) {
3117
3188
  return getVerifier("regex");
3118
3189
  }
3119
3190
 
3120
- // src/verification/cache.ts
3121
- import { stat as stat2, readFile as readFile2 } from "fs/promises";
3122
- import { createHash } from "crypto";
3123
- var AstCache = class {
3124
- cache = /* @__PURE__ */ new Map();
3125
- async get(filePath, project) {
3126
- try {
3127
- const stats = await stat2(filePath);
3128
- const cached = this.cache.get(filePath);
3129
- if (cached && cached.mtimeMs >= stats.mtimeMs) {
3130
- return cached.sourceFile;
3191
+ // src/verification/file-verifier.ts
3192
+ async function verifySingleFile(options, dependencies) {
3193
+ const { filePath, decisions, severityFilter, cwd, reporter, signal } = options;
3194
+ const { project, astCache, resultsCache, logger: logger2 } = dependencies;
3195
+ const violations = [];
3196
+ const warnings = [];
3197
+ const errors = [];
3198
+ if (signal?.aborted) {
3199
+ return { violations, warnings, errors };
3200
+ }
3201
+ const sourceFile = await astCache.get(filePath, project);
3202
+ if (!sourceFile) {
3203
+ return { violations, warnings, errors };
3204
+ }
3205
+ let fileHash = null;
3206
+ try {
3207
+ const content = await readFile3(filePath, "utf-8");
3208
+ fileHash = createHash2("sha256").update(content).digest("hex");
3209
+ } catch {
3210
+ fileHash = null;
3211
+ }
3212
+ for (const decision of decisions) {
3213
+ for (const constraint of decision.constraints) {
3214
+ if (!shouldApplyConstraintToFile({ filePath, constraint, cwd, severityFilter })) {
3215
+ if (reporter) {
3216
+ reporter.add({
3217
+ file: filePath,
3218
+ decision,
3219
+ constraint,
3220
+ applied: false,
3221
+ reason: "File does not match scope pattern or severity filter"
3222
+ });
3223
+ }
3224
+ continue;
3131
3225
  }
3132
- const content = await readFile2(filePath, "utf-8");
3133
- const hash = createHash("sha256").update(content).digest("hex");
3134
- if (cached && cached.hash === hash) {
3135
- cached.mtimeMs = stats.mtimeMs;
3136
- return cached.sourceFile;
3226
+ const verifier = selectVerifierForConstraint(
3227
+ constraint.rule,
3228
+ constraint.verifier,
3229
+ constraint.check
3230
+ );
3231
+ if (!verifier) {
3232
+ const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
3233
+ logger2.warn(
3234
+ {
3235
+ decisionId: decision.metadata.id,
3236
+ constraintId: constraint.id,
3237
+ requestedVerifier,
3238
+ availableVerifiers: getVerifierIds()
3239
+ },
3240
+ "No verifier found for constraint"
3241
+ );
3242
+ warnings.push({
3243
+ type: "missing_verifier",
3244
+ message: `No verifier found for constraint (requested: ${requestedVerifier})`,
3245
+ decisionId: decision.metadata.id,
3246
+ constraintId: constraint.id,
3247
+ file: filePath
3248
+ });
3249
+ if (reporter) {
3250
+ reporter.add({
3251
+ file: filePath,
3252
+ decision,
3253
+ constraint,
3254
+ applied: false,
3255
+ reason: `No verifier found (requested: ${requestedVerifier})`
3256
+ });
3257
+ }
3258
+ continue;
3137
3259
  }
3138
- let sourceFile = project.getSourceFile(filePath);
3139
- if (!sourceFile) {
3140
- sourceFile = project.addSourceFileAtPath(filePath);
3141
- } else {
3142
- sourceFile.refreshFromFileSystemSync();
3260
+ let constraintViolations;
3261
+ if (fileHash) {
3262
+ const cacheKey = {
3263
+ filePath,
3264
+ decisionId: decision.metadata.id,
3265
+ constraintId: constraint.id,
3266
+ fileHash
3267
+ };
3268
+ const cached = resultsCache.get(cacheKey);
3269
+ if (cached) {
3270
+ constraintViolations = cached;
3271
+ violations.push(...constraintViolations);
3272
+ if (reporter) {
3273
+ reporter.add({
3274
+ file: filePath,
3275
+ decision,
3276
+ constraint,
3277
+ applied: true,
3278
+ reason: "Constraint matches file scope (cached)",
3279
+ selectedVerifier: verifier.id
3280
+ });
3281
+ }
3282
+ continue;
3283
+ }
3284
+ }
3285
+ if (constraint.check?.verifier && constraint.check?.params) {
3286
+ const validationResult = getPluginLoader().validateParams(
3287
+ constraint.check.verifier,
3288
+ constraint.check.params
3289
+ );
3290
+ if (!validationResult.success) {
3291
+ warnings.push({
3292
+ type: "invalid_params",
3293
+ message: validationResult.error,
3294
+ decisionId: decision.metadata.id,
3295
+ constraintId: constraint.id,
3296
+ file: filePath
3297
+ });
3298
+ if (reporter) {
3299
+ reporter.add({
3300
+ file: filePath,
3301
+ decision,
3302
+ constraint,
3303
+ applied: false,
3304
+ reason: `Params validation failed: ${validationResult.error}`
3305
+ });
3306
+ }
3307
+ continue;
3308
+ }
3309
+ }
3310
+ const verificationContext = {
3311
+ filePath,
3312
+ sourceFile,
3313
+ constraint,
3314
+ decisionId: decision.metadata.id,
3315
+ signal
3316
+ };
3317
+ const verificationStart = Date.now();
3318
+ try {
3319
+ constraintViolations = await verifier.verify(verificationContext);
3320
+ violations.push(...constraintViolations);
3321
+ if (fileHash) {
3322
+ resultsCache.set(
3323
+ {
3324
+ filePath,
3325
+ decisionId: decision.metadata.id,
3326
+ constraintId: constraint.id,
3327
+ fileHash
3328
+ },
3329
+ constraintViolations
3330
+ );
3331
+ }
3332
+ if (reporter) {
3333
+ reporter.add({
3334
+ file: filePath,
3335
+ decision,
3336
+ constraint,
3337
+ applied: true,
3338
+ reason: "Constraint matches file scope",
3339
+ selectedVerifier: verifier.id,
3340
+ verifierOutput: {
3341
+ violations: constraintViolations.length,
3342
+ duration: Date.now() - verificationStart
3343
+ }
3344
+ });
3345
+ }
3346
+ } catch (error) {
3347
+ const errorMessage = error instanceof Error ? error.message : String(error);
3348
+ const errorStack = error instanceof Error ? error.stack : void 0;
3349
+ logger2.error(
3350
+ {
3351
+ verifierId: verifier.id,
3352
+ filePath,
3353
+ decisionId: decision.metadata.id,
3354
+ constraintId: constraint.id,
3355
+ error: errorMessage,
3356
+ stack: errorStack
3357
+ },
3358
+ "Verifier execution failed"
3359
+ );
3360
+ errors.push({
3361
+ type: "verifier_exception",
3362
+ message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
3363
+ decisionId: decision.metadata.id,
3364
+ constraintId: constraint.id,
3365
+ file: filePath,
3366
+ stack: errorStack
3367
+ });
3368
+ if (reporter) {
3369
+ reporter.add({
3370
+ file: filePath,
3371
+ decision,
3372
+ constraint,
3373
+ applied: true,
3374
+ reason: "Constraint matches file scope",
3375
+ selectedVerifier: verifier.id,
3376
+ verifierOutput: {
3377
+ violations: 0,
3378
+ duration: Date.now() - verificationStart,
3379
+ error: errorMessage
3380
+ }
3381
+ });
3382
+ }
3143
3383
  }
3144
- this.cache.set(filePath, {
3145
- sourceFile,
3146
- hash,
3147
- mtimeMs: stats.mtimeMs
3148
- });
3149
- return sourceFile;
3150
- } catch {
3151
- return null;
3152
3384
  }
3153
3385
  }
3154
- clear() {
3155
- this.cache.clear();
3156
- }
3157
- getStats() {
3158
- return {
3159
- entries: this.cache.size,
3160
- memoryEstimate: this.cache.size * 5e4
3161
- // Rough estimate: 50KB per AST
3162
- };
3386
+ return { violations, warnings, errors };
3387
+ }
3388
+ async function verifyFilesInBatches(options) {
3389
+ const { files, signal, verifyFile, onFileVerified, batchSize = 50 } = options;
3390
+ for (let i = 0; i < files.length; i += batchSize) {
3391
+ if (signal.aborted) {
3392
+ break;
3393
+ }
3394
+ const batch = files.slice(i, i + batchSize);
3395
+ const results = await Promise.all(batch.map((filePath) => verifyFile(filePath)));
3396
+ for (const result of results) {
3397
+ onFileVerified(result);
3398
+ }
3163
3399
  }
3164
- };
3400
+ }
3165
3401
 
3166
3402
  // src/verification/results-cache.ts
3167
3403
  var ResultsCache = class {
@@ -3233,30 +3469,63 @@ var ResultsCache = class {
3233
3469
  }
3234
3470
  };
3235
3471
 
3236
- // src/verification/applicability.ts
3237
- function isConstraintExcepted(filePath, constraint, cwd) {
3238
- if (!constraint.exceptions) return false;
3239
- return constraint.exceptions.some((exception) => {
3240
- if (exception.expiresAt) {
3241
- const expiryDate = new Date(exception.expiresAt);
3242
- if (expiryDate < /* @__PURE__ */ new Date()) {
3243
- return false;
3244
- }
3245
- }
3246
- return matchesPattern(filePath, exception.pattern, { cwd });
3472
+ // src/verification/run-settings.ts
3473
+ function resolveVerificationRunOptions(config, options) {
3474
+ const level = options.level || "full";
3475
+ const levelConfig = config.verification?.levels?.[level];
3476
+ const severityFilter = options.severity || levelConfig?.severity;
3477
+ const timeout = options.timeout || levelConfig?.timeout || 6e4;
3478
+ return {
3479
+ level,
3480
+ specificFiles: options.files,
3481
+ decisionIds: options.decisions,
3482
+ severityFilter,
3483
+ timeout,
3484
+ cwd: options.cwd || process.cwd()
3485
+ };
3486
+ }
3487
+ function selectDecisionsForRun(decisions, decisionIds) {
3488
+ if (!decisionIds || decisionIds.length === 0) {
3489
+ return decisions;
3490
+ }
3491
+ return decisions.filter((decision) => decisionIds.includes(decision.metadata.id));
3492
+ }
3493
+ async function resolveFilesForRun(config, specificFiles, cwd) {
3494
+ if (specificFiles) {
3495
+ return specificFiles;
3496
+ }
3497
+ return glob(config.project.sourceRoots, {
3498
+ cwd,
3499
+ ignore: config.project.exclude,
3500
+ absolute: true
3247
3501
  });
3248
3502
  }
3249
- function shouldApplyConstraintToFile(params) {
3250
- const { filePath, constraint, cwd, severityFilter } = params;
3251
- if (!matchesPattern(filePath, constraint.scope, { cwd })) return false;
3252
- if (severityFilter && !severityFilter.includes(constraint.severity)) return false;
3253
- if (isConstraintExcepted(filePath, constraint, cwd)) return false;
3254
- return true;
3503
+ function createEmptyRunResult(startTime) {
3504
+ return {
3505
+ success: true,
3506
+ violations: [],
3507
+ checked: 0,
3508
+ passed: 0,
3509
+ failed: 0,
3510
+ skipped: 0,
3511
+ duration: Date.now() - startTime,
3512
+ warnings: [],
3513
+ errors: []
3514
+ };
3515
+ }
3516
+ function hasBlockingViolations(violations, level) {
3517
+ return violations.some((violation) => {
3518
+ if (level === "commit") {
3519
+ return violation.type === "invariant" || violation.severity === "critical";
3520
+ }
3521
+ if (level === "pr") {
3522
+ return violation.type === "invariant" || violation.severity === "critical" || violation.severity === "high";
3523
+ }
3524
+ return violation.type === "invariant";
3525
+ });
3255
3526
  }
3256
3527
 
3257
3528
  // src/verification/engine.ts
3258
- import { createHash as createHash2 } from "crypto";
3259
- import { readFile as readFile3 } from "fs/promises";
3260
3529
  var VerificationEngine = class {
3261
3530
  registry;
3262
3531
  project;
@@ -3283,333 +3552,90 @@ var VerificationEngine = class {
3283
3552
  */
3284
3553
  async verify(config, options = {}) {
3285
3554
  const startTime = Date.now();
3286
- const {
3287
- level = "full",
3288
- files: specificFiles,
3289
- decisions: decisionIds,
3290
- cwd = process.cwd()
3291
- } = options;
3292
- const levelConfig = config.verification?.levels?.[level];
3293
- const severityFilter = options.severity || levelConfig?.severity;
3294
- const timeout = options.timeout || levelConfig?.timeout || 6e4;
3295
- if (!this.pluginsLoaded) {
3296
- await getPluginLoader().loadPlugins(cwd);
3297
- this.pluginsLoaded = true;
3298
- }
3555
+ const settings = resolveVerificationRunOptions(config, options);
3556
+ await this.ensurePluginsLoaded(settings.cwd);
3299
3557
  await this.registry.load();
3300
- let decisions = this.registry.getActive();
3301
- if (decisionIds && decisionIds.length > 0) {
3302
- decisions = decisions.filter((d) => decisionIds.includes(d.metadata.id));
3303
- }
3304
- const filesToVerify = specificFiles ? specificFiles : await glob(config.project.sourceRoots, {
3305
- cwd,
3306
- ignore: config.project.exclude,
3307
- absolute: true
3308
- });
3558
+ const decisions = selectDecisionsForRun(this.registry.getActive(), settings.decisionIds);
3559
+ const filesToVerify = await resolveFilesForRun(config, settings.specificFiles, settings.cwd);
3309
3560
  if (filesToVerify.length === 0) {
3310
- return {
3311
- success: true,
3312
- violations: [],
3313
- checked: 0,
3314
- passed: 0,
3315
- failed: 0,
3316
- skipped: 0,
3317
- duration: Date.now() - startTime,
3318
- warnings: [],
3319
- errors: []
3320
- };
3561
+ return createEmptyRunResult(startTime);
3321
3562
  }
3322
- const allViolations = [];
3323
- const allWarnings = [];
3324
- const allErrors = [];
3325
- let checked = 0;
3326
- let passed = 0;
3327
- let failed = 0;
3328
- const skipped = 0;
3563
+ const accumulator = this.createAccumulator();
3329
3564
  const abortController = new AbortController();
3330
3565
  let timeoutHandle = null;
3331
3566
  const timeoutPromise = new Promise((resolve2) => {
3332
3567
  timeoutHandle = setTimeout(() => {
3333
3568
  abortController.abort();
3334
3569
  resolve2("timeout");
3335
- }, timeout);
3570
+ }, settings.timeout);
3336
3571
  timeoutHandle.unref();
3337
3572
  });
3338
- const verificationPromise = this.verifyFiles(
3339
- filesToVerify,
3340
- decisions,
3341
- severityFilter,
3342
- cwd,
3343
- options.reporter,
3344
- abortController.signal,
3345
- (violations, warnings, errors) => {
3346
- allViolations.push(...violations);
3347
- allWarnings.push(...warnings);
3348
- allErrors.push(...errors);
3349
- checked++;
3350
- if (violations.length > 0) {
3351
- failed++;
3352
- } else {
3353
- passed++;
3354
- }
3355
- }
3356
- );
3357
- let result;
3573
+ const verificationPromise = verifyFilesInBatches({
3574
+ files: filesToVerify,
3575
+ signal: abortController.signal,
3576
+ verifyFile: (filePath) => this.verifyFile(
3577
+ filePath,
3578
+ decisions,
3579
+ settings.severityFilter,
3580
+ settings.cwd,
3581
+ options.reporter,
3582
+ abortController.signal
3583
+ ),
3584
+ onFileVerified: (result) => this.addFileResult(accumulator, result)
3585
+ });
3586
+ let raceResult;
3358
3587
  try {
3359
- result = await Promise.race([verificationPromise, timeoutPromise]);
3360
- if (result === "timeout") {
3361
- return {
3362
- success: false,
3363
- violations: allViolations,
3364
- checked,
3365
- passed,
3366
- failed,
3367
- skipped: filesToVerify.length - checked,
3368
- duration: timeout,
3369
- warnings: allWarnings,
3370
- errors: allErrors
3371
- };
3372
- }
3588
+ raceResult = await Promise.race([verificationPromise, timeoutPromise]);
3373
3589
  } finally {
3374
3590
  if (timeoutHandle) {
3375
3591
  clearTimeout(timeoutHandle);
3376
- timeoutHandle = null;
3377
3592
  }
3378
3593
  }
3379
- const hasBlockingViolations = allViolations.some((v) => {
3380
- if (level === "commit") {
3381
- return v.type === "invariant" || v.severity === "critical";
3382
- }
3383
- if (level === "pr") {
3384
- return v.type === "invariant" || v.severity === "critical" || v.severity === "high";
3385
- }
3386
- return v.type === "invariant";
3387
- });
3594
+ if (raceResult === "timeout") {
3595
+ return {
3596
+ success: false,
3597
+ violations: accumulator.violations,
3598
+ checked: accumulator.checked,
3599
+ passed: accumulator.passed,
3600
+ failed: accumulator.failed,
3601
+ skipped: filesToVerify.length - accumulator.checked,
3602
+ duration: settings.timeout,
3603
+ warnings: accumulator.warnings,
3604
+ errors: accumulator.errors
3605
+ };
3606
+ }
3388
3607
  return {
3389
- success: !hasBlockingViolations,
3390
- violations: allViolations,
3391
- checked,
3392
- passed,
3393
- failed,
3394
- skipped,
3608
+ success: !hasBlockingViolations(accumulator.violations, settings.level),
3609
+ violations: accumulator.violations,
3610
+ checked: accumulator.checked,
3611
+ passed: accumulator.passed,
3612
+ failed: accumulator.failed,
3613
+ skipped: 0,
3395
3614
  duration: Date.now() - startTime,
3396
- warnings: allWarnings,
3397
- errors: allErrors
3615
+ warnings: accumulator.warnings,
3616
+ errors: accumulator.errors
3398
3617
  };
3399
3618
  }
3400
3619
  /**
3401
3620
  * Verify a single file
3402
3621
  */
3403
3622
  async verifyFile(filePath, decisions, severityFilter, cwd = process.cwd(), reporter, signal) {
3404
- const violations = [];
3405
- const warnings = [];
3406
- const errors = [];
3407
- if (signal?.aborted) {
3408
- return { violations, warnings, errors };
3409
- }
3410
- const sourceFile = await this.astCache.get(filePath, this.project);
3411
- if (!sourceFile) return { violations, warnings, errors };
3412
- let fileHash = null;
3413
- try {
3414
- const content = await readFile3(filePath, "utf-8");
3415
- fileHash = createHash2("sha256").update(content).digest("hex");
3416
- } catch {
3417
- fileHash = null;
3418
- }
3419
- for (const decision of decisions) {
3420
- for (const constraint of decision.constraints) {
3421
- if (!shouldApplyConstraintToFile({ filePath, constraint, cwd, severityFilter })) {
3422
- if (reporter) {
3423
- reporter.add({
3424
- file: filePath,
3425
- decision,
3426
- constraint,
3427
- applied: false,
3428
- reason: "File does not match scope pattern or severity filter"
3429
- });
3430
- }
3431
- continue;
3432
- }
3433
- const verifier = selectVerifierForConstraint(
3434
- constraint.rule,
3435
- constraint.verifier,
3436
- constraint.check
3437
- );
3438
- if (!verifier) {
3439
- const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
3440
- this.logger.warn(
3441
- {
3442
- decisionId: decision.metadata.id,
3443
- constraintId: constraint.id,
3444
- requestedVerifier,
3445
- availableVerifiers: getVerifierIds()
3446
- },
3447
- "No verifier found for constraint"
3448
- );
3449
- warnings.push({
3450
- type: "missing_verifier",
3451
- message: `No verifier found for constraint (requested: ${requestedVerifier})`,
3452
- decisionId: decision.metadata.id,
3453
- constraintId: constraint.id,
3454
- file: filePath
3455
- });
3456
- if (reporter) {
3457
- reporter.add({
3458
- file: filePath,
3459
- decision,
3460
- constraint,
3461
- applied: false,
3462
- reason: `No verifier found (requested: ${requestedVerifier})`
3463
- });
3464
- }
3465
- continue;
3466
- }
3467
- let constraintViolations;
3468
- if (fileHash) {
3469
- const cacheKey = {
3470
- filePath,
3471
- decisionId: decision.metadata.id,
3472
- constraintId: constraint.id,
3473
- fileHash
3474
- };
3475
- const cached = this.resultsCache.get(cacheKey);
3476
- if (cached) {
3477
- constraintViolations = cached;
3478
- violations.push(...constraintViolations);
3479
- if (reporter) {
3480
- reporter.add({
3481
- file: filePath,
3482
- decision,
3483
- constraint,
3484
- applied: true,
3485
- reason: "Constraint matches file scope (cached)",
3486
- selectedVerifier: verifier.id
3487
- });
3488
- }
3489
- continue;
3490
- }
3491
- }
3492
- if (constraint.check?.verifier && constraint.check?.params) {
3493
- const pluginLoader2 = getPluginLoader();
3494
- const validationResult = pluginLoader2.validateParams(
3495
- constraint.check.verifier,
3496
- constraint.check.params
3497
- );
3498
- if (!validationResult.success) {
3499
- warnings.push({
3500
- type: "invalid_params",
3501
- message: validationResult.error,
3502
- decisionId: decision.metadata.id,
3503
- constraintId: constraint.id,
3504
- file: filePath
3505
- });
3506
- if (reporter) {
3507
- reporter.add({
3508
- file: filePath,
3509
- decision,
3510
- constraint,
3511
- applied: false,
3512
- reason: `Params validation failed: ${validationResult.error}`
3513
- });
3514
- }
3515
- continue;
3516
- }
3517
- }
3518
- const ctx = {
3519
- filePath,
3520
- sourceFile,
3521
- constraint,
3522
- decisionId: decision.metadata.id,
3523
- signal
3524
- };
3525
- const verificationStart = Date.now();
3526
- try {
3527
- constraintViolations = await verifier.verify(ctx);
3528
- violations.push(...constraintViolations);
3529
- if (fileHash) {
3530
- this.resultsCache.set(
3531
- {
3532
- filePath,
3533
- decisionId: decision.metadata.id,
3534
- constraintId: constraint.id,
3535
- fileHash
3536
- },
3537
- constraintViolations
3538
- );
3539
- }
3540
- if (reporter) {
3541
- reporter.add({
3542
- file: filePath,
3543
- decision,
3544
- constraint,
3545
- applied: true,
3546
- reason: "Constraint matches file scope",
3547
- selectedVerifier: verifier.id,
3548
- verifierOutput: {
3549
- violations: constraintViolations.length,
3550
- duration: Date.now() - verificationStart
3551
- }
3552
- });
3553
- }
3554
- } catch (error) {
3555
- const errorMessage = error instanceof Error ? error.message : String(error);
3556
- const errorStack = error instanceof Error ? error.stack : void 0;
3557
- this.logger.error(
3558
- {
3559
- verifierId: verifier.id,
3560
- filePath,
3561
- decisionId: decision.metadata.id,
3562
- constraintId: constraint.id,
3563
- error: errorMessage,
3564
- stack: errorStack
3565
- },
3566
- "Verifier execution failed"
3567
- );
3568
- errors.push({
3569
- type: "verifier_exception",
3570
- message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
3571
- decisionId: decision.metadata.id,
3572
- constraintId: constraint.id,
3573
- file: filePath,
3574
- stack: errorStack
3575
- });
3576
- if (reporter) {
3577
- reporter.add({
3578
- file: filePath,
3579
- decision,
3580
- constraint,
3581
- applied: true,
3582
- reason: "Constraint matches file scope",
3583
- selectedVerifier: verifier.id,
3584
- verifierOutput: {
3585
- violations: 0,
3586
- duration: Date.now() - verificationStart,
3587
- error: errorMessage
3588
- }
3589
- });
3590
- }
3591
- }
3592
- }
3593
- }
3594
- return { violations, warnings, errors };
3595
- }
3596
- /**
3597
- * Verify multiple files
3598
- */
3599
- async verifyFiles(files, decisions, severityFilter, cwd, reporter, signal, onFileVerified) {
3600
- const BATCH_SIZE = 50;
3601
- for (let i = 0; i < files.length; i += BATCH_SIZE) {
3602
- if (signal.aborted) {
3603
- break;
3604
- }
3605
- const batch = files.slice(i, i + BATCH_SIZE);
3606
- const results = await Promise.all(
3607
- batch.map((file) => this.verifyFile(file, decisions, severityFilter, cwd, reporter, signal))
3608
- );
3609
- for (const result of results) {
3610
- onFileVerified(result.violations, result.warnings, result.errors);
3623
+ return verifySingleFile(
3624
+ {
3625
+ filePath,
3626
+ decisions,
3627
+ severityFilter,
3628
+ cwd,
3629
+ reporter,
3630
+ signal
3631
+ },
3632
+ {
3633
+ project: this.project,
3634
+ astCache: this.astCache,
3635
+ resultsCache: this.resultsCache,
3636
+ logger: this.logger
3611
3637
  }
3612
- }
3638
+ );
3613
3639
  }
3614
3640
  /**
3615
3641
  * Get registry
@@ -3617,6 +3643,34 @@ var VerificationEngine = class {
3617
3643
  getRegistry() {
3618
3644
  return this.registry;
3619
3645
  }
3646
+ async ensurePluginsLoaded(cwd) {
3647
+ if (this.pluginsLoaded) {
3648
+ return;
3649
+ }
3650
+ await getPluginLoader().loadPlugins(cwd);
3651
+ this.pluginsLoaded = true;
3652
+ }
3653
+ createAccumulator() {
3654
+ return {
3655
+ violations: [],
3656
+ warnings: [],
3657
+ errors: [],
3658
+ checked: 0,
3659
+ passed: 0,
3660
+ failed: 0
3661
+ };
3662
+ }
3663
+ addFileResult(accumulator, result) {
3664
+ accumulator.violations.push(...result.violations);
3665
+ accumulator.warnings.push(...result.warnings);
3666
+ accumulator.errors.push(...result.errors);
3667
+ accumulator.checked++;
3668
+ if (result.violations.length > 0) {
3669
+ accumulator.failed++;
3670
+ } else {
3671
+ accumulator.passed++;
3672
+ }
3673
+ }
3620
3674
  };
3621
3675
  function createVerificationEngine(registry) {
3622
3676
  return new VerificationEngine(registry);
@@ -5382,12 +5436,11 @@ async function generateFormattedContext(filePath, config, options = {}) {
5382
5436
 
5383
5437
  // src/cli/commands/context.ts
5384
5438
  var contextCommand = new Command11("context").description("Generate architectural context for a file (for AI agents)").argument("<file>", "File path to generate context for").option("-f, --format <format>", "Output format (markdown, json, mcp)", "markdown").option("-o, --output <file>", "Output file path").option("--no-rationale", "Exclude rationale/summary from output").action(async (file, options) => {
5385
- const cwd = process.cwd();
5386
- if (!await pathExists(getSpecBridgeDir(cwd))) {
5387
- throw new NotInitializedError();
5388
- }
5389
5439
  try {
5390
- const config = await loadConfig(cwd);
5440
+ const { context, config } = await createConfiguredCommandContext({
5441
+ outputFormat: options.format === "json" ? "json" : "markdown"
5442
+ });
5443
+ const { cwd } = context;
5391
5444
  const output = await generateFormattedContext(file, config, {
5392
5445
  format: options.format,
5393
5446
  includeRationale: options.rationale !== false,
@@ -5641,11 +5694,8 @@ import chalk14 from "chalk";
5641
5694
  import chokidar from "chokidar";
5642
5695
  import path4 from "path";
5643
5696
  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) => {
5644
- const cwd = process.cwd();
5645
- if (!await pathExists(getSpecBridgeDir(cwd))) {
5646
- throw new NotInitializedError();
5647
- }
5648
- const config = await loadConfig(cwd);
5697
+ const { context, config } = await createConfiguredCommandContext();
5698
+ const { cwd } = context;
5649
5699
  const engine = createVerificationEngine();
5650
5700
  const level = options.level || "full";
5651
5701
  const debounceMs = Number.parseInt(options.debounce || "150", 10);
@@ -6183,10 +6233,10 @@ var AnalyticsEngine = class {
6183
6233
 
6184
6234
  // src/cli/commands/analytics.ts
6185
6235
  var analyticsCommand = new Command16("analytics").description("Analyze compliance trends and decision impact").argument("[decision-id]", "Specific decision to analyze").option("--insights", "Show AI-generated insights").option("--days <n>", "Number of days of history to analyze", "90").option("-f, --format <format>", "Output format (console, json)", "console").action(async (decisionId, options) => {
6186
- const cwd = process.cwd();
6187
- if (!await pathExists(getSpecBridgeDir(cwd))) {
6188
- throw new NotInitializedError();
6189
- }
6236
+ const { context } = await createConfiguredCommandContext({
6237
+ outputFormat: options.format === "json" ? "json" : "console"
6238
+ });
6239
+ const { cwd } = context;
6190
6240
  const spinner = ora7("Analyzing compliance data...").start();
6191
6241
  try {
6192
6242
  const storage = new ReportStorage(cwd);
@@ -6639,13 +6689,10 @@ function createDashboardServer(options) {
6639
6689
 
6640
6690
  // src/cli/commands/dashboard.ts
6641
6691
  var dashboardCommand = new Command17("dashboard").description("Start compliance dashboard web server").option("-p, --port <port>", "Port to listen on", "3000").option("-h, --host <host>", "Host to bind to", "localhost").action(async (options) => {
6642
- const cwd = process.cwd();
6643
- if (!await pathExists(getSpecBridgeDir(cwd))) {
6644
- throw new NotInitializedError();
6645
- }
6646
6692
  console.log(chalk16.blue("Starting SpecBridge dashboard..."));
6647
6693
  try {
6648
- const config = await loadConfig(cwd);
6694
+ const { context, config } = await createConfiguredCommandContext();
6695
+ const { cwd } = context;
6649
6696
  const server = createDashboardServer({ cwd, config });
6650
6697
  await server.start();
6651
6698
  const port = parseInt(options.port || "3000", 10);
@@ -6891,13 +6938,12 @@ function createPropagationEngine(registry) {
6891
6938
 
6892
6939
  // src/cli/commands/impact.ts
6893
6940
  var impactCommand = new Command18("impact").description("Analyze impact of decision changes").argument("<decision-id>", "Decision ID to analyze").option("-c, --change <type>", "Type of change (created, modified, deprecated)", "modified").option("--json", "Output as JSON").option("--show-steps", "Show detailed migration steps", true).action(async (decisionId, options) => {
6894
- const cwd = process.cwd();
6895
- if (!await pathExists(getSpecBridgeDir(cwd))) {
6896
- throw new NotInitializedError();
6897
- }
6898
6941
  const spinner = ora8("Loading configuration...").start();
6899
6942
  try {
6900
- const config = await loadConfig(cwd);
6943
+ const { context, config } = await createConfiguredCommandContext({
6944
+ outputFormat: options.json ? "json" : "console"
6945
+ });
6946
+ const { cwd } = context;
6901
6947
  const changeType = options.change || "modified";
6902
6948
  if (!["created", "modified", "deprecated"].includes(changeType)) {
6903
6949
  spinner.fail();