depwire-cli 1.1.8 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,8 +39,9 @@ function scanDirectory(rootDir, baseDir = rootDir) {
39
39
  const isPhp = entry.endsWith(".php");
40
40
  const isSwift = entry.endsWith(".swift");
41
41
  const isMojo = entry.endsWith(".mojo") || entry.endsWith(".\u{1F525}");
42
+ const isRuby = entry.endsWith(".rb") || entry.endsWith(".rake") || entry.endsWith(".gemspec") || entry.endsWith(".ru") || entry === "Gemfile";
42
43
  const isCppBuild = entry === "CMakeLists.txt" || entry === "conanfile.txt" || entry === "vcpkg.json";
43
- if (isTypeScript || isJavaScript || isPython || isGo || isRust || isC || isCpp || isCSharp || isJava || isKotlin || isPhp || isSwift || isMojo || isCppBuild) {
44
+ if (isTypeScript || isJavaScript || isPython || isGo || isRust || isC || isCpp || isCSharp || isJava || isKotlin || isPhp || isSwift || isMojo || isRuby || isCppBuild) {
44
45
  files.push(relative(rootDir, fullPath));
45
46
  }
46
47
  }
@@ -90,6 +91,8 @@ function findProjectRoot(startDir = process.cwd()) {
90
91
  // Swift (SPM)
91
92
  "mojoproject.toml",
92
93
  // Mojo
94
+ "Gemfile",
95
+ // Ruby (Bundler)
93
96
  ".git"
94
97
  // Any git repo
95
98
  ];
@@ -123,11 +126,11 @@ function findProjectRoot(startDir = process.cwd()) {
123
126
  }
124
127
 
125
128
  // src/parser/index.ts
126
- import { readFileSync as readFileSync11, statSync as statSync8 } from "fs";
127
- import { join as join15, resolve as resolve9 } from "path";
129
+ import { readFileSync as readFileSync12, statSync as statSync9 } from "fs";
130
+ import { join as join16, resolve as resolve10 } from "path";
128
131
 
129
132
  // src/parser/detect.ts
130
- import { extname as extname10, basename as basename8 } from "path";
133
+ import { extname as extname11, basename as basename9 } from "path";
131
134
 
132
135
  // src/parser/wasm-init.ts
133
136
  import { Parser, Language } from "web-tree-sitter";
@@ -160,7 +163,8 @@ async function initParser() {
160
163
  "cpp": "tree-sitter-cpp.wasm",
161
164
  "kotlin": "tree-sitter-kotlin.wasm",
162
165
  "php": "tree-sitter-php.wasm",
163
- "swift": "tree-sitter-swift.wasm"
166
+ "swift": "tree-sitter-swift.wasm",
167
+ "ruby": "tree-sitter-ruby.wasm"
164
168
  // Note: Mojo uses a pattern-based parser (no tree-sitter-mojo WASM available)
165
169
  };
166
170
  for (const [name, file] of Object.entries(grammarFiles)) {
@@ -7146,12 +7150,566 @@ function parseMojoProject(filePath, sourceCode, projectRoot) {
7146
7150
  });
7147
7151
  }
7148
7152
  }
