depwire-cli 1.0.7 → 1.0.8

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.
@@ -36,8 +36,9 @@ function scanDirectory(rootDir, baseDir = rootDir) {
36
36
  const isCSharp = entry.endsWith(".cs") || entry.endsWith(".csx") || entry.endsWith(".csproj");
37
37
  const isJava = entry.endsWith(".java") || entry === "pom.xml" || entry === "build.gradle" || entry === "build.gradle.kts";
38
38
  const isKotlin = entry.endsWith(".kt") || entry.endsWith(".kts") || entry === "settings.gradle.kts" || entry === "settings.gradle";
39
+ const isPhp = entry.endsWith(".php");
39
40
  const isCppBuild = entry === "CMakeLists.txt" || entry === "conanfile.txt" || entry === "vcpkg.json";
40
- if (isTypeScript || isJavaScript || isPython || isGo || isRust || isC || isCpp || isCSharp || isJava || isKotlin || isCppBuild) {
41
+ if (isTypeScript || isJavaScript || isPython || isGo || isRust || isC || isCpp || isCSharp || isJava || isKotlin || isPhp || isCppBuild) {
41
42
  files.push(relative(rootDir, fullPath));
42
43
  }
43
44
  }
@@ -81,6 +82,8 @@ function findProjectRoot(startDir = process.cwd()) {
81
82
  // Java (Gradle)
82
83
  "build.gradle.kts",
83
84
  // Kotlin (Gradle KTS)
85
+ "composer.json",
86
+ // PHP
84
87
  ".git"
85
88
  // Any git repo
86
89
  ];
@@ -114,11 +117,11 @@ function findProjectRoot(startDir = process.cwd()) {
114
117
  }
115
118
 
116
119
  // src/parser/index.ts
117
- import { readFileSync as readFileSync9, statSync as statSync6 } from "fs";
118
- import { join as join12, resolve as resolve6 } from "path";
120
+ import { readFileSync as readFileSync10, statSync as statSync7 } from "fs";
121
+ import { join as join13, resolve as resolve7 } from "path";
119
122
 
120
123
  // src/parser/detect.ts
121
- import { extname as extname7, basename as basename5 } from "path";
124
+ import { extname as extname8, basename as basename6 } from "path";
122
125
 
123
126
  // src/parser/wasm-init.ts
124
127
  import { Parser, Language } from "web-tree-sitter";
@@ -149,7 +152,8 @@ async function initParser() {
149
152
  "c_sharp": "tree-sitter-c_sharp.wasm",
150
153
  "java": "tree-sitter-java.wasm",
151
154
  "cpp": "tree-sitter-cpp.wasm",
152
- "kotlin": "tree-sitter-kotlin.wasm"
155
+ "kotlin": "tree-sitter-kotlin.wasm",
156
+ "php": "tree-sitter-php.wasm"
153
157
  };
154
158
  for (const [name, file] of Object.entries(grammarFiles)) {
155
159
  const wasmPath = path.join(grammarsDir, file);
@@ -5549,7 +5553,611 @@ function resolveKotlinImport(importPath, currentFile, projectRoot) {
5549
5553
  }
5550
5554
  return null;
5551
5555
  }
5552
- function resolveSymbol9(name, context) {
5556
+ function resolveSymbol9(name, context) {
5557
+ if (context.imports.has(name)) {
5558
+ return context.imports.get(name) || null;
5559
+ }
5560
+ const currentFileId = `${context.filePath}::${name}`;
5561
+ if (context.symbols.find((s) => s.id === currentFileId)) {
5562
+ return currentFileId;
5563
+ }
5564
+ if (context.currentClass) {
5565
+ const classMethodId = `${context.filePath}::${context.currentClass}.${name}`;
5566
+ if (context.symbols.find((s) => s.id === classMethodId)) {
5567
+ return classMethodId;
5568
+ }
5569
+ }
5570
+ return null;
5571
+ }
5572
+ function getModifiers(node, context) {
5573
+ const modifiers = [];
5574
+ const modList = findChildByType10(node, "modifiers");
5575
+ if (modList) {
5576
+ for (let i = 0; i < modList.childCount; i++) {
5577
+ const child = modList.child(i);
5578
+ if (child) {
5579
+ const text = nodeText9(child, context).trim();
5580
+ if (text) modifiers.push(text);
5581
+ }
5582
+ }
5583
+ }
5584
+ return modifiers;
5585
+ }
5586
+ function extractTypeName5(node, context) {
5587
+ const text = nodeText9(node, context).trim();
5588
+ if (!text || text === "," || text === ":") return null;
5589
+ let name = text;
5590
+ const angleBracketIdx = name.indexOf("<");
5591
+ if (angleBracketIdx > 0) name = name.substring(0, angleBracketIdx);
5592
+ const parenIdx = name.indexOf("(");
5593
+ if (parenIdx > 0) name = name.substring(0, parenIdx);
5594
+ const dotIdx = name.lastIndexOf(".");
5595
+ name = dotIdx >= 0 ? name.substring(dotIdx + 1) : name;
5596
+ return name.trim() || null;
5597
+ }
5598
+ function findChildByType10(node, type) {
5599
+ for (let i = 0; i < node.childCount; i++) {
5600
+ const child = node.child(i);
5601
+ if (child && child.type === type) return child;
5602
+ }
5603
+ return null;
5604
+ }
5605
+ function findChildrenByType4(node, type) {
5606
+ const results = [];
5607
+ for (let i = 0; i < node.childCount; i++) {
5608
+ const child = node.child(i);
5609
+ if (child && child.type === type) results.push(child);
5610
+ }
5611
+ return results;
5612
+ }
5613
+ function findDescendantByTypes2(node, types) {
5614
+ for (let i = 0; i < node.childCount; i++) {
5615
+ const child = node.child(i);
5616
+ if (!child) continue;
5617
+ if (types.includes(child.type)) return child;
5618
+ const found = findDescendantByTypes2(child, types);
5619
+ if (found) return found;
5620
+ }
5621
+ return null;
5622
+ }
5623
+ function nodeText9(node, context) {
5624
+ return context.sourceCode.substring(node.startIndex, node.endIndex);
5625
+ }
5626
+ function getCurrentSymbolId10(context) {
5627
+ if (context.currentScope.length === 0) return null;
5628
+ return `${context.filePath}::${context.currentScope[context.currentScope.length - 1]}`;
5629
+ }
5630
+ var kotlinParser = {
5631
+ name: "kotlin",
5632
+ extensions: [".kt", ".kts", "build.gradle.kts", "settings.gradle.kts", "settings.gradle"],
5633
+ parseFile: parseKotlinFile
5634
+ };
5635
+
5636
+ // src/parser/php.ts
5637
+ import { dirname as dirname11, join as join12 } from "path";
5638
+ import { existsSync as existsSync12 } from "fs";
5639
+ function parsePhpFile(filePath, sourceCode, projectRoot) {
5640
+ const parser = getParser("php");
5641
+ const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
5642
+ const context = {
5643
+ filePath,
5644
+ projectRoot,
5645
+ sourceCode,
5646
+ symbols: [],
5647
+ edges: [],
5648
+ currentScope: [],
5649
+ currentClass: null,
5650
+ currentNamespace: null,
5651
+ imports: /* @__PURE__ */ new Map()
5652
+ };
5653
+ walkNode11(tree.rootNode, context);
5654
+ return {
5655
+ filePath,
5656
+ symbols: context.symbols,
5657
+ edges: context.edges
5658
+ };
5659
+ }
5660
+ var SCOPE_TYPES = /* @__PURE__ */ new Set([
5661
+ "class_declaration",
5662
+ "interface_declaration",
5663
+ "trait_declaration",
5664
+ "enum_declaration",
5665
+ "function_definition",
5666
+ "method_declaration"
5667
+ ]);
5668
+ function walkNode11(node, context) {
5669
+ processNode11(node, context);
5670
+ if (SCOPE_TYPES.has(node.type)) return;
5671
+ for (let i = 0; i < node.childCount; i++) {
5672
+ const child = node.child(i);
5673
+ if (child) {
5674
+ walkNode11(child, context);
5675
+ }
5676
+ }
5677
+ }
5678
+ function processNode11(node, context) {
5679
+ switch (node.type) {
5680
+ case "namespace_definition":
5681
+ processNamespaceDefinition2(node, context);
5682
+ break;
5683
+ case "namespace_use_declaration":
5684
+ processUseDeclaration2(node, context);
5685
+ break;
5686
+ case "class_declaration":
5687
+ processClassDeclaration6(node, context);
5688
+ break;
5689
+ case "interface_declaration":
5690
+ processInterfaceDeclaration4(node, context);
5691
+ break;
5692
+ case "trait_declaration":
5693
+ processTraitDeclaration(node, context);
5694
+ break;
5695
+ case "enum_declaration":
5696
+ processEnumDeclaration4(node, context);
5697
+ break;
5698
+ case "function_definition":
5699
+ processFunctionDefinition4(node, context);
5700
+ break;
5701
+ case "method_declaration":
5702
+ processMethodDeclaration4(node, context);
5703
+ break;
5704
+ case "property_declaration":
5705
+ processPropertyDeclaration3(node, context);
5706
+ break;
5707
+ case "const_declaration":
5708
+ processConstDeclaration2(node, context);
5709
+ break;
5710
+ case "function_call_expression":
5711
+ processCallExpression11(node, context);
5712
+ break;
5713
+ case "member_call_expression":
5714
+ processMemberCallExpression(node, context);
5715
+ break;
5716
+ case "scoped_call_expression":
5717
+ processScopedCallExpression(node, context);
5718
+ break;
5719
+ case "include_expression":
5720
+ case "include_once_expression":
5721
+ case "require_expression":
5722
+ case "require_once_expression":
5723
+ processIncludeRequire(node, context);
5724
+ break;
5725
+ }
5726
+ }
5727
+ function processNamespaceDefinition2(node, context) {
5728
+ const nameNode = findChildByType11(node, "namespace_name");
5729
+ if (!nameNode) return;
5730
+ const name = nodeText10(nameNode, context);
5731
+ const symbolId = `${context.filePath}::${name}`;
5732
+ context.symbols.push({
5733
+ id: symbolId,
5734
+ name,
5735
+ kind: "module",
5736
+ filePath: context.filePath,
5737
+ startLine: node.startPosition.row + 1,
5738
+ endLine: node.endPosition.row + 1,
5739
+ exported: true
5740
+ });
5741
+ context.currentNamespace = name;
5742
+ }
5743
+ function processUseDeclaration2(node, context) {
5744
+ const clauses = findChildrenByType5(node, "namespace_use_clause");
5745
+ for (const clause of clauses) {
5746
+ const nameNode = findChildByType11(clause, "namespace_name") || findChildByType11(clause, "qualified_name");
5747
+ if (!nameNode) continue;
5748
+ const importPath = nodeText10(nameNode, context);
5749
+ const aliasNode = findChildByType11(clause, "namespace_aliasing_clause");
5750
+ const alias = aliasNode ? nodeText10(findChildByType11(aliasNode, "name") || aliasNode, context).trim() : null;
5751
+ const parts = importPath.split("\\");
5752
+ const simpleName = alias || parts[parts.length - 1];
5753
+ const resolvedPath = resolvePhpImport(importPath, context.filePath, context.projectRoot);
5754
+ if (resolvedPath) {
5755
+ const sourceId = `${context.filePath}::__file__`;
5756
+ const targetId = `${resolvedPath}::__file__`;
5757
+ context.edges.push({
5758
+ source: sourceId,
5759
+ target: targetId,
5760
+ kind: "imports",
5761
+ filePath: context.filePath,
5762
+ line: node.startPosition.row + 1
5763
+ });
5764
+ context.imports.set(simpleName, `${resolvedPath}::${parts[parts.length - 1]}`);
5765
+ }
5766
+ const symbolId = `${context.filePath}::import:${importPath}`;
5767
+ context.symbols.push({
5768
+ id: symbolId,
5769
+ name: importPath,
5770
+ kind: "import",
5771
+ filePath: context.filePath,
5772
+ startLine: node.startPosition.row + 1,
5773
+ endLine: node.endPosition.row + 1,
5774
+ exported: false
5775
+ });
5776
+ }
5777
+ }
5778
+ function processClassDeclaration6(node, context) {
5779
+ const nameNode = findChildByType11(node, "name");
5780
+ if (!nameNode) return;
5781
+ const name = nodeText10(nameNode, context);
5782
+ const symbolId = `${context.filePath}::${name}`;
5783
+ const text = nodeText10(node, context);
5784
+ const isAbstract = text.trimStart().startsWith("abstract");
5785
+ context.symbols.push({
5786
+ id: symbolId,
5787
+ name,
5788
+ kind: "class",
5789
+ filePath: context.filePath,
5790
+ startLine: node.startPosition.row + 1,
5791
+ endLine: node.endPosition.row + 1,
5792
+ exported: true,
5793
+ scope: context.currentClass || void 0
5794
+ });
5795
+ const baseClause = findChildByType11(node, "base_clause");
5796
+ if (baseClause) {
5797
+ const baseName = extractQualifiedName(baseClause, context);
5798
+ if (baseName) {
5799
+ const baseId = resolveSymbol10(baseName, context);
5800
+ if (baseId) {
5801
+ context.edges.push({
5802
+ source: symbolId,
5803
+ target: baseId,
5804
+ kind: "inherits",
5805
+ filePath: context.filePath,
5806
+ line: node.startPosition.row + 1
5807
+ });
5808
+ }
5809
+ }
5810
+ }
5811
+ const interfaceClause = findChildByType11(node, "class_interface_clause");
5812
+ if (interfaceClause) {
5813
+ const names = findChildrenByType5(interfaceClause, "name");
5814
+ const qualifiedNames = findChildrenByType5(interfaceClause, "qualified_name");
5815
+ for (const n of [...names, ...qualifiedNames]) {
5816
+ const ifaceName = nodeText10(n, context).trim();
5817
+ if (ifaceName && ifaceName !== ",") {
5818
+ const ifaceId = resolveSymbol10(ifaceName, context);
5819
+ if (ifaceId) {
5820
+ context.edges.push({
5821
+ source: symbolId,
5822
+ target: ifaceId,
5823
+ kind: "implements",
5824
+ filePath: context.filePath,
5825
+ line: node.startPosition.row + 1
5826
+ });
5827
+ }
5828
+ }
5829
+ }
5830
+ }
5831
+ const oldClass = context.currentClass;
5832
+ context.currentClass = name;
5833
+ context.currentScope.push(name);
5834
+ const body = findChildByType11(node, "declaration_list");
5835
+ if (body) {
5836
+ walkNode11(body, context);
5837
+ }
5838
+ context.currentScope.pop();
5839
+ context.currentClass = oldClass;
5840
+ }
5841
+ function processInterfaceDeclaration4(node, context) {
5842
+ const nameNode = findChildByType11(node, "name");
5843
+ if (!nameNode) return;
5844
+ const name = nodeText10(nameNode, context);
5845
+ const symbolId = `${context.filePath}::${name}`;
5846
+ context.symbols.push({
5847
+ id: symbolId,
5848
+ name,
5849
+ kind: "interface",
5850
+ filePath: context.filePath,
5851
+ startLine: node.startPosition.row + 1,
5852
+ endLine: node.endPosition.row + 1,
5853
+ exported: true,
5854
+ scope: context.currentClass || void 0
5855
+ });
5856
+ const oldClass = context.currentClass;
5857
+ context.currentClass = name;
5858
+ context.currentScope.push(name);
5859
+ const body = findChildByType11(node, "declaration_list");
5860
+ if (body) {
5861
+ walkNode11(body, context);
5862
+ }
5863
+ context.currentScope.pop();
5864
+ context.currentClass = oldClass;
5865
+ }
5866
+ function processTraitDeclaration(node, context) {
5867
+ const nameNode = findChildByType11(node, "name");
5868
+ if (!nameNode) return;
5869
+ const name = nodeText10(nameNode, context);
5870
+ const symbolId = `${context.filePath}::${name}`;
5871
+ context.symbols.push({
5872
+ id: symbolId,
5873
+ name,
5874
+ kind: "class",
5875
+ // Traits map to class kind
5876
+ filePath: context.filePath,
5877
+ startLine: node.startPosition.row + 1,
5878
+ endLine: node.endPosition.row + 1,
5879
+ exported: true,
5880
+ scope: context.currentClass || void 0
5881
+ });
5882
+ const oldClass = context.currentClass;
5883
+ context.currentClass = name;
5884
+ context.currentScope.push(name);
5885
+ const body = findChildByType11(node, "declaration_list");
5886
+ if (body) {
5887
+ walkNode11(body, context);
5888
+ }
5889
+ context.currentScope.pop();
5890
+ context.currentClass = oldClass;
5891
+ }
5892
+ function processEnumDeclaration4(node, context) {
5893
+ const nameNode = findChildByType11(node, "name");
5894
+ if (!nameNode) return;
5895
+ const name = nodeText10(nameNode, context);
5896
+ const symbolId = `${context.filePath}::${name}`;
5897
+ context.symbols.push({
5898
+ id: symbolId,
5899
+ name,
5900
+ kind: "enum",
5901
+ filePath: context.filePath,
5902
+ startLine: node.startPosition.row + 1,
5903
+ endLine: node.endPosition.row + 1,
5904
+ exported: true
5905
+ });
5906
+ const oldClass = context.currentClass;
5907
+ context.currentClass = name;
5908
+ context.currentScope.push(name);
5909
+ const body = findChildByType11(node, "declaration_list");
5910
+ if (body) {
5911
+ walkNode11(body, context);
5912
+ }
5913
+ context.currentScope.pop();
5914
+ context.currentClass = oldClass;
5915
+ }
5916
+ function processFunctionDefinition4(node, context) {
5917
+ const nameNode = findChildByType11(node, "name");
5918
+ if (!nameNode) return;
5919
+ const name = nodeText10(nameNode, context);
5920
+ const scope = context.currentClass || void 0;
5921
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
5922
+ context.symbols.push({
5923
+ id: symbolId,
5924
+ name,
5925
+ kind: "function",
5926
+ filePath: context.filePath,
5927
+ startLine: node.startPosition.row + 1,
5928
+ endLine: node.endPosition.row + 1,
5929
+ exported: true,
5930
+ scope
5931
+ });
5932
+ const scopeName = scope ? `${scope}.${name}` : name;
5933
+ context.currentScope.push(scopeName);
5934
+ const body = findChildByType11(node, "compound_statement");
5935
+ if (body) {
5936
+ walkNode11(body, context);
5937
+ }
5938
+ context.currentScope.pop();
5939
+ }
5940
+ function processMethodDeclaration4(node, context) {
5941
+ const nameNode = findChildByType11(node, "name");
5942
+ if (!nameNode) return;
5943
+ const name = nodeText10(nameNode, context);
5944
+ const modifiers = getModifiers2(node, context);
5945
+ const exported = !modifiers.includes("private");
5946
+ const scope = context.currentClass || void 0;
5947
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
5948
+ context.symbols.push({
5949
+ id: symbolId,
5950
+ name,
5951
+ kind: "method",
5952
+ filePath: context.filePath,
5953
+ startLine: node.startPosition.row + 1,
5954
+ endLine: node.endPosition.row + 1,
5955
+ exported,
5956
+ scope
5957
+ });
5958
+ const scopeName = scope ? `${scope}.${name}` : name;
5959
+ context.currentScope.push(scopeName);
5960
+ const body = findChildByType11(node, "compound_statement");
5961
+ if (body) {
5962
+ walkNode11(body, context);
5963
+ }
5964
+ context.currentScope.pop();
5965
+ }
5966
+ function processPropertyDeclaration3(node, context) {
5967
+ const varNode = findDescendantByTypes3(node, ["variable_name"]);
5968
+ if (!varNode) return;
5969
+ const name = nodeText10(varNode, context).replace(/^\$/, "");
5970
+ const modifiers = getModifiers2(node, context);
5971
+ const exported = !modifiers.includes("private");
5972
+ const scope = context.currentClass || void 0;
5973
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
5974
+ context.symbols.push({
5975
+ id: symbolId,
5976
+ name,
5977
+ kind: "property",
5978
+ filePath: context.filePath,
5979
+ startLine: node.startPosition.row + 1,
5980
+ endLine: node.endPosition.row + 1,
5981
+ exported,
5982
+ scope
5983
+ });
5984
+ }
5985
+ function processConstDeclaration2(node, context) {
5986
+ const elements = findChildrenByType5(node, "const_element");
5987
+ for (const elem of elements) {
5988
+ const nameNode = findChildByType11(elem, "name");
5989
+ if (!nameNode) continue;
5990
+ const name = nodeText10(nameNode, context);
5991
+ const scope = context.currentClass || void 0;
5992
+ const symbolId = scope ? `${context.filePath}::${scope}.${name}` : `${context.filePath}::${name}`;
5993
+ context.symbols.push({
5994
+ id: symbolId,
5995
+ name,
5996
+ kind: "constant",
5997
+ filePath: context.filePath,
5998
+ startLine: elem.startPosition.row + 1,
5999
+ endLine: elem.endPosition.row + 1,
6000
+ exported: true,
6001
+ scope
6002
+ });
6003
+ }
6004
+ }
6005
+ function processCallExpression11(node, context) {
6006
+ if (context.currentScope.length === 0) return;
6007
+ const firstChild = node.child(0);
6008
+ if (!firstChild) return;
6009
+ let calleeName = null;
6010
+ if (firstChild.type === "name") {
6011
+ calleeName = nodeText10(firstChild, context);
6012
+ } else if (firstChild.type === "qualified_name") {
6013
+ const parts = nodeText10(firstChild, context).split("\\");
6014
+ calleeName = parts[parts.length - 1];
6015
+ }
6016
+ if (!calleeName) return;
6017
+ const builtins = /* @__PURE__ */ new Set([
6018
+ "echo",
6019
+ "print",
6020
+ "var_dump",
6021
+ "print_r",
6022
+ "isset",
6023
+ "unset",
6024
+ "empty",
6025
+ "array",
6026
+ "list",
6027
+ "count",
6028
+ "strlen",
6029
+ "strpos",
6030
+ "substr",
6031
+ "explode",
6032
+ "implode",
6033
+ "array_map",
6034
+ "array_filter",
6035
+ "array_merge",
6036
+ "array_push",
6037
+ "array_pop",
6038
+ "array_shift",
6039
+ "array_unshift",
6040
+ "array_keys",
6041
+ "array_values",
6042
+ "in_array",
6043
+ "json_encode",
6044
+ "json_decode",
6045
+ "sprintf",
6046
+ "printf",
6047
+ "is_array",
6048
+ "is_string",
6049
+ "is_int",
6050
+ "is_null",
6051
+ "is_bool",
6052
+ "intval",
6053
+ "floatval",
6054
+ "strval",
6055
+ "boolval",
6056
+ "trim",
6057
+ "ltrim",
6058
+ "rtrim",
6059
+ "strtolower",
6060
+ "strtoupper",
6061
+ "str_replace",
6062
+ "preg_match",
6063
+ "preg_replace",
6064
+ "file_exists",
6065
+ "is_file",
6066
+ "is_dir",
6067
+ "dirname",
6068
+ "basename",
6069
+ "date",
6070
+ "time",
6071
+ "strtotime",
6072
+ "compact",
6073
+ "extract",
6074
+ "defined",
6075
+ "define",
6076
+ "class_exists",
6077
+ "function_exists",
6078
+ "throw",
6079
+ "die",
6080
+ "exit"
6081
+ ]);
6082
+ if (builtins.has(calleeName)) return;
6083
+ const callerId = getCurrentSymbolId11(context);
6084
+ if (!callerId) return;
6085
+ const calleeId = resolveSymbol10(calleeName, context);
6086
+ if (calleeId) {
6087
+ context.edges.push({
6088
+ source: callerId,
6089
+ target: calleeId,
6090
+ kind: "calls",
6091
+ filePath: context.filePath,
6092
+ line: node.startPosition.row + 1
6093
+ });
6094
+ }
6095
+ }
6096
+ function processMemberCallExpression(node, context) {
6097
+ }
6098
+ function processScopedCallExpression(node, context) {
6099
+ }
6100
+ function processIncludeRequire(node, context) {
6101
+ const text = nodeText10(node, context);
6102
+ const pathMatch = text.match(/['"]([^'"]+)['"]/);
6103
+ if (!pathMatch) return;
6104
+ const includePath = pathMatch[1];
6105
+ const resolvedPath = resolvePhpInclude(includePath, context.filePath, context.projectRoot);
6106
+ if (resolvedPath) {
6107
+ const sourceId = `${context.filePath}::__file__`;
6108
+ const targetId = `${resolvedPath}::__file__`;
6109
+ context.edges.push({
6110
+ source: sourceId,
6111
+ target: targetId,
6112
+ kind: "imports",
6113
+ filePath: context.filePath,
6114
+ line: node.startPosition.row + 1
6115
+ });
6116
+ }
6117
+ }
6118
+ function resolvePhpImport(importPath, currentFile, projectRoot) {
6119
+ const parts = importPath.split("\\");
6120
+ const filePath = parts.join("/") + ".php";
6121
+ const sourceRoots = [
6122
+ "",
6123
+ "src",
6124
+ "app",
6125
+ "lib",
6126
+ "includes",
6127
+ "wp-content/plugins",
6128
+ "wp-content/themes"
6129
+ ];
6130
+ for (const root of sourceRoots) {
6131
+ const candidate = root ? join12(root, filePath) : filePath;
6132
+ const fullPath = join12(projectRoot, candidate);
6133
+ if (existsSync12(fullPath)) {
6134
+ return candidate;
6135
+ }
6136
+ const loweredParts = [...parts];
6137
+ loweredParts[0] = loweredParts[0].toLowerCase();
6138
+ const loweredFilePath = loweredParts.join("/") + ".php";
6139
+ const loweredCandidate = root ? join12(root, loweredFilePath) : loweredFilePath;
6140
+ const loweredFullPath = join12(projectRoot, loweredCandidate);
6141
+ if (existsSync12(loweredFullPath)) {
6142
+ return loweredCandidate;
6143
+ }
6144
+ }
6145
+ return null;
6146
+ }
6147
+ function resolvePhpInclude(includePath, currentFile, projectRoot) {
6148
+ const currentDir = dirname11(join12(projectRoot, currentFile));
6149
+ const relativePath = join12(currentDir, includePath);
6150
+ const relativeToRoot = relativePath.replace(projectRoot + "/", "");
6151
+ if (existsSync12(relativePath)) {
6152
+ return relativeToRoot;
6153
+ }
6154
+ const fromRoot = join12(projectRoot, includePath);
6155
+ if (existsSync12(fromRoot)) {
6156
+ return includePath;
6157
+ }
6158
+ return null;
6159
+ }
6160
+ function resolveSymbol10(name, context) {
5553
6161
  if (context.imports.has(name)) {
5554
6162
  return context.imports.get(name) || null;
5555
6163
  }
@@ -5565,40 +6173,34 @@ function resolveSymbol9(name, context) {
5565
6173
  }
5566
6174
  return null;
5567
6175
  }
5568
- function getModifiers(node, context) {
6176
+ function getModifiers2(node, context) {
5569
6177
  const modifiers = [];
5570
- const modList = findChildByType10(node, "modifiers");
5571
- if (modList) {
5572
- for (let i = 0; i < modList.childCount; i++) {
5573
- const child = modList.child(i);
5574
- if (child) {
5575
- const text = nodeText9(child, context).trim();
5576
- if (text) modifiers.push(text);
5577
- }
6178
+ for (let i = 0; i < node.childCount; i++) {
6179
+ const child = node.child(i);
6180
+ if (!child) continue;
6181
+ const type = child.type;
6182
+ if (type === "visibility_modifier" || type === "static_modifier" || type === "abstract_modifier" || type === "final_modifier" || type === "readonly_modifier") {
6183
+ modifiers.push(nodeText10(child, context).trim());
5578
6184
  }
5579
6185
  }
5580
6186
  return modifiers;
5581
6187
  }
5582
- function extractTypeName5(node, context) {
5583
- const text = nodeText9(node, context).trim();
5584
- if (!text || text === "," || text === ":") return null;
5585
- let name = text;
5586
- const angleBracketIdx = name.indexOf("<");
5587
- if (angleBracketIdx > 0) name = name.substring(0, angleBracketIdx);
5588
- const parenIdx = name.indexOf("(");
5589
- if (parenIdx > 0) name = name.substring(0, parenIdx);
5590
- const dotIdx = name.lastIndexOf(".");
5591
- name = dotIdx >= 0 ? name.substring(dotIdx + 1) : name;
5592
- return name.trim() || null;
6188
+ function extractQualifiedName(node, context) {
6189
+ const nameNode = findDescendantByTypes3(node, ["name", "qualified_name", "namespace_name"]);
6190
+ if (!nameNode) return null;
6191
+ const text = nodeText10(nameNode, context).trim();
6192
+ if (!text) return null;
6193
+ const parts = text.split("\\");
6194
+ return parts[parts.length - 1];
5593
6195
  }
5594
- function findChildByType10(node, type) {
6196
+ function findChildByType11(node, type) {
5595
6197
  for (let i = 0; i < node.childCount; i++) {
5596
6198
  const child = node.child(i);
5597
6199
  if (child && child.type === type) return child;
5598
6200
  }
5599
6201
  return null;
5600
6202
  }
5601
- function findChildrenByType4(node, type) {
6203
+ function findChildrenByType5(node, type) {
5602
6204
  const results = [];
5603
6205
  for (let i = 0; i < node.childCount; i++) {
5604
6206
  const child = node.child(i);
@@ -5606,27 +6208,27 @@ function findChildrenByType4(node, type) {
5606
6208
  }
5607
6209
  return results;
5608
6210
  }
5609
- function findDescendantByTypes2(node, types) {
6211
+ function findDescendantByTypes3(node, types) {
5610
6212
  for (let i = 0; i < node.childCount; i++) {
5611
6213
  const child = node.child(i);
5612
6214
  if (!child) continue;
5613
6215
  if (types.includes(child.type)) return child;
5614
- const found = findDescendantByTypes2(child, types);
6216
+ const found = findDescendantByTypes3(child, types);
5615
6217
  if (found) return found;
5616
6218
  }
5617
6219
  return null;
5618
6220
  }
5619
- function nodeText9(node, context) {
6221
+ function nodeText10(node, context) {
5620
6222
  return context.sourceCode.substring(node.startIndex, node.endIndex);
5621
6223
  }
5622
- function getCurrentSymbolId10(context) {
6224
+ function getCurrentSymbolId11(context) {
5623
6225
  if (context.currentScope.length === 0) return null;
5624
6226
  return `${context.filePath}::${context.currentScope[context.currentScope.length - 1]}`;
5625
6227
  }
5626
- var kotlinParser = {
5627
- name: "kotlin",
5628
- extensions: [".kt", ".kts", "build.gradle.kts", "settings.gradle.kts", "settings.gradle"],
5629
- parseFile: parseKotlinFile
6228
+ var phpParser = {
6229
+ name: "php",
6230
+ extensions: [".php"],
6231
+ parseFile: parsePhpFile
5630
6232
  };
5631
6233
 
5632
6234
  // src/parser/detect.ts
@@ -5640,12 +6242,13 @@ var parsers = [
5640
6242
  csharpParser,
5641
6243
  javaParser,
5642
6244
  cppParser,
5643
- kotlinParser
6245
+ kotlinParser,
6246
+ phpParser
5644
6247
  ];
5645
6248
  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/;
5646
6249
  function getParserForFile(filePath, content) {
5647
- const ext = extname7(filePath).toLowerCase();
5648
- const fileName = basename5(filePath);
6250
+ const ext = extname8(filePath).toLowerCase();
6251
+ const fileName = basename6(filePath);
5649
6252
  if (ext === ".h" && content) {
5650
6253
  if (CPP_KEYWORDS.test(content)) {
5651
6254
  return cppParser;
@@ -5663,7 +6266,7 @@ import { minimatch } from "minimatch";
5663
6266
  var MAX_FILE_SIZE = 1e6;
5664
6267
  function shouldParseFile(fullPath) {
5665
6268
  try {
5666
- const stats = statSync6(fullPath);
6269
+ const stats = statSync7(fullPath);
5667
6270
  if (stats.size > MAX_FILE_SIZE) {
5668
6271
  console.error(`[Parser] Skipping ${fullPath} \u2014 file too large (${(stats.size / 1024).toFixed(0)}KB)`);
5669
6272
  return false;
@@ -5681,8 +6284,8 @@ async function parseProject(projectRoot, options) {
5681
6284
  let errorFiles = 0;
5682
6285
  for (const file of files) {
5683
6286
  try {
5684
- const fullPath = join12(projectRoot, file);
5685
- if (!resolve6(fullPath).startsWith(resolve6(projectRoot))) {
6287
+ const fullPath = join13(projectRoot, file);
6288
+ if (!resolve7(fullPath).startsWith(resolve7(projectRoot))) {
5686
6289
  skippedFiles++;
5687
6290
  continue;
5688
6291
  }
@@ -5705,7 +6308,7 @@ async function parseProject(projectRoot, options) {
5705
6308
  if (options?.verbose) {
5706
6309
  console.error(`[Parser] Parsing: ${file}`);
5707
6310
  }
5708
- const sourceCode = readFileSync9(fullPath, "utf-8");
6311
+ const sourceCode = readFileSync10(fullPath, "utf-8");
5709
6312
  const parser = getParserForFile(file, sourceCode);
5710
6313
  if (!parser) {
5711
6314
  console.error(`No parser found for file: ${file}`);
@@ -5734,8 +6337,8 @@ async function parseProject(projectRoot, options) {
5734
6337
  }
5735
6338
 
5736
6339
  // src/cross-language/detectors/rest-api.ts
5737
- import { readFileSync as readFileSync10 } from "fs";
5738
- import { join as join13, resolve as resolve7 } from "path";
6340
+ import { readFileSync as readFileSync11 } from "fs";
6341
+ import { join as join14, resolve as resolve8 } from "path";
5739
6342
  function getLanguage(filePath) {
5740
6343
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
5741
6344
  if (filePath.endsWith(".js") || filePath.endsWith(".jsx") || filePath.endsWith(".mjs") || filePath.endsWith(".cjs")) return "javascript";
@@ -5744,6 +6347,7 @@ function getLanguage(filePath) {
5744
6347
  if (filePath.endsWith(".cs") || filePath.endsWith(".csx")) return "csharp";
5745
6348
  if (filePath.endsWith(".java")) return "java";
5746
6349
  if (filePath.endsWith(".kt") || filePath.endsWith(".kts")) return "kotlin";
6350
+ if (filePath.endsWith(".php")) return "php";
5747
6351
  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";
5748
6352
  return "unknown";
5749
6353
  }
@@ -6102,6 +6706,67 @@ function extractRouteDefinitions(source, filePath) {
6102
6706
  if (!path6.startsWith("/")) path6 = "/" + path6;
6103
6707
  }
6104
6708
  }
6709
+ if (lang === "php") {
6710
+ const laravelRouteMatch = line.match(/Route\s*::\s*(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/i);
6711
+ if (laravelRouteMatch) {
6712
+ const path6 = laravelRouteMatch[2];
6713
+ if (path6.startsWith("/")) {
6714
+ routes.push({
6715
+ method: laravelRouteMatch[1].toUpperCase(),
6716
+ path: path6,
6717
+ normalizedPath: normalizePath(path6),
6718
+ file: filePath,
6719
+ line: i + 1
6720
+ });
6721
+ }
6722
+ }
6723
+ const symfonyRouteMatch = line.match(/#\[Route\s*\(\s*['"]([^'"]+)['"]/);
6724
+ if (symfonyRouteMatch) {
6725
+ const path6 = symfonyRouteMatch[1];
6726
+ if (path6.startsWith("/")) {
6727
+ const methodsMatch = line.match(/methods\s*:\s*\[([^\]]+)\]/);
6728
+ const methods = methodsMatch ? methodsMatch[1].match(/['"](\w+)['"]/g)?.map((m) => m.replace(/['"]/g, "").toUpperCase()) || ["ANY"] : ["ANY"];
6729
+ for (const method of methods) {
6730
+ routes.push({
6731
+ method,
6732
+ path: path6,
6733
+ normalizedPath: normalizePath(path6),
6734
+ file: filePath,
6735
+ line: i + 1
6736
+ });
6737
+ }
6738
+ }
6739
+ }
6740
+ const slimMatch = line.match(/\$(?:app|group)\s*->\s*(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/i);
6741
+ if (slimMatch) {
6742
+ const path6 = slimMatch[2];
6743
+ if (path6.startsWith("/")) {
6744
+ routes.push({
6745
+ method: slimMatch[1].toUpperCase(),
6746
+ path: path6,
6747
+ normalizedPath: normalizePath(path6),
6748
+ file: filePath,
6749
+ line: i + 1
6750
+ });
6751
+ }
6752
+ }
6753
+ const wpRestMatch = line.match(/register_rest_route\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/);
6754
+ if (wpRestMatch) {
6755
+ const namespace = wpRestMatch[1];
6756
+ let path6 = wpRestMatch[2];
6757
+ if (!path6.startsWith("/")) path6 = "/" + path6;
6758
+ const fullPath = `/wp-json/${namespace}${path6}`;
6759
+ const methodMatch = line.match(/methods\s*['"=>\s]+['"](\w+)['"]/i);
6760
+ const method = methodMatch ? methodMatch[1].toUpperCase() : "ANY";
6761
+ routes.push({
6762
+ method,
6763
+ path: fullPath,
6764
+ normalizedPath: normalizePath(fullPath),
6765
+ file: filePath,
6766
+ line: i + 1
6767
+ });
6768
+ }
6769
+ }
6105
6770
  if (lang === "cpp") {
6106
6771
  const crowMatch = line.match(/CROW_ROUTE\s*\(\s*\w+\s*,\s*"([^"]+)"/);
6107
6772
  if (crowMatch) {
@@ -6216,11 +6881,11 @@ function detectRestApiEdges(files, projectRoot) {
6216
6881
  const allCalls = [];
6217
6882
  const allRoutes = [];
6218
6883
  for (const file of files) {
6219
- const fullPath = join13(projectRoot, file.filePath);
6220
- if (!resolve7(fullPath).startsWith(resolve7(projectRoot))) continue;
6884
+ const fullPath = join14(projectRoot, file.filePath);
6885
+ if (!resolve8(fullPath).startsWith(resolve8(projectRoot))) continue;
6221
6886
  let source;
6222
6887
  try {
6223
- source = readFileSync10(fullPath, "utf-8");
6888
+ source = readFileSync11(fullPath, "utf-8");
6224
6889
  } catch {
6225
6890
  continue;
6226
6891
  }
@@ -6240,6 +6905,34 @@ function detectRestApiEdges(files, projectRoot) {
6240
6905
  }
6241
6906
  }
6242
6907
  }
6908
+ if (lang === "php") {
6909
+ const phpLines = source.split("\n");
6910
+ for (let i = 0; i < phpLines.length; i++) {
6911
+ const line = phpLines[i];
6912
+ const guzzleMatch = line.match(/\$\w+\s*->\s*(get|post|put|delete|patch|request)\s*\(\s*['"]([^'"]+)['"]/i);
6913
+ if (guzzleMatch) {
6914
+ let method = guzzleMatch[1].toUpperCase();
6915
+ let path6 = guzzleMatch[2];
6916
+ if (method === "REQUEST") {
6917
+ const reqMethodMatch = line.match(/request\s*\(\s*['"](\w+)['"]\s*,\s*['"]([^'"]+)['"]/i);
6918
+ if (reqMethodMatch) {
6919
+ method = reqMethodMatch[1].toUpperCase();
6920
+ path6 = reqMethodMatch[2];
6921
+ }
6922
+ }
6923
+ if (isLocalApiPath(path6)) {
6924
+ allCalls.push({ method, path: cleanPath(path6), file: file.filePath, line: i + 1 });
6925
+ }
6926
+ }
6927
+ const fgcMatch = line.match(/file_get_contents\s*\(\s*['"]([^'"]+)['"]/);
6928
+ if (fgcMatch) {
6929
+ const path6 = fgcMatch[1];
6930
+ if (isLocalApiPath(path6)) {
6931
+ allCalls.push({ method: "GET", path: cleanPath(path6), file: file.filePath, line: i + 1 });
6932
+ }
6933
+ }
6934
+ }
6935
+ }
6243
6936
  allRoutes.push(...extractRouteDefinitions(source, file.filePath));
6244
6937
  }
6245
6938
  for (const call of allCalls) {
@@ -6268,8 +6961,8 @@ function detectRestApiEdges(files, projectRoot) {
6268
6961
  }
6269
6962
 
6270
6963
  // src/cross-language/detectors/subprocess.ts
6271
- import { readFileSync as readFileSync11 } from "fs";
6272
- import { join as join14, resolve as resolve8, basename as basename6 } from "path";
6964
+ import { readFileSync as readFileSync12 } from "fs";
6965
+ import { join as join15, resolve as resolve9, basename as basename7 } from "path";
6273
6966
  var SCRIPT_EXTENSIONS = [".py", ".js", ".ts", ".go", ".rs"];
6274
6967
  function getLanguage2(filePath) {
6275
6968
  if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
@@ -6368,16 +7061,16 @@ function detectSubprocessEdges(files, projectRoot) {
6368
7061
  const knownFiles = new Set(files.map((f) => f.filePath));
6369
7062
  const basenameMap = /* @__PURE__ */ new Map();
6370
7063
  for (const f of files) {
6371
- const base = basename6(f.filePath);
7064
+ const base = basename7(f.filePath);
6372
7065
  if (!basenameMap.has(base)) basenameMap.set(base, []);
6373
7066
  basenameMap.get(base).push(f.filePath);
6374
7067
  }
6375
7068
  for (const file of files) {
6376
- const fullPath = join14(projectRoot, file.filePath);
6377
- if (!resolve8(fullPath).startsWith(resolve8(projectRoot))) continue;
7069
+ const fullPath = join15(projectRoot, file.filePath);
7070
+ if (!resolve9(fullPath).startsWith(resolve9(projectRoot))) continue;
6378
7071
  let source;
6379
7072
  try {
6380
- source = readFileSync11(fullPath, "utf-8");
7073
+ source = readFileSync12(fullPath, "utf-8");
6381
7074
  } catch {
6382
7075
  continue;
6383
7076
  }
@@ -6389,7 +7082,7 @@ function detectSubprocessEdges(files, projectRoot) {
6389
7082
  targetFile = call.calledFile;
6390
7083
  confidence = "high";
6391
7084
  } else {
6392
- const base = basename6(call.calledFile);
7085
+ const base = basename7(call.calledFile);
6393
7086
  const candidates = basenameMap.get(base);
6394
7087
  if (candidates && candidates.length > 0) {
6395
7088
  const exactCandidate = candidates.find((c) => c.endsWith(call.calledFile));
@@ -6768,7 +7461,7 @@ function getArchitectureSummary(graph) {
6768
7461
  }
6769
7462
 
6770
7463
  // src/health/metrics.ts
6771
- import { dirname as dirname11 } from "path";
7464
+ import { dirname as dirname12 } from "path";
6772
7465
  function scoreToGrade(score) {
6773
7466
  if (score >= 90) return "A";
6774
7467
  if (score >= 80) return "B";
@@ -6801,8 +7494,8 @@ function calculateCouplingScore(graph) {
6801
7494
  totalEdges++;
6802
7495
  fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
6803
7496
  fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
6804
- const sourceDir = dirname11(sourceAttrs.filePath).split("/")[0];
6805
- const targetDir = dirname11(targetAttrs.filePath).split("/")[0];
7497
+ const sourceDir = dirname12(sourceAttrs.filePath).split("/")[0];
7498
+ const targetDir = dirname12(targetAttrs.filePath).split("/")[0];
6806
7499
  if (sourceDir !== targetDir) {
6807
7500
  crossDirEdges++;
6808
7501
  }
@@ -6849,8 +7542,8 @@ function calculateCohesionScore(graph) {
6849
7542
  const sourceAttrs = graph.getNodeAttributes(source);
6850
7543
  const targetAttrs = graph.getNodeAttributes(target);
6851
7544
  if (sourceAttrs.filePath !== targetAttrs.filePath) {
6852
- const sourceDir = dirname11(sourceAttrs.filePath);
6853
- const targetDir = dirname11(targetAttrs.filePath);
7545
+ const sourceDir = dirname12(sourceAttrs.filePath);
7546
+ const targetDir = dirname12(targetAttrs.filePath);
6854
7547
  if (!dirEdges.has(sourceDir)) {
6855
7548
  dirEdges.set(sourceDir, { internal: 0, total: 0 });
6856
7549
  }
@@ -7132,8 +7825,8 @@ function calculateDepthScore(graph) {
7132
7825
  }
7133
7826
 
7134
7827
  // src/health/index.ts
7135
- import { readFileSync as readFileSync12, writeFileSync, existsSync as existsSync12, mkdirSync } from "fs";
7136
- import { dirname as dirname12, resolve as resolve9 } from "path";
7828
+ import { readFileSync as readFileSync13, writeFileSync, existsSync as existsSync13, mkdirSync } from "fs";
7829
+ import { dirname as dirname13, resolve as resolve10 } from "path";
7137
7830
  function calculateHealthScore(graph, projectRoot) {
7138
7831
  const coupling = calculateCouplingScore(graph);
7139
7832
  const cohesion = calculateCohesionScore(graph);
@@ -7232,8 +7925,8 @@ function getHealthTrend(projectRoot, currentScore) {
7232
7925
  }
7233
7926
  }
7234
7927
  function saveHealthHistory(projectRoot, report) {
7235
- const resolvedRoot = resolve9(projectRoot);
7236
- const historyFile = resolve9(resolvedRoot, ".depwire", "health-history.json");
7928
+ const resolvedRoot = resolve10(projectRoot);
7929
+ const historyFile = resolve10(resolvedRoot, ".depwire", "health-history.json");
7237
7930
  if (!historyFile.startsWith(resolvedRoot)) {
7238
7931
  return;
7239
7932
  }
@@ -7248,10 +7941,10 @@ function saveHealthHistory(projectRoot, report) {
7248
7941
  }))
7249
7942
  };
7250
7943
  let history = [];
7251
- if (existsSync12(historyFile)) {
7944
+ if (existsSync13(historyFile)) {
7252
7945
  try {
7253
7946
  if (!historyFile.startsWith(resolvedRoot)) return;
7254
- const content = readFileSync12(historyFile, "utf-8");
7947
+ const content = readFileSync13(historyFile, "utf-8");
7255
7948
  history = JSON.parse(content);
7256
7949
  } catch {
7257
7950
  }
@@ -7260,19 +7953,19 @@ function saveHealthHistory(projectRoot, report) {
7260
7953
  if (history.length > 50) {
7261
7954
  history = history.slice(-50);
7262
7955
  }
7263
- mkdirSync(dirname12(historyFile), { recursive: true });
7956
+ mkdirSync(dirname13(historyFile), { recursive: true });
7264
7957
  if (!historyFile.startsWith(resolvedRoot)) return;
7265
7958
  writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
7266
7959
  }
7267
7960
  function loadHealthHistory(projectRoot) {
7268
- const resolvedRoot = resolve9(projectRoot);
7269
- const historyFile = resolve9(resolvedRoot, ".depwire", "health-history.json");
7270
- if (!historyFile.startsWith(resolvedRoot) || !existsSync12(historyFile)) {
7961
+ const resolvedRoot = resolve10(projectRoot);
7962
+ const historyFile = resolve10(resolvedRoot, ".depwire", "health-history.json");
7963
+ if (!historyFile.startsWith(resolvedRoot) || !existsSync13(historyFile)) {
7271
7964
  return [];
7272
7965
  }
7273
7966
  try {
7274
7967
  if (!historyFile.startsWith(resolvedRoot)) return [];
7275
- const content = readFileSync12(historyFile, "utf-8");
7968
+ const content = readFileSync13(historyFile, "utf-8");
7276
7969
  return JSON.parse(content);
7277
7970
  } catch {
7278
7971
  return [];
@@ -7281,7 +7974,7 @@ function loadHealthHistory(projectRoot) {
7281
7974
 
7282
7975
  // src/dead-code/detector.ts
7283
7976
  import path2 from "path";
7284
- import { readFileSync as readFileSync13, existsSync as existsSync13 } from "fs";
7977
+ import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
7285
7978
  function findDeadSymbols(graph, projectRoot, includeTests = false, debug = false) {
7286
7979
  const deadSymbols = [];
7287
7980
  const context = { graph, projectRoot };
@@ -7414,11 +8107,11 @@ function getPackageEntryPoints(projectRoot) {
7414
8107
  const entryPoints = /* @__PURE__ */ new Set();
7415
8108
  const resolvedRoot = path2.resolve(projectRoot);
7416
8109
  const packageJsonPath = path2.resolve(resolvedRoot, "package.json");
7417
- if (!packageJsonPath.startsWith(resolvedRoot) || !existsSync13(packageJsonPath)) {
8110
+ if (!packageJsonPath.startsWith(resolvedRoot) || !existsSync14(packageJsonPath)) {
7418
8111
  return entryPoints;
7419
8112
  }
7420
8113
  try {
7421
- const packageJson = JSON.parse(readFileSync13(packageJsonPath, "utf-8"));
8114
+ const packageJson = JSON.parse(readFileSync14(packageJsonPath, "utf-8"));
7422
8115
  if (packageJson.main) {
7423
8116
  entryPoints.add(path2.resolve(projectRoot, packageJson.main));
7424
8117
  }
@@ -7475,6 +8168,9 @@ function shouldExclude(attrs, context, includeTests, packageEntryPoints) {
7475
8168
  if (isKotlinExcluded(attrs)) {
7476
8169
  return "framework";
7477
8170
  }
8171
+ if (isPhpExcluded(attrs)) {
8172
+ return "framework";
8173
+ }
7478
8174
  return null;
7479
8175
  }
7480
8176
  function isRealPackageEntryPoint(filePath, packageEntryPoints) {
@@ -7540,6 +8236,71 @@ function isKotlinExcluded(attrs) {
7540
8236
  if (name.startsWith("operator")) return true;
7541
8237
  return false;
7542
8238
  }
8239
+ function isPhpExcluded(attrs) {
8240
+ const filePath = attrs.file || attrs.filePath || "";
8241
+ const name = attrs.name || "";
8242
+ if (!filePath.endsWith(".php")) return false;
8243
+ const magicMethods = [
8244
+ "__construct",
8245
+ "__destruct",
8246
+ "__call",
8247
+ "__callStatic",
8248
+ "__get",
8249
+ "__set",
8250
+ "__isset",
8251
+ "__unset",
8252
+ "__sleep",
8253
+ "__wakeup",
8254
+ "__serialize",
8255
+ "__unserialize",
8256
+ "__toString",
8257
+ "__invoke",
8258
+ "__set_state",
8259
+ "__clone",
8260
+ "__debugInfo"
8261
+ ];
8262
+ if (magicMethods.includes(name)) return true;
8263
+ const wpHooks = [
8264
+ "init",
8265
+ "admin_init",
8266
+ "wp_enqueue_scripts",
8267
+ "admin_enqueue_scripts",
8268
+ "widgets_init",
8269
+ "register_activation_hook",
8270
+ "register_deactivation_hook",
8271
+ "add_action",
8272
+ "add_filter",
8273
+ "activate",
8274
+ "deactivate"
8275
+ ];
8276
+ if (wpHooks.includes(name)) return true;
8277
+ const laravelMethods = [
8278
+ "register",
8279
+ "boot",
8280
+ "handle",
8281
+ "authorize",
8282
+ "rules",
8283
+ "messages",
8284
+ "prepareForValidation",
8285
+ "failed",
8286
+ "broadcastOn",
8287
+ "broadcastAs",
8288
+ "broadcastWith"
8289
+ ];
8290
+ if (laravelMethods.includes(name)) return true;
8291
+ const symfonyMethods = [
8292
+ "__invoke",
8293
+ "getSubscribedEvents",
8294
+ "getSubscribedServices",
8295
+ "configureOptions",
8296
+ "buildForm",
8297
+ "load",
8298
+ "getConfigTreeBuilder"
8299
+ ];
8300
+ if (symfonyMethods.includes(name)) return true;
8301
+ if (name.startsWith("test") || name === "setUp" || name === "tearDown" || name === "setUpBeforeClass" || name === "tearDownAfterClass") return true;
8302
+ return false;
8303
+ }
7543
8304
 
7544
8305
  // src/dead-code/classifier.ts
7545
8306
  import path3 from "path";
@@ -7606,8 +8367,8 @@ function generateReason(symbol, confidence) {
7606
8367
  return "Potentially unused";
7607
8368
  }
7608
8369
  function isBarrelFile(filePath) {
7609
- const basename10 = path3.basename(filePath);
7610
- return basename10 === "index.ts" || basename10 === "index.js";
8370
+ const basename11 = path3.basename(filePath);
8371
+ return basename11 === "index.ts" || basename11 === "index.js";
7611
8372
  }
7612
8373
  function isTestFile2(filePath) {
7613
8374
  return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
@@ -7786,11 +8547,11 @@ function filterByConfidence(symbols, minConfidence) {
7786
8547
  }
7787
8548
 
7788
8549
  // src/docs/generator.ts
7789
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync16 } from "fs";
7790
- import { join as join18 } from "path";
8550
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync17 } from "fs";
8551
+ import { join as join19 } from "path";
7791
8552
 
7792
8553
  // src/docs/architecture.ts
7793
- import { dirname as dirname13 } from "path";
8554
+ import { dirname as dirname14 } from "path";
7794
8555
 
7795
8556
  // src/docs/templates.ts
7796
8557
  function header(text, level = 1) {
@@ -7941,7 +8702,7 @@ function generateModuleStructure(graph) {
7941
8702
  function getDirectoryStats(graph) {
7942
8703
  const dirMap = /* @__PURE__ */ new Map();
7943
8704
  graph.forEachNode((node, attrs) => {
7944
- const dir = dirname13(attrs.filePath);
8705
+ const dir = dirname14(attrs.filePath);
7945
8706
  if (dir === ".") return;
7946
8707
  if (!dirMap.has(dir)) {
7947
8708
  dirMap.set(dir, {
@@ -7966,7 +8727,7 @@ function getDirectoryStats(graph) {
7966
8727
  });
7967
8728
  const filesPerDir = /* @__PURE__ */ new Map();
7968
8729
  graph.forEachNode((node, attrs) => {
7969
- const dir = dirname13(attrs.filePath);
8730
+ const dir = dirname14(attrs.filePath);
7970
8731
  if (!filesPerDir.has(dir)) {
7971
8732
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
7972
8733
  }
@@ -7981,8 +8742,8 @@ function getDirectoryStats(graph) {
7981
8742
  graph.forEachEdge((edge, attrs, source, target) => {
7982
8743
  const sourceAttrs = graph.getNodeAttributes(source);
7983
8744
  const targetAttrs = graph.getNodeAttributes(target);
7984
- const sourceDir = dirname13(sourceAttrs.filePath);
7985
- const targetDir = dirname13(targetAttrs.filePath);
8745
+ const sourceDir = dirname14(sourceAttrs.filePath);
8746
+ const targetDir = dirname14(targetAttrs.filePath);
7986
8747
  if (sourceDir !== targetDir) {
7987
8748
  if (!dirEdges.has(sourceDir)) {
7988
8749
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -8189,7 +8950,7 @@ function detectCycles(graph) {
8189
8950
  }
8190
8951
 
8191
8952
  // src/docs/conventions.ts
8192
- import { basename as basename7, extname as extname8 } from "path";
8953
+ import { basename as basename8, extname as extname9 } from "path";
8193
8954
  function generateConventions(graph, projectRoot, version) {
8194
8955
  let output = "";
8195
8956
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -8227,7 +8988,7 @@ function generateFileOrganization(graph) {
8227
8988
  graph.forEachNode((node, attrs) => {
8228
8989
  if (!files.has(attrs.filePath)) {
8229
8990
  files.add(attrs.filePath);
8230
- const fileName = basename7(attrs.filePath);
8991
+ const fileName = basename8(attrs.filePath);
8231
8992
  if (fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx") {
8232
8993
  barrelFileCount++;
8233
8994
  }
@@ -8286,7 +9047,7 @@ function generateNamingPatterns(graph) {
8286
9047
  graph.forEachNode((node, attrs) => {
8287
9048
  if (!files.has(attrs.filePath)) {
8288
9049
  files.add(attrs.filePath);
8289
- const fileName = basename7(attrs.filePath, extname8(attrs.filePath));
9050
+ const fileName = basename8(attrs.filePath, extname9(attrs.filePath));
8290
9051
  if (isCamelCase(fileName)) patterns.files.camelCase++;
8291
9052
  else if (isPascalCase(fileName)) patterns.files.PascalCase++;
8292
9053
  else if (isKebabCase(fileName)) patterns.files.kebabCase++;
@@ -8963,7 +9724,7 @@ function detectCyclesDetailed(graph) {
8963
9724
  }
8964
9725
 
8965
9726
  // src/docs/onboarding.ts
8966
- import { dirname as dirname14 } from "path";
9727
+ import { dirname as dirname15 } from "path";
8967
9728
  function generateOnboarding(graph, projectRoot, version) {
8968
9729
  let output = "";
8969
9730
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -9022,7 +9783,7 @@ function generateQuickOrientation(graph) {
9022
9783
  const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
9023
9784
  const dirs = /* @__PURE__ */ new Set();
9024
9785
  graph.forEachNode((node, attrs) => {
9025
- const dir = dirname14(attrs.filePath);
9786
+ const dir = dirname15(attrs.filePath);
9026
9787
  if (dir !== ".") {
9027
9788
  const topLevel = dir.split("/")[0];
9028
9789
  dirs.add(topLevel);
@@ -9126,7 +9887,7 @@ function generateModuleMap(graph) {
9126
9887
  function getDirectoryStats2(graph) {
9127
9888
  const dirMap = /* @__PURE__ */ new Map();
9128
9889
  graph.forEachNode((node, attrs) => {
9129
- const dir = dirname14(attrs.filePath);
9890
+ const dir = dirname15(attrs.filePath);
9130
9891
  if (dir === ".") return;
9131
9892
  if (!dirMap.has(dir)) {
9132
9893
  dirMap.set(dir, {
@@ -9141,7 +9902,7 @@ function getDirectoryStats2(graph) {
9141
9902
  });
9142
9903
  const filesPerDir = /* @__PURE__ */ new Map();
9143
9904
  graph.forEachNode((node, attrs) => {
9144
- const dir = dirname14(attrs.filePath);
9905
+ const dir = dirname15(attrs.filePath);
9145
9906
  if (!filesPerDir.has(dir)) {
9146
9907
  filesPerDir.set(dir, /* @__PURE__ */ new Set());
9147
9908
  }
@@ -9156,8 +9917,8 @@ function getDirectoryStats2(graph) {
9156
9917
  graph.forEachEdge((edge, attrs, source, target) => {
9157
9918
  const sourceAttrs = graph.getNodeAttributes(source);
9158
9919
  const targetAttrs = graph.getNodeAttributes(target);
9159
- const sourceDir = dirname14(sourceAttrs.filePath);
9160
- const targetDir = dirname14(targetAttrs.filePath);
9920
+ const sourceDir = dirname15(sourceAttrs.filePath);
9921
+ const targetDir = dirname15(targetAttrs.filePath);
9161
9922
  if (sourceDir !== targetDir) {
9162
9923
  if (!dirEdges.has(sourceDir)) {
9163
9924
  dirEdges.set(sourceDir, { in: 0, out: 0 });
@@ -9238,7 +9999,7 @@ function detectClusters(graph) {
9238
9999
  const dirFiles = /* @__PURE__ */ new Map();
9239
10000
  const fileEdges = /* @__PURE__ */ new Map();
9240
10001
  graph.forEachNode((node, attrs) => {
9241
- const dir = dirname14(attrs.filePath);
10002
+ const dir = dirname15(attrs.filePath);
9242
10003
  if (!dirFiles.has(dir)) {
9243
10004
  dirFiles.set(dir, /* @__PURE__ */ new Set());
9244
10005
  }
@@ -9292,8 +10053,8 @@ function inferClusterName(files) {
9292
10053
  if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
9293
10054
  return capitalizeFirst2(sortedWords[0][0]);
9294
10055
  }
9295
- const commonDir = dirname14(files[0]);
9296
- if (files.every((f) => dirname14(f) === commonDir)) {
10056
+ const commonDir = dirname15(files[0]);
10057
+ if (files.every((f) => dirname15(f) === commonDir)) {
9297
10058
  return capitalizeFirst2(commonDir.split("/").pop() || "Core");
9298
10059
  }
9299
10060
  return "Core";
@@ -9351,7 +10112,7 @@ function generateDepwireUsage(projectRoot) {
9351
10112
  }
9352
10113
 
9353
10114
  // src/docs/files.ts
9354
- import { dirname as dirname15, basename as basename8 } from "path";
10115
+ import { dirname as dirname16, basename as basename9 } from "path";
9355
10116
  function generateFiles(graph, projectRoot, version) {
9356
10117
  let output = "";
9357
10118
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -9455,7 +10216,7 @@ function generateDirectoryBreakdown(graph) {
9455
10216
  const fileStats = getFileStats2(graph);
9456
10217
  const dirMap = /* @__PURE__ */ new Map();
9457
10218
  for (const file of fileStats) {
9458
- const dir = dirname15(file.filePath);
10219
+ const dir = dirname16(file.filePath);
9459
10220
  const topDir = dir === "." ? "." : dir.split("/")[0];
9460
10221
  if (!dirMap.has(topDir)) {
9461
10222
  dirMap.set(topDir, {
@@ -9470,7 +10231,7 @@ function generateDirectoryBreakdown(graph) {
9470
10231
  dirStats.symbolCount += file.symbolCount;
9471
10232
  if (file.totalConnections > dirStats.maxConnections) {
9472
10233
  dirStats.maxConnections = file.totalConnections;
9473
- dirStats.mostConnectedFile = basename8(file.filePath);
10234
+ dirStats.mostConnectedFile = basename9(file.filePath);
9474
10235
  }
9475
10236
  }
9476
10237
  if (dirMap.size === 0) {
@@ -10098,7 +10859,7 @@ function generateRecommendations(graph) {
10098
10859
  }
10099
10860
 
10100
10861
  // src/docs/tests.ts
10101
- import { basename as basename9, dirname as dirname16 } from "path";
10862
+ import { basename as basename10, dirname as dirname17 } from "path";
10102
10863
  function generateTests(graph, projectRoot, version) {
10103
10864
  let output = "";
10104
10865
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -10126,8 +10887,8 @@ function getFileCount8(graph) {
10126
10887
  return files.size;
10127
10888
  }
10128
10889
  function isTestFile3(filePath) {
10129
- const fileName = basename9(filePath).toLowerCase();
10130
- const dirPath = dirname16(filePath).toLowerCase();
10890
+ const fileName = basename10(filePath).toLowerCase();
10891
+ const dirPath = dirname17(filePath).toLowerCase();
10131
10892
  if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
10132
10893
  return true;
10133
10894
  }
@@ -10185,13 +10946,13 @@ function generateTestFileInventory(graph) {
10185
10946
  return output;
10186
10947
  }
10187
10948
  function matchTestToSource(testFile) {
10188
- const testFileName = basename9(testFile);
10189
- const testDir = dirname16(testFile);
10949
+ const testFileName = basename10(testFile);
10950
+ const testDir = dirname17(testFile);
10190
10951
  let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
10191
10952
  const possiblePaths = [];
10192
10953
  possiblePaths.push(testDir + "/" + sourceFileName);
10193
10954
  if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
10194
- const parentDir = dirname16(testDir);
10955
+ const parentDir = dirname17(testDir);
10195
10956
  possiblePaths.push(parentDir + "/" + sourceFileName);
10196
10957
  }
10197
10958
  if (testDir.includes("test")) {
@@ -10347,7 +11108,7 @@ function generateTestCoverageMap(graph) {
10347
11108
  const rows = mappings.slice(0, 30).map((m) => [
10348
11109
  `\`${m.sourceFile}\``,
10349
11110
  m.hasTest ? "\u2705" : "\u274C",
10350
- m.testFile ? `\`${basename9(m.testFile)}\`` : "-",
11111
+ m.testFile ? `\`${basename10(m.testFile)}\`` : "-",
10351
11112
  formatNumber(m.symbolCount)
10352
11113
  ]);
10353
11114
  let output = table(headers, rows);
@@ -10388,7 +11149,7 @@ function generateTestStatistics(graph) {
10388
11149
  `;
10389
11150
  const dirTestCoverage = /* @__PURE__ */ new Map();
10390
11151
  for (const sourceFile of sourceFiles) {
10391
- const dir = dirname16(sourceFile).split("/")[0];
11152
+ const dir = dirname17(sourceFile).split("/")[0];
10392
11153
  if (!dirTestCoverage.has(dir)) {
10393
11154
  dirTestCoverage.set(dir, { total: 0, tested: 0 });
10394
11155
  }
@@ -10411,7 +11172,7 @@ function generateTestStatistics(graph) {
10411
11172
  }
10412
11173
 
10413
11174
  // src/docs/history.ts
10414
- import { dirname as dirname17 } from "path";
11175
+ import { dirname as dirname18 } from "path";
10415
11176
  import { execSync } from "child_process";
10416
11177
  function generateHistory(graph, projectRoot, version) {
10417
11178
  let output = "";
@@ -10692,7 +11453,7 @@ function generateFeatureClusters(graph) {
10692
11453
  const dirFiles = /* @__PURE__ */ new Map();
10693
11454
  const fileEdges = /* @__PURE__ */ new Map();
10694
11455
  graph.forEachNode((node, attrs) => {
10695
- const dir = dirname17(attrs.filePath);
11456
+ const dir = dirname18(attrs.filePath);
10696
11457
  if (!dirFiles.has(dir)) {
10697
11458
  dirFiles.set(dir, /* @__PURE__ */ new Set());
10698
11459
  }
@@ -10774,7 +11535,7 @@ function capitalizeFirst3(str) {
10774
11535
  }
10775
11536
 
10776
11537
  // src/docs/current.ts
10777
- import { dirname as dirname18 } from "path";
11538
+ import { dirname as dirname19 } from "path";
10778
11539
  function generateCurrent(graph, projectRoot, version) {
10779
11540
  let output = "";
10780
11541
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -10912,7 +11673,7 @@ function generateCompleteFileIndex(graph) {
10912
11673
  fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
10913
11674
  const dirGroups = /* @__PURE__ */ new Map();
10914
11675
  for (const info of fileInfos) {
10915
- const dir = dirname18(info.filePath);
11676
+ const dir = dirname19(info.filePath);
10916
11677
  const topDir = dir === "." ? "root" : dir.split("/")[0];
10917
11678
  if (!dirGroups.has(topDir)) {
10918
11679
  dirGroups.set(topDir, []);
@@ -11123,8 +11884,8 @@ function getTopLevelDir2(filePath) {
11123
11884
  }
11124
11885
 
11125
11886
  // src/docs/status.ts
11126
- import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
11127
- import { resolve as resolve10 } from "path";
11887
+ import { readFileSync as readFileSync15, existsSync as existsSync15 } from "fs";
11888
+ import { resolve as resolve11 } from "path";
11128
11889
  function generateStatus(graph, projectRoot, version) {
11129
11890
  let output = "";
11130
11891
  const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -11157,16 +11918,16 @@ function getFileCount11(graph) {
11157
11918
  }
11158
11919
  function extractComments(projectRoot, filePath) {
11159
11920
  const comments = [];
11160
- const resolvedRoot = resolve10(projectRoot);
11161
- const fullPath = resolve10(resolvedRoot, filePath);
11921
+ const resolvedRoot = resolve11(projectRoot);
11922
+ const fullPath = resolve11(resolvedRoot, filePath);
11162
11923
  if (!fullPath.startsWith(resolvedRoot)) {
11163
11924
  return comments;
11164
11925
  }
11165
- if (!existsSync14(fullPath)) {
11926
+ if (!existsSync15(fullPath)) {
11166
11927
  return comments;
11167
11928
  }
11168
11929
  try {
11169
- const content = readFileSync14(fullPath, "utf-8");
11930
+ const content = readFileSync15(fullPath, "utf-8");
11170
11931
  const lines = content.split("\n");
11171
11932
  const patterns = [
11172
11933
  { type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
@@ -11745,16 +12506,16 @@ function generateConfidenceSection(title, description, symbols, projectRoot) {
11745
12506
  }
11746
12507
 
11747
12508
  // src/docs/metadata.ts
11748
- import { existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync2 } from "fs";
11749
- import { resolve as resolve11 } from "path";
12509
+ import { existsSync as existsSync16, readFileSync as readFileSync16, writeFileSync as writeFileSync2 } from "fs";
12510
+ import { resolve as resolve12 } from "path";
11750
12511
  function loadMetadata(outputDir) {
11751
- const resolvedDir = resolve11(outputDir);
11752
- const metadataPath = resolve11(resolvedDir, "metadata.json");
11753
- if (!metadataPath.startsWith(resolvedDir) || !existsSync15(metadataPath)) {
12512
+ const resolvedDir = resolve12(outputDir);
12513
+ const metadataPath = resolve12(resolvedDir, "metadata.json");
12514
+ if (!metadataPath.startsWith(resolvedDir) || !existsSync16(metadataPath)) {
11754
12515
  return null;
11755
12516
  }
11756
12517
  try {
11757
- const content = readFileSync15(metadataPath, "utf-8");
12518
+ const content = readFileSync16(metadataPath, "utf-8");
11758
12519
  return JSON.parse(content);
11759
12520
  } catch (err) {
11760
12521
  console.error("Failed to load metadata:", err);
@@ -11762,8 +12523,8 @@ function loadMetadata(outputDir) {
11762
12523
  }
11763
12524
  }
11764
12525
  function saveMetadata(outputDir, metadata) {
11765
- const resolvedDir = resolve11(outputDir);
11766
- const metadataPath = resolve11(resolvedDir, "metadata.json");
12526
+ const resolvedDir = resolve12(outputDir);
12527
+ const metadataPath = resolve12(resolvedDir, "metadata.json");
11767
12528
  if (!metadataPath.startsWith(resolvedDir)) {
11768
12529
  throw new Error(`Path traversal attempt blocked: ${metadataPath}`);
11769
12530
  }
@@ -11809,7 +12570,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11809
12570
  const generated = [];
11810
12571
  const errors = [];
11811
12572
  try {
11812
- if (!existsSync16(options.outputDir)) {
12573
+ if (!existsSync17(options.outputDir)) {
11813
12574
  mkdirSync2(options.outputDir, { recursive: true });
11814
12575
  if (options.verbose) {
11815
12576
  console.log(`Created output directory: ${options.outputDir}`);
@@ -11848,7 +12609,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11848
12609
  try {
11849
12610
  if (options.verbose) console.log("Generating ARCHITECTURE.md...");
11850
12611
  const content = generateArchitecture(graph, projectRoot, version, parseTime);
11851
- const filePath = join18(options.outputDir, "ARCHITECTURE.md");
12612
+ const filePath = join19(options.outputDir, "ARCHITECTURE.md");
11852
12613
  writeFileSync3(filePath, content, "utf-8");
11853
12614
  generated.push("ARCHITECTURE.md");
11854
12615
  } catch (err) {
@@ -11859,7 +12620,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11859
12620
  try {
11860
12621
  if (options.verbose) console.log("Generating CONVENTIONS.md...");
11861
12622
  const content = generateConventions(graph, projectRoot, version);
11862
- const filePath = join18(options.outputDir, "CONVENTIONS.md");
12623
+ const filePath = join19(options.outputDir, "CONVENTIONS.md");
11863
12624
  writeFileSync3(filePath, content, "utf-8");
11864
12625
  generated.push("CONVENTIONS.md");
11865
12626
  } catch (err) {
@@ -11870,7 +12631,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11870
12631
  try {
11871
12632
  if (options.verbose) console.log("Generating DEPENDENCIES.md...");
11872
12633
  const content = generateDependencies(graph, projectRoot, version);
11873
- const filePath = join18(options.outputDir, "DEPENDENCIES.md");
12634
+ const filePath = join19(options.outputDir, "DEPENDENCIES.md");
11874
12635
  writeFileSync3(filePath, content, "utf-8");
11875
12636
  generated.push("DEPENDENCIES.md");
11876
12637
  } catch (err) {
@@ -11881,7 +12642,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11881
12642
  try {
11882
12643
  if (options.verbose) console.log("Generating ONBOARDING.md...");
11883
12644
  const content = generateOnboarding(graph, projectRoot, version);
11884
- const filePath = join18(options.outputDir, "ONBOARDING.md");
12645
+ const filePath = join19(options.outputDir, "ONBOARDING.md");
11885
12646
  writeFileSync3(filePath, content, "utf-8");
11886
12647
  generated.push("ONBOARDING.md");
11887
12648
  } catch (err) {
@@ -11892,7 +12653,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11892
12653
  try {
11893
12654
  if (options.verbose) console.log("Generating FILES.md...");
11894
12655
  const content = generateFiles(graph, projectRoot, version);
11895
- const filePath = join18(options.outputDir, "FILES.md");
12656
+ const filePath = join19(options.outputDir, "FILES.md");
11896
12657
  writeFileSync3(filePath, content, "utf-8");
11897
12658
  generated.push("FILES.md");
11898
12659
  } catch (err) {
@@ -11903,7 +12664,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11903
12664
  try {
11904
12665
  if (options.verbose) console.log("Generating API_SURFACE.md...");
11905
12666
  const content = generateApiSurface(graph, projectRoot, version);
11906
- const filePath = join18(options.outputDir, "API_SURFACE.md");
12667
+ const filePath = join19(options.outputDir, "API_SURFACE.md");
11907
12668
  writeFileSync3(filePath, content, "utf-8");
11908
12669
  generated.push("API_SURFACE.md");
11909
12670
  } catch (err) {
@@ -11914,7 +12675,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11914
12675
  try {
11915
12676
  if (options.verbose) console.log("Generating ERRORS.md...");
11916
12677
  const content = generateErrors(graph, projectRoot, version);
11917
- const filePath = join18(options.outputDir, "ERRORS.md");
12678
+ const filePath = join19(options.outputDir, "ERRORS.md");
11918
12679
  writeFileSync3(filePath, content, "utf-8");
11919
12680
  generated.push("ERRORS.md");
11920
12681
  } catch (err) {
@@ -11925,7 +12686,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11925
12686
  try {
11926
12687
  if (options.verbose) console.log("Generating TESTS.md...");
11927
12688
  const content = generateTests(graph, projectRoot, version);
11928
- const filePath = join18(options.outputDir, "TESTS.md");
12689
+ const filePath = join19(options.outputDir, "TESTS.md");
11929
12690
  writeFileSync3(filePath, content, "utf-8");
11930
12691
  generated.push("TESTS.md");
11931
12692
  } catch (err) {
@@ -11936,7 +12697,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11936
12697
  try {
11937
12698
  if (options.verbose) console.log("Generating HISTORY.md...");
11938
12699
  const content = generateHistory(graph, projectRoot, version);
11939
- const filePath = join18(options.outputDir, "HISTORY.md");
12700
+ const filePath = join19(options.outputDir, "HISTORY.md");
11940
12701
  writeFileSync3(filePath, content, "utf-8");
11941
12702
  generated.push("HISTORY.md");
11942
12703
  } catch (err) {
@@ -11947,7 +12708,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11947
12708
  try {
11948
12709
  if (options.verbose) console.log("Generating CURRENT.md...");
11949
12710
  const content = generateCurrent(graph, projectRoot, version);
11950
- const filePath = join18(options.outputDir, "CURRENT.md");
12711
+ const filePath = join19(options.outputDir, "CURRENT.md");
11951
12712
  writeFileSync3(filePath, content, "utf-8");
11952
12713
  generated.push("CURRENT.md");
11953
12714
  } catch (err) {
@@ -11958,7 +12719,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11958
12719
  try {
11959
12720
  if (options.verbose) console.log("Generating STATUS.md...");
11960
12721
  const content = generateStatus(graph, projectRoot, version);
11961
- const filePath = join18(options.outputDir, "STATUS.md");
12722
+ const filePath = join19(options.outputDir, "STATUS.md");
11962
12723
  writeFileSync3(filePath, content, "utf-8");
11963
12724
  generated.push("STATUS.md");
11964
12725
  } catch (err) {
@@ -11969,7 +12730,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11969
12730
  try {
11970
12731
  if (options.verbose) console.log("Generating HEALTH.md...");
11971
12732
  const content = generateHealth(graph, projectRoot, version);
11972
- const filePath = join18(options.outputDir, "HEALTH.md");
12733
+ const filePath = join19(options.outputDir, "HEALTH.md");
11973
12734
  writeFileSync3(filePath, content, "utf-8");
11974
12735
  generated.push("HEALTH.md");
11975
12736
  } catch (err) {
@@ -11980,7 +12741,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
11980
12741
  try {
11981
12742
  if (options.verbose) console.log("Generating DEAD_CODE.md...");
11982
12743
  const content = generateDeadCode(graph, projectRoot, version);
11983
- const filePath = join18(options.outputDir, "DEAD_CODE.md");
12744
+ const filePath = join19(options.outputDir, "DEAD_CODE.md");
11984
12745
  writeFileSync3(filePath, content, "utf-8");
11985
12746
  generated.push("DEAD_CODE.md");
11986
12747
  } catch (err) {
@@ -12024,7 +12785,7 @@ function getFileCount13(graph) {
12024
12785
  }
12025
12786
 
12026
12787
  // src/simulation/engine.ts
12027
- import { dirname as dirname19, join as join19 } from "path";
12788
+ import { dirname as dirname20, join as join20 } from "path";
12028
12789
  function normalizePath2(p) {
12029
12790
  return p.replace(/^\.\//, "").replace(/\/+$/, "");
12030
12791
  }
@@ -12156,7 +12917,7 @@ var SimulationEngine = class {
12156
12917
  }
12157
12918
  }
12158
12919
  applyRename(clone, target, newName, brokenImports) {
12159
- const destination = join19(dirname19(target), newName);
12920
+ const destination = join20(dirname20(target), newName);
12160
12921
  this.applyMove(clone, target, destination, brokenImports);
12161
12922
  }
12162
12923
  applySplit(clone, target, newFile, symbols, brokenImports) {
@@ -12356,13 +13117,13 @@ var SimulationEngine = class {
12356
13117
  };
12357
13118
 
12358
13119
  // src/security/scanner.ts
12359
- import { existsSync as existsSync18 } from "fs";
12360
- import { join as join29 } from "path";
13120
+ import { existsSync as existsSync19 } from "fs";
13121
+ import { join as join30 } from "path";
12361
13122
 
12362
13123
  // src/security/checks/dependencies.ts
12363
13124
  import { execSync as execSync2 } from "child_process";
12364
- import { existsSync as existsSync17, readFileSync as readFileSync16, readdirSync as readdirSync9 } from "fs";
12365
- import { join as join20 } from "path";
13125
+ import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync10 } from "fs";
13126
+ import { join as join21 } from "path";
12366
13127
  function cvssToSeverity(score) {
12367
13128
  if (score >= 9) return "critical";
12368
13129
  if (score >= 7) return "high";
@@ -12372,18 +13133,18 @@ function cvssToSeverity(score) {
12372
13133
  async function checkDependencies(_files, projectRoot) {
12373
13134
  const findings = [];
12374
13135
  try {
12375
- if (existsSync17(join20(projectRoot, "package.json"))) {
13136
+ if (existsSync18(join21(projectRoot, "package.json"))) {
12376
13137
  findings.push(...checkNpmAudit(projectRoot));
12377
13138
  findings.push(...checkPackageJsonPatterns(projectRoot));
12378
13139
  findings.push(...checkPostinstallScripts(projectRoot));
12379
13140
  }
12380
- if (existsSync17(join20(projectRoot, "requirements.txt")) || existsSync17(join20(projectRoot, "pyproject.toml"))) {
13141
+ if (existsSync18(join21(projectRoot, "requirements.txt")) || existsSync18(join21(projectRoot, "pyproject.toml"))) {
12381
13142
  findings.push(...checkPipAudit(projectRoot));
12382
13143
  }
12383
- if (existsSync17(join20(projectRoot, "Cargo.toml"))) {
13144
+ if (existsSync18(join21(projectRoot, "Cargo.toml"))) {
12384
13145
  findings.push(...checkCargoAudit(projectRoot));
12385
13146
  }
12386
- if (existsSync17(join20(projectRoot, "go.mod"))) {
13147
+ if (existsSync18(join21(projectRoot, "go.mod"))) {
12387
13148
  findings.push(...checkGoVerify(projectRoot));
12388
13149
  }
12389
13150
  } catch (err) {
@@ -12472,8 +13233,8 @@ function checkNpmAudit(projectRoot) {
12472
13233
  function checkPackageJsonPatterns(projectRoot) {
12473
13234
  const findings = [];
12474
13235
  try {
12475
- const pkgPath = join20(projectRoot, "package.json");
12476
- const pkg = JSON.parse(readFileSync16(pkgPath, "utf-8"));
13236
+ const pkgPath = join21(projectRoot, "package.json");
13237
+ const pkg = JSON.parse(readFileSync17(pkgPath, "utf-8"));
12477
13238
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
12478
13239
  for (const [name, version] of Object.entries(allDeps)) {
12479
13240
  if (version.startsWith("^") || version.startsWith("~")) {
@@ -12495,15 +13256,15 @@ function checkPackageJsonPatterns(projectRoot) {
12495
13256
  }
12496
13257
  function checkPostinstallScripts(projectRoot) {
12497
13258
  const findings = [];
12498
- const nodeModules = join20(projectRoot, "node_modules");
12499
- if (!existsSync17(nodeModules)) return findings;
13259
+ const nodeModules = join21(projectRoot, "node_modules");
13260
+ if (!existsSync18(nodeModules)) return findings;
12500
13261
  try {
12501
- const topLevelDeps = readdirSync9(nodeModules).filter((d) => !d.startsWith("."));
13262
+ const topLevelDeps = readdirSync10(nodeModules).filter((d) => !d.startsWith("."));
12502
13263
  for (const dep of topLevelDeps) {
12503
- const depPkgPath = join20(nodeModules, dep, "package.json");
12504
- if (!existsSync17(depPkgPath)) continue;
13264
+ const depPkgPath = join21(nodeModules, dep, "package.json");
13265
+ if (!existsSync18(depPkgPath)) continue;
12505
13266
  try {
12506
- const depPkg = JSON.parse(readFileSync16(depPkgPath, "utf-8"));
13267
+ const depPkg = JSON.parse(readFileSync17(depPkgPath, "utf-8"));
12507
13268
  const scripts = depPkg.scripts || {};
12508
13269
  if (scripts.postinstall || scripts.preinstall || scripts.install) {
12509
13270
  const scriptName = scripts.postinstall ? "postinstall" : scripts.preinstall ? "preinstall" : "install";
@@ -12541,7 +13302,7 @@ function checkPipAudit(projectRoot) {
12541
13302
  id: "",
12542
13303
  severity: cvssToSeverity(vuln.cvss?.score || 5),
12543
13304
  vulnerabilityClass: "dependency-cve",
12544
- file: existsSync17(join20(projectRoot, "requirements.txt")) ? "requirements.txt" : "pyproject.toml",
13305
+ file: existsSync18(join21(projectRoot, "requirements.txt")) ? "requirements.txt" : "pyproject.toml",
12545
13306
  title: `Vulnerable Python dependency: ${vuln.name}`,
12546
13307
  description: `${vuln.name}@${vuln.version} \u2014 ${vuln.id}: ${vuln.description || "Known vulnerability"}`,
12547
13308
  attackScenario: `An attacker could exploit the vulnerability in ${vuln.name}.`,
@@ -12638,8 +13399,8 @@ function checkGoVerify(projectRoot) {
12638
13399
  }
12639
13400
 
12640
13401
  // src/security/checks/injection.ts
12641
- import { readFileSync as readFileSync17 } from "fs";
12642
- import { join as join21 } from "path";
13402
+ import { readFileSync as readFileSync18 } from "fs";
13403
+ import { join as join22 } from "path";
12643
13404
  var SKIP_DIRS = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
12644
13405
  var TEST_PATTERNS = ["test", "spec", "fixture", "mock", "__tests__", "__mocks__"];
12645
13406
  var USER_INPUT_NAMES = /(?:input|user|name|path|query|branch|hash|cmd|command|req\.|params|body|args|url|dir|file|subdirectory)/i;
@@ -12871,6 +13632,61 @@ var PATTERNS = [
12871
13632
  description: "permitAll() applied to a path that appears security-sensitive.",
12872
13633
  attackScenario: "An attacker could access administrative or destructive endpoints without authentication.",
12873
13634
  suggestedFix: 'Use .hasRole("ADMIN") or .authenticated() for sensitive endpoints.'
13635
+ },
13636
+ // PHP injection patterns
13637
+ {
13638
+ regex: /\$wpdb\s*->\s*query\s*\(\s*["'][^"']*\$|.*\$wpdb\s*->\s*query\s*\(\s*[^"']*\.\s*\$/,
13639
+ title: "PHP SQL injection via $wpdb->query with string concatenation",
13640
+ vulnClass: "code-injection",
13641
+ baseSeverity: "high",
13642
+ description: "WordPress $wpdb->query() called with direct variable interpolation \u2014 vulnerable to SQL injection.",
13643
+ attackScenario: "An attacker could inject SQL through unescaped user input to read, modify, or delete database data.",
13644
+ suggestedFix: 'Use $wpdb->prepare() with placeholders: $wpdb->query($wpdb->prepare("SELECT * FROM table WHERE id = %d", $id))'
13645
+ },
13646
+ {
13647
+ regex: /\beval\s*\(\s*\$/,
13648
+ title: "PHP eval() with variable input",
13649
+ vulnClass: "code-injection",
13650
+ baseSeverity: "high",
13651
+ description: "eval() executes arbitrary PHP code from a variable \u2014 potential RCE.",
13652
+ attackScenario: "An attacker could inject malicious PHP code if user input reaches eval().",
13653
+ suggestedFix: "Remove eval() entirely. Use safe alternatives like json_decode() for data or specific parsers."
13654
+ },
13655
+ {
13656
+ regex: /\b(?:system|exec|shell_exec|passthru)\s*\(\s*\$/,
13657
+ title: "PHP command injection via system/exec/shell_exec/passthru",
13658
+ vulnClass: "shell-injection",
13659
+ baseSeverity: "high",
13660
+ description: "Shell command execution with a variable argument \u2014 potential command injection.",
13661
+ attackScenario: "An attacker could inject shell metacharacters to execute arbitrary commands on the server.",
13662
+ suggestedFix: "Use escapeshellarg() and escapeshellcmd() to sanitize input, or avoid shell commands entirely."
13663
+ },
13664
+ {
13665
+ regex: /preg_replace\s*\(\s*['"]\/[^'"]*\/e['"]/,
13666
+ title: "PHP preg_replace with /e modifier (code execution)",
13667
+ vulnClass: "code-injection",
13668
+ baseSeverity: "high",
13669
+ description: "preg_replace() with the /e modifier evaluates the replacement as PHP code \u2014 deprecated and dangerous.",
13670
+ attackScenario: "An attacker could inject PHP code through the matched string to achieve remote code execution.",
13671
+ suggestedFix: "Use preg_replace_callback() instead of the /e modifier."
13672
+ },
13673
+ {
13674
+ regex: /\bunserialize\s*\(\s*\$(?:_GET|_POST|_REQUEST|_COOKIE)/,
13675
+ title: "PHP insecure deserialization of user input",
13676
+ vulnClass: "code-injection",
13677
+ baseSeverity: "high",
13678
+ description: "unserialize() called on user-controlled superglobal input \u2014 potential RCE via PHP object injection.",
13679
+ attackScenario: "An attacker could craft a malicious serialized PHP object to achieve remote code execution.",
13680
+ suggestedFix: "Use json_decode() instead of unserialize() for user input. If unserialize is necessary, use the allowed_classes option."
13681
+ },
13682
+ {
13683
+ regex: /\bextract\s*\(\s*\$(?:_GET|_POST|_REQUEST)/,
13684
+ title: "PHP extract() on superglobal input",
13685
+ vulnClass: "code-injection",
13686
+ baseSeverity: "high",
13687
+ description: "extract() on $_GET/$_POST/$_REQUEST overwrites local variables \u2014 can bypass security checks.",
13688
+ attackScenario: "An attacker could set arbitrary variables by crafting request parameters, potentially overwriting auth flags or config values.",
13689
+ suggestedFix: "Avoid extract() on user input. Access superglobals directly or use a whitelist of expected keys."
12874
13690
  }
12875
13691
  ];
12876
13692
  function shouldSkip(filePath) {
@@ -12887,7 +13703,7 @@ async function checkInjection(files, projectRoot) {
12887
13703
  if (shouldSkip(file.filePath) || isTestFile4(file.filePath)) continue;
12888
13704
  let content;
12889
13705
  try {
12890
- content = readFileSync17(join21(projectRoot, file.filePath), "utf-8");
13706
+ content = readFileSync18(join22(projectRoot, file.filePath), "utf-8");
12891
13707
  } catch {
12892
13708
  continue;
12893
13709
  }
@@ -12925,8 +13741,8 @@ async function checkInjection(files, projectRoot) {
12925
13741
  }
12926
13742
 
12927
13743
  // src/security/checks/secrets.ts
12928
- import { readFileSync as readFileSync18 } from "fs";
12929
- import { join as join22 } from "path";
13744
+ import { readFileSync as readFileSync19 } from "fs";
13745
+ import { join as join23 } from "path";
12930
13746
  var SKIP_DIRS2 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
12931
13747
  var TEST_PATTERNS2 = ["test", "spec", "fixture", "mock", "__tests__", "__mocks__", ".example", ".sample"];
12932
13748
  var SECRET_PATTERNS = [
@@ -12959,7 +13775,7 @@ async function checkSecrets(files, projectRoot) {
12959
13775
  if (shouldSkip2(file.filePath) || isTestFile5(file.filePath)) continue;
12960
13776
  let content;
12961
13777
  try {
12962
- content = readFileSync18(join22(projectRoot, file.filePath), "utf-8");
13778
+ content = readFileSync19(join23(projectRoot, file.filePath), "utf-8");
12963
13779
  } catch {
12964
13780
  continue;
12965
13781
  }
@@ -12992,8 +13808,8 @@ async function checkSecrets(files, projectRoot) {
12992
13808
  }
12993
13809
 
12994
13810
  // src/security/checks/path-traversal.ts
12995
- import { readFileSync as readFileSync19 } from "fs";
12996
- import { join as join23 } from "path";
13811
+ import { readFileSync as readFileSync20 } from "fs";
13812
+ import { join as join24 } from "path";
12997
13813
  var SKIP_DIRS3 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
12998
13814
  var USER_INPUT_VARS = /(?:req\.|params|query|body|input|path|dir|subdirectory|file|userInput|fileName|filePath)/i;
12999
13815
  var PATTERNS2 = [
@@ -13040,7 +13856,7 @@ async function checkPathTraversal(files, projectRoot) {
13040
13856
  if (shouldSkip3(file.filePath)) continue;
13041
13857
  let content;
13042
13858
  try {
13043
- content = readFileSync19(join23(projectRoot, file.filePath), "utf-8");
13859
+ content = readFileSync20(join24(projectRoot, file.filePath), "utf-8");
13044
13860
  } catch {
13045
13861
  continue;
13046
13862
  }
@@ -13082,8 +13898,8 @@ async function checkPathTraversal(files, projectRoot) {
13082
13898
  }
13083
13899
 
13084
13900
  // src/security/checks/auth.ts
13085
- import { readFileSync as readFileSync20 } from "fs";
13086
- import { join as join24 } from "path";
13901
+ import { readFileSync as readFileSync21 } from "fs";
13902
+ import { join as join25 } from "path";
13087
13903
  var SKIP_DIRS4 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
13088
13904
  function shouldSkip4(filePath) {
13089
13905
  return SKIP_DIRS4.some((d) => filePath.includes(d));
@@ -13099,7 +13915,7 @@ async function checkAuth(files, projectRoot) {
13099
13915
  if (shouldSkip4(file.filePath)) continue;
13100
13916
  let content;
13101
13917
  try {
13102
- content = readFileSync20(join24(projectRoot, file.filePath), "utf-8");
13918
+ content = readFileSync21(join25(projectRoot, file.filePath), "utf-8");
13103
13919
  } catch {
13104
13920
  continue;
13105
13921
  }
@@ -13190,8 +14006,8 @@ async function checkAuth(files, projectRoot) {
13190
14006
  }
13191
14007
 
13192
14008
  // src/security/checks/input-validation.ts
13193
- import { readFileSync as readFileSync21 } from "fs";
13194
- import { join as join25 } from "path";
14009
+ import { readFileSync as readFileSync22 } from "fs";
14010
+ import { join as join26 } from "path";
13195
14011
  var SKIP_DIRS5 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
13196
14012
  function shouldSkip5(filePath) {
13197
14013
  return SKIP_DIRS5.some((d) => filePath.includes(d));
@@ -13203,7 +14019,7 @@ async function checkInputValidation(files, projectRoot) {
13203
14019
  if (shouldSkip5(file.filePath)) continue;
13204
14020
  let content;
13205
14021
  try {
13206
- content = readFileSync21(join25(projectRoot, file.filePath), "utf-8");
14022
+ content = readFileSync22(join26(projectRoot, file.filePath), "utf-8");
13207
14023
  } catch {
13208
14024
  continue;
13209
14025
  }
@@ -13280,8 +14096,8 @@ async function checkInputValidation(files, projectRoot) {
13280
14096
  }
13281
14097
 
13282
14098
  // src/security/checks/information-disclosure.ts
13283
- import { readFileSync as readFileSync22 } from "fs";
13284
- import { join as join26 } from "path";
14099
+ import { readFileSync as readFileSync23 } from "fs";
14100
+ import { join as join27 } from "path";
13285
14101
  var SKIP_DIRS6 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
13286
14102
  function shouldSkip6(filePath) {
13287
14103
  return SKIP_DIRS6.some((d) => filePath.includes(d));
@@ -13293,7 +14109,7 @@ async function checkInformationDisclosure(files, projectRoot) {
13293
14109
  if (shouldSkip6(file.filePath)) continue;
13294
14110
  let content;
13295
14111
  try {
13296
- content = readFileSync22(join26(projectRoot, file.filePath), "utf-8");
14112
+ content = readFileSync23(join27(projectRoot, file.filePath), "utf-8");
13297
14113
  } catch {
13298
14114
  continue;
13299
14115
  }
@@ -13363,8 +14179,8 @@ async function checkInformationDisclosure(files, projectRoot) {
13363
14179
  }
13364
14180
 
13365
14181
  // src/security/checks/cryptography.ts
13366
- import { readFileSync as readFileSync23 } from "fs";
13367
- import { join as join27 } from "path";
14182
+ import { readFileSync as readFileSync24 } from "fs";
14183
+ import { join as join28 } from "path";
13368
14184
  var SKIP_DIRS7 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
13369
14185
  var USER_INPUT_NAMES2 = /(?:input|user|name|path|query|param|request|body|args|url)/i;
13370
14186
  function shouldSkip7(filePath) {
@@ -13381,7 +14197,7 @@ async function checkCryptography(files, projectRoot) {
13381
14197
  if (shouldSkip7(file.filePath)) continue;
13382
14198
  let content;
13383
14199
  try {
13384
- content = readFileSync23(join27(projectRoot, file.filePath), "utf-8");
14200
+ content = readFileSync24(join28(projectRoot, file.filePath), "utf-8");
13385
14201
  } catch {
13386
14202
  continue;
13387
14203
  }
@@ -13561,6 +14377,84 @@ async function checkCryptography(files, projectRoot) {
13561
14377
  suggestedFix: "Use HTTPS for all external URLs to ensure data confidentiality and integrity."
13562
14378
  });
13563
14379
  }
14380
+ if (/\bmd5\s*\(/.test(line) && /password|passwd|pass|pwd/i.test(line)) {
14381
+ findings.push({
14382
+ id: "",
14383
+ severity: "high",
14384
+ vulnerabilityClass: "cryptography",
14385
+ file: file.filePath,
14386
+ line: i + 1,
14387
+ title: "PHP md5() used for password hashing",
14388
+ description: "md5() is cryptographically broken and should never be used for password hashing.",
14389
+ attackScenario: "An attacker could crack MD5 password hashes in seconds using rainbow tables or GPU brute force.",
14390
+ suggestedFix: "Use password_hash() with PASSWORD_BCRYPT or PASSWORD_ARGON2ID."
14391
+ });
14392
+ }
14393
+ if (/\bsha1\s*\(/.test(line) && /password|passwd|pass|pwd/i.test(line)) {
14394
+ findings.push({
14395
+ id: "",
14396
+ severity: "high",
14397
+ vulnerabilityClass: "cryptography",
14398
+ file: file.filePath,
14399
+ line: i + 1,
14400
+ title: "PHP sha1() used for password hashing",
14401
+ description: "SHA-1 has known collision attacks and should not be used for password hashing.",
14402
+ attackScenario: "An attacker could crack SHA-1 password hashes using precomputed tables.",
14403
+ suggestedFix: "Use password_hash() with PASSWORD_BCRYPT or PASSWORD_ARGON2ID."
14404
+ });
14405
+ }
14406
+ if (/\bcrypt\s*\(\s*[^,]+,\s*['"][\$]?[12a-zA-Z]{0,3}['"]/.test(line)) {
14407
+ findings.push({
14408
+ id: "",
14409
+ severity: "medium",
14410
+ vulnerabilityClass: "cryptography",
14411
+ file: file.filePath,
14412
+ line: i + 1,
14413
+ title: "PHP crypt() with potentially weak salt",
14414
+ description: "crypt() with a short or weak salt may use DES or MD5 algorithm.",
14415
+ attackScenario: "An attacker could crack weakly-salted crypt() hashes using brute force.",
14416
+ suggestedFix: "Use password_hash() instead of crypt(). It automatically uses a strong algorithm and salt."
14417
+ });
14418
+ }
14419
+ if (/\bmcrypt_/.test(line)) {
14420
+ findings.push({
14421
+ id: "",
14422
+ severity: "high",
14423
+ vulnerabilityClass: "cryptography",
14424
+ file: file.filePath,
14425
+ line: i + 1,
14426
+ title: "PHP deprecated mcrypt_* function",
14427
+ description: "mcrypt extension was deprecated in PHP 7.1 and removed in PHP 7.2. It has known vulnerabilities.",
14428
+ attackScenario: "An attacker could exploit known weaknesses in mcrypt implementations.",
14429
+ suggestedFix: "Use openssl_encrypt()/openssl_decrypt() or the sodium extension (sodium_crypto_*)."
14430
+ });
14431
+ }
14432
+ if (/\b(?:rand|mt_rand)\s*\(/.test(line) && isCryptoFile) {
14433
+ findings.push({
14434
+ id: "",
14435
+ severity: "medium",
14436
+ vulnerabilityClass: "cryptography",
14437
+ file: file.filePath,
14438
+ line: i + 1,
14439
+ title: "PHP rand()/mt_rand() in security context",
14440
+ description: "rand() and mt_rand() are not cryptographically secure \u2014 their output can be predicted.",
14441
+ attackScenario: "An attacker could predict random values to forge tokens or bypass security checks.",
14442
+ suggestedFix: "Use random_bytes() or random_int() for cryptographic purposes."
14443
+ });
14444
+ }
14445
+ if (/\$(?:password|secret|api_?key|token)\s*=\s*['"][^'"]{4,}['"]/i.test(line)) {
14446
+ findings.push({
14447
+ id: "",
14448
+ severity: "high",
14449
+ vulnerabilityClass: "cryptography",
14450
+ file: file.filePath,
14451
+ line: i + 1,
14452
+ title: "Hardcoded credentials in PHP source",
14453
+ description: "A password, secret, or API key is hardcoded as a string literal.",
14454
+ attackScenario: "An attacker with access to the source could extract the credential.",
14455
+ suggestedFix: "Load credentials from environment variables using getenv() or $_ENV."
14456
+ });
14457
+ }
13564
14458
  }
13565
14459
  }
13566
14460
  } catch {
@@ -13569,8 +14463,8 @@ async function checkCryptography(files, projectRoot) {
13569
14463
  }
13570
14464
 
13571
14465
  // src/security/checks/frontend.ts
13572
- import { readFileSync as readFileSync24 } from "fs";
13573
- import { join as join28 } from "path";
14466
+ import { readFileSync as readFileSync25 } from "fs";
14467
+ import { join as join29 } from "path";
13574
14468
  var SKIP_DIRS8 = ["node_modules/", "dist/", ".git/", ".wrangler/", "src/security/checks/"];
13575
14469
  function shouldSkip8(filePath) {
13576
14470
  return SKIP_DIRS8.some((d) => filePath.includes(d));
@@ -13586,7 +14480,7 @@ async function checkFrontend(files, projectRoot) {
13586
14480
  if (!isFrontendFile(file.filePath)) continue;
13587
14481
  let content;
13588
14482
  try {
13589
- content = readFileSync24(join28(projectRoot, file.filePath), "utf-8");
14483
+ content = readFileSync25(join29(projectRoot, file.filePath), "utf-8");
13590
14484
  } catch {
13591
14485
  continue;
13592
14486
  }
@@ -14046,13 +14940,13 @@ async function scanSecurity(projectRoot, graph, options = {}) {
14046
14940
  };
14047
14941
  }
14048
14942
  function detectPackageManager(projectRoot) {
14049
- if (existsSync18(join29(projectRoot, "package.json"))) return "npm";
14050
- if (existsSync18(join29(projectRoot, "requirements.txt"))) return "pip";
14051
- if (existsSync18(join29(projectRoot, "pyproject.toml"))) return "pip";
14052
- if (existsSync18(join29(projectRoot, "Cargo.toml"))) return "cargo";
14053
- if (existsSync18(join29(projectRoot, "go.mod"))) return "go";
14054
- if (existsSync18(join29(projectRoot, "pom.xml"))) return "maven";
14055
- if (existsSync18(join29(projectRoot, "build.gradle")) || existsSync18(join29(projectRoot, "build.gradle.kts"))) return "gradle";
14943
+ if (existsSync19(join30(projectRoot, "package.json"))) return "npm";
14944
+ if (existsSync19(join30(projectRoot, "requirements.txt"))) return "pip";
14945
+ if (existsSync19(join30(projectRoot, "pyproject.toml"))) return "pip";
14946
+ if (existsSync19(join30(projectRoot, "Cargo.toml"))) return "cargo";
14947
+ if (existsSync19(join30(projectRoot, "go.mod"))) return "go";
14948
+ if (existsSync19(join30(projectRoot, "pom.xml"))) return "maven";
14949
+ if (existsSync19(join30(projectRoot, "build.gradle")) || existsSync19(join30(projectRoot, "build.gradle.kts"))) return "gradle";
14056
14950
  return "unknown";
14057
14951
  }
14058
14952