wize-dev-kit 0.3.0 → 0.3.1

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 (31) hide show
  1. package/AGENTS.md +21 -0
  2. package/ARCH.md +40 -4
  3. package/CHANGELOG.md +26 -0
  4. package/README.md +3 -1
  5. package/package.json +3 -2
  6. package/src/method-skills/1-analysis/wize-document-project/documentation-requirements.csv +12 -0
  7. package/src/method-skills/1-analysis/wize-document-project/templates/api-contracts-template.md +38 -0
  8. package/src/method-skills/1-analysis/wize-document-project/templates/architecture-template.md +54 -0
  9. package/src/method-skills/1-analysis/wize-document-project/templates/component-inventory-template.md +40 -0
  10. package/src/method-skills/1-analysis/wize-document-project/templates/contribution-guide-template.md +34 -0
  11. package/src/method-skills/1-analysis/wize-document-project/templates/data-models-template.md +36 -0
  12. package/src/method-skills/1-analysis/wize-document-project/templates/deep-dive-template.md +312 -0
  13. package/src/method-skills/1-analysis/wize-document-project/templates/deployment-guide-template.md +42 -0
  14. package/src/method-skills/1-analysis/wize-document-project/templates/development-guide-template.md +61 -0
  15. package/src/method-skills/1-analysis/wize-document-project/templates/index-template.md +185 -0
  16. package/src/method-skills/1-analysis/wize-document-project/templates/project-overview-template.md +110 -0
  17. package/src/method-skills/1-analysis/wize-document-project/templates/project-scan-report-schema.json +159 -0
  18. package/src/method-skills/1-analysis/wize-document-project/templates/source-tree-template.md +142 -0
  19. package/src/method-skills/1-analysis/wize-document-project/workflow.md +23 -0
  20. package/src/tea-skills/wize-tea-risk/workflow.md +11 -0
  21. package/tools/installer/commands/doctor.js +27 -1
  22. package/tools/installer/commands/document-project.js +93 -0
  23. package/tools/installer/document-project/batch-scanner.js +93 -0
  24. package/tools/installer/document-project/classify.js +170 -0
  25. package/tools/installer/document-project/modes/deep-dive.js +196 -0
  26. package/tools/installer/document-project/modes/full-rescan.js +15 -0
  27. package/tools/installer/document-project/modes/initial-scan.js +100 -0
  28. package/tools/installer/document-project/modes/quick.js +211 -0
  29. package/tools/installer/document-project/render-index.js +101 -0
  30. package/tools/installer/document-project/state.js +110 -0
  31. package/tools/installer/wize-cli.js +46 -30
