@lakindu_perera/toren 1.0.5 → 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.
@@ -31,34 +31,6 @@ import path from 'node:path';
31
31
  // Tree builder (plain-text, HTML-safe — same algorithm as markdown-renderer)
32
32
  // ---------------------------------------------------------------------------
33
33
 
34
- /**
35
- * Build an in-memory nested tree from a flat list of relative file paths.
36
- *
37
- * @param {string[]} flatFiles - Relative file paths produced by scan()
38
- * @returns {{ type: string, children: Record<string, object> }}
39
- */
40
- function buildInternalTree(flatFiles) {
41
- const root = { type: 'directory', children: {} };
42
-
43
- for (const filePath of flatFiles) {
44
- const parts = filePath.split(/[/\\]/).filter(Boolean);
45
- let node = root;
46
-
47
- for (let i = 0; i < parts.length; i++) {
48
- const part = parts[i];
49
- const isLeaf = i === parts.length - 1;
50
-
51
- if (!node.children[part]) {
52
- node.children[part] = isLeaf
53
- ? { type: 'file', name: part }
54
- : { type: 'directory', name: part, children: {} };
55
- }
56
- node = node.children[part];
57
- }
58
- }
59
-
60
- return root;
61
- }
62
34
 
63
35
  /**
64
36
  * Recursively serialise a tree node into classic tree-connector lines.
@@ -80,10 +52,7 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
80
52
  lines.push(`${prefix}${connector}${label}`);
81
53
 
82
54
  if (node.type === 'directory') {
83
- const children = Object.values(node.children || {}).sort((a, b) => {
84
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
85
- return a.name.localeCompare(b.name);
86
- });
55
+ const children = node.children || [];
87
56
 
88
57
  if (depth === maxDepth - 1 && children.length > 0) {
89
58
  lines.push(`${prefix}${childPad}└── ...`);
@@ -104,21 +73,18 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
104
73
  }
105
74
 
106
75
  /**
107
- * Convert a flat file list into a plain-text tree string.
76
+ * Convert a ScanResult tree into a plain-text tree string.
108
77
  *
109
- * @param {string[]} flatFiles
78
+ * @param {import('../scanner/scan.js').DirNode} tree
110
79
  * @param {string} rootName
80
+ * @param {number} totalFiles
111
81
  * @returns {string}
112
82
  */
