laporan-generator 2.6.2 → 2.7.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.
@@ -2,11 +2,19 @@
2
2
  """report-doctor.py: Audit komprehensif kesehatan dan integritas proyek laporan."""
3
3
 
4
4
  import glob
5
+ import json
5
6
  import os
6
7
  import re
7
8
  import shutil
8
9
  import sys
9
10
 
11
+ if sys.platform == "win32":
12
+ try:
13
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
14
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
15
+ except Exception:
16
+ pass
17
+
10
18
  GREEN = "\033[0;32m"
11
19
  BLUE = "\033[0;34m"
12
20
  CYAN = "\033[0;36m"
@@ -98,101 +106,166 @@ def check_style_anomalies(markdown_files):
98
106
 
99
107
 
100
108
  def main():
101
- root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
102
- os.chdir(root)
103
-
104
- print(f"{CYAN}{BOLD}")
105
- print(" ========================================================")
106
- print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
107
- print(" ========================================================")
108
- print(f"{NC}")
109
+ json_mode = "--json" in sys.argv
110
+ pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
111
+
112
+ target_dir = os.getcwd()
113
+ if pos_args:
114
+ target_dir = os.path.abspath(pos_args[0])
115
+ elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
116
+ pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
117
+ if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
118
+ target_dir = pkg_root
119
+
120
+ os.chdir(target_dir)
121
+
122
+ if not json_mode:
123
+ print(f"{CYAN}{BOLD}")
124
+ print(" ========================================================")
125
+ print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
126
+ print(" ========================================================")
127
+ print(f"{NC}")
109
128
 
110
129
  errors = 0
111
130
  warnings = 0
112
131
 
113
132
  # 1. Dependensi
114
- print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
133
+ if not json_mode:
134
+ print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
115
135
  deps, has_im = check_dependencies()
136
+ dep_list = []
116
137
  for cmd, name, req, found in deps:
117
138
  if "ImageMagick" in name:
118
139
  continue
119
- if found:
120
- print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
121
- else:
122
- if req:
123
- print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
124
- errors += 1
140
+ dep_list.append({"command": cmd, "name": name, "required": req, "found": found})
141
+ if not json_mode:
142
+ if found:
143
+ print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
125
144
  else:
126
- print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
145
+ if req:
146
+ print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
147
+ else:
148
+ print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
149
+ if not found and req:
150
+ errors += 1
127
151
 
128
- if has_im:
129
- print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
130
- else:
131
- print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
132
- print("")
152
+ dep_list.append({"command": "magick/convert", "name": "ImageMagick", "required": False, "found": has_im})
153
+ if not json_mode:
154
+ if has_im:
155
+ print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
156
+ else:
157
+ print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
158
+ print("")
133
159
 
134
160
  # 2. Berkas & Struktur
135
- print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
161
+ if not json_mode:
162
+ print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
136
163
  req_files = [
137
164
  "metadata.yml", "template.typ", "reference.docx",
138
165
  "references.bib", "docx.lua", "cover.md"
139
166
  ]
167
+ file_status = []
140
168
  for rf in req_files:
141
- if os.path.isfile(rf):
142
- print(f" {GREEN}[OK]{NC} Berkas {rf}")
143
- else:
144
- print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
169
+ exists = os.path.isfile(rf)
170
+ file_status.append({"file": rf, "found": exists})
171
+ if not json_mode:
172
+ if exists:
173
+ print(f" {GREEN}[OK]{NC} Berkas {rf}")
174
+ else:
175
+ print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
176
+ if not exists:
145
177
  errors += 1
146
178
 
147
179
  chapter_files = sorted(glob.glob("chapters/*.md"))
148
- if chapter_files:
149
- print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
150
- else:
151
- print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
180
+ if not json_mode:
181
+ if chapter_files:
182
+ print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
183
+ else:
184
+ print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
185
+ if not chapter_files:
152
186
  errors += 1
153
- print("")
187
+ if not json_mode:
188
+ print("")
154
189
 
155
190
  # 3. Sitasi & Referensi
156
- print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
191
+ if not json_mode:
192
+ print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
157
193
  bib_keys, cited_keys, missing_keys, orphan_keys = check_citations("references.bib", chapter_files)
158
- print(f" * Total entri .bib : {len(bib_keys)}")
159
- print(f" * Total sitasi di teks : {len(cited_keys)}")
194
+ if not json_mode:
195
+ print(f" * Total entri .bib : {len(bib_keys)}")
196
+ print(f" * Total sitasi di teks : {len(cited_keys)}")
160
197
 
161
198
  if missing_keys:
162
199
  for k, locs in missing_keys.items():
163
200
  loc_str = ", ".join([f"{f}:{l}" for f, l in locs])
164
- print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
201
+ if not json_mode:
202
+ print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
165
203
  errors += 1
