chati-dev 1.4.0 → 2.0.2

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 (208) hide show
  1. package/README.md +40 -24
  2. package/framework/agents/build/dev.md +343 -0
  3. package/framework/agents/clarity/architect.md +112 -0
  4. package/framework/agents/clarity/brief.md +182 -0
  5. package/framework/agents/clarity/brownfield-wu.md +181 -0
  6. package/framework/agents/clarity/detail.md +110 -0
  7. package/framework/agents/clarity/greenfield-wu.md +153 -0
  8. package/framework/agents/clarity/ux.md +112 -0
  9. package/framework/config.yaml +3 -3
  10. package/framework/constitution.md +31 -1
  11. package/framework/context/governance.md +37 -0
  12. package/framework/context/protocols.md +34 -0
  13. package/framework/context/quality.md +27 -0
  14. package/framework/context/root.md +24 -0
  15. package/framework/data/entity-registry.yaml +1 -1
  16. package/framework/domains/agents/architect.yaml +51 -0
  17. package/framework/domains/agents/brief.yaml +47 -0
  18. package/framework/domains/agents/brownfield-wu.yaml +49 -0
  19. package/framework/domains/agents/detail.yaml +47 -0
  20. package/framework/domains/agents/dev.yaml +49 -0
  21. package/framework/domains/agents/devops.yaml +43 -0
  22. package/framework/domains/agents/greenfield-wu.yaml +47 -0
  23. package/framework/domains/agents/orchestrator.yaml +49 -0
  24. package/framework/domains/agents/phases.yaml +47 -0
  25. package/framework/domains/agents/qa-implementation.yaml +43 -0
  26. package/framework/domains/agents/qa-planning.yaml +44 -0
  27. package/framework/domains/agents/tasks.yaml +48 -0
  28. package/framework/domains/agents/ux.yaml +50 -0
  29. package/framework/domains/constitution.yaml +77 -0
  30. package/framework/domains/global.yaml +64 -0
  31. package/framework/domains/workflows/brownfield-discovery.yaml +16 -0
  32. package/framework/domains/workflows/brownfield-fullstack.yaml +26 -0
  33. package/framework/domains/workflows/brownfield-service.yaml +22 -0
  34. package/framework/domains/workflows/brownfield-ui.yaml +22 -0
  35. package/framework/domains/workflows/greenfield-fullstack.yaml +26 -0
  36. package/framework/hooks/constitution-guard.js +101 -0
  37. package/framework/hooks/mode-governance.js +92 -0
  38. package/framework/hooks/model-governance.js +76 -0
  39. package/framework/hooks/prism-engine.js +89 -0
  40. package/framework/hooks/session-digest.js +60 -0
  41. package/framework/hooks/settings.json +44 -0
  42. package/framework/i18n/en.yaml +3 -3
  43. package/framework/i18n/es.yaml +3 -3
  44. package/framework/i18n/fr.yaml +3 -3
  45. package/framework/i18n/pt.yaml +3 -3
  46. package/framework/intelligence/decision-engine.md +1 -1
  47. package/framework/migrations/v1.4-to-v2.0.yaml +167 -0
  48. package/framework/migrations/v2.0-to-v2.0.1.yaml +132 -0
  49. package/framework/orchestrator/chati.md +284 -6
  50. package/framework/tasks/architect-api-design.md +63 -0
  51. package/framework/tasks/architect-consolidate.md +47 -0
  52. package/framework/tasks/architect-db-design.md +73 -0
  53. package/framework/tasks/architect-design.md +95 -0
  54. package/framework/tasks/architect-security-review.md +62 -0
  55. package/framework/tasks/architect-stack-selection.md +53 -0
  56. package/framework/tasks/brief-consolidate.md +249 -0
  57. package/framework/tasks/brief-constraint-identify.md +277 -0
  58. package/framework/tasks/brief-extract-requirements.md +339 -0
  59. package/framework/tasks/brief-stakeholder-map.md +176 -0
  60. package/framework/tasks/brief-validate-completeness.md +121 -0
  61. package/framework/tasks/brownfield-wu-architecture-map.md +394 -0
  62. package/framework/tasks/brownfield-wu-deep-discovery.md +312 -0
  63. package/framework/tasks/brownfield-wu-dependency-scan.md +359 -0
  64. package/framework/tasks/brownfield-wu-migration-plan.md +483 -0
  65. package/framework/tasks/brownfield-wu-report.md +325 -0
  66. package/framework/tasks/brownfield-wu-risk-assess.md +424 -0
  67. package/framework/tasks/detail-acceptance-criteria.md +372 -0
  68. package/framework/tasks/detail-consolidate.md +138 -0
  69. package/framework/tasks/detail-edge-case-analysis.md +300 -0
  70. package/framework/tasks/detail-expand-prd.md +389 -0
  71. package/framework/tasks/detail-nfr-extraction.md +223 -0
  72. package/framework/tasks/dev-code-review.md +404 -0
  73. package/framework/tasks/dev-consolidate.md +543 -0
  74. package/framework/tasks/dev-debug.md +322 -0
  75. package/framework/tasks/dev-implement.md +252 -0
  76. package/framework/tasks/dev-iterate.md +411 -0
  77. package/framework/tasks/dev-pr-prepare.md +497 -0
  78. package/framework/tasks/dev-refactor.md +342 -0
  79. package/framework/tasks/dev-test-write.md +306 -0
  80. package/framework/tasks/devops-ci-setup.md +412 -0
  81. package/framework/tasks/devops-consolidate.md +712 -0
  82. package/framework/tasks/devops-deploy-config.md +598 -0
  83. package/framework/tasks/devops-monitoring-setup.md +658 -0
  84. package/framework/tasks/devops-release-prepare.md +673 -0
  85. package/framework/tasks/greenfield-wu-analyze-empty.md +169 -0
  86. package/framework/tasks/greenfield-wu-report.md +266 -0
  87. package/framework/tasks/greenfield-wu-scaffold-detection.md +203 -0
  88. package/framework/tasks/greenfield-wu-tech-stack-assess.md +255 -0
  89. package/framework/tasks/orchestrator-deviation.md +260 -0
  90. package/framework/tasks/orchestrator-escalate.md +276 -0
  91. package/framework/tasks/orchestrator-handoff.md +243 -0
  92. package/framework/tasks/orchestrator-health.md +372 -0
  93. package/framework/tasks/orchestrator-mode-switch.md +262 -0
  94. package/framework/tasks/orchestrator-resume.md +189 -0
  95. package/framework/tasks/orchestrator-route.md +169 -0
  96. package/framework/tasks/orchestrator-spawn-terminal.md +358 -0
  97. package/framework/tasks/orchestrator-status.md +260 -0
  98. package/framework/tasks/orchestrator-suggest-mode.md +372 -0
  99. package/framework/tasks/phases-breakdown.md +91 -0
  100. package/framework/tasks/phases-dependency-mapping.md +67 -0
  101. package/framework/tasks/phases-mvp-scoping.md +94 -0
  102. package/framework/tasks/qa-impl-consolidate.md +522 -0
  103. package/framework/tasks/qa-impl-performance-test.md +487 -0
  104. package/framework/tasks/qa-impl-regression-check.md +413 -0
  105. package/framework/tasks/qa-impl-sast-scan.md +402 -0
  106. package/framework/tasks/qa-impl-test-execute.md +344 -0
  107. package/framework/tasks/qa-impl-verdict.md +339 -0
  108. package/framework/tasks/qa-planning-consolidate.md +309 -0
  109. package/framework/tasks/qa-planning-coverage-plan.md +338 -0
  110. package/framework/tasks/qa-planning-gate-define.md +339 -0
  111. package/framework/tasks/qa-planning-risk-matrix.md +631 -0
  112. package/framework/tasks/qa-planning-test-strategy.md +217 -0
  113. package/framework/tasks/tasks-acceptance-write.md +75 -0
  114. package/framework/tasks/tasks-consolidate.md +57 -0
  115. package/framework/tasks/tasks-decompose.md +80 -0
  116. package/framework/tasks/tasks-estimate.md +66 -0
  117. package/framework/tasks/ux-a11y-check.md +49 -0
  118. package/framework/tasks/ux-component-map.md +55 -0
  119. package/framework/tasks/ux-consolidate.md +46 -0
  120. package/framework/tasks/ux-user-flow.md +46 -0
  121. package/framework/tasks/ux-wireframe.md +76 -0
  122. package/package.json +2 -2
  123. package/scripts/bundle-framework.js +2 -0
  124. package/scripts/changelog-generator.js +222 -0
  125. package/scripts/codebase-mapper.js +728 -0
  126. package/scripts/commit-message-generator.js +167 -0
  127. package/scripts/coverage-analyzer.js +260 -0
  128. package/scripts/dependency-analyzer.js +280 -0
  129. package/scripts/framework-analyzer.js +308 -0
  130. package/scripts/generate-constitution-domain.js +253 -0
  131. package/scripts/health-check.js +481 -0
  132. package/scripts/ide-sync.js +327 -0
  133. package/scripts/performance-analyzer.js +325 -0
  134. package/scripts/plan-tracker.js +278 -0
  135. package/scripts/populate-entity-registry.js +481 -0
  136. package/scripts/pr-review.js +317 -0
  137. package/scripts/rollback-manager.js +310 -0
  138. package/scripts/stuck-detector.js +343 -0
  139. package/scripts/test-quality-assessment.js +257 -0
  140. package/scripts/validate-agents.js +367 -0
  141. package/scripts/validate-tasks.js +465 -0
  142. package/src/autonomy/autonomous-gate.js +293 -0
  143. package/src/autonomy/index.js +51 -0
  144. package/src/autonomy/mode-manager.js +225 -0
  145. package/src/autonomy/mode-suggester.js +283 -0
  146. package/src/autonomy/progress-reporter.js +268 -0
  147. package/src/autonomy/safety-net.js +320 -0
  148. package/src/context/bracket-tracker.js +79 -0
  149. package/src/context/domain-loader.js +107 -0
  150. package/src/context/engine.js +144 -0
  151. package/src/context/formatter.js +184 -0
  152. package/src/context/index.js +4 -0
  153. package/src/context/layers/l0-constitution.js +28 -0
  154. package/src/context/layers/l1-global.js +37 -0
  155. package/src/context/layers/l2-agent.js +39 -0
  156. package/src/context/layers/l3-workflow.js +42 -0
  157. package/src/context/layers/l4-task.js +24 -0
  158. package/src/decision/analyzer.js +167 -0
  159. package/src/decision/engine.js +270 -0
  160. package/src/decision/index.js +38 -0
  161. package/src/decision/registry-healer.js +450 -0
  162. package/src/decision/registry-updater.js +330 -0
  163. package/src/gates/circuit-breaker.js +119 -0
  164. package/src/gates/g1-planning-complete.js +153 -0
  165. package/src/gates/g2-qa-planning.js +153 -0
  166. package/src/gates/g3-implementation.js +188 -0
  167. package/src/gates/g4-qa-implementation.js +207 -0
  168. package/src/gates/g5-deploy-ready.js +180 -0
  169. package/src/gates/gate-base.js +144 -0
  170. package/src/gates/index.js +46 -0
  171. package/src/installer/brownfield-upgrader.js +249 -0
  172. package/src/installer/core.js +82 -11
  173. package/src/installer/file-hasher.js +51 -0
  174. package/src/installer/manifest.js +117 -0
  175. package/src/installer/templates.js +17 -15
  176. package/src/installer/transaction.js +229 -0
  177. package/src/installer/validator.js +18 -1
  178. package/src/intelligence/registry-manager.js +2 -2
  179. package/src/memory/agent-memory.js +255 -0
  180. package/src/memory/gotchas-injector.js +72 -0
  181. package/src/memory/gotchas.js +361 -0
  182. package/src/memory/index.js +35 -0
  183. package/src/memory/search.js +233 -0
  184. package/src/memory/session-digest.js +239 -0
  185. package/src/merger/env-merger.js +112 -0
  186. package/src/merger/index.js +56 -0
  187. package/src/merger/replace-merger.js +51 -0
  188. package/src/merger/yaml-merger.js +127 -0
  189. package/src/orchestrator/agent-selector.js +285 -0
  190. package/src/orchestrator/deviation-handler.js +350 -0
  191. package/src/orchestrator/handoff-engine.js +271 -0
  192. package/src/orchestrator/index.js +67 -0
  193. package/src/orchestrator/intent-classifier.js +264 -0
  194. package/src/orchestrator/pipeline-manager.js +492 -0
  195. package/src/orchestrator/pipeline-state.js +223 -0
  196. package/src/orchestrator/session-manager.js +409 -0
  197. package/src/tasks/executor.js +195 -0
  198. package/src/tasks/handoff.js +226 -0
  199. package/src/tasks/index.js +4 -0
  200. package/src/tasks/loader.js +210 -0
  201. package/src/tasks/router.js +182 -0
  202. package/src/terminal/collector.js +216 -0
  203. package/src/terminal/index.js +30 -0
  204. package/src/terminal/isolation.js +129 -0
  205. package/src/terminal/monitor.js +277 -0
  206. package/src/terminal/spawner.js +269 -0
  207. package/src/upgrade/checker.js +1 -1
  208. package/src/wizard/i18n.js +3 -3
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Commit Message Generator — Produces conventional commit messages from staged changes.
3
+ *
4
+ * Analyzes changed file paths to determine type and scope, then builds
5
+ * a properly formatted conventional commit string.
6
+ *
7
+ * @module scripts/commit-message-generator
8
+ */
9
+
10
+ import { dirname, basename, extname } from 'node:path';
11
+
12
+ /**
13
+ * Path-based rules for detecting commit type.
14
+ * Order matters: first match wins.
15
+ *
16
+ * @type {Array<{ pattern: RegExp, type: string }>}
17
+ */
18
+ const TYPE_RULES = [
19
+ { pattern: /\.github\/|\.gitlab-ci|Jenkinsfile|\.circleci/i, type: 'ci' },
20
+ { pattern: /\.test\.|\.spec\.|__tests__|test\/|tests\//i, type: 'test' },
21
+ { pattern: /docs\/|\.md$/i, type: 'docs' },
22
+ { pattern: /package\.json$|package-lock\.json$|yarn\.lock$|pnpm-lock/i, type: 'chore' },
23
+ { pattern: /\.eslint|\.prettier|tsconfig|\.editorconfig|\.gitignore/i, type: 'chore' },
24
+ { pattern: /Dockerfile|docker-compose|\.dockerignore/i, type: 'ci' },
25
+ { pattern: /webpack|rollup|vite\.config|esbuild|babel\.config/i, type: 'build' },
26
+ ];
27
+
28
+ /**
29
+ * Detects the conventional commit type from a list of changed files.
30
+ *
31
+ * @param {string[]} files — array of file paths (relative)
32
+ * @returns {string}
33
+ */
34
+ export function detectCommitType(files) {
35
+ if (!files || files.length === 0) return 'chore';
36
+
37
+ const typeCounts = {};
38
+
39
+ for (const file of files) {
40
+ let detected = null;
41
+ for (const rule of TYPE_RULES) {
42
+ if (rule.pattern.test(file)) {
43
+ detected = rule.type;
44
+ break;
45
+ }
46
+ }
47
+ // Default to 'feat' for unmatched source files
48
+ if (!detected) {
49
+ detected = 'feat';
50
+ }
51
+ typeCounts[detected] = (typeCounts[detected] || 0) + 1;
52
+ }
53
+
54
+ // Pick the type that covers most files
55
+ let maxType = 'chore';
56
+ let maxCount = 0;
57
+ for (const [type, count] of Object.entries(typeCounts)) {
58
+ if (count > maxCount) {
59
+ maxCount = count;
60
+ maxType = type;
61
+ }
62
+ }
63
+
64
+ return maxType;
65
+ }
66
+
67
+ /**
68
+ * Detects the scope from file paths.
69
+ * Uses the common parent directory or module name.
70
+ *
71
+ * @param {string[]} files — array of file paths (relative)
72
+ * @returns {string}
73
+ */
74
+ export function detectScope(files) {
75
+ if (!files || files.length === 0) return '';
76
+
77
+ // Normalize paths and extract directory parts
78
+ const dirParts = files.map((f) => {
79
+ const dir = dirname(f);
80
+ return dir.split(/[/\\]/).filter(Boolean);
81
+ });
82
+
83
+ if (dirParts.length === 0) return '';
84
+
85
+ // Single file: use its immediate parent directory
86
+ if (files.length === 1) {
87
+ const parts = dirParts[0];
88
+ if (parts.length === 0 || (parts.length === 1 && parts[0] === '.')) return '';
89
+ return parts[parts.length - 1];
90
+ }
91
+
92
+ // Multiple files: find common prefix
93
+ const shortest = Math.min(...dirParts.map((p) => p.length));
94
+ let commonDepth = 0;
95
+ for (let i = 0; i < shortest; i++) {
96
+ const segment = dirParts[0][i];
97
+ if (dirParts.every((p) => p[i] === segment)) {
98
+ commonDepth = i + 1;
99
+ } else {
100
+ break;
101
+ }
102
+ }
103
+
104
+ if (commonDepth === 0) return '';
105
+
106
+ const commonParts = dirParts[0].slice(0, commonDepth);
107
+ // Skip generic top-level directories for the scope name
108
+ const skipDirs = new Set(['src', 'lib', 'packages', 'apps', 'modules']);
109
+ const meaningful = commonParts.filter((p) => !skipDirs.has(p));
110
+
111
+ if (meaningful.length === 0 && commonParts.length > 0) {
112
+ // If all parts are generic, use the deepest one
113
+ return commonParts[commonParts.length - 1];
114
+ }
115
+
116
+ return meaningful.length > 0 ? meaningful[meaningful.length - 1] : '';
117
+ }
118
+
119
+ /**
120
+ * Formats a conventional commit message.
121
+ *
122
+ * @param {string} type
123
+ * @param {string} scope
124
+ * @param {string} description
125
+ * @returns {string}
126
+ */
127
+ export function formatCommitMessage(type, scope, description) {
128
+ const scopePart = scope ? `(${scope})` : '';
129
+ return `${type}${scopePart}: ${description}`;
130
+ }
131
+
132
+ /**
133
+ * Generates a conventional commit message from options.
134
+ *
135
+ * @param {Object} options
136
+ * @param {string[]} options.files — list of changed file paths
137
+ * @param {string} [options.diff] — git diff string (used for description hints)
138
+ * @param {string} [options.type] — override commit type
139
+ * @returns {string}
140
+ */
141
+ export function generateCommitMessage(options = {}) {
142
+ const { files = [], type: overrideType } = options;
143
+
144
+ if (files.length === 0) {
145
+ return 'chore: update files';
146
+ }
147
+
148
+ const type = overrideType || detectCommitType(files);
149
+ const scope = detectScope(files);
150
+
151
+ // Generate description from file names
152
+ let description;
153
+ if (files.length === 1) {
154
+ const name = basename(files[0], extname(files[0]));
155
+ const verb = type === 'feat' ? 'add' : type === 'fix' ? 'fix' : 'update';
156
+ description = `${verb} ${name}`;
157
+ } else if (files.length <= 3) {
158
+ const names = files.map((f) => basename(f, extname(f)));
159
+ const verb = type === 'feat' ? 'add' : type === 'fix' ? 'fix' : 'update';
160
+ description = `${verb} ${names.join(', ')}`;
161
+ } else {
162
+ const verb = type === 'feat' ? 'add' : type === 'fix' ? 'fix' : 'update';
163
+ description = `${verb} ${files.length} files`;
164
+ }
165
+
166
+ return formatCommitMessage(type, scope, description);
167
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Coverage Analyzer — Identifies code coverage gaps by mapping source to test files.
3
+ *
4
+ * Uses naming conventions to correlate source files with their test counterparts,
5
+ * detects uncovered exports, and generates human-readable reports.
6
+ *
7
+ * @module scripts/coverage-analyzer
8
+ */
9
+
10
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
11
+ import { join, relative, extname } from 'node:path';
12
+
13
+ /**
14
+ * @typedef {Object} CoverageReport
15
+ * @property {string[]} coveredFiles — source files with corresponding tests
16
+ * @property {string[]} uncoveredFiles — source files without tests
17
+ * @property {string[]} orphanedTests — test files without corresponding source
18
+ * @property {number} coverageRatio
19
+ * @property {number} totalSourceFiles
20
+ * @property {number} totalTestFiles
21
+ */
22
+
23
+ /**
24
+ * Recursively collects JS files from a directory, excluding node_modules and .git.
25
+ * @param {string} dir
26
+ * @param {string} [ext='.js']
27
+ * @returns {string[]}
28
+ */
29
+ function collectFiles(dir, ext = '.js') {
30
+ const results = [];
31
+
32
+ function walk(currentDir) {
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(currentDir);
36
+ } catch {
37
+ return;
38
+ }
39
+ for (const entry of entries) {
40
+ if (entry === 'node_modules' || entry === '.git') continue;
41
+ const fullPath = join(currentDir, entry);
42
+ let stat;
43
+ try {
44
+ stat = statSync(fullPath);
45
+ } catch {
46
+ continue;
47
+ }
48
+ if (stat.isDirectory()) {
49
+ walk(fullPath);
50
+ } else if (stat.isFile() && fullPath.endsWith(ext)) {
51
+ results.push(fullPath);
52
+ }
53
+ }
54
+ }
55
+
56
+ walk(dir);
57
+ return results.sort();
58
+ }
59
+
60
+ /**
61
+ * Derives the expected test file path for a source file.
62
+ * Convention: src/foo/bar.js -> test/foo/bar.test.js
63
+ *
64
+ * @param {string} srcFile — absolute path to source file
65
+ * @param {string} srcDir — root source directory
66
+ * @param {string} testDir — root test directory
67
+ * @returns {string}
68
+ */
69
+ function expectedTestPath(srcFile, srcDir, testDir) {
70
+ const rel = relative(srcDir, srcFile);
71
+ const ext = extname(rel);
72
+ const withoutExt = rel.slice(0, -ext.length);
73
+ return join(testDir, `${withoutExt}.test${ext}`);
74
+ }
75
+
76
+ /**
77
+ * Derives the expected source file path for a test file.
78
+ * Convention: test/foo/bar.test.js -> src/foo/bar.js
79
+ *
80
+ * @param {string} testFile
81
+ * @param {string} srcDir
82
+ * @param {string} testDir
83
+ * @returns {string}
84
+ */
85
+ function expectedSourcePath(testFile, srcDir, testDir) {
86
+ const rel = relative(testDir, testFile);
87
+ const noTest = rel.replace(/\.test\./, '.').replace(/\.spec\./, '.');
88
+ return join(srcDir, noTest);
89
+ }
90
+
91
+ /**
92
+ * Analyzes coverage by mapping source files to test files.
93
+ *
94
+ * @param {string} srcDir
95
+ * @param {string} testDir
96
+ * @returns {CoverageReport}
97
+ */
98
+ export function analyzeCoverage(srcDir, testDir) {
99
+ const sourceFiles = collectFiles(srcDir, '.js').filter(
100
+ (f) => !f.endsWith('.test.js') && !f.endsWith('.spec.js')
101
+ );
102
+ const testFiles = collectFiles(testDir, '.js').filter(
103
+ (f) => f.endsWith('.test.js') || f.endsWith('.spec.js')
104
+ );
105
+
106
+ const coveredFiles = [];
107
+ const uncoveredFiles = [];
108
+
109
+ for (const src of sourceFiles) {
110
+ const expected = expectedTestPath(src, srcDir, testDir);
111
+ if (existsSync(expected)) {
112
+ coveredFiles.push(relative(srcDir, src));
113
+ } else {
114
+ uncoveredFiles.push(relative(srcDir, src));
115
+ }
116
+ }
117
+
118
+ const orphanedTests = [];
119
+ for (const test of testFiles) {
120
+ const expected = expectedSourcePath(test, srcDir, testDir);
121
+ if (!existsSync(expected)) {
122
+ orphanedTests.push(relative(testDir, test));
123
+ }
124
+ }
125
+
126
+ const totalSourceFiles = sourceFiles.length;
127
+ const coverageRatio =
128
+ totalSourceFiles > 0
129
+ ? Math.round((coveredFiles.length / totalSourceFiles) * 10000) / 10000
130
+ : 0;
131
+
132
+ return {
133
+ coveredFiles,
134
+ uncoveredFiles,
135
+ orphanedTests,
136
+ coverageRatio,
137
+ totalSourceFiles,
138
+ totalTestFiles: testFiles.length,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Parses exported function names from a source file via regex heuristics.
144
+ * Handles: export function, export const/let, export default, module.exports.
145
+ *
146
+ * @param {string} srcFilePath
147
+ * @returns {string[]}
148
+ */
149
+ export function getUncoveredFunctions(srcFilePath, testFilePath) {
150
+ let srcContent;
151
+ try {
152
+ srcContent = readFileSync(srcFilePath, 'utf-8');
153
+ } catch {
154
+ return [];
155
+ }
156
+
157
+ // Extract exported function/const names
158
+ const exportedNames = new Set();
159
+
160
+ // ESM: export function foo(
161
+ const exportFuncRe = /export\s+(?:async\s+)?function\s+(\w+)/g;
162
+ let m;
163
+ while ((m = exportFuncRe.exec(srcContent)) !== null) {
164
+ exportedNames.add(m[1]);
165
+ }
166
+
167
+ // ESM: export const foo = / export let foo =
168
+ const exportConstRe = /export\s+(?:const|let|var)\s+(\w+)\s*=/g;
169
+ while ((m = exportConstRe.exec(srcContent)) !== null) {
170
+ exportedNames.add(m[1]);
171
+ }
172
+
173
+ // ESM: export class Foo
174
+ const exportClassRe = /export\s+class\s+(\w+)/g;
175
+ while ((m = exportClassRe.exec(srcContent)) !== null) {
176
+ exportedNames.add(m[1]);
177
+ }
178
+
179
+ // ESM: export { foo, bar }
180
+ const exportBracketRe = /export\s*\{([^}]+)\}/g;
181
+ while ((m = exportBracketRe.exec(srcContent)) !== null) {
182
+ const names = m[1].split(',').map((n) => n.trim().split(/\s+as\s+/)[0].trim());
183
+ for (const name of names) {
184
+ if (name) exportedNames.add(name);
185
+ }
186
+ }
187
+
188
+ if (exportedNames.size === 0) return [];
189
+
190
+ // If no test file, all exported names are uncovered
191
+ if (!testFilePath) {
192
+ return [...exportedNames];
193
+ }
194
+
195
+ let testContent;
196
+ try {
197
+ testContent = readFileSync(testFilePath, 'utf-8');
198
+ } catch {
199
+ return [...exportedNames];
200
+ }
201
+
202
+ // Check which names appear in the test file
203
+ const uncovered = [];
204
+ for (const name of exportedNames) {
205
+ // Look for the name being referenced in tests (import, call, etc.)
206
+ const nameRegex = new RegExp(`\\b${name}\\b`);
207
+ if (!nameRegex.test(testContent)) {
208
+ uncovered.push(name);
209
+ }
210
+ }
211
+
212
+ return uncovered;
213
+ }
214
+
215
+ /**
216
+ * Generates a formatted coverage report string.
217
+ *
218
+ * @param {string} srcDir
219
+ * @param {string} testDir
220
+ * @returns {string}
221
+ */
222
+ export function generateCoverageReport(srcDir, testDir) {
223
+ const report = analyzeCoverage(srcDir, testDir);
224
+ const lines = [];
225
+
226
+ lines.push('=== Coverage Analysis Report ===');
227
+ lines.push('');
228
+ lines.push(`Source directory : ${srcDir}`);
229
+ lines.push(`Test directory : ${testDir}`);
230
+ lines.push(`Source files : ${report.totalSourceFiles}`);
231
+ lines.push(`Test files : ${report.totalTestFiles}`);
232
+ lines.push(`Coverage ratio : ${(report.coverageRatio * 100).toFixed(1)}%`);
233
+ lines.push('');
234
+
235
+ if (report.coveredFiles.length > 0) {
236
+ lines.push(`--- Covered (${report.coveredFiles.length}) ---`);
237
+ for (const f of report.coveredFiles) {
238
+ lines.push(` [OK] ${f}`);
239
+ }
240
+ lines.push('');
241
+ }
242
+
243
+ if (report.uncoveredFiles.length > 0) {
244
+ lines.push(`--- Uncovered (${report.uncoveredFiles.length}) ---`);
245
+ for (const f of report.uncoveredFiles) {
246
+ lines.push(` [!!] ${f}`);
247
+ }
248
+ lines.push('');
249
+ }
250
+
251
+ if (report.orphanedTests.length > 0) {
252
+ lines.push(`--- Orphaned Tests (${report.orphanedTests.length}) ---`);
253
+ for (const f of report.orphanedTests) {
254
+ lines.push(` [??] ${f}`);
255
+ }
256
+ lines.push('');
257
+ }
258
+
259
+ return lines.join('\n');
260
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Dependency Analyzer — Inspects project dependencies for issues and metrics.
3
+ *
4
+ * Reads package.json and optional lock files to produce a comprehensive
5
+ * dependency report including counts, version patterns, and potential problems.
6
+ *
7
+ * @module scripts/dependency-analyzer
8
+ */
9
+
10
+ import { readFileSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ /**
14
+ * @typedef {Object} Issue
15
+ * @property {string} severity — 'error' | 'warning' | 'info'
16
+ * @property {string} name — dependency name
17
+ * @property {string} message
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} DependencyStats
22
+ * @property {number} total
23
+ * @property {number} prod
24
+ * @property {number} dev
25
+ * @property {number} peer
26
+ * @property {number} optional
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} DependencyReport
31
+ * @property {DependencyStats} stats
32
+ * @property {Issue[]} issues
33
+ * @property {string[]} prodDeps
34
+ * @property {string[]} devDeps
35
+ * @property {string[]} peerDeps
36
+ * @property {string[]} optionalDeps
37
+ * @property {string[]} duplicatesAcrossGroups
38
+ * @property {boolean} hasLockFile
39
+ */
40
+
41
+ /**
42
+ * Safely reads and parses a JSON file.
43
+ * @param {string} filePath
44
+ * @returns {Object|null}
45
+ */
46
+ function readJSON(filePath) {
47
+ try {
48
+ const raw = readFileSync(filePath, 'utf-8');
49
+ return JSON.parse(raw);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Gets basic dependency counts from a target directory.
57
+ *
58
+ * @param {string} targetDir
59
+ * @returns {DependencyStats}
60
+ */
61
+ export function getDependencyStats(targetDir) {
62
+ const pkg = readJSON(join(targetDir, 'package.json'));
63
+ if (!pkg) {
64
+ return { total: 0, prod: 0, dev: 0, peer: 0, optional: 0 };
65
+ }
66
+
67
+ const prod = Object.keys(pkg.dependencies || {}).length;
68
+ const dev = Object.keys(pkg.devDependencies || {}).length;
69
+ const peer = Object.keys(pkg.peerDependencies || {}).length;
70
+ const optional = Object.keys(pkg.optionalDependencies || {}).length;
71
+
72
+ return {
73
+ total: prod + dev + peer + optional,
74
+ prod,
75
+ dev,
76
+ peer,
77
+ optional,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Checks dependencies for common issues.
83
+ *
84
+ * @param {Record<string, string>} deps — name-to-version map
85
+ * @param {string} group — group label ('prod', 'dev', etc.)
86
+ * @returns {Issue[]}
87
+ */
88
+ export function checkForIssues(deps, group = 'prod') {
89
+ const issues = [];
90
+
91
+ for (const [name, version] of Object.entries(deps)) {
92
+ // Wildcard version
93
+ if (version === '*' || version === 'latest') {
94
+ issues.push({
95
+ severity: 'error',
96
+ name,
97
+ message: `${group} dependency "${name}" uses wildcard version "${version}" — pin to a specific range`,
98
+ });
99
+ }
100
+
101
+ // URL-based version (git, http)
102
+ if (/^(git|http|https|ssh|file):/.test(version) || version.includes('github.com')) {
103
+ issues.push({
104
+ severity: 'warning',
105
+ name,
106
+ message: `${group} dependency "${name}" uses a URL-based version — may cause reproducibility issues`,
107
+ });
108
+ }
109
+
110
+ // Very broad range (>= without upper bound)
111
+ if (/^>=/.test(version) && !version.includes('<') && !version.includes(' ')) {
112
+ issues.push({
113
+ severity: 'warning',
114
+ name,
115
+ message: `${group} dependency "${name}" has an unbounded lower version "${version}"`,
116
+ });
117
+ }
118
+
119
+ // Version 0.x (potentially unstable)
120
+ if (/^[\^~]?0\./.test(version)) {
121
+ issues.push({
122
+ severity: 'info',
123
+ name,
124
+ message: `${group} dependency "${name}" is pre-1.0 (${version}) — API may be unstable`,
125
+ });
126
+ }
127
+
128
+ // Empty version
129
+ if (!version || version.trim() === '') {
130
+ issues.push({
131
+ severity: 'error',
132
+ name,
133
+ message: `${group} dependency "${name}" has an empty version`,
134
+ });
135
+ }
136
+ }
137
+
138
+ return issues;
139
+ }
140
+
141
+ /**
142
+ * Full dependency analysis for a target directory.
143
+ *
144
+ * @param {string} targetDir
145
+ * @returns {DependencyReport}
146
+ */
147
+ export function analyzeDependencies(targetDir) {
148
+ const pkg = readJSON(join(targetDir, 'package.json'));
149
+ if (!pkg) {
150
+ return {
151
+ stats: { total: 0, prod: 0, dev: 0, peer: 0, optional: 0 },
152
+ issues: [{ severity: 'error', name: '-', message: 'No package.json found' }],
153
+ prodDeps: [],
154
+ devDeps: [],
155
+ peerDeps: [],
156
+ optionalDeps: [],
157
+ duplicatesAcrossGroups: [],
158
+ hasLockFile: false,
159
+ };
160
+ }
161
+
162
+ const prodDeps = pkg.dependencies || {};
163
+ const devDeps = pkg.devDependencies || {};
164
+ const peerDeps = pkg.peerDependencies || {};
165
+ const optionalDeps = pkg.optionalDependencies || {};
166
+
167
+ const stats = getDependencyStats(targetDir);
168
+
169
+ // Collect issues from all groups
170
+ const issues = [
171
+ ...checkForIssues(prodDeps, 'prod'),
172
+ ...checkForIssues(devDeps, 'dev'),
173
+ ...checkForIssues(peerDeps, 'peer'),
174
+ ...checkForIssues(optionalDeps, 'optional'),
175
+ ];
176
+
177
+ // Find duplicates across prod and dev
178
+ const prodNames = new Set(Object.keys(prodDeps));
179
+ const devNames = new Set(Object.keys(devDeps));
180
+ const duplicatesAcrossGroups = [...prodNames].filter((n) => devNames.has(n));
181
+
182
+ if (duplicatesAcrossGroups.length > 0) {
183
+ for (const dup of duplicatesAcrossGroups) {
184
+ issues.push({
185
+ severity: 'warning',
186
+ name: dup,
187
+ message: `"${dup}" appears in both dependencies and devDependencies`,
188
+ });
189
+ }
190
+ }
191
+
192
+ // Check for lock file
193
+ const hasLockFile =
194
+ existsSync(join(targetDir, 'package-lock.json')) ||
195
+ existsSync(join(targetDir, 'yarn.lock')) ||
196
+ existsSync(join(targetDir, 'pnpm-lock.yaml')) ||
197
+ existsSync(join(targetDir, 'bun.lockb'));
198
+
199
+ if (!hasLockFile) {
200
+ issues.push({
201
+ severity: 'warning',
202
+ name: '-',
203
+ message: 'No lock file found — builds may not be reproducible',
204
+ });
205
+ }
206
+
207
+ return {
208
+ stats,
209
+ issues,
210
+ prodDeps: Object.keys(prodDeps).sort(),
211
+ devDeps: Object.keys(devDeps).sort(),
212
+ peerDeps: Object.keys(peerDeps).sort(),
213
+ optionalDeps: Object.keys(optionalDeps).sort(),
214
+ duplicatesAcrossGroups,
215
+ hasLockFile,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Formats a dependency report as a human-readable string.
221
+ *
222
+ * @param {DependencyReport} report
223
+ * @returns {string}
224
+ */
225
+ export function formatDependencyReport(report) {
226
+ const lines = [];
227
+
228
+ lines.push('=== Dependency Analysis Report ===');
229
+ lines.push('');
230
+ lines.push(`Total dependencies : ${report.stats.total}`);
231
+ lines.push(` Production : ${report.stats.prod}`);
232
+ lines.push(` Development : ${report.stats.dev}`);
233
+ lines.push(` Peer : ${report.stats.peer}`);
234
+ lines.push(` Optional : ${report.stats.optional}`);
235
+ lines.push(`Lock file : ${report.hasLockFile ? 'Yes' : 'No'}`);
236
+ lines.push('');
237
+
238
+ if (report.duplicatesAcrossGroups.length > 0) {
239
+ lines.push(`--- Duplicates (prod & dev) ---`);
240
+ for (const dup of report.duplicatesAcrossGroups) {
241
+ lines.push(` [DUP] ${dup}`);
242
+ }
243
+ lines.push('');
244
+ }
245
+
246
+ const errors = report.issues.filter((i) => i.severity === 'error');
247
+ const warnings = report.issues.filter((i) => i.severity === 'warning');
248
+ const infos = report.issues.filter((i) => i.severity === 'info');
249
+
250
+ if (errors.length > 0) {
251
+ lines.push(`--- Errors (${errors.length}) ---`);
252
+ for (const issue of errors) {
253
+ lines.push(` [ERR] ${issue.message}`);
254
+ }
255
+ lines.push('');
256
+ }
257
+
258
+ if (warnings.length > 0) {
259
+ lines.push(`--- Warnings (${warnings.length}) ---`);
260
+ for (const issue of warnings) {
261
+ lines.push(` [WRN] ${issue.message}`);
262
+ }
263
+ lines.push('');
264
+ }
265
+
266
+ if (infos.length > 0) {
267
+ lines.push(`--- Info (${infos.length}) ---`);
268
+ for (const issue of infos) {
269
+ lines.push(` [INF] ${issue.message}`);
270
+ }
271
+ lines.push('');
272
+ }
273
+
274
+ if (report.issues.length === 0) {
275
+ lines.push('No issues found.');
276
+ lines.push('');
277
+ }
278
+
279
+ return lines.join('\n');
280
+ }