113
- function buildTreeString(flatFiles, rootName) {
114
- if (flatFiles.length === 0) return 'No files scanned.';
83
+ function buildTreeString(tree, rootName, totalFiles) {
84
+ if (totalFiles === 0) return 'No files scanned.';
115
85
 
116
- const root = buildInternalTree(flatFiles);
117
86
  const lines = [`${rootName}/`];
118
- const children = Object.values(root.children).sort((a, b) => {
119
- if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
120
- return a.name.localeCompare(b.name);
121
- });
87
+ const children = tree.children || [];
122
88
 
123
89
  for (let i = 0; i < children.length; i++) {
124
90
  serializeNode(children[i], '', i === children.length - 1, lines);
@@ -148,12 +114,13 @@ function esc(value) {
148
114
 
149
115
  /**
150
116
  * Format a scan duration in milliseconds to a human-readable string.
117
+ * Returns plain text only — callers are responsible for HTML-escaping via esc().
151
118
  *
152
119
  * @param {number} ms
153
120
  * @returns {string}
154
121
  */
155
122
  function formatDuration(ms) {
156
- if (ms < 1) return '&lt; 1 ms';
123
+ if (ms < 1) return '< 1 ms';
157
124
  if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
158
125
  return `${Math.round(ms)} ms`;
159
126
  }
@@ -632,6 +599,19 @@ const icon = {
632
599
  // Section builders (one function per report section)
633
600
  // ---------------------------------------------------------------------------
634
601
 
602
+ /**
603
+ * Derive a frameworks array from the projectType string.
604
+ * Returns [] when no specific framework is detected.
605
+ * Mirrors the identical derivation in console-renderer.js and json-renderer.js.
606
+ *
607
+ * @param {string} projectType
608
+ * @returns {string[]}
609
+ */
610
+ function deriveFrameworks(projectType) {
611
+ if (!projectType || projectType === 'Unknown') return [];
612
+ return [projectType];
613
+ }
614
+
635
615
  /**
636
616
  * Render the gradient page header.
637
617
  *
@@ -696,7 +676,7 @@ function renderSummaryCards(result) {
696
676
  {
697
677
  label: 'Scan Duration',
698
678
  icon: icon.clock(),
699
- value: formatDuration(scanDurationMs),
679
+ value: esc(formatDuration(scanDurationMs)),
700
680
  isText: true,
701
681
  sub: 'wall-clock time',
702
682
  },
@@ -715,6 +695,41 @@ function renderSummaryCards(result) {
715
695
  return `<div class="cards">${cardHTML}</div>`;
716
696
  }
717
697
 
698
+ /**
699
+ * Render the frameworks section.
700
+ *
701
+ * @param {string} projectType
702
+ * @returns {string}
703
+ */
704
+ function renderFrameworks(projectType) {
705
+ const frameworks = deriveFrameworks(projectType);
706
+ const count = frameworks.length;
707
+
708
+ const body = count === 0
709
+ ? `<p class="empty-msg">No frameworks detected.</p>`
710
+ : `<ul class="entry-list">
711
+ ${frameworks.map(fw => `
712
+ <li class="entry-item">
713
+ <span class="entry-dot"></span>
714
+ ${esc(fw)}
715
+ </li>`).join('')}
716
+ </ul>`;
717
+
718
+ const countBadge = count > 0
719
+ ? `<span class="section-count">${count} found</span>`
720
+ : '';
721
+
722
+ return `
723
+ <section class="section">
724
+ <div class="section-header">
725
+ <div class="section-icon">${icon.code()}</div>
726
+ <h2 class="section-title">Frameworks</h2>
727
+ ${countBadge}
728
+ </div>
729
+ <div class="section-body">${body}</div>
730
+ </section>`;
731
+ }
732
+
718
733
  /**
719
734
  * Render the entry points section.
720
735
  *
@@ -739,14 +754,14 @@ function renderEntryPoints(entryPoints) {
739
754
  : '';
740
755
 
741
756
  return `
742
- <div class="section">
757
+ <section class="section">
743
758
  <div class="section-header">
744
759
  <div class="section-icon">${icon.door()}</div>
745
- <span class="section-title">Entry Points</span>
760
+ <h2 class="section-title">Entry Points</h2>
746
761
  ${countBadge}
747
762
  </div>
748
763
  <div class="section-body">${body}</div>
749
- </div>`;
764
+ </section>`;
750
765
  }
751
766
 
752
767
  /**
@@ -773,14 +788,14 @@ function renderConfigurationFiles(configs) {
773
788
  : '';
774
789
 
775
790
  return `
776
- <div class="section">
791
+ <section class="section">
777
792
  <div class="section-header">
778
793
  <div class="section-icon">${icon.file()}</div>
779
- <span class="section-title">Configuration Files</span>
794
+ <h2 class="section-title">Configuration Files</h2>
780
795
  ${countBadge}
781
796
  </div>
782
797
  <div class="section-body">${body}</div>
783
- </div>`;
798
+ </section>`;
784
799
  }
785
800
 
786
801
  /**
@@ -789,6 +804,118 @@ function renderConfigurationFiles(configs) {
789
804
  * @param {Array<{name: string, command: string}>} scripts
790
805
  * @returns {string}
791
806
  */
807
+ function renderProjectInfo(projectInfo, packageManager) {
808
+ const rows = [];
809
+ if (projectInfo?.name) rows.push(['Name', projectInfo.name]);
810
+ if (packageManager) rows.push(['Package Manager', packageManager]);
811
+ if (projectInfo?.runtime) rows.push(['Runtime', projectInfo.runtime]);
812
+ if (projectInfo?.language) rows.push(['Language', projectInfo.language]);
813
+ if (projectInfo?.architecture) rows.push(['Architecture', projectInfo.architecture]);
814
+ if (projectInfo?.framework) rows.push(['Framework', projectInfo.framework]);
815
+ if (projectInfo?.entryPoint) rows.push(['Entry Point', projectInfo.entryPoint]);
816
+ if (projectInfo?.sourceDirectory) rows.push(['Source Directory', projectInfo.sourceDirectory]);
817
+
818
+ if (rows.length === 0) return '';
819
+
820
+ const rowsHTML = rows.map(([prop, value]) => `
821
+ <tr>
822
+ <td>${esc(prop)}</td>
823
+ <td class="val-plain">${esc(value)}</td>
824
+ </tr>`).join('');
825
+
826
+ return `
827
+ <section class="section">
828
+ <div class="section-header">
829
+ <div class="section-icon">${icon.info()}</div>
830
+ <h2 class="section-title">Project Info</h2>
831
+ </div>
832
+ <div class="section-body" style="padding:0">
833
+ <table class="data-table">
834
+ <thead>
835
+ <tr>
836
+ <th>Property</th>
837
+ <th style="text-align:right">Value</th>
838
+ </tr>
839
+ </thead>
840
+ <tbody>${rowsHTML}</tbody>
841
+ </table>
842
+ </div>
843
+ </section>`;
844
+ }
845
+
846
+ function renderImportantFiles(importantFiles) {
847
+ const count = importantFiles.length;
848
+
849
+ const body = count === 0
850
+ ? `<p class="empty-msg" style="padding: 1.5rem">No important files detected.</p>`
851
+ : `<table class="data-table">
852
+ <thead>
853
+ <tr>
854
+ <th>Path</th>
855
+ <th>Reason</th>
856
+ </tr>
857
+ </thead>
858
+ <tbody>
859
+ ${importantFiles.map(f => `
860
+ <tr>
861
+ <td><code>${esc(f.path)}</code></td>
862
+ <td class="val-plain">${esc(f.reason)}</td>
863
+ </tr>`).join('')}
864
+ </tbody>
865
+ </table>`;
866
+
867
+ const countBadge = count > 0
868
+ ? `<span class="section-count">${count} found</span>`
869
+ : '';
870
+
871
+ return `
872
+ <section class="section">
873
+ <div class="section-header">
874
+ <div class="section-icon">${icon.file()}</div>
875
+ <h2 class="section-title">Important Files</h2>
876
+ ${countBadge}
877
+ </div>
878
+ <div class="section-body" style="padding:0">
879
+ ${body}
880
+ </div>
881
+ </section>`;
882
+ }
883
+
884
+ function renderProjectHealth(health) {
885
+ const count = health.length;
886
+
887
+ const body = count === 0
888
+ ? `<p class="empty-msg" style="padding: 1.5rem">No project health observations available.</p>`
889
+ : `<ul class="entry-list">
890
+ ${health.map(h => {
891
+ let emoji = 'ℹ';
892
+ if (h.status === 'pass') emoji = '✓';
893
+ else if (h.status === 'warning') emoji = '⚠';
894
+ return `
895
+ <li class="entry-item">
896
+ <span>${esc(emoji)}</span>
897
+ ${esc(h.message)}
898
+ </li>`;
899
+ }).join('')}
900
+ </ul>`;
901
+
902
+ const countBadge = count > 0
903
+ ? `<span class="section-count">${count} observations</span>`
904
+ : '';
905
+
906
+ return `
907
+ <section class="section">
908
+ <div class="section-header">
909
+ <div class="section-icon">${icon.info()}</div>
910
+ <h2 class="section-title">Project Health</h2>
911
+ ${countBadge}
912
+ </div>
913
+ <div class="section-body">
914
+ ${body}
915
+ </div>
916
+ </section>`;
917
+ }
918
+
792
919
  function renderPackageScripts(scripts) {
793
920
  const count = scripts.length;
794
921
 
@@ -797,16 +924,21 @@ function renderPackageScripts(scripts) {
797
924
  : `<table class="data-table">
798
925
  <thead>
799
926
  <tr>
800
- <th>Script</th>
927
+ <th>Usage</th>
928
+ <th>Description</th>
801
929
  <th>Command</th>
802
930
  </tr>
803
931
  </thead>
804
932
  <tbody>
805
- ${scripts.map(s => `
933
+ ${scripts.map(s => {
934
+ const usage = s.usage || `npm run ${s.name}`;
935
+ return `
806
936
  <tr>
807
- <td><code>${esc(s.name)}</code></td>
937
+ <td><code>${esc(usage)}</code></td>
938
+ <td class="val-plain">${esc(s.description || '')}</td>
808
939
  <td class="val-plain"><code>${esc(s.command)}</code></td>
809
- </tr>`).join('')}
940
+ </tr>`;
941
+ }).join('')}
810
942
  </tbody>
811
943
  </table>`;
812
944
 
@@ -815,31 +947,34 @@ function renderPackageScripts(scripts) {
815
947
  : '';
816
948
 
817
949
  return `
818
- <div class="section">
950
+ <section class="section">
819
951
  <div class="section-header">
820
952
  <div class="section-icon">${icon.terminal()}</div>
821
- <span class="section-title">Package Scripts</span>
953
+ <h2 class="section-title">Package Scripts</h2>
822
954
  ${countBadge}
823
955
  </div>
824
- <div class="section-body" style="padding:0">${body}</div>
825
- </div>`;
956
+ <div class="section-body" style="padding:0">
957
+ ${body}
958
+ </div>
959
+ </section>`;
826
960
  }
827
961
 
828
962
  /**
829
- * Render the folder structure section with a dark <pre><code> tree.
963
+ * Render the folder structure section.
830
964
  *
965
+ * @param {import('../scanner/scan.js').DirNode} tree
831
966
  * @param {string[]} flatFiles
832
- * @param {string} rootName
967
+ * @param {string} rootName
833
968
  * @returns {string}
834
969
  */
835
- function renderFolderStructure(flatFiles, rootName) {
836
- const treeStr = buildTreeString(flatFiles, rootName);
970
+ function renderFolderStructure(tree, flatFiles, rootName) {
971
+ const treeStr = buildTreeString(tree, rootName, flatFiles.length);
837
972
 
838
973
  return `
839
- <div class="section">
974
+ <section class="section">
840
975
  <div class="section-header">
841
976
  <div class="section-icon">${icon.tree()}</div>
842
- <span class="section-title">Folder Structure</span>
977
+ <h2 class="section-title">Folder Structure</h2>
843
978
  ${flatFiles.length > 0 ? `<span class="section-count">${flatFiles.length} files</span>` : ''}
844
979
  </div>
845
980
  <div class="section-body">
@@ -847,7 +982,7 @@ function renderFolderStructure(flatFiles, rootName) {
847
982
  <pre><code>${esc(treeStr)}</code></pre>
848
983
  </div>
849
984
  </div>
850
- </div>`;
985
+ </section>`;
851
986
  }
852
987
 
853
988
  /**
@@ -868,14 +1003,14 @@ function renderStats(result) {
868
1003
  const rowsHTML = rows.map(([metric, value, isNum]) => `
869
1004
  <tr>
870
1005
  <td>${esc(metric)}</td>
871
- <td class="${isNum ? 'val' : 'val-plain'}">${value}</td>
1006
+ <td class="${isNum ? 'val' : 'val-plain'}">${esc(value)}</td>
872
1007
  </tr>`).join('');
873
1008
 
874
1009
  return `
875
- <div class="section">
1010
+ <section class="section">
876
1011
  <div class="section-header">
877
1012
  <div class="section-icon">${icon.bar()}</div>
878
- <span class="section-title">Statistics</span>
1013
+ <h2 class="section-title">Statistics</h2>
879
1014
  </div>
880
1015
  <div class="section-body">
881
1016
  <table class="data-table">
@@ -888,7 +1023,7 @@ function renderStats(result) {
888
1023
  <tbody>${rowsHTML}</tbody>
889
1024
  </table>
890
1025
  </div>
891
- </div>`;
1026
+ </section>`;
892
1027
  }
893
1028
 
894
1029
  /**
@@ -909,17 +1044,17 @@ function renderScanInfo() {
909
1044
  </tr>`).join('');
910
1045
 
911
1046
  return `
912
- <div class="section">
1047
+ <section class="section">
913
1048
  <div class="section-header">
914
1049
  <div class="section-icon">${icon.info()}</div>
915
- <span class="section-title">Scan Information</span>
1050
+ <h2 class="section-title">Scan Information</h2>
916
1051
  </div>
917
1052
  <div class="section-body">
918
1053
  <table class="data-table">
919
1054
  <tbody>${rowsHTML}</tbody>
920
1055
  </table>
921
1056
  </div>
922
- </div>`;
1057
+ </section>`;
923
1058
  }
924
1059
 
925
1060
  /**
@@ -950,7 +1085,7 @@ function renderFooter() {
950
1085
  * @param {{ cwd?: string }} [options]
951
1086
  */
952
1087
  export function render(result, options = {}) {
953
- const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles } = result;
1088
+ const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree, importantFiles = [], health = [], projectInfo, packageManager } = result;
954
1089
 
955
1090
  const cwd = options.cwd ?? process.cwd();
956
1091
  const relRoot = path.relative(cwd, rootPath) || '.';
@@ -974,11 +1109,15 @@ export function render(result, options = {}) {
974
1109
 
975
1110
  <main>
976
1111
  ${renderSummaryCards(result)}
1112
+ ${renderProjectInfo(projectInfo, packageManager)}
1113
+ ${renderProjectHealth(health)}
1114
+ ${renderImportantFiles(importantFiles)}
1115
+ ${renderFrameworks(projectType)}
977
1116
  ${renderEntryPoints(entryPoints)}
978
1117
  ${renderConfigurationFiles(configs)}
979
1118
  ${renderPackageScripts(scripts)}
980
- ${renderFolderStructure(flatFiles, rootName)}
981
1119
  ${renderStats(result)}
1120
+ ${renderFolderStructure(tree, flatFiles, rootName)}
982
1121
  ${renderScanInfo()}
983
1122
  </main>
984
1123
 
@@ -1,57 +1,167 @@
1
1
  /**
2
2
  * @fileoverview Toren — JSON Renderer
3
3
  *
4
- * Consumes a ScanResult and produces a clean JSON object for programmatic use.
4
+ * Consumes a ScanResult and produces a stable, consistently-ordered JSON
5
+ * object suitable for programmatic consumption.
6
+ *
7
+ * Design contract (mirrors all other renderers):
8
+ * - Accepts a ScanResult and an optional options object.
9
+ * - Never scans files or modifies the data it receives.
10
+ * - All arrays are always present — never undefined or missing.
11
+ * - Property order is fixed and documented below.
12
+ * - Backward compatible: no existing field is removed or renamed.
13
+ *
14
+ * Output property order:
15
+ * 1. meta — schema version + generator provenance
16
+ * 2. project — name (basename), path (relative), detected type
17
+ * 3. frameworks — derived array of detected frameworks ([] when none)
18
+ * 4. entryPoints — array of detected entry-point paths
19
+ * 5. configs — array of detected configuration file paths
20
+ * 6. scripts — array of { name, command } objects
21
+ * 7. statistics — file/folder counts and scan duration (structured)
22
+ * 8. structure — recursive file-tree array
23
+ * 9. summary — retained for backward compatibility (same data as statistics)
5
24
  */
6
25
 
7
26
  import path from 'node:path';
27
+ import { createRequire } from 'node:module';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const pkg = require('../../package.json');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Tree mapper
34
+ // ---------------------------------------------------------------------------
8
35
 
36
+ /**
37
+ * Recursively map an internal DirNode / FileNode to a clean JSON shape.
38
+ * Directory nodes always include a `children` array (never undefined).
39
+ *
40
+ * @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
41
+ * @returns {{ type: 'folder'|'file', name: string, children?: object[] }}
42
+ */
9
43
  function mapTree(node) {
10
44
  if (node.type === 'directory') {
11
45
  return {
12
- type: 'folder',
13
- name: node.name,
14
- children: (node.children || []).map(mapTree)
15
- };
16
- } else {
17
- return {
18
- type: 'file',
19
- name: node.name
46
+ type: 'folder',
47
+ name: node.name,
48
+ children: (node.children || []).map(mapTree),
20
49
  };
21
50
  }
51
+ return {
52
+ type: 'file',
53
+ name: node.name,
54
+ };
22
55
  }
23
56
 
57
+ // ---------------------------------------------------------------------------
58
+ // Helpers
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Derive a `frameworks` array from the scanner's `projectType` string.
63
+ * Returns a single-element array when a framework is detected, empty otherwise.
64
+ * This is a pure presentation decision — no business logic is added.
65
+ *
66
+ * @param {string} projectType
67
+ * @returns {string[]}
68
+ */
69
+ function deriveFrameworks(projectType) {
70
+ if (!projectType || projectType === 'Unknown') return [];
71
+ return [projectType];
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Public API
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Render a ScanResult as a stable JSON document to stdout.
80
+ *
81
+ * @param {import('../scanner/scan.js').ScanResult} result
82
+ * @param {{ cwd?: string }} [options]
83
+ */
24
84
  export function render(result, options = {}) {
25
85
  const {
26
86
  rootPath,
27
87
  projectType,
28
- entryPoints,
29
- configs = [],
30
- scripts = [],
88
+ entryPoints = [],
89
+ configs = [],
90
+ scripts = [],
91
+ packageManager,
92
+ importantFiles = [],
93
+ projectInfo = null,
94
+ health = [],
31
95
  tree,
32
- flatFiles,
33
- totalFolders,
96
+ flatFiles = [],
97
+ totalFolders = 0,
34
98
  scanDurationMs,
35
99
  } = result;
36
100
 
37
- const cwd = options.cwd ?? process.cwd();
38
- const relRoot = path.relative(cwd, rootPath) || '.';
101
+ const cwd = options.cwd ?? process.cwd();
102
+ const relRoot = path.relative(cwd, rootPath) || '.';
103
+ const rootName = path.basename(rootPath) || relRoot;
104
+
105
+ // Shared statistics values — computed once, used in both `statistics` and
106
+ // the backward-compatible `summary` block.
107
+ // Guard against NaN/undefined: JSON.stringify(NaN) produces null, breaking
108
+ // the schema guarantee that durationMs is always a number.
109
+ const totalFiles = flatFiles.length;
110
+ const durationMs = Number.isFinite(scanDurationMs) ? Math.round(scanDurationMs) : 0;
39
111
 
40
112
  const output = {
113
+ // 1. Provenance — lets consumers detect schema changes.
114
+ meta: {
115
+ generatedBy: 'Toren',
116
+ version: pkg.version,
117
+ schema: 1,
118
+ },
119
+
120
+ // 2. Project identity — name is the human-readable basename; path is the
121
+ // relative path used for filesystem resolution.
41
122
  project: {
123
+ name: (projectInfo && projectInfo.name) ? projectInfo.name : rootName,
42
124
  path: relRoot,
43
125
  type: projectType,
44
- framework: projectType,
126
+ packageManager: packageManager || null,
127
+ runtime: projectInfo ? projectInfo.runtime : null,
128
+ language: projectInfo ? projectInfo.language : null,
129
+ framework: projectInfo ? projectInfo.framework : null,
130
+ architecture: projectInfo ? projectInfo.architecture : null,
131
+ entryPoint: projectInfo ? projectInfo.entryPoint : null,
132
+ sourceDirectory: projectInfo ? projectInfo.sourceDirectory : null,
45
133
  },
134
+
135
+ // 3. Detected frameworks — always an array.
136
+ frameworks: deriveFrameworks(projectType),
137
+
138
+ // 4–6. Discovery results — all arrays, always present, never null.
139
+ entryPoints: Array.isArray(entryPoints) ? entryPoints : [],
140
+ configs: Array.isArray(configs) ? configs : [],
141
+ scripts: Array.isArray(scripts) ? scripts : [],
142
+
143
+ importantFiles: Array.isArray(importantFiles) ? importantFiles : [],
144
+ health: Array.isArray(health) ? health : [],
145
+
146
+ // 7. Structured statistics — always a complete object with numeric values.
147
+ // durationMs is rounded to the nearest millisecond (integer).
148
+ statistics: {
149
+ files: totalFiles,
150
+ folders: totalFolders,
151
+ durationMs,
152
+ },
153
+
154
+ // 8. File-tree — array of root-level nodes; always present.
155
+ // Empty array when the scanned directory is empty.
156
+ structure: (tree?.children || []).map(mapTree),
157
+
158
+ // 9. Backward-compatible summary block — preserved for existing consumers.
159
+ // Contains the same data under the original field names.
46
160
  summary: {
47
- totalFiles: flatFiles.length,
161
+ totalFiles,
48
162
  totalFolders,
49
- scanDurationMs: Math.round(scanDurationMs),
163
+ scanDurationMs: durationMs,
50
164
  },
51
- entryPoints,
52
- configs,
53
- scripts,
54
- structure: (tree.children || []).map(mapTree)
55
165
  };
56
166
 
57
167
  console.log(JSON.stringify(output, null, 2));