166
- else:
204
+ elif not json_mode:
167
205
  print(f" {GREEN}[OK]{NC} Semua sitasi dalam teks valid terdaftar di references.bib")
168
206
 
169
207
  if orphan_keys:
170
- print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
171
- for ok in list(orphan_keys)[:5]:
172
- print(f" - @{ok}")
173
- if len(orphan_keys) > 5:
174
- print(f" - ... dan {len(orphan_keys) - 5} lainnya")
208
+ if not json_mode:
209
+ print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
210
+ for ok in list(orphan_keys)[:5]:
211
+ print(f" - @{ok}")
212
+ if len(orphan_keys) > 5:
213
+ print(f" - ... dan {len(orphan_keys) - 5} lainnya")
175
214
  warnings += 1
176
- print("")
215
+ if not json_mode:
216
+ print("")
177
217
 
178
218
  # 4. Gambar & Format Teks
179
- print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
219
+ if not json_mode:
220
+ print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
180
221
  total_imgs, broken_imgs = check_images(chapter_files)
181
222
  if broken_imgs:
182
223
  for p, f, l in broken_imgs:
183
- print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
224
+ if not json_mode:
225
+ print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
184
226
  errors += 1
185
- else:
227
+ elif not json_mode:
186
228
  print(f" {GREEN}[OK]{NC} Semua tautan gambar ({total_imgs} gambar) valid dan ada di disk")
187
229
 
188
230
  anomalies = check_style_anomalies(chapter_files)
189
231
  if anomalies:
190
232
  for f, l, msg in anomalies:
191
- print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
233
+ if not json_mode:
234
+ print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
192
235
  warnings += 1
193
- else:
236
+ elif not json_mode:
194
237
  print(f" {GREEN}[OK]{NC} Bebas dari manual heading numbering dan box-drawing characters")
195
- print("")
238
+ if not json_mode:
239
+ print("")
240
+
241
+ score = 100 if (errors == 0 and warnings == 0) else (95 if errors == 0 else max(0, 90 - errors * 15 - warnings * 5))
242
+ status_label = "HEALTHY" if errors == 0 and warnings == 0 else ("WARNING" if errors == 0 else "ERROR")
243
+
244
+ if json_mode:
245
+ result = {
246
+ "status": status_label,
247
+ "score": score,
248
+ "errors_count": errors,
249
+ "warnings_count": warnings,
250
+ "target_dir": os.getcwd(),
251
+ "dependencies": dep_list,
252
+ "required_files": file_status,
253
+ "chapters_count": len(chapter_files),
254
+ "chapters": [os.path.basename(c) for c in chapter_files],
255
+ "citations": {
256
+ "bib_count": len(bib_keys),
257
+ "cited_count": len(cited_keys),
258
+ "missing_keys": {k: [f"{f}:{l}" for f, l in locs] for k, locs in missing_keys.items()},
259
+ "orphan_keys": list(orphan_keys),
260
+ },
261
+ "images": {
262
+ "total": total_imgs,
263
+ "broken": [{"path": p, "file": f, "line": l} for p, f, l in broken_imgs],
264
+ },
265
+ "anomalies": [{"file": f, "line": l, "message": msg} for f, l, msg in anomalies],
266
+ }
267
+ print(json.dumps(result, indent=2))
268
+ return 1 if errors > 0 else 0
196
269
 
197
270
  # Skor Kesehatan
198
271
  print(" ========================================================")
@@ -2,6 +2,7 @@
2
2
  """report-stats.py: Menghitung statistik komprehensif dokumen laporan akademik."""
3
3
 
4
4
  import glob
5
+ import json
5
6
  import os
6
7
  import re
7
8
  import sys
@@ -55,8 +56,18 @@ def count_file_stats(filepath):
55
56
 
56
57
 
57
58
  def main():
58
- root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
59
- os.chdir(root)
59
+ json_mode = "--json" in sys.argv
60
+ pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
61
+
62
+ target_dir = os.getcwd()
63
+ if pos_args:
64
+ target_dir = os.path.abspath(pos_args[0])
65
+ elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
66
+ pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
67
+ if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
68
+ target_dir = pkg_root
69
+
70
+ os.chdir(target_dir)
60
71
 
61
72
  title = "Laporan Akademik"
62
73
  preset = "standard"
@@ -116,6 +127,39 @@ def main():
116
127
  sz = os.path.getsize("Laporan.docx")
117
128
  docx_size = f"{sz / 1024:.1f} KB"
118
129
 
