@pipemd-core/pipemd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/AI_SETUP_PIPEMD.md +184 -0
  2. package/CHANGELOG.md +47 -0
  3. package/LICENSE +15 -0
  4. package/README.md +535 -0
  5. package/dist/index.js +6647 -0
  6. package/dist/plugins/opencode-server.js +235 -0
  7. package/dist/plugins/opencode-tui.js +914 -0
  8. package/dist/templates/agent-decision-tree.md +113 -0
  9. package/dist/templates/static-rules.md +7 -0
  10. package/package.json +68 -0
  11. package/scripts/C-CPP/architecture/arch.sh +229 -0
  12. package/scripts/C-CPP/lib/limit.sh +146 -0
  13. package/scripts/C-CPP/project/class-diagram.sh +96 -0
  14. package/scripts/C-CPP/project/cmake-targets.sh +68 -0
  15. package/scripts/C-CPP/project/deps.sh +44 -0
  16. package/scripts/C-CPP/project/find-todos.sh +6 -0
  17. package/scripts/C-CPP/project/include-graph.sh +110 -0
  18. package/scripts/C-CPP/project/interfaces.sh +108 -0
  19. package/scripts/C-CPP/project/tree.sh +5 -0
  20. package/scripts/C-CPP/quality/lint.sh +14 -0
  21. package/scripts/C-CPP/quality/test-summary.sh +22 -0
  22. package/scripts/C-CPP/quality/type-check.sh +26 -0
  23. package/scripts/DevOps/architecture/arch.sh +186 -0
  24. package/scripts/DevOps/devops/aws-context.sh +34 -0
  25. package/scripts/DevOps/devops/docker-stats.sh +42 -0
  26. package/scripts/DevOps/devops/k8s-unhealthy.sh +41 -0
  27. package/scripts/DevOps/devops/tf-state.sh +65 -0
  28. package/scripts/DevOps/lib/limit.sh +143 -0
  29. package/scripts/Generic/architecture/arch.sh +570 -0
  30. package/scripts/Generic/lib/limit.sh +140 -0
  31. package/scripts/Go/architecture/arch.sh +79 -0
  32. package/scripts/Go/lib/limit.sh +142 -0
  33. package/scripts/Go/project/deps.sh +35 -0
  34. package/scripts/Go/project/find-todos.sh +6 -0
  35. package/scripts/Go/project/go-interfaces.sh +18 -0
  36. package/scripts/Go/project/go-packages.sh +28 -0
  37. package/scripts/Go/project/tree.sh +5 -0
  38. package/scripts/Go/quality/lint.sh +16 -0
  39. package/scripts/Go/quality/test-summary.sh +16 -0
  40. package/scripts/Go/quality/type-check.sh +16 -0
  41. package/scripts/Node-TypeScript/api/express-routes.sh +14 -0
  42. package/scripts/Node-TypeScript/api/nest-controllers.sh +18 -0
  43. package/scripts/Node-TypeScript/architecture/arch.sh +174 -0
  44. package/scripts/Node-TypeScript/frontend/angular-routes.sh +15 -0
  45. package/scripts/Node-TypeScript/frontend/nextjs-app-router.sh +13 -0
  46. package/scripts/Node-TypeScript/frontend/react-components.sh +20 -0
  47. package/scripts/Node-TypeScript/lib/limit.sh +146 -0
  48. package/scripts/Node-TypeScript/project/deps.sh +15 -0
  49. package/scripts/Node-TypeScript/project/find-todos.sh +6 -0
  50. package/scripts/Node-TypeScript/quality/lint.sh +10 -0
  51. package/scripts/Node-TypeScript/quality/test-summary.sh +39 -0
  52. package/scripts/Node-TypeScript/quality/type-check.sh +10 -0
  53. package/scripts/Python/api/fastapi-routes.sh +12 -0
  54. package/scripts/Python/architecture/arch.sh +220 -0
  55. package/scripts/Python/db/django-models.sh +12 -0
  56. package/scripts/Python/db/sqlalchemy.sh +17 -0
  57. package/scripts/Python/lib/limit.sh +144 -0
  58. package/scripts/Python/project/deps.sh +28 -0
  59. package/scripts/Python/project/find-todos.sh +6 -0
  60. package/scripts/Python/quality/lint.sh +13 -0
  61. package/scripts/Python/quality/test-summary.sh +11 -0
  62. package/scripts/Python/quality/type-check.sh +10 -0
  63. package/scripts/Rust/architecture/arch.sh +176 -0
  64. package/scripts/Rust/lib/limit.sh +142 -0
  65. package/scripts/Rust/project/cargo-deps.sh +42 -0
  66. package/scripts/Rust/project/cargo-features.sh +26 -0
  67. package/scripts/Rust/project/find-todos.sh +6 -0
  68. package/scripts/Rust/project/tree.sh +5 -0
  69. package/scripts/Rust/quality/lint.sh +16 -0
  70. package/scripts/Rust/quality/test-summary.sh +16 -0
  71. package/scripts/Rust/quality/type-check.sh +16 -0
  72. package/scripts/Shared/api/express-routes.sh +11 -0
  73. package/scripts/Shared/api/fastapi-routes.sh +10 -0
  74. package/scripts/Shared/api/nest-controllers.sh +22 -0
  75. package/scripts/Shared/architecture/normalize.sh +178 -0
  76. package/scripts/Shared/crew/crew.sh +15 -0
  77. package/scripts/Shared/db/django-models.sh +11 -0
  78. package/scripts/Shared/db/prisma.sh +33 -0
  79. package/scripts/Shared/db/sqlalchemy.sh +12 -0
  80. package/scripts/Shared/frontend/angular-routes.sh +11 -0
  81. package/scripts/Shared/frontend/nextjs-app-router.sh +13 -0
  82. package/scripts/Shared/frontend/react-components.sh +11 -0
  83. package/scripts/Shared/git/diff-stat.sh +6 -0
  84. package/scripts/Shared/git/git-branch.sh +16 -0
  85. package/scripts/Shared/git/git-log.sh +6 -0
  86. package/scripts/Shared/git/git-status.sh +6 -0
  87. package/scripts/Shared/lib/limit.sh +144 -0
  88. package/scripts/Shared/project/compose-md.sh +182 -0
  89. package/scripts/Shared/project/deps.sh +69 -0
  90. package/scripts/Shared/project/find-todos.sh +6 -0
  91. package/scripts/Shared/project/tree.sh +5 -0
  92. package/scripts/Shared/quality/lint.sh +81 -0
  93. package/scripts/Shared/quality/test-summary.sh +103 -0
  94. package/scripts/Shared/quality/type-check.sh +114 -0
  95. package/scripts/copy-plugins.mjs +4 -0
  96. package/scripts/copy-templates.mjs +5 -0
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Architecture map — Python module dependencies
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+
6
+ : "${MAX_ARCH:=100}"
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ NORMALIZE="$SCRIPT_DIR/normalize.sh"
10
+ [ -f "$NORMALIZE" ] || NORMALIZE="$SCRIPT_DIR/../../Shared/architecture/normalize.sh"
11
+
12
+ if ! command -v python3 &>/dev/null; then
13
+ echo "python3 is required for Python architecture extraction"
14
+ exit 0
15
+ fi
16
+
17
+ SRC_DIR=""
18
+ for dir in src app lib .; do
19
+ if [ -d "$dir" ]; then
20
+ has_py=$(find "$dir" -maxdepth 3 -name "*.py" -not -path "*/__pycache__/*" -print -quit 2>/dev/null)
21
+ if [ -n "$has_py" ]; then
22
+ SRC_DIR="$dir"
23
+ break
24
+ fi
25
+ fi
26
+ done
27
+ [ -z "$SRC_DIR" ] && { echo "No source directory found"; exit 0; }
28
+ SRC_DIR="$(cd "$SRC_DIR" && pwd)"
29
+
30
+ SRC_DIR_ABS="$SRC_DIR" python3 -c "
31
+ import os, re, ast, sys
32
+ from collections import defaultdict
33
+
34
+ SRC_DIR = os.environ.get('SRC_DIR_ABS', 'src')
35
+ MAX_MODULES = 40
36
+
37
+ SKIP_DIRS = {'__pycache__', 'venv', '.venv', '.mypy_cache', '.tox', 'dist', 'build',
38
+ '.eggs', '.pytest_cache', 'node_modules', '.git', '.pipemd', 'migrations'}
39
+
40
+ def module_name(rel_path):
41
+ rel_path = rel_path.replace(os.sep, '/')
42
+ parts = rel_path.split('/')
43
+ filename = os.path.splitext(parts[-1])[0] if parts else ''
44
+ dirpath = parts[:-1] if len(parts) > 1 else []
45
+
46
+ if filename == '__init__':
47
+ return '/'.join(dirpath) if dirpath else None
48
+ if not dirpath:
49
+ return filename
50
+ return '/'.join(dirpath) + '/' + filename
51
+
52
+ def collect_pyproject_deps():
53
+ deps = set()
54
+ for pkgfile in ('requirements.txt', 'pyproject.toml', 'Pipfile', 'setup.py', 'setup.cfg'):
55
+ try:
56
+ with open(pkgfile, 'r', errors='replace') as f:
57
+ content = f.read()
58
+ except Exception:
59
+ continue
60
+
61
+ if pkgfile == 'requirements.txt':
62
+ for line in content.split('\n'):
63
+ line = line.strip()
64
+ if not line or line.startswith('#') or line.startswith('-'):
65
+ continue
66
+ name = re.split(r'[<>=!;\[]', line)[0].strip().lower()
67
+ if name:
68
+ deps.add(name)
69
+
70
+ elif pkgfile == 'pyproject.toml':
71
+ m = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL)
72
+ if m:
73
+ for item in re.findall(r'[\"\'](.*?)[\"\']', m.group(1)):
74
+ name = re.split(r'[<>=!;\[]', item)[0].strip().lower()
75
+ if name:
76
+ deps.add(name)
77
+ m2 = re.search(r'\[project\.dependencies\](.*?)(?=\[|\Z)', content, re.DOTALL)
78
+ if m2:
79
+ for line in m2.group(1).strip().split('\n'):
80
+ line = line.strip()
81
+ if line and line[0].isalpha():
82
+ name = re.split(r'[<>=!;\[]', line)[0].strip().lower()
83
+ if name:
84
+ deps.add(name)
85
+
86
+ elif pkgfile == 'Pipfile':
87
+ in_packages = False
88
+ for line in content.split('\n'):
89
+ stripped = line.strip()
90
+ if stripped == '[packages]':
91
+ in_packages = True
92
+ continue
93
+ if stripped.startswith('['):
94
+ in_packages = False
95
+ continue
96
+ if in_packages and stripped and stripped[0].isalpha():
97
+ name = re.split(r'[=<>#\s]', stripped)[0].strip().lower()
98
+ if name:
99
+ deps.add(name)
100
+
101
+ elif pkgfile in ('setup.py', 'setup.cfg'):
102
+ for m_pat in re.finditer(r'(?:install_requires|dependencies)\s*=\s*\[(.*?)\]', content, re.DOTALL):
103
+ for item in re.findall(r'[\"\'](.*?)[\"\']', m_pat.group(1)):
104
+ name = re.split(r'[<>=!;\[]', item)[0].strip().lower()
105
+ if name:
106
+ deps.add(name)
107
+
108
+ return deps
109
+
110
+ ext_deps = collect_pyproject_deps()
111
+
112
+ files = []
113
+ for root, dirs, fnames in os.walk(SRC_DIR):
114
+ dirs[:] = [d for d in sorted(dirs) if d not in SKIP_DIRS and not d.startswith('.')]
115
+ for fn in sorted(fnames):
116
+ if fn.endswith('.py') and not fn.startswith('.'):
117
+ files.append(os.path.join(root, fn))
118
+ if len(files) >= 300:
119
+ files = files[:300]
120
+ break
121
+
122
+ if not files:
123
+ sys.exit(0)
124
+
125
+ module_set = set()
126
+ for fpath in files:
127
+ rel = os.path.relpath(fpath, SRC_DIR).replace(os.sep, '/')
128
+ mod = module_name(rel)
129
+ if mod:
130
+ module_set.add(mod)
131
+
132
+ if len(module_set) > MAX_MODULES:
133
+ module_set = set()
134
+ for fpath in files:
135
+ rel = os.path.relpath(fpath, SRC_DIR).replace(os.sep, '/')
136
+ mod = module_name(rel)
137
+ if mod:
138
+ parts = mod.split('/')
139
+ module_set.add(parts[0] if len(parts) > 1 else parts[0])
140
+
141
+ def top_pkg(name):
142
+ return name.split('.')[0].split('/')[0].lower()
143
+
144
+ def resolve_internal(import_path):
145
+ check = import_path.replace('.', '/')
146
+ for mod in module_set:
147
+ if mod == check or mod.startswith(check + '/'):
148
+ return check.split('/')[0]
149
+ check_dir = os.path.join(SRC_DIR, check)
150
+ if os.path.isdir(check_dir):
151
+ return import_path.split('.')[0].split('/')[0].lower()
152
+ check_file = os.path.join(SRC_DIR, check + '.py')
153
+ if os.path.isfile(check_file):
154
+ return import_path.split('.')[0].split('/')[0].lower()
155
+ return None
156
+
157
+ edges = set()
158
+
159
+ for fpath in files:
160
+ rel = os.path.relpath(fpath, SRC_DIR).replace(os.sep, '/')
161
+ src_mod = module_name(rel)
162
+ if not src_mod or src_mod not in module_set:
163
+ continue
164
+ src_short = src_mod.split('/')[0] if '/' in src_mod else src_mod
165
+
166
+ try:
167
+ with open(fpath, 'r', errors='replace') as f:
168
+ content = f.read()
169
+ except Exception:
170
+ continue
171
+
172
+ try:
173
+ tree = ast.parse(content)
174
+ except SyntaxError:
175
+ for line in content.split('\n'):
176
+ line = line.strip()
177
+ if line.startswith('import ') or line.startswith('from '):
178
+ pass
179
+ continue
180
+
181
+ for node in ast.walk(tree):
182
+ if isinstance(node, ast.Import):
183
+ for alias in node.names:
184
+ pkg = alias.name.split('.')[0].lower()
185
+ if pkg in {m.split('/')[0].split('.')[0] for m in module_set if '/' not in m and '.' not in m} or \
186
+ pkg in {m.split('/')[0] for m in module_set if '/' in m}:
187
+ internal = resolve_internal(alias.name)
188
+ if internal and internal != src_short:
189
+ edges.add((src_short, internal))
190
+ elif pkg in ext_deps:
191
+ edges.add((src_short, 'ext:' + pkg))
192
+
193
+ if isinstance(node, ast.ImportFrom):
194
+ if node.level and node.level > 0:
195
+ file_dir = os.path.dirname(rel) if '/' in rel else ''
196
+ parts = file_dir.split('/') if file_dir else []
197
+ go_up = node.level - 1
198
+ if go_up > len(parts):
199
+ go_up = len(parts)
200
+ base_parts = parts[:len(parts) - go_up] if go_up > 0 else list(parts)
201
+ if node.module:
202
+ base_parts = base_parts + node.module.split('.')
203
+ target = '/'.join(base_parts) if base_parts else file_dir
204
+ if target:
205
+ target_short = target.split('/')[0] if '/' in target else target
206
+ if target_short and target_short != src_short:
207
+ edges.add((src_short, target_short))
208
+ elif node.module:
209
+ top = node.module.split('.')[0].lower()
210
+ if top in {m.split('/')[0].split('.')[0] for m in module_set if '/' not in m and '.' not in m} or \
211
+ top in {m.split('/')[0] for m in module_set if '/' in m}:
212
+ internal = resolve_internal(node.module)
213
+ if internal and internal != src_short:
214
+ edges.add((src_short, internal))
215
+ elif top in ext_deps:
216
+ edges.add((src_short, 'ext:' + top))
217
+
218
+ for s, d in sorted(edges):
219
+ sys.stdout.write(s + '\t' + d + '\n')
220
+ " | MAX_ARCH="$MAX_ARCH" bash "$NORMALIZE"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Django model metadata — model names and field summaries
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+ files=$(find . -name 'models.py' -not -path '*/venv/*' -not -path '*/.venv/*' -not -path '*/node_modules/*' -not -path '*/__pycache__/*' 2>/dev/null | head -10)
6
+ [ -z "$files" ] && echo "No Django models.py found" && exit 0
7
+
8
+ echo "$files" | while IFS= read -r f; do
9
+ [ -z "$f" ] && continue
10
+ echo "=== $f ==="
11
+ awk '/^class [A-Z]/{gsub(/\(.*/,"",$0); print "Model: "$2; next} /^[[:space:]]+[a-z_]+ = models\./{gsub(/ = .*/,"",$1); print " "$1; next}' "$f" | head -"$MAX_DJANGO"
12
+ done | head -50
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # SQLAlchemy model metadata — model class names and table names
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+ files=$(find . -name '*.py' -not -path '*/venv/*' -not -path '*/.venv/*' -not -path '*/node_modules/*' -not -path '*/__pycache__/*' 2>/dev/null | head -20)
6
+ [ -z "$files" ] && echo "No Python files found" && exit 0
7
+
8
+ grep -rn --include='*.py' -E 'class [A-Z][A-Za-z0-9_]*\(.*Base.*\)|__tablename__' . 2>/dev/null \
9
+ | grep -v 'venv' | grep -v '.venv' | grep -v '__pycache__' \
10
+ | sed -E 's/.*:class ([A-Z][A-Za-z0-9_]*)\(.*/\1/' \
11
+ | grep -E '^[A-Z]' | sort -u | head -"$MAX_SQLALCHEMY"
12
+
13
+ tablenames=$(grep -rn --include='*.py' -E '__tablename__' . 2>/dev/null \
14
+ | grep -v 'venv' | grep -v '__pycache__' \
15
+ | sed -E "s/.*__tablename__[[:space:]]*=[[:space:]]*['\"]([^'\"]*)['\"].*/\1/" \
16
+ | sort -u | head -10)
17
+ [ -n "$tablenames" ] && echo "" && echo "Tables:" && echo "$tablenames"
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # limit.sh — Smart output limiter with compact fallbacks
4
+ # TOKEN profile multiplier (override via PMD_TOKEN_PROFILE env var)
5
+ PMD_TOKEN_PROFILE="${PMD_TOKEN_PROFILE:-medium}"
6
+ case "$PMD_TOKEN_PROFILE" in
7
+ low) MULT_NUM=1; MULT_DEN=2 ;;
8
+ medium) MULT_NUM=1; MULT_DEN=1 ;;
9
+ high) MULT_NUM=3; MULT_DEN=2 ;;
10
+ xhigh) MULT_NUM=2; MULT_DEN=1 ;;
11
+ unlimited) PMD_UNLIMITED=1 ;;
12
+ *) MULT_NUM=1; MULT_DEN=1 ;;
13
+ esac
14
+
15
+ # Token budget constants (override via PMD_MAX_* env vars, scaled by profile)
16
+ if [ "${PMD_UNLIMITED:-0}" = "1" ]; then
17
+ MAX_TREE=${PMD_MAX_TREE:-99999}
18
+ MAX_DEPS=${PMD_MAX_DEPS:-99999}
19
+ MAX_TODOS=${PMD_MAX_TODOS:-99999}
20
+ MAX_LOG=${PMD_MAX_LOG:-99999}
21
+ MAX_BRANCH=${PMD_MAX_BRANCH:-99999}
22
+ MAX_STATUS=${PMD_MAX_STATUS:-99999}
23
+ MAX_DIFF=${PMD_MAX_DIFF:-99999}
24
+ MAX_TYPECHECK=${PMD_MAX_TYPECHECK:-99999}
25
+ MAX_LINT=${PMD_MAX_LINT:-99999}
26
+ MAX_TEST=${PMD_MAX_TEST:-99999}
27
+ MAX_PRISMA=${PMD_MAX_PRISMA:-99999}
28
+ MAX_EXPRESS=${PMD_MAX_EXPRESS:-99999}
29
+ MAX_FASTAPI=${PMD_MAX_FASTAPI:-99999}
30
+ MAX_DJANGO=${PMD_MAX_DJANGO:-99999}
31
+ MAX_SQLALCHEMY=${PMD_MAX_SQLALCHEMY:-99999}
32
+ MAX_NEST=${PMD_MAX_NEST:-99999}
33
+ MAX_NEXTJS=${PMD_MAX_NEXTJS:-99999}
34
+ MAX_REACT=${PMD_MAX_REACT:-99999}
35
+ MAX_ANGULAR=${PMD_MAX_ANGULAR:-99999}
36
+ MAX_CMAKE=${PMD_MAX_CMAKE:-99999}
37
+ MAX_CLASS=${PMD_MAX_CLASS:-99999}
38
+ MAX_INTERFACE=${PMD_MAX_INTERFACE:-99999}
39
+ MAX_INCLUDE=${PMD_MAX_INCLUDE:-99999}
40
+ MAX_CARGO=${PMD_MAX_CARGO:-99999}
41
+ MAX_GO_PKGS=${PMD_MAX_GO_PKGS:-99999}
42
+ MAX_CARGO_FEATURES=${PMD_MAX_CARGO_FEATURES:-99999}
43
+ MAX_GO_INTERFACES=${PMD_MAX_GO_INTERFACES:-99999}
44
+ MAX_DOCKER=${PMD_MAX_DOCKER:-99999}
45
+ MAX_K8S=${PMD_MAX_K8S:-99999}
46
+ MAX_TF=${PMD_MAX_TF:-99999}
47
+ MAX_AWS=${PMD_MAX_AWS:-99999}
48
+ MAX_ARCH=${PMD_MAX_ARCH:-99999}
49
+ MAX_COMPOSE=${PMD_MAX_COMPOSE:-99999}
50
+ MAX_CREW=${PMD_MAX_CREW:-99999}
51
+ else
52
+ MAX_TREE=$(( (${PMD_MAX_TREE:-50} * MULT_NUM) / MULT_DEN ))
53
+ MAX_DEPS=$(( (${PMD_MAX_DEPS:-40} * MULT_NUM) / MULT_DEN ))
54
+ MAX_TODOS=$(( (${PMD_MAX_TODOS:-20} * MULT_NUM) / MULT_DEN ))
55
+ MAX_LOG=$(( (${PMD_MAX_LOG:-20} * MULT_NUM) / MULT_DEN ))
56
+ MAX_BRANCH=$(( (${PMD_MAX_BRANCH:-20} * MULT_NUM) / MULT_DEN ))
57
+ MAX_STATUS=$(( (${PMD_MAX_STATUS:-30} * MULT_NUM) / MULT_DEN ))
58
+ MAX_DIFF=$(( (${PMD_MAX_DIFF:-30} * MULT_NUM) / MULT_DEN ))
59
+ MAX_TYPECHECK=$(( (${PMD_MAX_TYPECHECK:-30} * MULT_NUM) / MULT_DEN ))
60
+ MAX_LINT=$(( (${PMD_MAX_LINT:-20} * MULT_NUM) / MULT_DEN ))
61
+ MAX_TEST=$(( (${PMD_MAX_TEST:-10} * MULT_NUM) / MULT_DEN ))
62
+ MAX_PRISMA=$(( (${PMD_MAX_PRISMA:-40} * MULT_NUM) / MULT_DEN ))
63
+ MAX_EXPRESS=$(( (${PMD_MAX_EXPRESS:-30} * MULT_NUM) / MULT_DEN ))
64
+ MAX_FASTAPI=$(( (${PMD_MAX_FASTAPI:-30} * MULT_NUM) / MULT_DEN ))
65
+ MAX_DJANGO=$(( (${PMD_MAX_DJANGO:-40} * MULT_NUM) / MULT_DEN ))
66
+ MAX_SQLALCHEMY=$(( (${PMD_MAX_SQLALCHEMY:-40} * MULT_NUM) / MULT_DEN ))
67
+ MAX_NEST=$(( (${PMD_MAX_NEST:-30} * MULT_NUM) / MULT_DEN ))
68
+ MAX_NEXTJS=$(( (${PMD_MAX_NEXTJS:-30} * MULT_NUM) / MULT_DEN ))
69
+ MAX_REACT=$(( (${PMD_MAX_REACT:-30} * MULT_NUM) / MULT_DEN ))
70
+ MAX_ANGULAR=$(( (${PMD_MAX_ANGULAR:-30} * MULT_NUM) / MULT_DEN ))
71
+ MAX_CMAKE=$(( (${PMD_MAX_CMAKE:-40} * MULT_NUM) / MULT_DEN ))
72
+ MAX_CLASS=$(( (${PMD_MAX_CLASS:-40} * MULT_NUM) / MULT_DEN ))
73
+ MAX_INTERFACE=$(( (${PMD_MAX_INTERFACE:-30} * MULT_NUM) / MULT_DEN ))
74
+ MAX_INCLUDE=$(( (${PMD_MAX_INCLUDE:-40} * MULT_NUM) / MULT_DEN ))
75
+ MAX_CARGO=$(( (${PMD_MAX_CARGO:-40} * MULT_NUM) / MULT_DEN ))
76
+ MAX_GO_PKGS=$(( (${PMD_MAX_GO_PKGS:-40} * MULT_NUM) / MULT_DEN ))
77
+ MAX_CARGO_FEATURES=$(( (${PMD_MAX_CARGO_FEATURES:-20} * MULT_NUM) / MULT_DEN ))
78
+ MAX_GO_INTERFACES=$(( (${PMD_MAX_GO_INTERFACES:-30} * MULT_NUM) / MULT_DEN ))
79
+ MAX_DOCKER=$(( (${PMD_MAX_DOCKER:-30} * MULT_NUM) / MULT_DEN ))
80
+ MAX_K8S=$(( (${PMD_MAX_K8S:-20} * MULT_NUM) / MULT_DEN ))
81
+ MAX_TF=$(( (${PMD_MAX_TF:-40} * MULT_NUM) / MULT_DEN ))
82
+ MAX_AWS=$(( (${PMD_MAX_AWS:-10} * MULT_NUM) / MULT_DEN ))
83
+ MAX_ARCH=$(( (${PMD_MAX_ARCH:-100} * MULT_NUM) / MULT_DEN ))
84
+ MAX_COMPOSE=$(( (${PMD_MAX_COMPOSE:-150} * MULT_NUM) / MULT_DEN ))
85
+ MAX_CREW=$(( (${PMD_MAX_CREW:-40} * MULT_NUM) / MULT_DEN ))
86
+ fi
87
+
88
+ TREE_EXCLUDES="${PMD_TREE_EXCLUDES:-__pycache__|.git|.pipemd|venv|.venv|*.egg-info|node_modules|dist|build}"
89
+
90
+ limit_output() {
91
+ local text="$1"
92
+ local max="${2:-25}"
93
+ local fallback="$3"
94
+ local lines
95
+ lines=$(echo "$text" | wc -l)
96
+ if [ "$lines" -le "$max" ]; then
97
+ echo "$text"
98
+ else
99
+ echo "$fallback"
100
+ fi
101
+ }
102
+
103
+ limit_tree() {
104
+ local max="${1:-$MAX_TREE}"
105
+ local excl="${TREE_EXCLUDES}"
106
+
107
+ if command -v tree &>/dev/null; then
108
+ local out3
109
+ out3=$(tree -L 3 -I "$excl" --dirsfirst 2>/dev/null)
110
+ local lines3=$(echo "$out3" | wc -l)
111
+
112
+ if [ "$lines3" -le "$max" ]; then
113
+ echo "$out3"
114
+ return
115
+ fi
116
+
117
+ local out2
118
+ out2=$(tree -L 2 -I "$excl" --dirsfirst 2>/dev/null)
119
+ local lines2=$(echo "$out2" | wc -l)
120
+
121
+ if [ "$lines2" -le "$max" ]; then
122
+ echo "$out2"
123
+ echo "(${lines3} lines at depth 3, showing depth 2)"
124
+ return
125
+ fi
126
+
127
+ local out1
128
+ out1=$(tree -L 1 -I "$excl" --dirsfirst 2>/dev/null)
129
+ echo "$out1"
130
+ echo "(${lines3} lines at depth 3, showing depth 1)"
131
+ else
132
+ echo "Project structure:"
133
+ find . -maxdepth 3 \
134
+ -not -path '*/__pycache__/*' \
135
+ -not -path '*/.git/*' \
136
+ -not -path '*/.pipemd/*' \
137
+ -not -path '*/venv/*' \
138
+ -not -path '*/.venv/*' \
139
+ -not -name '__pycache__' \
140
+ -not -name '.git' \
141
+ -not -name '.pipemd' \
142
+ 2>/dev/null | head -"$max" | sort
143
+ fi
144
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Direct dependencies — summarize if too many
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+
6
+ if [ -f requirements.txt ]; then
7
+ out=$(head -20 requirements.txt)
8
+ limit_output "$out" "$MAX_DEPS" "$(head -5 requirements.txt && echo "... and $(wc -l < requirements.txt) total lines")"
9
+ elif [ -f pyproject.toml ]; then
10
+ out=$(awk '
11
+ /^\[project\.dependencies\]/{in_deps=1;next}
12
+ /^\[/{in_deps=0}
13
+ in_deps && /^[a-zA-Z]/{
14
+ gsub(/#.*/,"")
15
+ gsub(/;.*$/,"")
16
+ gsub(/"/,"")
17
+ sub(/[<=>!].*/,"")
18
+ if(length>0) print
19
+ }
20
+ ' pyproject.toml 2>/dev/null)
21
+ if [ -n "$out" ]; then
22
+ limit_output "$out" "$MAX_DEPS" "$(echo "$out" | head -5 && echo '... more in pyproject.toml')"
23
+ else
24
+ echo "No dependencies found in pyproject.toml"
25
+ fi
26
+ else
27
+ echo "No recognized dependency file"
28
+ fi
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Find TODO, FIXME, HACK — Python files
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+ out=$(grep -rn 'TODO\|FIXME\|HACK' --include="*.py" . 2>/dev/null | grep -v '/__pycache__/' | grep -v '/.pipemd/' | grep -v '/venv/' | grep -v '/.venv/' | grep -v '/.git/')
6
+ limit_output "$out" "$MAX_TODOS" "$(echo "$out" | head -3 && echo "... and $(($(echo "$out" | wc -l) - 3)) more items")"
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Lint errors — ruff or flake8
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+ if command -v ruff &>/dev/null; then
6
+ out=$(ruff check . 2>&1)
7
+ limit_output "$out" "$MAX_LINT" "$(echo "$out" | head -3 && echo "... and $(echo "$out" | wc -l) more lint issues")"
8
+ elif command -v flake8 &>/dev/null; then
9
+ out=$(flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 2>&1)
10
+ limit_output "$out" "$MAX_LINT" "$(echo "$out" | head -3 && echo '... more flake8 errors')"
11
+ else
12
+ echo "No linter configured"
13
+ fi
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Test summary — actually run tests and show results
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+
6
+ if command -v pytest &>/dev/null || [ -f pytest.ini ] || [ -f conftest.py ] || [ -f pyproject.toml ]; then
7
+ out=$(python -m pytest --tb=no -q 2>&1 | tail -5)
8
+ echo "$out"
9
+ else
10
+ echo "No test runner configured for this ecosystem"
11
+ fi
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Python type check (mypy) — errors only
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+ out=$(python -m mypy . 2>&1)
6
+ if [ -z "$out" ]; then
7
+ echo "No type errors"
8
+ exit 0
9
+ fi
10
+ limit_output "$out" "$MAX_TYPECHECK" "$(echo "$out" | head -5 && echo '... more mypy errors')"
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+ # Architecture map — Rust module dependencies
4
+ source "$(dirname "$0")/../lib/limit.sh"
5
+
6
+ : "${MAX_ARCH:=100}"
7
+
8
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
+ NORMALIZE="$SCRIPT_DIR/normalize.sh"
10
+ [ -f "$NORMALIZE" ] || NORMALIZE="$SCRIPT_DIR/../../Shared/architecture/normalize.sh"
11
+
12
+ if [ ! -f Cargo.toml ]; then
13
+ echo "No Cargo.toml found"
14
+ exit 0
15
+ fi
16
+
17
+ if ! command -v python3 &>/dev/null; then
18
+ echo "python3 is required for Rust architecture extraction"
19
+ exit 0
20
+ fi
21
+
22
+ SRC_DIR=""
23
+ [ -d "src" ] && SRC_DIR="src"
24
+
25
+ if [ -z "$SRC_DIR" ]; then
26
+ echo "No source directory found"
27
+ exit 0
28
+ fi
29
+
30
+ SRC_DIR="$SRC_DIR" python3 -c "
31
+ import os, re, sys
32
+ from collections import defaultdict
33
+
34
+ SRC_DIR = os.environ.get('SRC_DIR', 'src')
35
+ MAX_MODULES = 40
36
+
37
+ def module_name(rel_path):
38
+ rel_path = rel_path.replace(os.sep, '/')
39
+ parts = rel_path.split('/')
40
+ filename = os.path.splitext(parts[-1])[0] if parts else ''
41
+ dirpath = parts[:-1] if len(parts) > 1 else []
42
+
43
+ if not dirpath:
44
+ return filename
45
+
46
+ if len(dirpath) == 1 and dirpath[0] == SRC_DIR:
47
+ return filename if filename in ('main', 'lib') else filename
48
+
49
+ last_dir = dirpath[-1]
50
+
51
+ if filename == 'mod' or filename == 'lib':
52
+ return last_dir
53
+
54
+ if filename == last_dir:
55
+ return last_dir
56
+
57
+ return last_dir + '/' + filename
58
+
59
+ def collect_cargo_deps():
60
+ deps = set()
61
+ try:
62
+ with open('Cargo.toml', 'r', errors='replace') as f:
63
+ content = f.read()
64
+ except Exception:
65
+ return deps
66
+
67
+ in_deps = False
68
+ for line in content.split('\n'):
69
+ stripped = line.strip()
70
+ if stripped.startswith('['):
71
+ in_deps = stripped == '[dependencies]'
72
+ continue
73
+ if in_deps and stripped and not stripped.startswith('#'):
74
+ name = stripped.split('=')[0].strip()
75
+ name = re.split(r'[^a-zA-Z0-9_-]', name)[0]
76
+ if name:
77
+ deps.add(name.lower())
78
+ return deps
79
+
80
+ cargo_deps = collect_cargo_deps()
81
+
82
+ files = []
83
+ for root, dirs, fnames in os.walk(SRC_DIR):
84
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'target']
85
+ for fn in fnames:
86
+ if fn.endswith('.rs'):
87
+ files.append(os.path.join(root, fn))
88
+ if len(files) >= 200:
89
+ files = files[:200]
90
+ break
91
+
92
+ if not files:
93
+ sys.exit(0)
94
+
95
+ module_set = set()
96
+ for fpath in files:
97
+ rel = os.path.relpath(fpath, '.').replace(os.sep, '/')
98
+ mod = module_name(rel)
99
+ if mod:
100
+ module_set.add(mod)
101
+
102
+ if len(module_set) > MAX_MODULES:
103
+ module_set = set()
104
+ for fpath in files:
105
+ rel = os.path.relpath(fpath, '.').replace(os.sep, '/')
106
+ parts = rel.split('/')
107
+ if len(parts) > 2:
108
+ module_set.add(parts[1])
109
+ elif len(parts) == 2:
110
+ module_set.add(parts[1].replace('.rs', ''))
111
+ else:
112
+ module_set.add(os.path.splitext(parts[0])[0])
113
+
114
+ edges = set()
115
+
116
+ use_re = re.compile(r'^\s*use\s+(.+?);', re.MULTILINE)
117
+ mod_re = re.compile(r'^\s*mod\s+([a-zA-Z_]\w*)\s*;', re.MULTILINE)
118
+
119
+ for fpath in files:
120
+ rel = os.path.relpath(fpath, '.').replace(os.sep, '/')
121
+ src_mod = module_name(rel)
122
+ if not src_mod or src_mod not in module_set:
123
+ continue
124
+
125
+ try:
126
+ with open(fpath, 'r', errors='replace') as f:
127
+ content = f.read()
128
+ except Exception:
129
+ continue
130
+
131
+ for m in mod_re.finditer(content):
132
+ mod_name = m.group(1)
133
+ if mod_name in module_set and mod_name != src_mod:
134
+ edges.add((src_mod, mod_name))
135
+
136
+ for m in use_re.finditer(content):
137
+ path = m.group(1).strip()
138
+ if path.startswith('crate::'):
139
+ crate_path = path[7:]
140
+ first = crate_path.split('::')[0].split('{')[0].strip()
141
+ first = first.replace(',', '').strip()
142
+ if first in module_set and first != src_mod:
143
+ edges.add((src_mod, first))
144
+ elif first and first != src_mod:
145
+ for part in crate_path.split('{'):
146
+ if '}' in part:
147
+ for sub in part.replace('}', '').split(','):
148
+ sub = sub.strip().split('::')[0]
149
+ if sub in module_set and sub != src_mod:
150
+ edges.add((src_mod, sub))
151
+ elif path.startswith('super::'):
152
+ rest = path[7:]
153
+ first = rest.split('::')[0].split('{')[0].strip()
154
+ first = first.replace(',', '').strip()
155
+ fdir = os.path.dirname(rel)
156
+ if SRC_DIR and fdir.startswith(SRC_DIR + '/'):
157
+ fdir = fdir[len(SRC_DIR) + 1:]
158
+ elif fdir == SRC_DIR:
159
+ fdir = ''
160
+ parent_parts = fdir.split('/') if fdir else []
161
+ if parent_parts:
162
+ parent_mod = parent_parts[-1]
163
+ if parent_mod in module_set and parent_mod != src_mod:
164
+ edges.add((src_mod, parent_mod))
165
+ elif path.startswith('self::'):
166
+ pass
167
+ else:
168
+ crate_name = path.split('::')[0].split('{')[0].strip()
169
+ crate_name = crate_name.replace(',', '').strip()
170
+ if crate_name and crate_name not in ('crate', 'super', 'self'):
171
+ if crate_name in cargo_deps:
172
+ edges.add((src_mod, 'ext:' + crate_name))
173
+
174
+ for s, d in sorted(edges):
175
+ sys.stdout.write(s + '\t' + d + '\n')
176
+ " | MAX_ARCH="$MAX_ARCH" bash "$NORMALIZE"