7149
- return { filePath, symbols, edges };
7153
+ return { filePath, symbols, edges };
7154
+ }
7155
+ var mojoParser = {
7156
+ name: "mojo",
7157
+ extensions: [".mojo", ".\u{1F525}", "mojoproject.toml"],
7158
+ parseFile: parseMojoFile
7159
+ };
7160
+
7161
+ // src/parser/ruby.ts
7162
+ import { dirname as dirname14, join as join15, basename as basename8 } from "path";
7163
+ import { existsSync as existsSync14 } from "fs";
7164
+ function parseRubyFile(filePath, sourceCode, projectRoot) {
7165
+ if (basename8(filePath) === "Gemfile") {
7166
+ return parseGemfile(filePath, sourceCode, projectRoot);
7167
+ }
7168
+ const parser = getParser("ruby");
7169
+ const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
7170
+ const context = {
7171
+ filePath,
7172
+ projectRoot,
7173
+ sourceCode,
7174
+ symbols: [],
7175
+ edges: [],
7176
+ currentScope: [],
7177
+ currentClass: null,
7178
+ currentModule: null,
7179
+ imports: /* @__PURE__ */ new Map(),
7180
+ isGemfile: false
7181
+ };
7182
+ walkNode13(tree.rootNode, context);
7183
+ return {
7184
+ filePath,
7185
+ symbols: context.symbols,
7186
+ edges: context.edges
7187
+ };
7188
+ }
7189
+ function walkNode13(node, context) {
7190
+ const handled = processNode13(node, context);
7191
+ if (handled) return;
7192
+ for (let i = 0; i < node.childCount; i++) {
7193
+ const child = node.child(i);
7194
+ if (child) {
7195
+ walkNode13(child, context);
7196
+ }
7197
+ }
7198
+ }
7199
+ function processNode13(node, context) {
7200
+ switch (node.type) {
7201
+ case "class":
7202
+ processClassDeclaration8(node, context);
7203
+ return true;
7204
+ case "module":
7205
+ processModuleDeclaration(node, context);
7206
+ return true;
7207
+ case "method":
7208
+ processMethodDeclaration5(node, context);
7209
+ return true;
7210
+ case "singleton_method":
7211
+ processSingletonMethod(node, context);
7212
+ return true;
7213
+ case "assignment":
7214
+ processAssignment(node, context);
7215
+ return false;
7216
+ case "call":
7217
+ case "method_call":
7218
+ processCallExpression13(node, context);
7219
+ return false;
7220
+ case "command":
7221
+ processCommand(node, context);
7222
+ return false;
7223
+ case "command_call":
7224
+ processCommandCall(node, context);
7225
+ return false;
7226
+ case "constant":
7227
+ return false;
7228
+ case "block":
7229
+ case "do_block":
7230
+ return false;
7231
+ case "lambda":
7232
+ processLambda(node, context);
7233
+ return false;
7234
+ default:
7235
+ return false;
7236
+ }
7237
+ }
7238
+ function processClassDeclaration8(node, context) {
7239
+ const nameNode = findChildByType13(node, "constant") || findChildByType13(node, "scope_resolution");
7240
+ if (!nameNode) return;
7241
+ const name = nodeText12(nameNode, context);
7242
+ const symbolId = `${context.filePath}::${name}`;
7243
+ context.symbols.push({
7244
+ id: symbolId,
7245
+ name,
7246
+ kind: "class",
7247
+ filePath: context.filePath,
7248
+ startLine: node.startPosition.row + 1,
7249
+ endLine: node.endPosition.row + 1,
7250
+ exported: true
7251
+ });
7252
+ const superclassNode = findChildByType13(node, "superclass");
7253
+ if (superclassNode) {
7254
+ const superName = nodeText12(superclassNode, context).replace(/^\s*<\s*/, "").trim();
7255
+ if (superName) {
7256
+ const baseId = resolveSymbol12(superName, context);
7257
+ if (baseId) {
7258
+ context.edges.push({
7259
+ source: symbolId,
7260
+ target: baseId,
7261
+ kind: "implements",
7262
+ filePath: context.filePath,
7263
+ line: node.startPosition.row + 1
7264
+ });
7265
+ }
7266
+ }
7267
+ }
7268
+ const oldClass = context.currentClass;
7269
+ context.currentClass = name;
7270
+ context.currentScope.push(name);
7271
+ const body = findChildByType13(node, "body_statement");
7272
+ if (body) {
7273
+ walkNode13(body, context);
7274
+ }
7275
+ context.currentScope.pop();
7276
+ context.currentClass = oldClass;
7277
+ }
7278
+ function processModuleDeclaration(node, context) {
7279
+ const nameNode = findChildByType13(node, "constant") || findChildByType13(node, "scope_resolution");
7280
+ if (!nameNode) return;
7281
+ const name = nodeText12(nameNode, context);
7282
+ const symbolId = `${context.filePath}::${name}`;
7283
+ context.symbols.push({
7284
+ id: symbolId,
7285
+ name,
7286
+ kind: "module",
7287
+ filePath: context.filePath,
7288
+ startLine: node.startPosition.row + 1,
7289
+ endLine: node.endPosition.row + 1,
7290
+ exported: true
7291
+ });
7292
+ const oldModule = context.currentModule;
7293
+ const oldClass = context.currentClass;
7294
+ context.currentModule = name;
7295
+ context.currentClass = name;
7296
+ context.currentScope.push(name);
7297
+ const body = findChildByType13(node, "body_statement");
7298
+ if (body) {
7299
+ walkNode13(body, context);
7300
+ }
7301
+ context.currentScope.pop();
7302
+ context.currentModule = oldModule;
7303
+ context.currentClass = oldClass;
7304
+ }
7305
+ function processMethodDeclaration5(node, context) {
7306
+ const nameNode = findChildByType13(node, "identifier");
7307
+ if (!nameNode) return;
7308
+ const name = nodeText12(nameNode, context);
7309
+ const scope = context.currentClass || void 0;
7310
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
7311
+ context.symbols.push({
7312
+ id: symbolId,
7313
+ name,
7314
+ kind: context.currentClass ? "method" : "function",
7315
+ filePath: context.filePath,
7316
+ startLine: node.startPosition.row + 1,
7317
+ endLine: node.endPosition.row + 1,
7318
+ exported: true,
7319
+ scope
7320
+ });
7321
+ const scopeName = scope ? `${scope}.${name}` : name;
7322
+ context.currentScope.push(scopeName);
7323
+ const body = findChildByType13(node, "body_statement");
7324
+ if (body) {
7325
+ walkNode13(body, context);
7326
+ }
7327
+ context.currentScope.pop();
7328
+ }
7329
+ function processSingletonMethod(node, context) {
7330
+ const nameNode = node.childCount > 2 ? node.child(node.childCount - 2) : null;
7331
+ let name = "";
7332
+ for (let i = 0; i < node.childCount; i++) {
7333
+ const child = node.child(i);
7334
+ if (child && child.type === "identifier") {
7335
+ name = nodeText12(child, context);
7336
+ }
7337
+ }
7338
+ if (!name) return;
7339
+ const scope = context.currentClass || void 0;
7340
+ const symbolId = scope ? `${context.filePath}::${scope}.self.${name}` : `${context.filePath}::self.${name}`;
7341
+ context.symbols.push({
7342
+ id: symbolId,
7343
+ name: `self.${name}`,
7344
+ kind: "method",
7345
+ filePath: context.filePath,
7346
+ startLine: node.startPosition.row + 1,
7347
+ endLine: node.endPosition.row + 1,
7348
+ exported: true,
7349
+ scope
7350
+ });
7351
+ const scopeName = scope ? `${scope}.self.${name}` : `self.${name}`;
7352
+ context.currentScope.push(scopeName);
7353
+ const body = findChildByType13(node, "body_statement");
7354
+ if (body) {
7355
+ walkNode13(body, context);
7356
+ }
7357
+ context.currentScope.pop();
7358
+ }
7359
+ function processAssignment(node, context) {
7360
+ const left = node.child(0);
7361
+ if (!left) return;
7362
+ const text = nodeText12(left, context);
7363
+ const scope = context.currentClass || void 0;
7364
+ if (left.type === "constant") {
7365
+ const symbolId = scope ? `${context.filePath}::${scope}.${text}` : `${context.filePath}::${text}`;
7366
+ context.symbols.push({
7367
+ id: symbolId,
7368
+ name: text,
7369
+ kind: "constant",
7370
+ filePath: context.filePath,
7371
+ startLine: node.startPosition.row + 1,
7372
+ endLine: node.endPosition.row + 1,
7373
+ exported: true,
7374
+ scope
7375
+ });
7376
+ }
7377
+ if (left.type === "instance_variable") {
7378
+ const symbolId = scope ? `${context.filePath}::${scope}.${text}` : `${context.filePath}::${text}`;
7379
+ context.symbols.push({
7380
+ id: symbolId,
7381
+ name: text,
7382
+ kind: "property",
7383
+ filePath: context.filePath,
7384
+ startLine: node.startPosition.row + 1,
7385
+ endLine: node.endPosition.row + 1,
7386
+ exported: false,
7387
+ scope
7388
+ });
7389
+ }
7390
+ if (left.type === "class_variable") {
7391
+ const symbolId = scope ? `${context.filePath}::${scope}.${text}` : `${context.filePath}::${text}`;
7392
+ context.symbols.push({
7393
+ id: symbolId,
7394
+ name: text,
7395
+ kind: "property",
7396
+ filePath: context.filePath,
7397
+ startLine: node.startPosition.row + 1,
7398
+ endLine: node.endPosition.row + 1,
7399
+ exported: false,
7400
+ scope
7401
+ });
7402
+ }
7403
+ }
7404
+ function processCallExpression13(node, context) {
7405
+ const text = nodeText12(node, context);
7406
+ const line = node.startPosition.row + 1;
7407
+ const requireMatch = text.match(/^require(?:_relative)?\s*\(?['"]([^'"]+)['"]\)?/);
7408
+ if (requireMatch) {
7409
+ processRequire(requireMatch[1], text.startsWith("require_relative"), context, line);
7410
+ return;
7411
+ }
7412
+ const mixinMatch = text.match(/^(?:include|extend|prepend)\s+([A-Z]\w*(?:::\w+)*)/);
7413
+ if (mixinMatch) {
7414
+ processMixin(mixinMatch[1], context, line);
7415
+ return;
7416
+ }
7417
+ const attrMatch = text.match(/^attr_(accessor|reader|writer)\s+(.+)/);
7418
+ if (attrMatch) {
7419
+ processAttrAccessor(attrMatch[2], context, line);
7420
+ return;
7421
+ }
7422
+ if (context.currentScope.length > 0) {
7423
+ const firstChild = node.child(0);
7424
+ let calleeName = null;
7425
+ if (firstChild && firstChild.type === "identifier") {
7426
+ calleeName = nodeText12(firstChild, context);
7427
+ }
7428
+ if (calleeName && !isBuiltin(calleeName)) {
7429
+ const callerId = getCurrentSymbolId13(context);
7430
+ if (callerId) {
7431
+ const calleeId = resolveSymbol12(calleeName, context);
7432
+ if (calleeId) {
7433
+ context.edges.push({
7434
+ source: callerId,
7435
+ target: calleeId,
7436
+ kind: "calls",
7437
+ filePath: context.filePath,
7438
+ line
7439
+ });
7440
+ }
7441
+ }
7442
+ }
7443
+ }
7444
+ }
7445
+ function processCommand(node, context) {
7446
+ const text = nodeText12(node, context).trim();
7447
+ const line = node.startPosition.row + 1;
7448
+ const requireMatch = text.match(/^require(?:_relative)?\s+['"]([^'"]+)['"]/);
7449
+ if (requireMatch) {
7450
+ processRequire(requireMatch[1], text.startsWith("require_relative"), context, line);
7451
+ return;
7452
+ }
7453
+ const mixinMatch = text.match(/^(?:include|extend|prepend)\s+([A-Z]\w*(?:::\w+)*)/);
7454
+ if (mixinMatch) {
7455
+ processMixin(mixinMatch[1], context, line);
7456
+ return;
7457
+ }
7458
+ const attrMatch = text.match(/^attr_(accessor|reader|writer)\s+(.+)/);
7459
+ if (attrMatch) {
7460
+ processAttrAccessor(attrMatch[2], context, line);
7461
+ return;
7462
+ }
7463
+ }
7464
+ function processCommandCall(node, context) {
7465
+ const text = nodeText12(node, context).trim();
7466
+ if (/Struct\.new/.test(text) || /OpenStruct\.new/.test(text)) {
7467
+ const parent = node.parent;
7468
+ if (parent && parent.type === "assignment") {
7469
+ const left = parent.child(0);
7470
+ if (left && left.type === "constant") {
7471
+ const name = nodeText12(left, context);
7472
+ const symbolId = `${context.filePath}::${name}`;
7473
+ context.symbols.push({
7474
+ id: symbolId,
7475
+ name,
7476
+ kind: "class",
7477
+ filePath: context.filePath,
7478
+ startLine: node.startPosition.row + 1,
7479
+ endLine: node.endPosition.row + 1,
7480
+ exported: true
7481
+ });
7482
+ }
7483
+ }
7484
+ }
7485
+ }
7486
+ function processLambda(node, context) {
7487
+ const parent = node.parent;
7488
+ if (parent && parent.type === "assignment") {
7489
+ const left = parent.child(0);
7490
+ if (left) {
7491
+ const name = nodeText12(left, context);
7492
+ const scope = context.currentClass || void 0;
7493
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
7494
+ context.symbols.push({
7495
+ id: symbolId,
7496
+ name,
7497
+ kind: "function",
7498
+ filePath: context.filePath,
7499
+ startLine: node.startPosition.row + 1,
7500
+ endLine: node.endPosition.row + 1,
7501
+ exported: true,
7502
+ scope
7503
+ });
7504
+ }
7505
+ }
7506
+ }
7507
+ function processRequire(path6, isRelative, context, line) {
7508
+ const resolvedPath = resolveRubyRequire(path6, isRelative, context.filePath, context.projectRoot);
7509
+ if (resolvedPath) {
7510
+ const sourceId = `${context.filePath}::__file__`;
7511
+ const targetId = `${resolvedPath}::__file__`;
7512
+ context.edges.push({
7513
+ source: sourceId,
7514
+ target: targetId,
7515
+ kind: "imports",
7516
+ filePath: context.filePath,
7517
+ line
7518
+ });
7519
+ }
7520
+ const symbolId = `${context.filePath}::require:${path6}`;
7521
+ context.symbols.push({
7522
+ id: symbolId,
7523
+ name: path6,
7524
+ kind: "import",
7525
+ filePath: context.filePath,
7526
+ startLine: line,
7527
+ endLine: line,
7528
+ exported: false
7529
+ });
7530
+ }
7531
+ function processMixin(moduleName, context, line) {
7532
+ const scope = context.currentClass || void 0;
7533
+ const sourceId = scope ? `${context.filePath}::${scope}` : `${context.filePath}::__file__`;
7534
+ const targetId = resolveSymbol12(moduleName, context);
7535
+ if (targetId) {
7536
+ context.edges.push({
7537
+ source: sourceId,
7538
+ target: targetId,
7539
+ kind: "implements",
7540
+ filePath: context.filePath,
7541
+ line
7542
+ });
7543
+ }
7544
+ }
7545
+ function processAttrAccessor(args, context, line) {
7546
+ const scope = context.currentClass || void 0;
7547
+ const symbols = args.match(/:\w+/g);
7548
+ if (!symbols) return;
7549
+ for (const sym of symbols) {
7550
+ const name = sym.slice(1);
7551
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
7552
+ context.symbols.push({
7553
+ id: symbolId,
7554
+ name,
7555
+ kind: "property",
7556
+ filePath: context.filePath,
7557
+ startLine: line,
7558
+ endLine: line,
7559
+ exported: true,
7560
+ scope
7561
+ });
7562
+ }
7563
+ }
7564
+ function parseGemfile(filePath, sourceCode, projectRoot) {
7565
+ const symbols = [];
7566
+ const edges = [];
7567
+ const lines = sourceCode.split("\n");
7568
+ symbols.push({
7569
+ id: `${filePath}::Gemfile`,
7570
+ name: "Gemfile",
7571
+ kind: "module",
7572
+ filePath,
7573
+ startLine: 1,
7574
+ endLine: lines.length,
7575
+ exported: true
7576
+ });
7577
+ for (let i = 0; i < lines.length; i++) {
7578
+ const line = lines[i].trim();
7579
+ const lineNum = i + 1;
7580
+ const gemMatch = line.match(/^\s*gem\s+['"]([^'"]+)['"]/);
7581
+ if (gemMatch) {
7582
+ const gemName = gemMatch[1];
7583
+ symbols.push({
7584
+ id: `${filePath}::gem:${gemName}`,
7585
+ name: gemName,
7586
+ kind: "import",
7587
+ filePath,
7588
+ startLine: lineNum,
7589
+ endLine: lineNum,
7590
+ exported: false
7591
+ });
7592
+ }
7593
+ }
7594
+ return { filePath, symbols, edges };
7595
+ }
7596
+ function resolveRubyRequire(requirePath, isRelative, currentFile, projectRoot) {
7597
+ const extensions = [".rb", ""];
7598
+ if (isRelative) {
7599
+ const dir = dirname14(join15(projectRoot, currentFile));
7600
+ for (const ext of extensions) {
7601
+ const candidate = join15(dir, requirePath + ext);
7602
+ if (existsSync14(candidate)) {
7603
+ const rel = candidate.replace(projectRoot + "/", "");
7604
+ return rel;
7605
+ }
7606
+ }
7607
+ } else {
7608
+ const searchRoots = ["lib", "app", "app/models", "app/controllers", "app/services", ""];
7609
+ for (const root of searchRoots) {
7610
+ for (const ext of extensions) {
7611
+ const candidate = root ? join15(projectRoot, root, requirePath + ext) : join15(projectRoot, requirePath + ext);
7612
+ if (existsSync14(candidate)) {
7613
+ const rel = candidate.replace(projectRoot + "/", "");
7614
+ return rel;
7615
+ }
7616
+ }
7617
+ }
7618
+ }
7619
+ return null;
7620
+ }
7621
+ function resolveSymbol12(name, context) {
7622
+ if (context.imports.has(name)) {
7623
+ return context.imports.get(name) || null;
7624
+ }
7625
+ const currentFileId = `${context.filePath}::${name}`;
7626
+ if (context.symbols.find((s) => s.id === currentFileId)) {
7627
+ return currentFileId;
7628
+ }
7629
+ if (context.currentClass) {
7630
+ const classMethodId = `${context.filePath}::${context.currentClass}.${name}`;
7631
+ if (context.symbols.find((s) => s.id === classMethodId)) {
7632
+ return classMethodId;
7633
+ }
7634
+ }
7635
+ return null;
7636
+ }
7637
+ function isBuiltin(name) {
7638
+ const builtins = /* @__PURE__ */ new Set([
7639
+ "puts",
7640
+ "print",
7641
+ "p",
7642
+ "pp",
7643
+ "warn",
7644
+ "raise",
7645
+ "fail",
7646
+ "require",
7647
+ "require_relative",
7648
+ "include",
7649
+ "extend",
7650
+ "prepend",
7651
+ "attr_accessor",
7652
+ "attr_reader",
7653
+ "attr_writer",
7654
+ "private",
7655
+ "protected",
7656
+ "public",
7657
+ "new",
7658
+ "initialize",
7659
+ "super",
7660
+ "self",
7661
+ "map",
7662
+ "each",
7663
+ "select",
7664
+ "reject",
7665
+ "reduce",
7666
+ "collect",
7667
+ "find",
7668
+ "detect",
7669
+ "any?",
7670
+ "all?",
7671
+ "none?",
7672
+ "count",
7673
+ "freeze",
7674
+ "dup",
7675
+ "clone",
7676
+ "nil?",
7677
+ "is_a?",
7678
+ "kind_of?",
7679
+ "respond_to?",
7680
+ "send",
7681
+ "class",
7682
+ "object_id",
7683
+ "to_s",
7684
+ "to_i",
7685
+ "to_f",
7686
+ "to_a",
7687
+ "to_h",
7688
+ "lambda",
7689
+ "proc",
7690
+ "block_given?",
7691
+ "yield"
7692
+ ]);
7693
+ return builtins.has(name);
7694
+ }
7695
+ function findChildByType13(node, type) {
7696
+ for (let i = 0; i < node.childCount; i++) {
7697
+ const child = node.child(i);
7698
+ if (child && child.type === type) return child;
7699
+ }
7700
+ return null;
7150
7701
  }