130
+ if json_mode:
131
+ result = {
132
+ "title": title,
133
+ "preset": preset,
134
+ "target_dir": os.getcwd(),
135
+ "chapters_count": len(chapter_files),
136
+ "total_words": total_words,
137
+ "total_chars": total_chars,
138
+ "est_pages": est_pages,
139
+ "read_mins": read_mins,
140
+ "headings": {
141
+ "h1": total_h1,
142
+ "h2": total_h2,
143
+ "h3": total_h3,
144
+ },
145
+ "elements": {
146
+ "images": total_images,
147
+ "tables": total_tables,
148
+ "equations": total_equations,
149
+ },
150
+ "citations": {
151
+ "unique_cited": len(all_citations),
152
+ "bib_entries": bib_count,
153
+ },
154
+ "compiled_files": {
155
+ "pdf": pdf_size,
156
+ "docx": docx_size,
157
+ },
158
+ "file_breakdown": [{"filename": f, "words": c} for f, c in file_breakdown],
159
+ }
160
+ print(json.dumps(result, indent=2))
161
+ return
162
+
119
163
  print(f"{CYAN}{BOLD}")
120
164
  print(" ========================================================")
121
165
  print(" LAPORAN GENERATOR - STATISTIK DOKUMEN ")
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  """test_scripts.py: Automated unit tests for Laporan Generator Python helper scripts."""
3
3
 
4
+ import json
4
5
  import os
6
+ import subprocess
5
7
  import sys
6
8
  import unittest
7
9
 
@@ -36,16 +38,39 @@ class TestScanPreset(unittest.TestCase):
36
38
  self.assertIn("UNIVERSITAS INDONESIA", inst)
37
39
 
38
40
 
41
+ def get_presets_dir():
42
+ for c in [
43
+ os.path.join(ROOT_DIR, "presets"),
44
+ os.path.join(ROOT_DIR, "resources", "presets"),
45
+ os.path.join(ROOT_DIR, "..", "..", "..", "presets"),
46
+ ]:
47
+ if os.path.isdir(c):
48
+ return os.path.abspath(c)
49
+ return os.path.join(ROOT_DIR, "presets")
50
+
51
+
52
+ def get_project_root():
53
+ for c in [
54
+ ROOT_DIR,
55
+ os.path.join(ROOT_DIR, "resources"),
56
+ os.path.join(ROOT_DIR, "..", "..", ".."),
57
+ ]:
58
+ if os.path.isfile(os.path.join(c, "metadata.yml")):
59
+ return os.path.abspath(c)
60
+ return ROOT_DIR
61
+
62
+
39
63
  class TestValidatePreset(unittest.TestCase):
40
64
  def test_parse_simple_yaml(self):
41
- test_yaml = os.path.join(ROOT_DIR, "presets", "standard.yml")
65
+ presets_dir = get_presets_dir()
66
+ test_yaml = os.path.join(presets_dir, "standard.yml")
42
67
  self.assertTrue(os.path.exists(test_yaml))
43
68
  data = validate_preset.parse_simple_yaml(test_yaml)
44
69
  self.assertEqual(data.get("preset_id"), "standard")
45
70
  self.assertEqual(data.get("margin_top"), "2cm")
46
71
 
47
72
  def test_validate_all_presets(self):
48
- presets_dir = os.path.join(ROOT_DIR, "presets")
73
+ presets_dir = get_presets_dir()
49
74
  for f in os.listdir(presets_dir):
50
75
  if f.endswith(".yml"):
51
76
  path = os.path.join(presets_dir, f)
@@ -128,7 +153,7 @@ report_stats = __import__("report-stats")
128
153
 
129
154
  class TestReportStats(unittest.TestCase):
130
155
  def test_count_file_stats_structure(self):
131
- sample_md = os.path.join(ROOT_DIR, "cover.md")
156
+ sample_md = os.path.join(get_project_root(), "cover.md")
132
157
  if os.path.exists(sample_md):
133
158
  stats = report_stats.count_file_stats(sample_md)
134
159
  self.assertIsInstance(stats, dict)
@@ -136,6 +161,28 @@ class TestReportStats(unittest.TestCase):
136
161
  self.assertIsInstance(stats["chars"], int)
137
162
  self.assertGreaterEqual(stats["words"], 0)
138
163
 
164
+ def test_json_stats_execution(self):
165
+ proj_root = get_project_root()
166
+ cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-stats.py"), proj_root, "--json"]
167
+ proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
168
+ self.assertEqual(proc.returncode, 0)
169
+ data = json.loads(proc.stdout)
170
+ self.assertIn("total_words", data)
171
+ self.assertIn("preset", data)
172
+ self.assertIn("target_dir", data)
173
+
174
+
175
+ class TestReportDoctor(unittest.TestCase):
176
+ def test_json_doctor_execution(self):
177
+ proj_root = get_project_root()
178
+ cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-doctor.py"), proj_root, "--json"]
179
+ proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
180
+ data = json.loads(proc.stdout)
181
+ self.assertIn("status", data)
182
+ self.assertIn("dependencies", data)
183
+ self.assertIn("required_files", data)
184
+ self.assertIn("citations", data)
185
+
139
186
 
140
187
  if __name__ == "__main__":
141
188
  unittest.main(verbosity=2)