@@ -0,0 +1,159 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://qwize.io/wize-dev-kit/schemas/project-scan-report.schema.json",
4
+ "title": "Project Scan Report",
5
+ "description": "State tracking file for wize-document-project resumability",
6
+ "type": "object",
7
+ "required": ["workflow_version", "timestamps", "mode", "scan_level", "completed_steps", "current_step"],
8
+ "properties": {
9
+ "workflow_version": {
10
+ "type": "string",
11
+ "description": "Version of document-project workflow"
12
+ },
13
+ "timestamps": {
14
+ "type": "object",
15
+ "required": ["started", "last_updated"],
16
+ "properties": {
17
+ "started": {
18
+ "type": "string",
19
+ "format": "date-time",
20
+ "description": "ISO 8601 timestamp when workflow started"
21
+ },
22
+ "last_updated": {
23
+ "type": "string",
24
+ "format": "date-time",
25
+ "description": "ISO 8601 timestamp of last state update"
26
+ },
27
+ "completed": {
28
+ "type": "string",
29
+ "format": "date-time",
30
+ "description": "ISO 8601 timestamp when workflow completed"
31
+ }
32
+ }
33
+ },
34
+ "mode": {
35
+ "type": "string",
36
+ "enum": ["initial_scan", "full_rescan", "deep_dive", "quick"],
37
+ "description": "Workflow execution mode"
38
+ },
39
+ "scan_level": {
40
+ "type": "string",
41
+ "enum": ["quick", "deep", "exhaustive"],
42
+ "description": "Scan depth level"
43
+ },
44
+ "project_root": {
45
+ "type": "string",
46
+ "description": "Absolute path to project root directory"
47
+ },
48
+ "project_knowledge": {
49
+ "type": "string",
50
+ "description": "Absolute path to project knowledge folder"
51
+ },
52
+ "completed_steps": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "required": ["step", "status"],
57
+ "properties": {
58
+ "step": {
59
+ "type": "string",
60
+ "description": "Step identifier"
61
+ },
62
+ "status": {
63
+ "type": "string",
64
+ "enum": ["completed", "partial", "failed"]
65
+ },
66
+ "timestamp": {
67
+ "type": "string",
68
+ "format": "date-time"
69
+ },
70
+ "outputs": {
71
+ "type": "array",
72
+ "items": { "type": "string" },
73
+ "description": "Files written during this step"
74
+ },
75
+ "summary": {
76
+ "type": "string",
77
+ "description": "1-2 sentence summary of step outcome"
78
+ }
79
+ }
80
+ }
81
+ },
82
+ "current_step": {
83
+ "type": "string",
84
+ "description": "Current step identifier for resumption"
85
+ },
86
+ "findings": {
87
+ "type": "object",
88
+ "description": "High-level summaries only",
89
+ "properties": {
90
+ "project_classification": {
91
+ "type": "object",
92
+ "properties": {
93
+ "repository_type": { "type": "string" },
94
+ "parts_count": { "type": "integer" },
95
+ "primary_language": { "type": "string" },
96
+ "architecture_type": { "type": "string" }
97
+ }
98
+ },
99
+ "technology_stack": {
100
+ "type": "array",
101
+ "items": {
102
+ "type": "object",
103
+ "properties": {
104
+ "part_id": { "type": "string" },
105
+ "tech_summary": { "type": "string" }
106
+ }
107
+ }
108
+ },
109
+ "batches_completed": {
110
+ "type": "array",
111
+ "items": {
112
+ "type": "object",
113
+ "properties": {
114
+ "path": { "type": "string" },
115
+ "files_scanned": { "type": "integer" },
116
+ "summary": { "type": "string" }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ },
122
+ "outputs_generated": {
123
+ "type": "array",
124
+ "items": { "type": "string" },
125
+ "description": "List of all output files generated"
126
+ },
127
+ "resume_instructions": {
128
+ "type": "string",
129
+ "description": "Instructions for resuming from current_step"
130
+ },
131
+ "validation_status": {
132
+ "type": "object",
133
+ "properties": {
134
+ "last_validated": {
135
+ "type": "string",
136
+ "format": "date-time"
137
+ },
138
+ "validation_errors": {
139
+ "type": "array",
140
+ "items": { "type": "string" }
141
+ }
142
+ }
143
+ },
144
+ "deep_dive_targets": {
145
+ "type": "array",
146
+ "description": "Track deep-dive areas analyzed",
147
+ "items": {
148
+ "type": "object",
149
+ "properties": {
150
+ "target_name": { "type": "string" },
151
+ "target_path": { "type": "string" },
152
+ "files_analyzed": { "type": "integer" },
153
+ "output_file": { "type": "string" },
154
+ "timestamp": { "type": "string", "format": "date-time" }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
@@ -0,0 +1,142 @@
1
+ ---
2
+ status: baseline
3
+ owner: Pepper Potts + Peggy Carter
4
+ created: {{date}}
5
+ last_refreshed: {{date}}
6
+ ---
7
+
8
+ # {{project_name}} - Source Tree Analysis
9
+
10
+ **Date:** {{date}}
11
+
12
+ ## Overview
13
+
14
+ {{source_tree_overview}}
15
+
16
+ {{#if is_multi_part}}
17
+
18
+ ## Multi-Part Structure
19
+
20
+ This project is organized into {{parts_count}} distinct parts:
21
+
22
+ {{#each project_parts}}
23
+
24
+ - **{{part_name}}** (`{{root_path}}`): {{purpose}}
25
+ {{/each}}
26
+ {{/if}}
27
+
28
+ ## Complete Directory Structure
29
+
30
+ ```
31
+ {{complete_source_tree}}
32
+ ```
33
+
34
+ ## Critical Directories
35
+
36
+ {{#each critical_folders}}
37
+
38
+ ### `{{folder_path}}`
39
+
40
+ {{description}}
41
+
42
+ **Purpose:** {{purpose}}
43
+ **Contains:** {{contents_summary}}
44
+ {{#if entry_points}}**Entry Points:** {{entry_points}}{{/if}}
45
+ {{#if integration_note}}**Integration:** {{integration_note}}{{/if}}
46
+
47
+ {{/each}}
48
+
49
+ {{#if is_multi_part}}
50
+
51
+ ## Part-Specific Trees
52
+
53
+ {{#each project_parts}}
54
+
55
+ ### {{part_name}} Structure
56
+
57
+ ```
58
+ {{source_tree}}
59
+ ```
60
+
61
+ **Key Directories:**
62
+ {{#each critical_directories}}
63
+
64
+ - **`{{path}}`**: {{description}}
65
+ {{/each}}
66
+
67
+ {{/each}}
68
+
69
+ ## Integration Points
70
+
71
+ {{#each integration_points}}
72
+
73
+ ### {{from_part}} → {{to_part}}
74
+
75
+ - **Location:** `{{integration_path}}`
76
+ - **Type:** {{integration_type}}
77
+ - **Details:** {{details}}
78
+ {{/each}}
79
+
80
+ {{/if}}
81
+
82
+ ## Entry Points
83
+
84
+ {{#if is_single_part}}
85
+
86
+ - **Main Entry:** `{{main_entry_point}}`
87
+ {{#if additional_entry_points}}
88
+ - **Additional:**
89
+ {{#each additional_entry_points}}
90
+ - `{{path}}`: {{description}}
91
+ {{/each}}
92
+ {{/if}}
93
+ {{else}}
94
+ {{#each project_parts}}
95
+
96
+ ### {{part_name}}
97
+
98
+ - **Entry Point:** `{{entry_point}}`
99
+ - **Bootstrap:** {{bootstrap_description}}
100
+ {{/each}}
101
+ {{/if}}
102
+
103
+ ## File Organization Patterns
104
+
105
+ {{file_organization_patterns}}
106
+
107
+ ## Key File Types
108
+
109
+ {{#each file_type_patterns}}
110
+
111
+ ### {{file_type}}
112
+
113
+ - **Pattern:** `{{pattern}}`
114
+ - **Purpose:** {{purpose}}
115
+ - **Examples:** {{examples}}
116
+ {{/each}}
117
+
118
+ ## Asset Locations
119
+
120
+ {{#if has_assets}}
121
+ {{#each asset_locations}}
122
+
123
+ - **{{asset_type}}**: `{{location}}` ({{file_count}} files, {{total_size}})
124
+ {{/each}}
125
+ {{else}}
126
+ No significant assets detected.
127
+ {{/if}}
128
+
129
+ ## Configuration Files
130
+
131
+ {{#each config_files}}
132
+
133
+ - **`{{path}}`**: {{description}}
134
+ {{/each}}
135
+
136
+ ## Notes for Development
137
+
138
+ {{development_notes}}
139
+
140
+ ---
141
+
142
+ _Generated using Wize Dev Kit `document-project` workflow_
@@ -28,6 +28,27 @@ Skip:
28
28
  - `git log --since="1 year ago" --oneline | wc -l` to scope.
29
29
  - Any prior README / ARCHITECTURE / docs that exist.
30
30
 
31
+ ## CLI usage
32
+
33
+ ```bash
34
+ wize-dev-kit document-project # quick baseline (default)
35
+ wize-dev-kit document-project quick # same as default
36
+ wize-dev-kit document-project initial_scan # pattern-only initial scan
37
+ wize-dev-kit document-project initial_scan deep # reads critical directories
38
+ wize-dev-kit document-project initial_scan exhaustive # reads all source files in batches
39
+ wize-dev-kit document-project full_rescan # archives old state, re-runs initial_scan
40
+ wize-dev-kit document-project deep_dive --target src/tools/installer
41
+ ```
42
+
43
+ ## Modes
44
+
45
+ | Mode | What it does | Scan levels |
46
+ |---|---|---|
47
+ | `quick` | Writes the 6 baseline files. Does not read source files. | `quick` only |
48
+ | `initial_scan` | Classifies project type, writes index + overview + source tree + conditional docs. | `quick`, `deep`, `exhaustive` |
49
+ | `full_rescan` | Archives `project-scan-report.json` and re-runs `initial_scan`. | `quick`, `deep`, `exhaustive` |
50
+ | `deep_dive` | Exhaustive analysis of a specific folder/file/feature. | `exhaustive` |
51
+
31
52
  ## Outputs
32
53
 
33
54
  - `.wize/knowledge/document-project/overview.md` — what the project is, who uses it, how big it is.
@@ -36,6 +57,8 @@ Skip:
36
57
  - `.wize/knowledge/document-project/dependencies.md` — runtime deps + dev deps + their roles.
37
58
  - `.wize/knowledge/document-project/risk-spots.md` — areas of concentrated complexity, undocumented behavior, or known fragility.
38
59
  - `.wize/knowledge/document-project/open-questions.md` — things the code doesn't answer; route to humans.
60
+ - `.wize/knowledge/document-project/index.md` — master navigation with `_(To be generated)_` markers for missing conditional docs.
61
+ - `.wize/knowledge/document-project/project-scan-report.json` — resume state.
39
62
 
40
63
  ## Steps
41
64
 
@@ -19,6 +19,7 @@ Hawkeye drives. Tony co-signs. Runs **once**, right after architecture is signed
19
19
  - `.wize/solutioning/epics/`
20
20
  - `.wize/planning/nfr-principles.md`
21
21
  - `.wize/knowledge/document-project/risk-spots.md` (brownfield)
22
+ - `.wize/knowledge/document-project/index.md` (documentation gap markers)
22
23
 
23
24
  ## Output
24
25
 
@@ -115,6 +116,16 @@ The narrative explains the matrix; the YAML is the structured truth. Hawkeye wri
115
116
  - Other stories follow the default 70/20/10 split.
116
117
  ```
117
118
 
119
+ ## Documentation gap risk category
120
+
121
+ In brownfield repos, missing or stale documentation is an operational risk: new contributors make unsafe assumptions, AI agents hallucinate context, and reviews miss implicit contracts. Hawkeye treats it as a first-class risk area.
122
+
123
+ | Symptom | Severity | Rationale | Mitigation |
124
+ |---|---|---|---|
125
+ | `index.md` contains `_(To be generated)_` markers | medium | Conditional docs were skipped; the baseline is incomplete. | Schedule `wize-document-project initial_scan` for the next cooldown. |
126
+ | Baseline files older than 60 days | medium | Docs may no longer reflect the codebase. | Run `wize-refresh-knowledge` at sprint end. |
127
+ | No baseline exists in a brownfield repo | high | Team is flying blind; architecture and conventions are tribal knowledge. | Run `wize-dev-kit document-project quick` immediately. |
128
+
118
129
  ## Anti-patterns Hawkeye rejects
119
130
 
120
131
  - **Findings without mitigation.** A finding without a contract is a wish.
@@ -130,7 +130,21 @@ function knowledgeStatus(projectRoot) {
130
130
  const pendingLines = fileExists(pendingFile)
131
131
  ? readSafe(pendingFile).split('\n').filter(l => l.trim() && !l.startsWith('#')).length
132
132
  : 0;
133
- return { exists: true, files: refreshed, pendingLines };
133
+
134
+ const indexPath = path.join(root, 'index.md');
135
+ const indexContent = fileExists(indexPath) ? readSafe(indexPath) : '';
136
+ const toBeGeneratedMarkers = (indexContent.match(/_\(To be generated\)_/g) || []).length;
137
+
138
+ const statePath = path.join(root, 'project-scan-report.json');
139
+ const stateExists = fileExists(statePath);
140
+ const stateContent = stateExists ? readSafe(statePath) : '';
141
+ let stateAgeDays = null;
142
+ try {
143
+ const state = JSON.parse(stateContent);
144
+ stateAgeDays = daysAgo(state.timestamps && state.timestamps.last_updated);
145
+ } catch (_) {}
146
+
147
+ return { exists: true, files: refreshed, pendingLines, toBeGeneratedMarkers, stateExists, stateAgeDays };
134
148
  }
135
149
 
136
150
  function gitInfo(projectRoot) {
@@ -247,6 +261,18 @@ async function cmdDoctor({ kitRoot, projectRoot, opts = {} } = {}) {
247
261
  suggestions.push({ level: 'info', text: `${knowledge.pendingLines} notes piled up in _pending.md. Time to run \`wize-refresh-knowledge\`.` });
248
262
  }
249
263
  }
264
+ const markerText = knowledge.toBeGeneratedMarkers > 0 ? `${knowledge.toBeGeneratedMarkers} marker(s)` : 'none';
265
+ log(` To be generated markers: ${markerText}`);
266
+ if (knowledge.toBeGeneratedMarkers >= 5) {
267
+ suggestions.push({ level: 'warn', text: `${knowledge.toBeGeneratedMarkers} docs marked "To be generated" in index.md. Run \`wize-dev-kit document-project initial_scan deep\` to fill them.` });
268
+ }
269
+ const stateText = knowledge.stateExists
270
+ ? (knowledge.stateAgeDays == null ? 'project-scan-report.json exists (age unknown)' : `project-scan-report.json ${knowledge.stateAgeDays}d old`)
271
+ : 'no project-scan-report.json';
272
+ log(` Scan state: ${stateText}`);
273
+ if (!knowledge.stateExists) {
274
+ suggestions.push({ level: 'info', text: 'No scan state file found. Run `wize-dev-kit document-project quick` to create a baseline + state.' });
275
+ }
250
276
  }
251
277
 
252
278
  // -- Section: Harness CLIs --
@@ -0,0 +1,93 @@
1
+ // `wize-dev-kit document-project` — CLI command entry point.
2
+ //
3
+ // Parses mode + flags and delegates to the appropriate scan implementation.
4
+ // Non-quick modes are stubs here; later stories fill them in.
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const { runQuick } = require('../document-project/modes/quick.js');
11
+ const { runInitialScan } = require('../document-project/modes/initial-scan.js');
12
+ const { runFullRescan } = require('../document-project/modes/full-rescan.js');
13
+ const { runDeepDive } = require('../document-project/modes/deep-dive.js');
14
+ const { loadState, archiveOldState } = require('../document-project/state.js');
15
+
16
+ const VALID_MODES = new Set(['quick', 'initial_scan', 'full_rescan', 'deep_dive']);
17
+ const VALID_SCAN_LEVELS = new Set(['quick', 'deep', 'exhaustive']);
18
+
19
+ function parseMode(args) {
20
+ let mode = 'quick';
21
+ let resume = false;
22
+ let scanLevel = 'quick';
23
+ let target = null;
24
+
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (arg === '--resume') {
28
+ resume = true;
29
+ continue;
30
+ }
31
+ if (arg === '--target') {
32
+ target = args[i + 1];
33
+ i++;
34
+ continue;
35
+ }
36
+ if (arg.startsWith('--')) {
37
+ return { mode: null, error: `unknown option: ${arg}` };
38
+ }
39
+ if (VALID_SCAN_LEVELS.has(arg)) {
40
+ scanLevel = arg;
41
+ continue;
42
+ }
43
+ if (mode === 'quick' && VALID_MODES.has(arg)) {
44
+ mode = arg;
45
+ } else if (mode === 'quick') {
46
+ return { mode: null, error: `unknown mode: ${arg}. Valid modes: ${[...VALID_MODES].join(', ')}` };
47
+ } else {
48
+ return { mode: null, error: `unexpected extra argument: ${arg}` };
49
+ }
50
+ }
51
+
52
+ if (!VALID_MODES.has(mode)) {
53
+ return { mode: null, error: `unknown mode: ${mode}. Valid modes: ${[...VALID_MODES].join(', ')}` };
54
+ }
55
+
56
+ return { mode, resume, scanLevel, target };
57
+ }
58
+
59
+ function cmdDocumentProject({ kitRoot, projectRoot, args = [], opts = {} }) {
60
+ const log = opts.log || console.log;
61
+ const parsed = parseMode(args);
62
+
63
+ if (parsed.mode === null) {
64
+ log(parsed.error);
65
+ return { ok: false, error: parsed.error, exitCode: 1 };
66
+ }
67
+
68
+ if (parsed.mode === 'quick') {
69
+ const r = runQuick(projectRoot, { log });
70
+ return { ok: true, mode: 'quick', ...r };
71
+ }
72
+
73
+ if (parsed.mode === 'initial_scan') {
74
+ const r = runInitialScan(projectRoot, { scanLevel: parsed.scanLevel, log });
75
+ return { ok: true, mode: 'initial_scan', ...r };
76
+ }
77
+
78
+ if (parsed.mode === 'full_rescan') {
79
+ const r = runFullRescan(projectRoot, { scanLevel: parsed.scanLevel, log });
80
+ return { ok: true, mode: 'full_rescan', ...r };
81
+ }
82
+
83
+ if (parsed.mode === 'deep_dive') {
84
+ const r = runDeepDive(projectRoot, { target: parsed.target, log });
85
+ return r;
86
+ }
87
+
88
+ log(`document-project ${parsed.mode} not implemented`);
89
+ return { ok: false, error: 'not implemented', exitCode: 1 };
90
+ }
91
+
92
+ module.exports = { cmdDocumentProject, parseMode };
93
+
@@ -0,0 +1,93 @@
1
+ // Batch scanner for `wize-dev-kit document-project`.
2
+ //
3
+ // Walks a repo in subfolder-sized batches, skipping noise directories.
4
+ // Returns summaries without loading entire trees into memory.
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+
11
+ const DEFAULT_IGNORE = new Set([
12
+ 'node_modules',
13
+ '.git',
14
+ 'dist',
15
+ 'build',
16
+ 'coverage',
17
+ '.cache',
18
+ '.tmp',
19
+ '.wize',
20
+ '.claude',
21
+ '.cursor',
22
+ '.kimi',
23
+ '.opencode',
24
+ '.windsurf',
25
+ '.continue',
26
+ '.codex',
27
+ '.antigravity'
28
+ ]);
29
+
30
+ const MAX_FILE_LOC = 5000;
31
+
32
+ function shouldIgnore(name, customIgnore = []) {
33
+ if (DEFAULT_IGNORE.has(name)) return true;
34
+ for (const pattern of customIgnore) {
35
+ if (name === pattern || (pattern.startsWith('*') && name.endsWith(pattern.slice(1)))) return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ function listSubfolders(rootDir, options = {}) {
41
+ const out = [];
42
+ let entries;
43
+ try { entries = fs.readdirSync(rootDir, { withFileTypes: true }); } catch { return out; }
44
+ for (const e of entries) {
45
+ if (!e.isDirectory()) continue;
46
+ if (shouldIgnore(e.name, options.ignore)) continue;
47
+ out.push(path.join(rootDir, e.name));
48
+ }
49
+ return out;
50
+ }
51
+
52
+ function scanFolder(folderPath, options = {}) {
53
+ const files = [];
54
+ let totalLoc = 0;
55
+ const stack = [folderPath];
56
+ while (stack.length) {
57
+ const dir = stack.pop();
58
+ let entries;
59
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
60
+ for (const e of entries) {
61
+ const full = path.join(dir, e.name);
62
+ if (e.isDirectory()) {
63
+ if (!shouldIgnore(e.name, options.ignore)) stack.push(full);
64
+ continue;
65
+ }
66
+ if (!e.isFile()) continue;
67
+ if (/\.(min\.js|map)$/.test(e.name)) continue;
68
+ const stat = fs.statSync(full);
69
+ const loc = stat.size; // proxy; exact line count is optional
70
+ const info = {
71
+ path: full,
72
+ relative: path.relative(folderPath, full),
73
+ size: stat.size,
74
+ loc: loc > MAX_FILE_LOC ? MAX_FILE_LOC : loc,
75
+ skipped: loc > MAX_FILE_LOC
76
+ };
77
+ files.push(info);
78
+ totalLoc += info.loc;
79
+ }
80
+ }
81
+ return { folder: folderPath, files, totalLoc, fileCount: files.length };
82
+ }
83
+
84
+ function batchScanner(rootDir, options = {}) {
85
+ const folders = options.folders || listSubfolders(rootDir, options);
86
+ const results = [];
87
+ for (const folder of folders) {
88
+ results.push(scanFolder(folder, options));
89
+ }
90
+ return results;
91
+ }
92
+
93
+ module.exports = { batchScanner, listSubfolders, scanFolder, shouldIgnore, MAX_FILE_LOC };