7151
- var mojoParser = {
7152
- name: "mojo",
7153
- extensions: [".mojo", ".\u{1F525}", "mojoproject.toml"],
7154
- parseFile: parseMojoFile
7702
+ function nodeText12(node, context) {
7703
+ return context.sourceCode.substring(node.startIndex, node.endIndex);
7704
+ }
7705
+ function getCurrentSymbolId13(context) {
7706
+ if (context.currentScope.length === 0) return null;
7707
+ return `${context.filePath}::${context.currentScope[context.currentScope.length - 1]}`;
7708
+ }
7709
+ var rubyParser = {
7710
+ name: "ruby",
7711
+ extensions: [".rb", ".rake", ".gemspec", "Gemfile"],
7712
+ parseFile: parseRubyFile
7155
7713
  };
7156
7714
 
7157
7715
  // src/parser/detect.ts
@@ -7168,12 +7726,13 @@ var parsers = [
7168
7726
  kotlinParser,
7169
7727
  phpParser,
7170
7728
  swiftParser,
7171
- mojoParser
7729
+ mojoParser,
7730
+ rubyParser
7172
7731
  ];
7173
7732
  var CPP_KEYWORDS = /\b(?:class|namespace|template|public:|private:|protected:|virtual|nullptr|constexpr|auto\s+\w+\s*=|using\s+\w+\s*=|static_cast|dynamic_cast|reinterpret_cast|const_cast|noexcept|override|final|decltype|concept|requires|co_await|co_yield|co_return|std::)\b/;
7174
7733
  function getParserForFile(filePath, content) {
7175
- const ext = extname10(filePath).toLowerCase();
7176
- const fileName = basename8(filePath);
7734
+ const ext = extname11(filePath).toLowerCase();
7735
+ const fileName = basename9(filePath);
7177
7736
  if (ext === ".h" && content) {
7178
7737
  if (CPP_KEYWORDS.test(content)) {
7179
7738
  return cppParser;
@@ -7191,7 +7750,7 @@ import { minimatch } from "minimatch";
7191
7750
  var MAX_FILE_SIZE = 1e6;
7192
7751
  function shouldParseFile(fullPath) {
7193
7752
  try {
7194
- const stats = statSync8(fullPath);
7753
+ const stats = statSync9(fullPath);
7195
7754
  if (stats.size > MAX_FILE_SIZE) {
7196
7755
  console.error(`[Parser] Skipping ${fullPath} \u2014 file too large (${(stats.size / 1024).toFixed(0)}KB)`);
7197
7756
  return false;
@@ -7209,8 +7768,8 @@ async function parseProject(projectRoot, options) {
7209
7768
  let errorFiles = 0;
7210
7769
  for (const file of files) {
7211
7770
  try {
7212
- const fullPath = join15(projectRoot, file);
7213
- if (!resolve9(fullPath).startsWith(resolve9(projectRoot))) {
7771
+ const fullPath = join16(projectRoot, file);
7772
+ if (!resolve10(fullPath).startsWith(resolve10(projectRoot))) {
7214
7773
  skippedFiles++;
7215
7774
  continue;
7216
7775
  }
@@ -7233,7 +7792,7 @@ async function parseProject(projectRoot, options) {
7233
7792
  if (options?.verbose) {
7234
7793
  console.error(`[Parser] Parsing: ${file}`);
7235
7794
  }
7236
- const sourceCode = readFileSync11(fullPath, "utf-8");
7795
+ const sourceCode = readFileSync12(fullPath, "utf-8");
7237
7796
  const parser = getParserForFile(file, sourceCode);
7238
7797
  if (!parser) {
7239
7798
  console.error(`No parser found for file: ${file}`);
@@ -7262,8 +7821,8 @@ async function parseProject(projectRoot, options) {
7262
7821
  }
7263
7822
 
7264
7823
  // src/cross-language/detectors/rest-api.ts
7265
- import { readFileSync as readFileSync12 } from "fs";
7266
- import { join as join16, resolve as resolve10 } from "path";
7824
+ import { readFileSync as readFileSync13 } from "fs";
7825
+ import { join as join17, resolve as resolve11 } from "path";
7267
7826
  function getLanguage(filePath) {
7268
7827
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
7269
7828
  if (filePath.endsWith(".js") || filePath.endsWith(".jsx") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs")) return "javascript";
@@ -7274,6 +7833,7 @@ function getLanguage(filePath) {
7274
7833
  if (filePath.endsWith(".kt") || filePath.endsWith(".kts")) return "kotlin";
7275
7834
  if (filePath.endsWith(".php")) return "php";
7276
7835
  if (filePath.endsWith(".swift")) return "swift";
7836
+ if (filePath.endsWith(".rb") || filePath.endsWith(".rake") || filePath.endsWith(".ru") || filePath.endsWith(".gemspec")) return "ruby";
7277
7837
  if (filePath.endsWith(".mojo") || filePath.endsWith(".\u{1F525}")) return "mojo";
7278
7838
  if (filePath.endsWith(".cpp") || filePath.endsWith(".cc") || filePath.endsWith(".cxx") || filePath.endsWith(".c++") || filePath.endsWith(".hpp") || filePath.endsWith(".hh") || filePath.endsWith(".hxx") || filePath.endsWith(".h++") || filePath.endsWith(".h") || filePath.endsWith(".inl") || filePath.endsWith(".ipp")) return "cpp";
7279
7839
  return "unknown";
@@ -7761,6 +8321,63 @@ function extractRouteDefinitions(source, filePath) {
7761
8321
  }
7762
8322
  }
7763
8323
  }
8324
+ if (lang === "ruby") {
8325
+ const railsRouteMatch = line.match(/^\s*(get|post|put|patch|delete)\s+['"]([^'"]+)['"]/);
8326
+ if (railsRouteMatch) {
8327
+ const path6 = railsRouteMatch[2].startsWith("/") ? railsRouteMatch[2] : "/" + railsRouteMatch[2];
8328
+ routes.push({
8329
+ method: railsRouteMatch[1].toUpperCase(),
8330
+ path: path6,
8331
+ normalizedPath: normalizePath(path6),
8332
+ file: filePath,
8333
+ line: i + 1
8334
+ });
8335
+ }
8336
+ const sinatraMatch = line.match(/^\s*(get|post|put|patch|delete)\s+['"]([^'"]+)['"]\s+do/);
8337
+ if (sinatraMatch && !railsRouteMatch) {
8338
+ const path6 = sinatraMatch[2].startsWith("/") ? sinatraMatch[2] : "/" + sinatraMatch[2];
8339
+ routes.push({
8340
+ method: sinatraMatch[1].toUpperCase(),
8341
+ path: path6,
8342
+ normalizedPath: normalizePath(path6),
8343
+ file: filePath,
8344
+ line: i + 1
8345
+ });
8346
+ }
8347
+ const resourcesMatch = line.match(/^\s*resources?\s+:(\w+)/);
8348
+ if (resourcesMatch) {
8349
+ const resourcePath = "/" + resourcesMatch[1];
8350
+ routes.push({
8351
+ method: "ANY",
8352
+ path: resourcePath,
8353
+ normalizedPath: normalizePath(resourcePath),
8354
+ file: filePath,
8355
+ line: i + 1
8356
+ });
8357
+ }
8358
+ const rackMatch = line.match(/^\s*map\s+['"]([^'"]+)['"]/);
8359
+ if (rackMatch) {
8360
+ const path6 = rackMatch[1].startsWith("/") ? rackMatch[1] : "/" + rackMatch[1];
8361
+ routes.push({
8362
+ method: "ANY",
8363
+ path: path6,
8364
+ normalizedPath: normalizePath(path6),
8365
+ file: filePath,
8366
+ line: i + 1
8367
+ });
8368
+ }
8369
+ const grapeMatch = line.match(/^\s*(get|post|put|patch|delete)\s+['"]([^'"]+)['"]/);
8370
+ if (grapeMatch && !railsRouteMatch) {
8371
+ const path6 = grapeMatch[2].startsWith("/") ? grapeMatch[2] : "/" + grapeMatch[2];
8372
+ routes.push({
8373
+ method: grapeMatch[1].toUpperCase(),
8374
+ path: path6,
8375
+ normalizedPath: normalizePath(path6),
8376
+ file: filePath,
8377
+ line: i + 1
8378
+ });
8379
+ }
8380
+ }
7764
8381
  if (lang === "cpp") {
7765
8382
  const crowMatch = line.match(/CROW_ROUTE\s*\(\s*\w+\s*,\s*"([^"]+)"/);
7766
8383
  if (crowMatch) {
@@ -7875,11 +8492,11 @@ function detectRestApiEdges(files, projectRoot) {
7875
8492
  const allCalls = [];
7876
8493
  const allRoutes = [];
7877
8494
  for (const file of files) {
7878
- const fullPath = join16(projectRoot, file.filePath);
7879
- if (!resolve10(fullPath).startsWith(resolve10(projectRoot))) continue;
8495
+ const fullPath = join17(projectRoot, file.filePath);
8496
+ if (!resolve11(fullPath).startsWith(resolve11(projectRoot))) continue;
7880
8497
  let source;
7881
8498
  try {
7882
- source = readFileSync12(fullPath, "utf-8");
8499
+ source = readFileSync13(fullPath, "utf-8");
7883
8500
  } catch {
7884
8501
  continue;
7885
8502
  }
@@ -7951,6 +8568,34 @@ function detectRestApiEdges(files, projectRoot) {
7951
8568
  }
7952
8569
  }
7953
8570
  }
8571
+ if (lang === "ruby") {
8572
+ const rubyLines = source.split("\n");
8573
+ for (let i = 0; i < rubyLines.length; i++) {
8574
+ const line = rubyLines[i];
8575
+ const faradayMatch = line.match(/\w+\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/i);
8576
+ if (faradayMatch) {
8577
+ const path6 = faradayMatch[2];
8578
+ if (isLocalApiPath(path6)) {
8579
+ allCalls.push({ method: faradayMatch[1].toUpperCase(), path: cleanPath(path6), file: file.filePath, line: i + 1 });
8580
+ }
8581
+ }
8582
+ const netHttpMatch = line.match(/Net::HTTP\s*\.\s*(get|post_form|post)\s*\(/i);
8583
+ if (netHttpMatch) {
8584
+ const uriMatch = line.match(/['"]([^'"]+)['"]/);
8585
+ if (uriMatch && isLocalApiPath(uriMatch[1])) {
8586
+ const method = netHttpMatch[1].toUpperCase().replace("POST_FORM", "POST");
8587
+ allCalls.push({ method, path: cleanPath(uriMatch[1]), file: file.filePath, line: i + 1 });
8588
+ }
8589
+ }
8590
+ const httpartyMatch = line.match(/(?:HTTParty|self)\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/i);
8591
+ if (httpartyMatch) {
8592
+ const path6 = httpartyMatch[2];
8593
+ if (isLocalApiPath(path6)) {
8594
+ allCalls.push({ method: httpartyMatch[1].toUpperCase(), path: cleanPath(path6), file: file.filePath, line: i + 1 });
8595
+ }
8596
+ }
8597
+ }
8598
+ }
7954
8599
  allRoutes.push(...extractRouteDefinitions(source, file.filePath));
7955
8600
  }
7956
8601
  for (const call of allCalls) {
@@ -7979,8 +8624,8 @@ function detectRestApiEdges(files, projectRoot) {
7979
8624
  }
7980
8625
 
7981
8626
  // src/cross-language/detectors/subprocess.ts
7982
- import { readFileSync as readFileSync13 } from "fs";
7983
- import { join as join17, resolve as resolve11, basename as basename9 } from "path";
8627
+ import { readFileSync as readFileSync14 } from "fs";
8628
+ import { join as join18, resolve as resolve12, basename as basename10 } from "path";
7984
8629
  var SCRIPT_EXTENSIONS = [".py", ".js", ".ts", ".go", ".rs"];
7985
8630
  function getLanguage2(filePath) {
7986
8631
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
@@ -8079,16 +8724,16 @@ function detectSubprocessEdges(files, projectRoot) {
8079
8724
  const knownFiles = new Set(files.map((f) => f.filePath));
8080
8725
  const basenameMap = /* @__PURE__ */ new Map();
8081
8726
  for (const f of files) {
8082
- const base = basename9(f.filePath);
8727
+ const base = basename10(f.filePath);
8083
8728
  if (!basenameMap.has(base)) basenameMap.set(base, []);
8084
8729
  basenameMap.get(base).push(f.filePath);
8085
8730
  }
8086
8731
  for (const file of files) {
8087
- const fullPath = join17(projectRoot, file.filePath);
8088
- if (!resolve11(fullPath).startsWith(resolve11(projectRoot))) continue;
8732
+ const fullPath = join18(projectRoot, file.filePath);
8733
+ if (!resolve12(fullPath).startsWith(resolve12(projectRoot))) continue;
8089
8734
  let source;
8090
8735
  try {
8091
- source = readFileSync13(fullPath, "utf-8");
8736
+ source = readFileSync14(fullPath, "utf-8");
8092
8737
  } catch {
8093
8738
  continue;
8094
8739
  }
@@ -8100,7 +8745,7 @@ function detectSubprocessEdges(files, projectRoot) {
8100
8745
  targetFile = call.calledFile;
8101
8746
  confidence = "high";
8102
8747
  } else {
8103
- const base = basename9(call.calledFile);
8748
+ const base = basename10(call.calledFile);
8104
8749
  const candidates = basenameMap.get(base);
8105
8750
  if (candidates && candidates.length > 0) {
8106
8751
  const exactCandidate = candidates.find((c) => c.endsWith(call.calledFile));
@@ -8479,7 +9124,7 @@ function getArchitectureSummary(graph) {
8479
9124
  }
8480
9125
 
8481
9126
  // src/health/metrics.ts
8482
- import { dirname as dirname14 } from "path";
9127
+ import { dirname as dirname15 } from "path";
8483
9128
  function scoreToGrade(score) {
8484
9129
  if (score >= 90) return "A";
8485
9130
  if (score >= 80) return "B";
@@ -8512,8 +9157,8 @@ function calculateCouplingScore(graph) {
8512
9157
  totalEdges++;
8513
9158
  fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
8514
9159
  fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
8515
- const sourceDir = dirname14(sourceAttrs.filePath).split("/")[0];
8516
- const targetDir = dirname14(targetAttrs.filePath).split("/")[0];
9160
+ const sourceDir = dirname15(sourceAttrs.filePath).split("/")[0];
9161
+ const targetDir = dirname15(targetAttrs.filePath).split("/")[0];
8517
9162
  if (sourceDir !== targetDir) {
8518
9163
  crossDirEdges++;
8519
9164
  }
@@ -8560,8 +9205,8 @@ function calculateCohesionScore(graph) {
8560
9205
  const sourceAttrs = graph.getNodeAttributes(source);
8561
9206
  const targetAttrs = graph.getNodeAttributes(target);
8562
9207
  if (sourceAttrs.filePath !== targetAttrs.filePath) {
8563
- const sourceDir = dirname14(sourceAttrs.filePath);
8564
- const targetDir = dirname14(targetAttrs.filePath);
9208
+ const sourceDir = dirname15(sourceAttrs.filePath);
9209
+ const targetDir = dirname15(targetAttrs.filePath);
8565
9210
  if (!dirEdges.has(sourceDir)) {
8566
9211
  dirEdges.set(sourceDir, { internal: 0, total: 0 });
8567
9212
  }
@@ -8843,8 +9488,8 @@ function calculateDepthScore(graph) {
8843
9488
  }
8844
9489
 
8845
9490
  // src/health/index.ts
8846
- import { readFileSync as readFileSync14, writeFileSync, existsSync as existsSync14, mkdirSync } from "fs";
8847
- import { dirname as dirname15, resolve as resolve12 } from "path";
9491
+ import { readFileSync as readFileSync15, writeFileSync, existsSync as existsSync15, mkdirSync } from "fs";
9492
+ import { dirname as dirname16, resolve as resolve13 } from "path";
8848
9493
  function calculateHealthScore(graph, projectRoot) {
8849
9494
  const coupling = calculateCouplingScore(graph);
8850
9495
  const cohesion = calculateCohesionScore(graph);
@@ -8943,8 +9588,8 @@ function getHealthTrend(projectRoot, currentScore) {
8943
9588
  }
8944
9589
  }
8945
9590
  function saveHealthHistory(projectRoot, report) {
8946
- const resolvedRoot = resolve12(projectRoot);
8947
- const historyFile = resolve12(resolvedRoot, ".depwire", "health-history.json");
9591
+ const resolvedRoot = resolve13(projectRoot);
9592
+ const historyFile = resolve13(resolvedRoot, ".depwire", "health-history.json");
8948
9593
  if (!historyFile.startsWith(resolvedRoot)) {
8949
9594
  return;
8950
9595
  }
@@ -8959,10 +9604,10 @@ function saveHealthHistory(projectRoot, report) {
8959
9604
  }))
8960
9605
  };
8961
9606
  let history = [];
8962
- if (existsSync14(historyFile)) {
9607
+ if (existsSync15(historyFile)) {
8963
9608
  try {
8964
9609
  if (!historyFile.startsWith(resolvedRoot)) return;
8965
- const content = readFileSync14(historyFile, "utf-8");
9610
+ const content = readFileSync15(historyFile, "utf-8");
8966
9611
  history = JSON.parse(content);
8967
9612
  } catch {
8968
9613
  }
@@ -8971,19 +9616,19 @@ function saveHealthHistory(projectRoot, report) {
8971
9616
  if (history.length > 50) {
8972
9617
  history = history.slice(-50);
8973
9618
  }
8974
- mkdirSync(dirname15(historyFile), { recursive: true });
9619
+ mkdirSync(dirname16(historyFile), { recursive: true });
8975
9620
  if (!historyFile.startsWith(resolvedRoot)) return;
8976
9621
  writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
8977
9622
  }
8978
9623
  function loadHealthHistory(projectRoot) {
8979
- const resolvedRoot = resolve12(projectRoot);
8980
- const historyFile = resolve12(resolvedRoot, ".depwire", "health-history.json");
8981
- if (!historyFile.startsWith(resolvedRoot) || !existsSync14(historyFile)) {
9624
+ const resolvedRoot = resolve13(projectRoot);
9625
+ const historyFile = resolve13(resolvedRoot, ".depwire", "health-history.json");
9626
+ if (!historyFile.startsWith(resolvedRoot) || !existsSync15(historyFile)) {
8982
9627
  return [];
8983
9628
  }
8984
9629
  try {
8985
9630
  if (!historyFile.startsWith(resolvedRoot)) return [];
8986
- const content = readFileSync14(historyFile, "utf-8");
9631
+ const content = readFileSync15(historyFile, "utf-8");
8987
9632
  return JSON.parse(content);
8988
9633
  } catch {
8989
9634
  return [];
@@ -8992,7 +9637,7 @@ function loadHealthHistory(projectRoot) {
8992
9637
 
8993
9638
  // src/dead-code/detector.ts
8994
9639
  import path2 from "path";
8995
- import { readFileSync as readFileSync15, existsSync as existsSync15 } from "fs";
9640
+ import { readFileSync as readFileSync16, existsSync as existsSync16 } from "fs";
8996
9641
  function findDeadSymbols(graph, projectRoot, includeTests = false, debug = false) {
8997
9642
  const deadSymbols = [];
8998
9643
  const context = { graph, projectRoot };
@@ -9125,11 +9770,11 @@ function getPackageEntryPoints(projectRoot) {
9125
9770
  const entryPoints = /* @__PURE__ */ new Set();
9126
9771
  const resolvedRoot = path2.resolve(projectRoot);
9127
9772
  const packageJsonPath = path2.resolve(resolvedRoot, "package.json");
9128
- if (!packageJsonPath.startsWith(resolvedRoot) || !existsSync15(packageJsonPath)) {
9773
+ if (!packageJsonPath.startsWith(resolvedRoot) || !existsSync16(packageJsonPath)) {
9129
9774
  return entryPoints;
9130
9775
  }
9131
9776
  try {
9132
- const packageJson = JSON.parse(readFileSync15(packageJsonPath, "utf-8"));
9777
+ const packageJson = JSON.parse(readFileSync16(packageJsonPath, "utf-8"));
9133
9778
  if (packageJson.main) {
9134
9779
  entryPoints.add(path2.resolve(projectRoot, packageJson.main));
9135
9780
  }
@@ -9195,6 +9840,9 @@ function shouldExclude(attrs, context, includeTests, packageEntryPoints) {
9195
9840
  if (isMojoExcluded(attrs)) {
9196
9841
  return "framework";
9197
9842
  }
9843
+ if (isRubyExcluded(attrs)) {
9844
+ return "framework";
9845
+ }
9198
9846
  return null;
9199
9847
  }
9200
9848
  function isRealPackageEntryPoint(filePath, packageEntryPoints) {
@@ -9407,6 +10055,50 @@ function isMojoExcluded(attrs) {
9407
10055
  if (name === "main") return true;
9408
10056
  return false;
9409
10057
  }
10058
+ function isRubyExcluded(attrs) {
10059
+ const filePath = attrs.file || attrs.filePath || "";
10060
+ const name = attrs.name || "";
10061
+ if (!filePath.endsWith(".rb") && !filePath.endsWith(".rake") && !filePath.endsWith(".gemspec")) return false;
10062
+ const railsCallbacks = [
10063
+ "before_action",
10064
+ "after_action",
10065
+ "around_action",
10066
+ "before_filter",
10067
+ "after_filter",
10068
+ "around_filter"
10069
+ ];
10070
+ if (railsCallbacks.includes(name)) return true;
10071
+ const arCallbacks = [
10072
+ "before_save",
10073
+ "after_save",
10074
+ "before_create",
10075
+ "after_create",
10076
+ "before_update",
10077
+ "after_update",
10078
+ "before_destroy",
10079
+ "after_destroy",
10080
+ "before_validation",
10081
+ "after_validation",
10082
+ "after_commit",
10083
+ "after_rollback",
10084
+ "after_initialize",
10085
+ "after_find"
10086
+ ];
10087
+ if (arCallbacks.includes(name)) return true;
10088
+ if (filePath.endsWith(".rake") || name === "task") return true;
10089
+ if (["it", "describe", "context", "specify", "subject", "let", "let!", "before", "after"].includes(name)) return true;
10090
+ if (name.startsWith("test_")) return true;
10091
+ if (name === "included" || name === "class_methods") return true;
10092
+ if (name === "initialize") return true;
10093
+ if (name === "method_missing" || name === "respond_to_missing?") return true;
10094
+ if (name === "concern" || name === "concerning") return true;
10095
+ const policyMethods = ["index?", "show?", "create?", "new?", "update?", "edit?", "destroy?"];
10096
+ if (policyMethods.includes(name)) return true;
10097
+ const deviseMethods = ["authenticate!", "valid?", "authenticate_user!", "current_user"];
10098
+ if (deviseMethods.includes(name)) return true;
10099
+ if (name === "main") return true;
10100
+ return false;
10101
+ }
9410
10102
 
9411
10103
  // src/dead-code/classifier.ts
9412
10104
  import path3 from "path";
@@ -9473,8 +10165,8 @@ function generateReason(symbol, confidence) {
9473
10165
  return "Potentially unused";
9474
10166
  }
9475
10167
  function isBarrelFile(filePath) {
9476
- const basename13 = path3.basename(filePath);
9477
- return basename13 === "index.ts" || basename13 === "index.js";
10168
+ const basename14 = path3.basename(filePath);
10169
+ return basename14 === "index.ts" || basename14 === "index.js";
9478
10170
  }
9479
10171
  function isTestFile2(filePath) {
9480
10172
  return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
@@ -9653,11 +10345,11 @@ function filterByConfidence(symbols, minConfidence) {
9653
10345
  }
9654
10346
 
9655
10347
  // src/docs/generator.ts
9656
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync18 } from "fs";
9657
- import { join as join21 } from "path";
10348
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync19 } from "fs";
10349
+ import { join as join22 } from "path";
9658
10350
 
9659
10351
  // src/docs/architecture.ts
9660
- import { dirname as dirname16 } from "path";
10352
+ import { dirname as dirname17 } from "path";
9661
10353
 
9662
10354
  // src/docs/templates.ts
9663
10355
  function header(text, level = 1) {
@@ -9808,7 +10500,7 @@ function generateModuleStructure(graph) {
9808
10500
  function getDirectoryStats(graph) {
9809
10501
  const dirMap = /* @__PURE__ */ new Map();
9810
10502
  graph.forEachNode((node, attrs) => {
9811
- const dir = dirname16(attrs.filePath);
10503
+ const dir = dirname17(attrs.filePath);
9812
10504
  if (dir === ".") return;
9813
10505
  if (!dirMap.has(dir)) {
9814
10506
  dirMap.set(dir, {
@@ -9833,7 +10525,7 @@ function getDirectoryStats(graph) {
9833
10525
  });
9834
10526
  const filesPerDir = /* @__PURE__ */ new Map();
9835
10527
  graph.forEachNode((node, attrs) => {
9836
- const dir = dirname16(attrs.filePath);
10528
+ const dir = dirname17(attrs.filePath);
9837
10529
  if (!filesPerDir.has(dir)) {
9838
10530
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
9839
10531
  }
@@ -9848,8 +10540,8 @@ function getDirectoryStats(graph) {
9848
10540
  graph.forEachEdge((edge, attrs, source, target) => {
9849
10541
  const sourceAttrs = graph.getNodeAttributes(source);
9850
10542
  const targetAttrs = graph.getNodeAttributes(target);
9851
- const sourceDir = dirname16(sourceAttrs.filePath);
9852
- const targetDir = dirname16(targetAttrs.filePath);
10543
+ const sourceDir = dirname17(sourceAttrs.filePath);
10544
+ const targetDir = dirname17(targetAttrs.filePath);
9853
10545
  if (sourceDir !== targetDir) {
9854
10546
  if (!dirEdges.has(sourceDir)) {
9855
10547
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -10056,7 +10748,7 @@ function detectCycles(graph) {
10056
10748
  }
10057
10749
 
10058
10750
  // src/docs/conventions.ts
10059
- import { basename as basename10, extname as extname11 } from "path";
10751
+ import { basename as basename11, extname as extname12 } from "path";
10060
10752
  function generateConventions(graph, projectRoot, version) {
10061
10753
  let output = "";
10062
10754
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -10094,7 +10786,7 @@ function generateFileOrganization(graph) {
10094
10786
  graph.forEachNode((node, attrs) => {
10095
10787
  if (!files.has(attrs.filePath)) {
10096
10788
  files.add(attrs.filePath);
10097
- const fileName = basename10(attrs.filePath);
10789
+ const fileName = basename11(attrs.filePath);
10098
10790
  if (fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx") {
10099
10791
  barrelFileCount++;
10100
10792
  }
@@ -10153,7 +10845,7 @@ function generateNamingPatterns(graph) {
10153
10845
  graph.forEachNode((node, attrs) => {
10154
10846
  if (!files.has(attrs.filePath)) {
10155
10847
  files.add(attrs.filePath);
10156
- const fileName = basename10(attrs.filePath, extname11(attrs.filePath));
10848
+ const fileName = basename11(attrs.filePath, extname12(attrs.filePath));
10157
10849
  if (isCamelCase(fileName)) patterns.files.camelCase++;
10158
10850
  else if (isPascalCase(fileName)) patterns.files.PascalCase++;
10159
10851
  else if (isKebabCase(fileName)) patterns.files.kebabCase++;
@@ -10830,7 +11522,7 @@ function detectCyclesDetailed(graph) {
10830
11522
  }
10831
11523
 
10832
11524
  // src/docs/onboarding.ts
10833
- import { dirname as dirname17 } from "path";
11525
+ import { dirname as dirname18 } from "path";
10834
11526
  function generateOnboarding(graph, projectRoot, version) {
10835
11527
  let output = "";
10836
11528
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -10889,7 +11581,7 @@ function generateQuickOrientation(graph) {
10889
11581
  const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
10890
11582
  const dirs = /* @__PURE__ */ new Set();
10891
11583
  graph.forEachNode((node, attrs) => {
10892
- const dir = dirname17(attrs.filePath);
11584
+ const dir = dirname18(attrs.filePath);
10893
11585
  if (dir !== ".") {
10894
11586
  const topLevel = dir.split("/")[0];
10895
11587
  dirs.add(topLevel);
@@ -10993,7 +11685,7 @@ function generateModuleMap(graph) {
10993
11685
  function getDirectoryStats2(graph) {
10994
11686
  const dirMap = /* @__PURE__ */ new Map();
10995
11687
  graph.forEachNode((node, attrs) => {
10996
- const dir = dirname17(attrs.filePath);
11688
+ const dir = dirname18(attrs.filePath);
10997
11689
  if (dir === ".") return;
10998
11690
  if (!dirMap.has(dir)) {
10999
11691
  dirMap.set(dir, {
@@ -11008,7 +11700,7 @@ function getDirectoryStats2(graph) {
11008
11700
  });
11009
11701
  const filesPerDir = /* @__PURE__ */ new Map();
11010
11702
  graph.forEachNode((node, attrs) => {
11011
- const dir = dirname17(attrs.filePath);
11703
+ const dir = dirname18(attrs.filePath);
11012
11704
  if (!filesPerDir.has(dir)) {
11013
11705
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
11014
11706
  }
@@ -11023,8 +11715,8 @@ function getDirectoryStats2(graph) {
11023
11715
  graph.forEachEdge((edge, attrs, source, target) => {
11024
11716
  const sourceAttrs = graph.getNodeAttributes(source);
11025
11717
  const targetAttrs = graph.getNodeAttributes(target);
11026
- const sourceDir = dirname17(sourceAttrs.filePath);
11027
- const targetDir = dirname17(targetAttrs.filePath);
11718
+ const sourceDir = dirname18(sourceAttrs.filePath);
11719
+ const targetDir = dirname18(targetAttrs.filePath);
11028
11720
  if (sourceDir !== targetDir) {
11029
11721
  if (!dirEdges.has(sourceDir)) {
11030
11722
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -11105,7 +11797,7 @@ function detectClusters(graph) {
11105
11797
  const dirFiles = /* @__PURE__ */ new Map();
11106
11798
  const fileEdges = /* @__PURE__ */ new Map();
11107
11799
  graph.forEachNode((node, attrs) => {
11108
- const dir = dirname17(attrs.filePath);
11800
+ const dir = dirname18(attrs.filePath);
11109
11801
  if (!dirFiles.has(dir)) {
11110
11802
  dirFiles.set(dir, /* @__PURE__ */ new Set());
11111
11803
  }
@@ -11159,8 +11851,8 @@ function inferClusterName(files) {
11159
11851
  if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
11160
11852
  return capitalizeFirst2(sortedWords[0][0]);
11161
11853
  }
11162
- const commonDir = dirname17(files[0]);
11163
- if (files.every((f) => dirname17(f) === commonDir)) {
11854
+ const commonDir = dirname18(files[0]);
11855
+ if (files.every((f) => dirname18(f) === commonDir)) {
11164
11856
  return capitalizeFirst2(commonDir.split("/").pop() || "Core");
11165
11857
  }
11166
11858
  return "Core";
@@ -11218,7 +11910,7 @@ function generateDepwireUsage(projectRoot) {
11218
11910
  }
11219
11911
 
11220
11912
  // src/docs/files.ts
11221
- import { dirname as dirname18, basename as basename11 } from "path";
11913
+ import { dirname as dirname19, basename as basename12 } from "path";
11222
11914
  function generateFiles(graph, projectRoot, version) {
11223
11915
  let output = "";
11224
11916
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -11322,7 +12014,7 @@ function generateDirectoryBreakdown(graph) {
11322
12014
  const fileStats = getFileStats2(graph);
11323
12015
  const dirMap = /* @__PURE__ */ new Map();
11324
12016
  for (const file of fileStats) {
11325
- const dir = dirname18(file.filePath);
12017
+ const dir = dirname19(file.filePath);
11326
12018
  const topDir = dir === "." ? "." : dir.split("/")[0];
11327
12019
  if (!dirMap.has(topDir)) {
11328
12020
  dirMap.set(topDir, {
@@ -11337,7 +12029,7 @@ function generateDirectoryBreakdown(graph) {
11337
12029
  dirStats.symbolCount += file.symbolCount;
11338
12030
  if (file.totalConnections > dirStats.maxConnections) {
11339
12031
  dirStats.maxConnections = file.totalConnections;
11340
- dirStats.mostConnectedFile = basename11(file.filePath);
12032
+ dirStats.mostConnectedFile = basename12(file.filePath);
11341
12033
  }
11342
12034
  }
11343
12035
  if (dirMap.size === 0) {
@@ -11965,7 +12657,7 @@ function generateRecommendations(graph) {
11965
12657
  }
11966
12658
 
11967
12659
  // src/docs/tests.ts
11968
- import { basename as basename12, dirname as dirname19 } from "path";
12660
+ import { basename as basename13, dirname as dirname20 } from "path";
11969
12661
  function generateTests(graph, projectRoot, version) {
11970
12662
  let output = "";
11971
12663
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -11993,8 +12685,8 @@ function getFileCount8(graph) {
11993
12685
  return files.size;
11994
12686
  }
11995
12687
  function isTestFile3(filePath) {
11996
- const fileName = basename12(filePath).toLowerCase();
11997
- const dirPath = dirname19(filePath).toLowerCase();
12688
+ const fileName = basename13(filePath).toLowerCase();
12689
+ const dirPath = dirname20(filePath).toLowerCase();
11998
12690
  if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
11999
12691
  return true;
12000
12692
  }
@@ -12052,13 +12744,13 @@ function generateTestFileInventory(graph) {
12052
12744
  return output;
12053
12745
  }
12054
12746
  function matchTestToSource(testFile) {
12055
- const testFileName = basename12(testFile);
12056
- const testDir = dirname19(testFile);
12747
+ const testFileName = basename13(testFile);
12748
+ const testDir = dirname20(testFile);
12057
12749
  let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
12058
12750
  const possiblePaths = [];
12059
12751
  possiblePaths.push(testDir + "/" + sourceFileName);
12060
12752
  if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
12061
- const parentDir = dirname19(testDir);
12753
+ const parentDir = dirname20(testDir);
12062
12754
  possiblePaths.push(parentDir + "/" + sourceFileName);
12063
12755
  }
12064
12756
  if (testDir.includes("test")) {
@@ -12214,7 +12906,7 @@ function generateTestCoverageMap(graph) {
12214
12906
  const rows = mappings.slice(0, 30).map((m) => [
12215
12907
  `\`${m.sourceFile}\``,
12216
12908
  m.hasTest ? "\u2705" : "\u274C",
12217
- m.testFile ? `\`${basename12(m.testFile)}\`` : "-",
12909
+ m.testFile ? `\`${basename13(m.testFile)}\`` : "-",
12218
12910
  formatNumber(m.symbolCount)
12219
12911
  ]);
12220
12912
  let output = table(headers, rows);
@@ -12255,7 +12947,7 @@ function generateTestStatistics(graph) {
12255
12947
  `;
12256
12948
  const dirTestCoverage = /* @__PURE__ */ new Map();
12257
12949
  for (const sourceFile of sourceFiles) {
12258
- const dir = dirname19(sourceFile).split("/")[0];
12950
+ const dir = dirname20(sourceFile).split("/")[0];
12259
12951
  if (!dirTestCoverage.has(dir)) {
12260
12952
  dirTestCoverage.set(dir, { total: 0, tested: 0 });
12261
12953
  }
@@ -12278,7 +12970,7 @@ function generateTestStatistics(graph) {
12278
12970
  }
12279
12971
 
12280
12972
  // src/docs/history.ts
12281
- import { dirname as dirname20 } from "path";
12973
+ import { dirname as dirname21 } from "path";
12282
12974
  import { execSync } from "child_process";
12283
12975
  function generateHistory(graph, projectRoot, version) {
12284
12976
  let output = "";
@@ -12559,7 +13251,7 @@ function generateFeatureClusters(graph) {
12559
13251
  const dirFiles = /* @__PURE__ */ new Map();
12560
13252
  const fileEdges = /* @__PURE__ */ new Map();
12561
13253
  graph.forEachNode((node, attrs) => {
12562
- const dir = dirname20(attrs.filePath);
13254
+ const dir = dirname21(attrs.filePath);
12563
13255
  if (!dirFiles.has(dir)) {
12564
13256
  dirFiles.set(dir, /* @__PURE__ */ new Set());
12565
13257
  }
@@ -12641,7 +13333,7 @@ function capitalizeFirst3(str) {
12641
13333
  }
12642
13334
 
12643
13335
  // src/docs/current.ts
12644
- import { dirname as dirname21 } from "path";
13336
+ import { dirname as dirname22 } from "path";
12645
13337
  function generateCurrent(graph, projectRoot, version) {
12646
13338
  let output = "";
12647
13339
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -12779,7 +13471,7 @@ function generateCompleteFileIndex(graph) {
12779
13471
  fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
12780
13472
  const dirGroups = /* @__PURE__ */ new Map();
12781
13473
  for (const info of fileInfos) {
12782
- const dir = dirname21(info.filePath);
13474
+ const dir = dirname22(info.filePath);
12783
13475
  const topDir = dir === "." ? "root" : dir.split("/")[0];
12784
13476
  if (!dirGroups.has(topDir)) {
12785
13477
  dirGroups.set(topDir, []);
@@ -12990,8 +13682,8 @@ function getTopLevelDir2(filePath) {
12990
13682
  }
12991
13683
 
12992
13684
  // src/docs/status.ts
12993
- import { readFileSync as readFileSync16, existsSync as existsSync16 } from "fs";
12994
- import { resolve as resolve13 } from "path";
13685
+ import { readFileSync as readFileSync17, existsSync as existsSync17 } from "fs";
13686
+ import { resolve as resolve14 } from "path";
12995
13687
  function generateStatus(graph, projectRoot, version) {
12996
13688
  let output = "";
12997
13689
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -13024,16 +13716,16 @@ function getFileCount11(graph) {
13024
13716
  }
13025
13717
  function extractComments(projectRoot, filePath) {
13026
13718
  const comments = [];
13027
- const resolvedRoot = resolve13(projectRoot);
13028
- const fullPath = resolve13(resolvedRoot, filePath);
13719
+ const resolvedRoot = resolve14(projectRoot);
13720
+ const fullPath = resolve14(resolvedRoot, filePath);
13029
13721
  if (!fullPath.startsWith(resolvedRoot)) {
13030
13722
  return comments;
13031
13723
  }
13032
- if (!existsSync16(fullPath)) {
13724
+ if (!existsSync17(fullPath)) {
13033
13725
  return comments;
13034
13726
  }
13035
13727
  try {
13036
- const content = readFileSync16(fullPath, "utf-8");
13728
+ const content = readFileSync17(fullPath, "utf-8");
13037
13729
  const lines = content.split("\n");
13038
13730
  const patterns = [
13039
13731
  { type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
@@ -13612,16 +14304,16 @@ function generateConfidenceSection(title, description, symbols, projectRoot) {
13612
14304
  }
13613
14305
 
13614
14306
  // src/docs/metadata.ts
13615
- import { existsSync as existsSync17, readFileSync as readFileSync17, writeFileSync as writeFileSync2 } from "fs";
13616
- import { resolve as resolve14 } from "path";
14307
+ import { existsSync as existsSync18, readFileSync as readFileSync18, writeFileSync as writeFileSync2 } from "fs";
14308
+ import { resolve as resolve15 } from "path";
13617
14309
  function loadMetadata(outputDir) {
13618
- const resolvedDir = resolve14(outputDir);
13619
- const metadataPath = resolve14(resolvedDir, "metadata.json");
13620
- if (!metadataPath.startsWith(resolvedDir) || !existsSync17(metadataPath)) {
14310
+ const resolvedDir = resolve15(outputDir);
14311
+ const metadataPath = resolve15(resolvedDir, "metadata.json");
14312
+ if (!metadataPath.startsWith(resolvedDir) || !existsSync18(metadataPath)) {
13621
14313
  return null;
13622
14314
  }
13623
14315
  try {
13624
- const content = readFileSync17(metadataPath, "utf-8");
14316
+ const content = readFileSync18(metadataPath, "utf-8");
13625
14317
  return JSON.parse(content);
13626
14318
  } catch (err) {
13627
14319
  console.error("Failed to load metadata:", err);
@@ -13629,8 +14321,8 @@ function loadMetadata(outputDir) {
13629
14321
  }
13630
14322
  }
13631
14323
  function saveMetadata(outputDir, metadata) {
13632
- const resolvedDir = resolve14(outputDir);
13633
- const metadataPath = resolve14(resolvedDir, "metadata.json");
14324
+ const resolvedDir = resolve15(outputDir);
14325
+ const metadataPath = resolve15(resolvedDir, "metadata.json");
13634
14326
  if (!metadataPath.startsWith(resolvedDir)) {
13635
14327
  throw new Error(`Path traversal attempt blocked: ${metadataPath}`);
13636
14328
  }
@@ -13676,7 +14368,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13676
14368
  const generated = [];
13677
14369
  const errors = [];
13678
14370
  try {
13679
- if (!existsSync18(options.outputDir)) {
14371
+ if (!existsSync19(options.outputDir)) {
13680
14372
  mkdirSync2(options.outputDir, { recursive: true });
13681
14373
  if (options.verbose) {
13682
14374
  console.log(`Created output directory: ${options.outputDir}`);
@@ -13715,7 +14407,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13715
14407
  try {
13716
14408
  if (options.verbose) console.log("Generating ARCHITECTURE.md...");
13717
14409
  const content = generateArchitecture(graph, projectRoot, version, parseTime);
13718
- const filePath = join21(options.outputDir, "ARCHITECTURE.md");
14410
+ const filePath = join22(options.outputDir, "ARCHITECTURE.md");
13719
14411
  writeFileSync3(filePath, content, "utf-8");
13720
14412
  generated.push("ARCHITECTURE.md");
13721
14413
  } catch (err) {
@@ -13726,7 +14418,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13726
14418
  try {
13727
14419
  if (options.verbose) console.log("Generating CONVENTIONS.md...");
13728
14420
  const content = generateConventions(graph, projectRoot, version);
13729
- const filePath = join21(options.outputDir, "CONVENTIONS.md");
14421
+ const filePath = join22(options.outputDir, "CONVENTIONS.md");
13730
14422
  writeFileSync3(filePath, content, "utf-8");
13731
14423
  generated.push("CONVENTIONS.md");
13732
14424
  } catch (err) {
@@ -13737,7 +14429,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13737
14429
  try {
13738
14430
  if (options.verbose) console.log("Generating DEPENDENCIES.md...");
13739
14431
  const content = generateDependencies(graph, projectRoot, version);
13740
- const filePath = join21(options.outputDir, "DEPENDENCIES.md");
14432
+ const filePath = join22(options.outputDir, "DEPENDENCIES.md");
13741
14433
  writeFileSync3(filePath, content, "utf-8");
13742
14434
  generated.push("DEPENDENCIES.md");
13743
14435
  } catch (err) {
@@ -13748,7 +14440,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13748
14440
  try {
13749
14441
  if (options.verbose) console.log("Generating ONBOARDING.md...");
13750
14442
  const content = generateOnboarding(graph, projectRoot, version);
13751
- const filePath = join21(options.outputDir, "ONBOARDING.md");
14443
+ const filePath = join22(options.outputDir, "ONBOARDING.md");
13752
14444
  writeFileSync3(filePath, content, "utf-8");
13753
14445
  generated.push("ONBOARDING.md");
13754
14446
  } catch (err) {
@@ -13759,7 +14451,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13759
14451
  try {
13760
14452
  if (options.verbose) console.log("Generating FILES.md...");
13761
14453
  const content = generateFiles(graph, projectRoot, version);
13762
- const filePath = join21(options.outputDir, "FILES.md");
14454
+ const filePath = join22(options.outputDir, "FILES.md");
13763
14455
  writeFileSync3(filePath, content, "utf-8");
13764
14456
  generated.push("FILES.md");
13765
14457
  } catch (err) {
@@ -13770,7 +14462,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13770
14462
  try {
13771
14463
  if (options.verbose) console.log("Generating API_SURFACE.md...");
13772
14464
  const content = generateApiSurface(graph, projectRoot, version);
13773
- const filePath = join21(options.outputDir, "API_SURFACE.md");
14465
+ const filePath = join22(options.outputDir, "API_SURFACE.md");
13774
14466
  writeFileSync3(filePath, content, "utf-8");
13775
14467
  generated.push("API_SURFACE.md");
13776
14468
  } catch (err) {
@@ -13781,7 +14473,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13781
14473
  try {
13782
14474
  if (options.verbose) console.log("Generating ERRORS.md...");
13783
14475
  const content = generateErrors(graph, projectRoot, version);
13784
- const filePath = join21(options.outputDir, "ERRORS.md");
14476
+ const filePath = join22(options.outputDir, "ERRORS.md");
13785
14477
  writeFileSync3(filePath, content, "utf-8");
13786
14478
  generated.push("ERRORS.md");
13787
14479
  } catch (err) {
@@ -13792,7 +14484,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13792
14484
  try {
13793
14485
  if (options.verbose) console.log("Generating TESTS.md...");
13794
14486
  const content = generateTests(graph, projectRoot, version);
13795
- const filePath = join21(options.outputDir, "TESTS.md");
14487
+ const filePath = join22(options.outputDir, "TESTS.md");
13796
14488
  writeFileSync3(filePath, content, "utf-8");
13797
14489
  generated.push("TESTS.md");
13798
14490
  } catch (err) {
@@ -13803,7 +14495,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13803
14495
  try {
13804
14496
  if (options.verbose) console.log("Generating HISTORY.md...");
13805
14497
  const content = generateHistory(graph, projectRoot, version);
13806
- const filePath = join21(options.outputDir, "HISTORY.md");
14498
+ const filePath = join22(options.outputDir, "HISTORY.md");
13807
14499
  writeFileSync3(filePath, content, "utf-8");
13808
14500
  generated.push("HISTORY.md");
13809
14501
  } catch (err) {
@@ -13814,7 +14506,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13814
14506
  try {
13815
14507
  if (options.verbose) console.log("Generating CURRENT.md...");
13816
14508
  const content = generateCurrent(graph, projectRoot, version);
13817
- const filePath = join21(options.outputDir, "CURRENT.md");
14509
+ const filePath = join22(options.outputDir, "CURRENT.md");
13818
14510
  writeFileSync3(filePath, content, "utf-8");
13819
14511
  generated.push("CURRENT.md");
13820
14512
  } catch (err) {
@@ -13825,7 +14517,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13825
14517
  try {
13826
14518
  if (options.verbose) console.log("Generating STATUS.md...");
13827
14519
  const content = generateStatus(graph, projectRoot, version);
13828
- const filePath = join21(options.outputDir, "STATUS.md");
14520
+ const filePath = join22(options.outputDir, "STATUS.md");
13829
14521
  writeFileSync3(filePath, content, "utf-8");
13830
14522
  generated.push("STATUS.md");
13831
14523
  } catch (err) {
@@ -13836,7 +14528,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13836
14528
  try {
13837
14529
  if (options.verbose) console.log("Generating HEALTH.md...");
13838
14530
  const content = generateHealth(graph, projectRoot, version);
13839
- const filePath = join21(options.outputDir, "HEALTH.md");
14531
+ const filePath = join22(options.outputDir, "HEALTH.md");
13840
14532
  writeFileSync3(filePath, content, "utf-8");
13841
14533
  generated.push("HEALTH.md");
13842
14534
  } catch (err) {
@@ -13847,7 +14539,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
13847
14539
  try {
13848
14540
  if (options.verbose) console.log("Generating DEAD_CODE.md...");
13849
14541
  const content = generateDeadCode(graph, projectRoot, version);
13850
- const filePath = join21(options.outputDir, "DEAD_CODE.md");
14542
+ const filePath = join22(options.outputDir, "DEAD_CODE.md");
13851
14543
  writeFileSync3(filePath, content, "utf-8");
13852
14544
  generated.push("DEAD_CODE.md");
13853
14545
  } catch (err) {
@@ -13891,7 +14583,7 @@ function getFileCount13(graph) {
13891
14583
  }
13892
14584
 
13893
14585
  // src/simulation/engine.ts
13894
- import { dirname as dirname22, join as join22 } from "path";
14586
+ import { dirname as dirname23, join as join23 } from "path";
13895
14587
  function normalizePath2(p) {
13896
14588
  return p.replace(/^\.\//, "").replace(/\/+$/, "");
13897
14589
  }
@@ -14023,7 +14715,7 @@ var SimulationEngine = class {
14023
14715
  }
14024
14716
  }
14025
14717
  applyRename(clone, target, newName, brokenImports) {
14026
- const destination = join22(dirname22(target), newName);
14718
+ const destination = join23(dirname23(target), newName);
14027
14719
  this.applyMove(clone, target, destination, brokenImports);
14028
14720
  }
14029
14721
  applySplit(clone, target, newFile, symbols, brokenImports) {
@@ -14223,13 +14915,13 @@ var SimulationEngine = class {
14223
14915
  };
14224
14916
 
14225
14917
  // src/security/scanner.ts
14226
- import { existsSync as existsSync20 } from "fs";
14227
- import { join as join32 } from "path";
14918
+ import { existsSync as existsSync21 } from "fs";
14919
+ import { join as join33 } from "path";
14228
14920
 
14229
14921
  // src/security/checks/dependencies.ts
14230
14922
  import { execSync as execSync2 } from "child_process";
14231
- import { existsSync as existsSync19, readFileSync as readFileSync18, readdirSync as readdirSync11 } from "fs";
14232
- import { join as join23 } from "path";
14923
+ import { existsSync as existsSync20, readFileSync as readFileSync19, readdirSync as readdirSync12 } from "fs";
14924
+ import { join as join24 } from "path";
14233
14925
  function cvssToSeverity(score) {
14234
14926
  if (score >= 9) return "critical";
14235
14927
  if (score >= 7) return "high";
@@ -14239,18 +14931,18 @@ function cvssToSeverity(score) {
14239
14931
  async function checkDependencies(_files, projectRoot) {
14240
14932
  const findings = [];
14241
14933
  try {
14242
- if (existsSync19(join23(projectRoot, "package.json"))) {
14934
+ if (existsSync20(join24(projectRoot, "package.json"))) {
14243
14935
  findings.push(...checkNpmAudit(projectRoot));
14244
14936
  findings.push(...checkPackageJsonPatterns(projectRoot));
14245
14937
  findings.push(...checkPostinstallScripts(projectRoot));
14246
14938
  }
14247
- if (existsSync19(join23(projectRoot, "requirements.txt")) || existsSync19(join23(projectRoot, "pyproject.toml"))) {
14939
+ if (existsSync20(join24(projectRoot, "requirements.txt")) || existsSync20(join24(projectRoot, "pyproject.toml"))) {
14248
14940
  findings.push(...checkPipAudit(projectRoot));
14249
14941
  }
14250
- if (existsSync19(join23(projectRoot, "Cargo.toml"))) {
14942
+ if (existsSync20(join24(projectRoot, "Cargo.toml"))) {
14251
14943
  findings.push(...checkCargoAudit(projectRoot));
14252
14944
  }
14253
- if (existsSync19(join23(projectRoot, "go.mod"))) {
14945
+ if (existsSync20(join24(projectRoot, "go.mod"))) {
14254
14946
  findings.push(...checkGoVerify(projectRoot));
14255
14947
  }
14256
14948
  } catch (err) {
@@ -14339,8 +15031,8 @@ function checkNpmAudit(projectRoot) {
14339
15031
  function checkPackageJsonPatterns(projectRoot) {
14340
15032
  const findings = [];
14341
15033
  try {
14342
- const pkgPath = join23(projectRoot, "package.json");
14343
- const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
15034
+ const pkgPath = join24(projectRoot, "package.json");
15035
+ const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
14344
15036
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
14345
15037
  for (const [name, version] of Object.entries(allDeps)) {
14346
15038
  if (version.startsWith("^") || version.startsWith("~")) {
@@ -14362,15 +15054,15 @@ function checkPackageJsonPatterns(projectRoot) {
14362
15054
  }
14363
15055
  function checkPostinstallScripts(projectRoot) {
14364
15056
  const findings = [];
14365
- const nodeModules = join23(projectRoot, "node_modules");
14366
- if (!existsSync19(nodeModules)) return findings;
15057
+ const nodeModules = join24(projectRoot, "node_modules");
15058
+ if (!existsSync20(nodeModules)) return findings;
14367
15059
  try {
14368
- const topLevelDeps = readdirSync11(nodeModules).filter((d) => !d.startsWith("."));
15060
+ const topLevelDeps = readdirSync12(nodeModules).filter((d) => !d.startsWith("."));
14369
15061
  for (const dep of topLevelDeps) {
14370
- const depPkgPath = join23(nodeModules, dep, "package.json");
14371
- if (!existsSync19(depPkgPath)) continue;
15062
+ const depPkgPath = join24(nodeModules, dep, "package.json");
15063
+ if (!existsSync20(depPkgPath)) continue;
14372
15064
  try {
14373
- const depPkg = JSON.parse(readFileSync18(depPkgPath, "utf-8"));
15065
+ const depPkg = JSON.parse(readFileSync19(depPkgPath, "utf-8"));
14374
15066
  const scripts = depPkg.scripts || {};
14375
15067
  if (scripts.postinstall || scripts.preinstall || scripts.install) {
14376
15068
  const scriptName = scripts.postinstall ? "postinstall" : scripts.preinstall ? "preinstall" : "install";
@@ -14408,7 +15100,7 @@ function checkPipAudit(projectRoot) {
14408
15100
  id: "",
14409
15101
  severity: cvssToSeverity(vuln.cvss?.score || 5),
14410
15102
  vulnerabilityClass: "dependency-cve",
14411
- file: existsSync19(join23(projectRoot, "requirements.txt")) ? "requirements.txt" : "pyproject.toml",
15103
+ file: existsSync20(join24(projectRoot, "requirements.txt")) ? "requirements.txt" : "pyproject.toml",
14412
15104
  title: `Vulnerable Python dependency: ${vuln.name}`,
14413
15105
  description: `${vuln.name}@${vuln.version} \u2014 ${vuln.id}: ${vuln.description || "Known vulnerability"}`,
14414
15106
  attackScenario: `An attacker could exploit the vulnerability in ${vuln.name}.`,
@@ -14505,8 +15197,8 @@ function checkGoVerify(projectRoot) {
14505
15197
  }
14506
15198
 
14507
15199
  // src/security/checks/injection.ts
14508
- import { readFileSync as readFileSync19 } from "fs";
14509
- import { join as join24 } from "path";
15200
+ import { readFileSync as readFileSync20 } from "fs";
15201
+ import { join as join25 } from "path";
14510
15202
  var SKIP_DIRS = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
14511
15203
  var TEST_PATTERNS = ["test", "spec", "fixture", "mock", "__tests__", "__mocks__"];
14512
15204
  var USER_INPUT_NAMES = /(?:input|user|name|path|query|branch|hash|cmd|command|req\.|params|body|args|url|dir|file|subdirectory)/i;
@@ -14876,6 +15568,106 @@ var PATTERNS = [
14876
15568
  description: "SIMD store/load operations without explicit bounds checking \u2014 buffer overflow risk.",
14877
15569
  attackScenario: "An attacker could trigger out-of-bounds SIMD operations to corrupt memory.",
14878
15570
  suggestedFix: "Validate buffer size against SIMD width before store/load operations."
15571
+ },
15572
+ // Ruby injection patterns
15573
+ {
15574
+ regex: /(?:where|find_by_sql|execute)\s*\(\s*["'][^"']*#\{/,
15575
+ title: "Ruby string interpolation in database query method",
15576
+ vulnClass: "code-injection",
15577
+ baseSeverity: "high",
15578
+ description: "Database query built with string interpolation \u2014 values are not parameterized.",
15579
+ attackScenario: "An attacker could manipulate interpolated values to alter query logic.",
15580
+ suggestedFix: 'Use parameterized queries: Model.where("column = ?", value) or ActiveRecord query interface.'
15581
+ },
15582
+ {
15583
+ regex: /`[^`]*#\{/,
15584
+ title: "Ruby backtick command with string interpolation",
15585
+ vulnClass: "shell-injection",
15586
+ baseSeverity: "high",
15587
+ description: "Backtick command execution with interpolated values \u2014 potential for unintended command execution.",
15588
+ attackScenario: "An attacker could inject shell metacharacters through the interpolated variable.",
15589
+ suggestedFix: "Use Open3.capture3 with separate arguments instead of backtick interpolation."
15590
+ },
15591
+ {
15592
+ regex: /\b(?:system|exec)\s*\(\s*["'][^"']*#\{/,
15593
+ title: "Ruby system/exec with string interpolation",
15594
+ vulnClass: "shell-injection",
15595
+ baseSeverity: "high",
15596
+ description: "system() or exec() called with interpolated string \u2014 potential for unintended command execution.",
15597
+ attackScenario: "An attacker could inject shell metacharacters through the interpolated variable.",
15598
+ suggestedFix: 'Use system() with separate arguments: system("cmd", arg1, arg2) instead of string interpolation.'
15599
+ },
15600
+ {
15601
+ regex: /%x\{[^}]*#\{/,
15602
+ title: "Ruby %x{} command with string interpolation",
15603
+ vulnClass: "shell-injection",
15604
+ baseSeverity: "high",
15605
+ description: "%x{} command execution with interpolated values \u2014 potential for unintended command execution.",
15606
+ attackScenario: "An attacker could inject shell metacharacters through the interpolated variable.",
15607
+ suggestedFix: "Use Open3.capture3 with separate arguments instead of %x{} interpolation."
15608
+ },
15609
+ {
15610
+ regex: /\beval\s*\(\s*(?!['"])/,
15611
+ title: "Ruby eval() with dynamic input",
15612
+ vulnClass: "code-injection",
15613
+ baseSeverity: "high",
15614
+ description: "eval() executes arbitrary Ruby code from a variable \u2014 potential for unintended code execution.",
15615
+ attackScenario: "An attacker could inject malicious code if user input reaches eval().",
15616
+ suggestedFix: "Remove eval() and use safe alternatives (JSON.parse for data, specific parsers for expressions)."
15617
+ },
15618
+ {
15619
+ regex: /\b(?:instance_eval|class_eval)\s*\(\s*(?!['"])/,
15620
+ title: "Ruby instance_eval/class_eval with dynamic input",
15621
+ vulnClass: "code-injection",
15622
+ baseSeverity: "high",
15623
+ description: "instance_eval/class_eval executes code in object context \u2014 dangerous with dynamic input.",
15624
+ attackScenario: "An attacker could inject code that executes with elevated privileges in the object context.",
15625
+ suggestedFix: "Use instance_exec with a block instead of string evaluation."
15626
+ },
15627
+ {
15628
+ regex: /\b(?:send|public_send)\s*\(\s*(?:params|request|input|user)/i,
15629
+ title: "Ruby send/public_send with user-controlled method name",
15630
+ vulnClass: "code-injection",
15631
+ baseSeverity: "high",
15632
+ description: "send() called with user-controlled method name \u2014 could invoke unintended methods.",
15633
+ attackScenario: "An attacker could call arbitrary methods on the receiver by controlling the method name.",
15634
+ suggestedFix: "Validate the method name against a strict allowlist before passing to send()."
15635
+ },
15636
+ {
15637
+ regex: /File\.(?:read|write|delete|open)\s*\(\s*(?:params|request|input|user)/i,
15638
+ title: "Ruby file operation with user-controlled path",
15639
+ vulnClass: "code-injection",
15640
+ baseSeverity: "high",
15641
+ description: "File operation with user-controlled path \u2014 potential for unintended file access.",
15642
+ attackScenario: "An attacker could traverse directories to access or modify arbitrary files.",
15643
+ suggestedFix: "Validate and sanitize file paths. Use File.expand_path and check against an allowed directory."
15644
+ },
15645
+ {
15646
+ regex: /YAML\.load\s*\(\s*(?!.*safe)/i,
15647
+ title: "Ruby YAML.load with potentially unsafe input",
15648
+ vulnClass: "code-injection",
15649
+ baseSeverity: "high",
15650
+ description: "YAML.load can instantiate arbitrary Ruby objects \u2014 potential for unintended code execution.",
15651
+ attackScenario: "An attacker could craft a YAML payload that instantiates dangerous objects during deserialization.",
15652
+ suggestedFix: "Use YAML.safe_load instead of YAML.load to restrict allowed classes."
15653
+ },
15654
+ {
15655
+ regex: /Marshal\.load\s*\(/,
15656
+ title: "Ruby Marshal.load with potentially unsafe data",
15657
+ vulnClass: "code-injection",
15658
+ baseSeverity: "high",
15659
+ description: "Marshal.load deserializes arbitrary Ruby objects \u2014 potential for unintended code execution.",
15660
+ attackScenario: "An attacker could craft a marshaled payload that executes code during deserialization.",
15661
+ suggestedFix: "Use JSON.parse or MessagePack for data exchange. Never Marshal.load untrusted data."
15662
+ },
15663
+ {
15664
+ regex: /ERB\.new\s*\(\s*(?:params|request|input|user)/i,
15665
+ title: "Ruby ERB template with user-controlled input",
15666
+ vulnClass: "code-injection",
15667
+ baseSeverity: "high",
15668
+ description: "ERB template created from user input \u2014 potential for unintended code execution via template injection.",
15669
+ attackScenario: "An attacker could inject ERB tags to execute arbitrary Ruby code on the server.",
15670
+ suggestedFix: "Never pass user input directly to ERB.new. Use parameterized templates with safe escaping."
14879
15671
  }
14880
15672
  ];
14881
15673
  function shouldSkip(filePath) {
@@ -14892,7 +15684,7 @@ async function checkInjection(files, projectRoot) {
14892
15684
  if (shouldSkip(file.filePath) || isTestFile4(file.filePath)) continue;
14893
15685
  let content;
14894
15686
  try {
14895
- content = readFileSync19(join24(projectRoot, file.filePath), "utf-8");
15687
+ content = readFileSync20(join25(projectRoot, file.filePath), "utf-8");
14896
15688
  } catch {
14897
15689
  continue;
14898
15690
  }
@@ -14930,8 +15722,8 @@ async function checkInjection(files, projectRoot) {
14930
15722
  }
14931
15723
 
14932
15724
  // src/security/checks/secrets.ts
14933
- import { readFileSync as readFileSync20 } from "fs";
14934
- import { join as join25 } from "path";
15725
+ import { readFileSync as readFileSync21 } from "fs";
15726
+ import { join as join26 } from "path";
14935
15727
  var SKIP_DIRS2 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
14936
15728
  var TEST_PATTERNS2 = ["test", "spec", "fixture", "mock", "__tests__", "__mocks__", ".example", ".sample"];
14937
15729
  var SECRET_PATTERNS = [
@@ -14964,7 +15756,7 @@ async function checkSecrets(files, projectRoot) {
14964
15756
  if (shouldSkip2(file.filePath) || isTestFile5(file.filePath)) continue;
14965
15757
  let content;
14966
15758
  try {
14967
- content = readFileSync20(join25(projectRoot, file.filePath), "utf-8");
15759
+ content = readFileSync21(join26(projectRoot, file.filePath), "utf-8");
14968
15760
  } catch {
14969
15761
  continue;
14970
15762
  }
@@ -14997,8 +15789,8 @@ async function checkSecrets(files, projectRoot) {
14997
15789
  }
14998
15790
 
14999
15791
  // src/security/checks/path-traversal.ts
15000
- import { readFileSync as readFileSync21 } from "fs";
15001
- import { join as join26 } from "path";
15792
+ import { readFileSync as readFileSync22 } from "fs";
15793
+ import { join as join27 } from "path";
15002
15794
  var SKIP_DIRS3 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
15003
15795
  var USER_INPUT_VARS = /(?:req\.|params|query|body|input|path|dir|subdirectory|file|userInput|fileName|filePath)/i;
15004
15796
  var PATTERNS2 = [
@@ -15045,7 +15837,7 @@ async function checkPathTraversal(files, projectRoot) {
15045
15837
  if (shouldSkip3(file.filePath)) continue;
15046
15838
  let content;
15047
15839
  try {
15048
- content = readFileSync21(join26(projectRoot, file.filePath), "utf-8");
15840
+ content = readFileSync22(join27(projectRoot, file.filePath), "utf-8");
15049
15841
  } catch {
15050
15842
  continue;
15051
15843
  }
@@ -15087,8 +15879,8 @@ async function checkPathTraversal(files, projectRoot) {
15087
15879
  }
15088
15880
 
15089
15881
  // src/security/checks/auth.ts
15090
- import { readFileSync as readFileSync22 } from "fs";
15091
- import { join as join27 } from "path";
15882
+ import { readFileSync as readFileSync23 } from "fs";
15883
+ import { join as join28 } from "path";
15092
15884
  var SKIP_DIRS4 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
15093
15885
  function shouldSkip4(filePath) {
15094
15886
  return SKIP_DIRS4.some((d) => filePath.includes(d));
@@ -15104,7 +15896,7 @@ async function checkAuth(files, projectRoot) {
15104
15896
  if (shouldSkip4(file.filePath)) continue;
15105
15897
  let content;
15106
15898
  try {
15107
- content = readFileSync22(join27(projectRoot, file.filePath), "utf-8");
15899
+ content = readFileSync23(join28(projectRoot, file.filePath), "utf-8");
15108
15900
  } catch {
15109
15901
  continue;
15110
15902
  }
@@ -15195,8 +15987,8 @@ async function checkAuth(files, projectRoot) {
15195
15987
  }
15196
15988
 
15197
15989
  // src/security/checks/input-validation.ts
15198
- import { readFileSync as readFileSync23 } from "fs";
15199
- import { join as join28 } from "path";
15990
+ import { readFileSync as readFileSync24 } from "fs";
15991
+ import { join as join29 } from "path";
15200
15992
  var SKIP_DIRS5 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
15201
15993
  function shouldSkip5(filePath) {
15202
15994
  return SKIP_DIRS5.some((d) => filePath.includes(d));
@@ -15208,7 +16000,7 @@ async function checkInputValidation(files, projectRoot) {
15208
16000
  if (shouldSkip5(file.filePath)) continue;
15209
16001
  let content;
15210
16002
  try {
15211
- content = readFileSync23(join28(projectRoot, file.filePath), "utf-8");
16003
+ content = readFileSync24(join29(projectRoot, file.filePath), "utf-8");
15212
16004
  } catch {
15213
16005
  continue;
15214
16006
  }
@@ -15285,8 +16077,8 @@ async function checkInputValidation(files, projectRoot) {
15285
16077
  }
15286
16078
 
15287
16079
  // src/security/checks/information-disclosure.ts
15288
- import { readFileSync as readFileSync24 } from "fs";
15289
- import { join as join29 } from "path";
16080
+ import { readFileSync as readFileSync25 } from "fs";
16081
+ import { join as join30 } from "path";
15290
16082
  var SKIP_DIRS6 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
15291
16083
  function shouldSkip6(filePath) {
15292
16084
  return SKIP_DIRS6.some((d) => filePath.includes(d));
@@ -15298,7 +16090,7 @@ async function checkInformationDisclosure(files, projectRoot) {
15298
16090
  if (shouldSkip6(file.filePath)) continue;
15299
16091
  let content;
15300
16092
  try {
15301
- content = readFileSync24(join29(projectRoot, file.filePath), "utf-8");
16093
+ content = readFileSync25(join30(projectRoot, file.filePath), "utf-8");
15302
16094
  } catch {
15303
16095
  continue;
15304
16096
  }
@@ -15368,8 +16160,8 @@ async function checkInformationDisclosure(files, projectRoot) {
15368
16160
  }
15369
16161
 
15370
16162
  // src/security/checks/cryptography.ts
15371
- import { readFileSync as readFileSync25 } from "fs";
15372
- import { join as join30 } from "path";
16163
+ import { readFileSync as readFileSync26 } from "fs";
16164
+ import { join as join31 } from "path";
15373
16165
  var SKIP_DIRS7 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
15374
16166
  var USER_INPUT_NAMES2 = /(?:input|user|name|path|query|param|request|body|args|url)/i;
15375
16167
  function shouldSkip7(filePath) {
@@ -15386,7 +16178,7 @@ async function checkCryptography(files, projectRoot) {
15386
16178
  if (shouldSkip7(file.filePath)) continue;
15387
16179
  let content;
15388
16180
  try {
15389
- content = readFileSync25(join30(projectRoot, file.filePath), "utf-8");
16181
+ content = readFileSync26(join31(projectRoot, file.filePath), "utf-8");
15390
16182
  } catch {
15391
16183
  continue;
15392
16184
  }
@@ -15763,6 +16555,86 @@ async function checkCryptography(files, projectRoot) {
15763
16555
  });
15764
16556
  }
15765
16557
  }
16558
+ if (/Digest::MD5/.test(line)) {
16559
+ findings.push({
16560
+ id: "",
16561
+ severity: isCryptoFile ? "high" : "medium",
16562
+ vulnerabilityClass: "cryptography",
16563
+ file: file.filePath,
16564
+ line: i + 1,
16565
+ title: "Weak hash algorithm: MD5 in Ruby",
16566
+ description: "MD5 is cryptographically broken \u2014 collisions can be generated in seconds.",
16567
+ attackScenario: "An attacker could generate MD5 collisions to bypass integrity checks or forge hashes.",
16568
+ suggestedFix: "Use Digest::SHA256 or bcrypt for password hashing."
16569
+ });
16570
+ }
16571
+ if (/Digest::SHA1/.test(line)) {
16572
+ findings.push({
16573
+ id: "",
16574
+ severity: isCryptoFile ? "high" : "medium",
16575
+ vulnerabilityClass: "cryptography",
16576
+ file: file.filePath,
16577
+ line: i + 1,
16578
+ title: "Weak hash algorithm: SHA-1 in Ruby",
16579
+ description: "SHA-1 has known collision attacks \u2014 should not be used for security purposes.",
16580
+ attackScenario: "An attacker could generate SHA-1 collisions to bypass integrity checks.",
16581
+ suggestedFix: "Use Digest::SHA256 or stronger for integrity checks."
16582
+ });
16583
+ }
16584
+ if (/\brand\s*\(/.test(line) && isCryptoFile) {
16585
+ findings.push({
16586
+ id: "",
16587
+ severity: "medium",
16588
+ vulnerabilityClass: "cryptography",
16589
+ file: file.filePath,
16590
+ line: i + 1,
16591
+ title: "Weak random: rand() in Ruby security context",
16592
+ description: "rand() is not cryptographically secure \u2014 its output can be predicted.",
16593
+ attackScenario: "An attacker could predict rand() values to forge tokens or bypass security checks.",
16594
+ suggestedFix: "Use SecureRandom.hex, SecureRandom.uuid, or SecureRandom.random_bytes for cryptographic purposes."
16595
+ });
16596
+ }
16597
+ if (/(?:password|secret|api_key|token)\s*=\s*['"][^'"]{4,}['"]/i.test(line)) {
16598
+ if (file.filePath.endsWith(".rb")) {
16599
+ findings.push({
16600
+ id: "",
16601
+ severity: "high",
16602
+ vulnerabilityClass: "cryptography",
16603
+ file: file.filePath,
16604
+ line: i + 1,
16605
+ title: "Hardcoded credentials in Ruby source",
16606
+ description: "A password, secret, or API key is hardcoded as a string literal.",
16607
+ attackScenario: "An attacker with access to the source could extract the credential.",
16608
+ suggestedFix: 'Load credentials from environment variables using ENV["KEY"] or Rails credentials.'
16609
+ });
16610
+ }
16611
+ }
16612
+ if (/verify_mode\s*=\s*OpenSSL::SSL::VERIFY_NONE/.test(line)) {
16613
+ findings.push({
16614
+ id: "",
16615
+ severity: "high",
16616
+ vulnerabilityClass: "cryptography",
16617
+ file: file.filePath,
16618
+ line: i + 1,
16619
+ title: "Ruby SSL verification disabled",
16620
+ description: "SSL certificate verification is disabled \u2014 connections are not authenticated.",
16621
+ attackScenario: "An attacker on the network could intercept and modify traffic without detection.",
16622
+ suggestedFix: "Use OpenSSL::SSL::VERIFY_PEER to verify server certificates."
16623
+ });
16624
+ }
16625
+ if (/OpenSSL::Cipher\s*\.\s*new\s*\(\s*['"](?:DES|RC4)/i.test(line)) {
16626
+ findings.push({
16627
+ id: "",
16628
+ severity: "high",
16629
+ vulnerabilityClass: "cryptography",
16630
+ file: file.filePath,
16631
+ line: i + 1,
16632
+ title: "Weak cipher algorithm in Ruby",
16633
+ description: "DES/RC4 ciphers are cryptographically weak and can be broken with modern hardware.",
16634
+ attackScenario: "An attacker could brute-force or exploit weaknesses in DES/RC4 to decrypt data.",
16635
+ suggestedFix: 'Use OpenSSL::Cipher.new("aes-256-gcm") for authenticated encryption.'
16636
+ });
16637
+ }
15766
16638
  }
15767
16639
  }
15768
16640
  } catch {
@@ -15771,8 +16643,8 @@ async function checkCryptography(files, projectRoot) {
15771
16643
  }
15772
16644
 
15773
16645
  // src/security/checks/frontend.ts
15774
- import { readFileSync as readFileSync26 } from "fs";
15775
- import { join as join31 } from "path";
16646
+ import { readFileSync as readFileSync27 } from "fs";
16647
+ import { join as join32 } from "path";
15776
16648
  var SKIP_DIRS8 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
15777
16649
  function shouldSkip8(filePath) {
15778
16650
  return SKIP_DIRS8.some((d) => filePath.includes(d));
@@ -15788,7 +16660,7 @@ async function checkFrontend(files, projectRoot) {
15788
16660
  if (!isFrontendFile(file.filePath)) continue;
15789
16661
  let content;
15790
16662
  try {
15791
- content = readFileSync26(join31(projectRoot, file.filePath), "utf-8");
16663
+ content = readFileSync27(join32(projectRoot, file.filePath), "utf-8");
15792
16664
  } catch {
15793
16665
  continue;
15794
16666
  }
@@ -16248,13 +17120,13 @@ async function scanSecurity(projectRoot, graph, options = {}) {
16248
17120
  };
16249
17121
  }
16250
17122
  function detectPackageManager(projectRoot) {
16251
- if (existsSync20(join32(projectRoot, "package.json"))) return "npm";
16252
- if (existsSync20(join32(projectRoot, "requirements.txt"))) return "pip";
16253
- if (existsSync20(join32(projectRoot, "pyproject.toml"))) return "pip";
16254
- if (existsSync20(join32(projectRoot, "Cargo.toml"))) return "cargo";
16255
- if (existsSync20(join32(projectRoot, "go.mod"))) return "go";
16256
- if (existsSync20(join32(projectRoot, "pom.xml"))) return "maven";
16257
- if (existsSync20(join32(projectRoot, "build.gradle")) || existsSync20(join32(projectRoot, "build.gradle.kts"))) return "gradle";
17123
+ if (existsSync21(join33(projectRoot, "package.json"))) return "npm";
17124
+ if (existsSync21(join33(projectRoot, "requirements.txt"))) return "pip";
17125
+ if (existsSync21(join33(projectRoot, "pyproject.toml"))) return "pip";
17126
+ if (existsSync21(join33(projectRoot, "Cargo.toml"))) return "cargo";
17127
+ if (existsSync21(join33(projectRoot, "go.mod"))) return "go";
17128
+ if (existsSync21(join33(projectRoot, "pom.xml"))) return "maven";
17129
+ if (existsSync21(join33(projectRoot, "build.gradle")) || existsSync21(join33(projectRoot, "build.gradle.kts"))) return "gradle";
16258
17130
  return "unknown";
16259
17131
  }
16260
17132