@pukujan/create-modular-monolith 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +94 -23
  2. package/index.js +47 -0
  3. package/package.json +16 -19
  4. package/template/.cursor/commands/planning-study-log.md +25 -0
  5. package/template/.cursor/commands/pre-push-dev-log.md +52 -0
  6. package/template/.cursor/rules/api-documentation.mdc +21 -0
  7. package/template/.cursor/rules/file-exchange-inbox.mdc +29 -0
  8. package/template/.github/workflows/ci.yml +44 -0
  9. package/template/AGENTS.md +41 -0
  10. package/template/README.md +25 -55
  11. package/template/backend/.env.example +38 -0
  12. package/template/backend/package-lock.json +1118 -24
  13. package/template/backend/package.json +14 -4
  14. package/template/backend/scripts/check-module-boundaries.mjs +3 -0
  15. package/template/backend/src/modules/model-condenser/README.md +7 -0
  16. package/template/backend/src/modules/model-condenser/config/index.js +20 -0
  17. package/template/backend/src/modules/model-condenser/events/index.js +1 -0
  18. package/template/backend/src/modules/model-condenser/index.js +12 -0
  19. package/template/backend/src/modules/model-condenser/routes/health.routes.js +10 -0
  20. package/template/backend/src/modules/model-condenser/routes/index.js +10 -0
  21. package/template/backend/src/modules/model-condenser/routes/modelCondenser.routes.js +44 -0
  22. package/template/backend/src/modules/model-condenser/services/health.service.js +8 -0
  23. package/template/backend/src/modules/model-condenser/services/modelCondenser.facade.js +58 -0
  24. package/template/backend/src/modules/model-condenser/services/modelCondenser.service.js +513 -0
  25. package/template/backend/src/modules/model-condenser/tests/integration/modelCondenser.routes.test.js +40 -0
  26. package/template/backend/src/modules/model-condenser/tests/unit/modelCondenser.service.test.js +31 -0
  27. package/template/backend/src/modules/model-condenser/utils/index.js +1 -0
  28. package/template/backend/src/shared/contracts/consolidatedExports.contract.js +19 -0
  29. package/template/backend/src/shared/contracts/prePushDevLog.contract.js +28 -0
  30. package/template/backend/src/shared/domain/case-filing/core-models.js +117 -0
  31. package/template/backend/src/shared/http/errors.js +8 -0
  32. package/template/backend/src/shared/utils/consolidatedExport.js +30 -0
  33. package/template/backend/src/shared/utils/formatExchangeTimestamp.js +47 -0
  34. package/template/backend/src/shared/utils/formatExchangeTimestamp.test.js +30 -0
  35. package/template/backend/src/shared/utils/traceId.js +19 -0
  36. package/template/docs/API.md +42 -0
  37. package/template/docs/PUBLISHING.md +13 -1
  38. package/template/docs/README.md +7 -1
  39. package/template/docs/STARTER_PACK.md +4 -0
  40. package/template/docs/architecture/API_DOCUMENTATION_CONTRACT.md +112 -0
  41. package/template/docs/architecture/CONTRACTS_OVERVIEW.md +180 -0
  42. package/template/docs/architecture/EVAL_AND_CI.md +79 -0
  43. package/template/docs/architecture/MODULE_INTERNAL_CONTRACT.md +2 -0
  44. package/template/docs/architecture/PLATFORM_ARCHITECTURE.md +221 -0
  45. package/template/docs/architecture/REPO_ARTIFACT_LAYOUT.md +33 -0
  46. package/template/docs/architecture/contracts/apiDocumentationRegistry.contract.md +40 -0
  47. package/template/docs/architecture/contracts/changelog.jsonl +12 -0
  48. package/template/docs/architecture/contracts/consolidatedExports.contract.md +58 -0
  49. package/template/docs/architecture/contracts/fileExchange.contract.md +47 -0
  50. package/template/docs/architecture/contracts/manifest.json +39 -0
  51. package/template/docs/architecture/contracts/prePushDevLog.contract.md +69 -0
  52. package/template/docs/model-condenser/API.md +102 -0
  53. package/template/file-exchange/README.md +41 -0
  54. package/template/file-exchange/exports/.gitkeep +0 -0
  55. package/template/file-exchange/exports/consolidated-models.json +625 -0
  56. package/template/file-exchange/imports/.gitkeep +0 -0
  57. package/template/frontend/.env.example +2 -0
  58. package/template/frontend/package-lock.json +125 -122
  59. package/template/frontend/package.json +1 -1
  60. package/template/frontend/src/index.css +311 -0
  61. package/template/frontend/src/modules/_reference/services/health-api.js +1 -1
  62. package/template/frontend/src/shared/api/client.js +67 -5
  63. package/template/models/.gitkeep +0 -0
  64. package/template/package.json +13 -4
  65. package/template/scripts/check-api-docs.mjs +183 -0
  66. package/template/scripts/condense-file-structure.mjs +44 -0
  67. package/template/scripts/condense-models.mjs +70 -0
  68. package/template/scripts/condense-prompts.mjs +161 -0
  69. package/template/scripts/consolidated-output.mjs +49 -0
  70. package/template/scripts/export-consolidated-models.mjs +11 -0
  71. package/template/scripts/git-hooks/pre-push.sample +15 -0
  72. package/template/scripts/import-to-file-exchange.mjs +43 -0
  73. package/template/scripts/lib/api-inventory.mjs +182 -0
  74. package/template/scripts/lib/dev-log-human-format.mjs +360 -0
  75. package/template/scripts/lib/git-snapshot.mjs +46 -0
  76. package/template/scripts/lib/module-scaffold.mjs +37 -1
  77. package/template/scripts/lib/repo-tree.mjs +127 -0
  78. package/template/scripts/lib/run-tests.mjs +60 -0
  79. package/template/scripts/lint-contracts.mjs +57 -0
  80. package/template/scripts/lint-repo-artifacts.mjs +37 -0
  81. package/template/scripts/new-module.mjs +7 -0
  82. package/template/scripts/resolve-import-stamp.mjs +50 -0
  83. package/template/scripts/verify-dev-log.mjs +50 -0
  84. package/template/scripts/write-pre-push-dev-log.mjs +220 -0
  85. package/template/work-log/INDEX.md +3 -0
  86. package/template/work-log/README.md +40 -0
  87. package/template/work-log/dev-logs/README.md +97 -0
  88. package/template/work-log/dev-logs/schemas/dev-log-agent.v1.schema.json +119 -0
  89. package/template/work-log/dev-logs/templates/dev-log-human.template.md +10 -0
  90. package/template/work-log/handoffs/README.md +36 -0
  91. package/template/work-log/study-docs/README.md +13 -0
  92. package/bin/create-modular-monolith.js +0 -132
  93. package/template/backend/src/modules/_reference/evals/README.md +0 -6
  94. package/template/backend/src/modules/_reference/evals/datasets/example.cases.json +0 -12
  95. package/template/backend/src/modules/_reference/evals/runners/example.eval.mjs +0 -25
  96. package/template/scripts/sync-cli-template.mjs +0 -44
  97. /package/template/{frontend/src/modules → backend/db/migrations}/.gitkeep +0 -0
@@ -51,3 +51,314 @@ a {
51
51
  .muted {
52
52
  color: #a8b4d8;
53
53
  }
54
+
55
+ .upload-panel {
56
+ margin-top: 20px;
57
+ }
58
+
59
+ .upload-label {
60
+ display: block;
61
+ margin-bottom: 8px;
62
+ font-weight: 600;
63
+ }
64
+
65
+ .upload-panel input[type="file"] {
66
+ display: block;
67
+ width: 100%;
68
+ margin-bottom: 12px;
69
+ padding: 10px;
70
+ border: 1px dashed #3657a7;
71
+ border-radius: 8px;
72
+ background: #0b1020;
73
+ color: #f5f7ff;
74
+ }
75
+
76
+ .upload-actions {
77
+ display: flex;
78
+ gap: 12px;
79
+ align-items: center;
80
+ flex-wrap: wrap;
81
+ }
82
+
83
+ .upload-actions button {
84
+ background: #3657a7;
85
+ color: #fff;
86
+ border: none;
87
+ border-radius: 8px;
88
+ padding: 10px 16px;
89
+ cursor: pointer;
90
+ }
91
+
92
+ .upload-actions button:disabled {
93
+ opacity: 0.6;
94
+ cursor: not-allowed;
95
+ }
96
+
97
+ .error-text {
98
+ color: #ff8f8f;
99
+ margin-top: 12px;
100
+ }
101
+
102
+ .results-panel,
103
+ .documents-list {
104
+ margin-top: 20px;
105
+ }
106
+
107
+ .result-card {
108
+ border: 1px solid #233257;
109
+ border-radius: 8px;
110
+ padding: 12px;
111
+ margin-top: 12px;
112
+ background: #0b1020;
113
+ }
114
+
115
+ .result-card pre {
116
+ white-space: pre-wrap;
117
+ word-break: break-word;
118
+ margin: 12px 0 0;
119
+ font-size: 13px;
120
+ line-height: 1.5;
121
+ }
122
+
123
+ .documents-list ul {
124
+ padding-left: 20px;
125
+ }
126
+
127
+ .panel {
128
+ border: 1px solid #233257;
129
+ border-radius: 8px;
130
+ padding: 14px;
131
+ margin-top: 16px;
132
+ background: #0b1020;
133
+ }
134
+
135
+ .rule-textarea {
136
+ width: 100%;
137
+ margin-top: 8px;
138
+ padding: 10px;
139
+ border: 1px solid #3657a7;
140
+ border-radius: 8px;
141
+ background: #121a31;
142
+ color: #f5f7ff;
143
+ font-family: inherit;
144
+ resize: vertical;
145
+ }
146
+
147
+ .rule-upload-row {
148
+ display: flex;
149
+ flex-wrap: wrap;
150
+ align-items: center;
151
+ gap: 12px;
152
+ margin-top: 12px;
153
+ }
154
+
155
+ .status-panel {
156
+ margin-top: 16px;
157
+ }
158
+
159
+ .results-panel details {
160
+ margin-top: 12px;
161
+ }
162
+
163
+ .results-panel summary {
164
+ cursor: pointer;
165
+ font-weight: 600;
166
+ margin-bottom: 8px;
167
+ }
168
+
169
+ .file-dropzone {
170
+ display: flex;
171
+ flex-wrap: wrap;
172
+ align-items: center;
173
+ gap: 12px;
174
+ margin-top: 10px;
175
+ padding: 16px;
176
+ border: 1px dashed #3657a7;
177
+ border-radius: 8px;
178
+ background: #121a31;
179
+ }
180
+
181
+ .pdf-filings-dropzone {
182
+ margin-top: 16px;
183
+ padding: 4px;
184
+ border-radius: 10px;
185
+ border: 2px dashed transparent;
186
+ transition: border-color 0.15s ease, background 0.15s ease;
187
+ }
188
+
189
+ .pdf-filings-dropzone.drag-active {
190
+ border-color: #6b9bff;
191
+ background: rgba(54, 87, 167, 0.12);
192
+ }
193
+
194
+ .pdf-filings-dropzone.disabled {
195
+ opacity: 0.7;
196
+ pointer-events: none;
197
+ }
198
+
199
+ .file-dropzone-inner {
200
+ display: flex;
201
+ flex-wrap: wrap;
202
+ align-items: center;
203
+ gap: 12px;
204
+ margin-top: 10px;
205
+ padding: 16px;
206
+ border: 1px dashed #3657a7;
207
+ border-radius: 8px;
208
+ background: #121a31;
209
+ }
210
+
211
+ .filing-upload-panel {
212
+ margin-top: 0;
213
+ }
214
+
215
+ .filing-upload-panel .supported-types {
216
+ font-size: 13px;
217
+ margin-top: 4px;
218
+ }
219
+
220
+ .pdf-file-list {
221
+ margin-top: 12px;
222
+ }
223
+
224
+ .file-list-header {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: space-between;
228
+ gap: 12px;
229
+ }
230
+
231
+ .file-list-header h3 {
232
+ margin: 0;
233
+ }
234
+
235
+ .file-list-hint {
236
+ margin-top: 12px;
237
+ margin-bottom: 0;
238
+ }
239
+
240
+ .link-button {
241
+ background: none;
242
+ border: none;
243
+ color: #a8c1ff;
244
+ cursor: pointer;
245
+ padding: 0;
246
+ font-size: 14px;
247
+ }
248
+
249
+ .link-button:hover {
250
+ text-decoration: underline;
251
+ }
252
+
253
+ .remove-file {
254
+ margin-left: 12px;
255
+ }
256
+
257
+ .pdf-file-list li {
258
+ margin-bottom: 6px;
259
+ }
260
+
261
+ .file-input-native-hidden {
262
+ display: none;
263
+ }
264
+
265
+ .file-picker-button {
266
+ display: inline-block;
267
+ background: #3657a7;
268
+ color: #fff;
269
+ border: none;
270
+ border-radius: 8px;
271
+ padding: 10px 16px;
272
+ cursor: pointer;
273
+ font: inherit;
274
+ font-weight: 600;
275
+ }
276
+
277
+ .file-picker-button:hover:not(:disabled) {
278
+ background: #4a6bc0;
279
+ }
280
+
281
+ .file-picker-button:disabled {
282
+ opacity: 0.6;
283
+ cursor: not-allowed;
284
+ }
285
+
286
+ .eval-panel {
287
+ margin-bottom: 20px;
288
+ padding-bottom: 0;
289
+ }
290
+
291
+ .eval-report-list {
292
+ display: flex;
293
+ flex-direction: column;
294
+ gap: 12px;
295
+ margin-top: 12px;
296
+ }
297
+
298
+ .eval-card {
299
+ border: 1px solid #233257;
300
+ border-radius: 8px;
301
+ padding: 12px;
302
+ background: #121a31;
303
+ }
304
+
305
+ .eval-card-header {
306
+ display: flex;
307
+ flex-wrap: wrap;
308
+ align-items: center;
309
+ gap: 8px;
310
+ margin-bottom: 8px;
311
+ }
312
+
313
+ .eval-badge {
314
+ font-size: 12px;
315
+ padding: 2px 8px;
316
+ border-radius: 999px;
317
+ text-transform: uppercase;
318
+ font-weight: 600;
319
+ }
320
+
321
+ .eval-status-pass {
322
+ border-color: #2d6a4f;
323
+ }
324
+
325
+ .eval-status-pass .eval-badge {
326
+ background: #1b4332;
327
+ color: #95d5b2;
328
+ }
329
+
330
+ .eval-status-partial {
331
+ border-color: #b08900;
332
+ }
333
+
334
+ .eval-status-partial .eval-badge {
335
+ background: #5c4a00;
336
+ color: #ffd166;
337
+ }
338
+
339
+ .eval-status-fail {
340
+ border-color: #9b2226;
341
+ }
342
+
343
+ .eval-status-fail .eval-badge {
344
+ background: #5c1a1d;
345
+ color: #ff8f8f;
346
+ }
347
+
348
+ .eval-critical ul,
349
+ .eval-scores ul,
350
+ .eval-mismatches {
351
+ margin: 8px 0 0;
352
+ padding-left: 20px;
353
+ }
354
+
355
+ .eval-scores ul li {
356
+ display: flex;
357
+ justify-content: space-between;
358
+ gap: 12px;
359
+ }
360
+
361
+ .eval-notes {
362
+ margin-top: 8px;
363
+ font-size: 13px;
364
+ }
@@ -1,4 +1,4 @@
1
- import { apiGet } from "../../shared/api/client.js";
1
+ import { apiGet } from "../../../shared/api/client.js";
2
2
 
3
3
  export function fetchModuleHealth() {
4
4
  return apiGet("/api/_reference/health");
@@ -1,10 +1,72 @@
1
1
  const BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3001";
2
2
 
3
- export async function apiGet(path) {
4
- const response = await fetch(`${BASE_URL}${path}`);
3
+ async function readResponseBody(response) {
4
+ const raw = await response.text();
5
+ if (!raw) return null;
6
+
7
+ try {
8
+ return JSON.parse(raw);
9
+ } catch {
10
+ return raw;
11
+ }
12
+ }
13
+
14
+ function errorMessageFromBody(body, status) {
15
+ if (typeof body === "string" && body.trim()) {
16
+ return body.trim();
17
+ }
18
+ if (body && typeof body === "object") {
19
+ return body.error || body.message || `Request failed: ${status}`;
20
+ }
21
+ return `Request failed: ${status}`;
22
+ }
23
+
24
+ async function parseResponse(response) {
25
+ const body = await readResponseBody(response);
26
+
5
27
  if (!response.ok) {
6
- const text = await response.text();
7
- throw new Error(text || `Request failed: ${response.status}`);
28
+ throw new Error(errorMessageFromBody(body, response.status));
8
29
  }
9
- return response.json();
30
+
31
+ return body;
32
+ }
33
+
34
+ export async function apiGet(path) {
35
+ const response = await fetch(`${BASE_URL}${path}`);
36
+ return parseResponse(response);
37
+ }
38
+
39
+ export async function apiPost(path, body) {
40
+ const response = await fetch(`${BASE_URL}${path}`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify(body)
44
+ });
45
+ return parseResponse(response);
46
+ }
47
+
48
+ export async function apiPostForm(path, formData) {
49
+ const response = await fetch(`${BASE_URL}${path}`, {
50
+ method: "POST",
51
+ body: formData
52
+ });
53
+ return parseResponse(response);
54
+ }
55
+
56
+ export async function apiPatch(path, body) {
57
+ const response = await fetch(`${BASE_URL}${path}`, {
58
+ method: "PATCH",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify(body)
61
+ });
62
+ return parseResponse(response);
63
+ }
64
+
65
+ export async function apiDelete(path, body) {
66
+ const response = await fetch(`${BASE_URL}${path}`, {
67
+ method: "DELETE",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify(body ?? {})
70
+ });
71
+ return parseResponse(response);
10
72
  }
File without changes
@@ -1,16 +1,25 @@
1
1
  {
2
- "name": "modular-monolith-starter",
2
+ "name": "my-modular-monolith",
3
3
  "private": true,
4
4
  "version": "2.0.0",
5
5
  "scripts": {
6
- "sync:cli-template": "node scripts/sync-cli-template.mjs",
7
6
  "dev:backend": "npm --prefix backend run dev",
8
7
  "dev:frontend": "npm --prefix frontend run dev",
9
8
  "lint:boundaries": "npm --prefix backend run lint:boundaries",
10
9
  "lint:layers": "npm --prefix backend run lint:layers",
10
+ "lint:api-docs": "node scripts/check-api-docs.mjs",
11
+ "lint:contracts": "node scripts/lint-contracts.mjs",
12
+ "lint:repo-artifacts": "node scripts/lint-repo-artifacts.mjs",
11
13
  "lint:architecture": "npm --prefix backend run lint:architecture",
12
14
  "test": "npm --prefix backend test && npm --prefix frontend test",
13
- "test:evals": "node scripts/run-module-evals.mjs",
14
- "new:module": "node scripts/new-module.mjs"
15
+ "test:evals": "npm --prefix backend run test:evals",
16
+ "test:ci": "npm run lint:contracts && npm run lint:repo-artifacts && npm run lint:architecture && npm test && npm run test:evals",
17
+ "new:module": "node scripts/new-module.mjs",
18
+ "condense-prompts": "node scripts/condense-prompts.mjs",
19
+ "condense-file-structure": "node scripts/condense-file-structure.mjs",
20
+ "condense:all": "npm run condense-prompts && npm run condense-file-structure && npm --prefix backend run condense-models -- --local",
21
+ "dev-log:pre-push": "node scripts/write-pre-push-dev-log.mjs",
22
+ "dev-log:verify": "node scripts/verify-dev-log.mjs",
23
+ "import:file-exchange": "node scripts/import-to-file-exchange.mjs"
15
24
  }
16
25
  }
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ensures every Express route under backend/src/modules is documented in:
4
+ * - docs/<module>/API.md (path + method)
5
+ * - docs/API.md endpoint registry (full path + method)
6
+ */
7
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const modulesDir = join(repoRoot, "backend/src/modules");
13
+ const masterApiPath = join(repoRoot, "docs/API.md");
14
+
15
+ const SKIP_MODULES = new Set(["_reference"]);
16
+ const ROUTE_RE = /router\.(get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]/gi;
17
+ const BASE_PATH_RE = /app\.use\(\s*["'`](\/api\/[^"'`]+)["'`]/;
18
+
19
+ function readText(path) {
20
+ return readFileSync(path, "utf8");
21
+ }
22
+
23
+ function listRouteFiles(moduleDir) {
24
+ const routesDir = join(moduleDir, "routes");
25
+ if (!existsSync(routesDir)) return [];
26
+ return readdirSync(routesDir)
27
+ .filter((f) => f.endsWith(".js"))
28
+ .map((f) => join(routesDir, f));
29
+ }
30
+
31
+ function extractRoutesFromFile(filePath) {
32
+ const content = readText(filePath);
33
+ const routes = [];
34
+ let match;
35
+ ROUTE_RE.lastIndex = 0;
36
+ while ((match = ROUTE_RE.exec(content)) !== null) {
37
+ routes.push({
38
+ method: match[1].toUpperCase(),
39
+ path: match[2]
40
+ });
41
+ }
42
+ return routes;
43
+ }
44
+
45
+ function extractBasePath(moduleDir) {
46
+ const indexPath = join(moduleDir, "index.js");
47
+ if (!existsSync(indexPath)) return null;
48
+ const match = readText(indexPath).match(BASE_PATH_RE);
49
+ return match ? match[1] : null;
50
+ }
51
+
52
+ function normalizePath(path) {
53
+ return path.replace(/\/+/g, "/");
54
+ }
55
+
56
+ function moduleDocPath(moduleName) {
57
+ return join(repoRoot, "docs", moduleName, "API.md");
58
+ }
59
+
60
+ function pathDocumentedInModuleDoc(docText, method, routePath) {
61
+ const methodOk =
62
+ new RegExp(`\\b${method}\\b`, "i").test(docText) ||
63
+ new RegExp(`\\| ${method} \\|`, "i").test(docText);
64
+ const pathVariants = [
65
+ routePath,
66
+ routePath.replace(/:([A-Za-z0-9_]+)/g, ":$1"),
67
+ `\`${routePath}\``,
68
+ `\`${method} ${routePath}\``,
69
+ `\`${method.toLowerCase()} ${routePath}\``
70
+ ];
71
+ const pathOk = pathVariants.some((p) => docText.includes(p));
72
+ return methodOk && pathOk;
73
+ }
74
+
75
+ function parseRegistryRows(masterText) {
76
+ const start = masterText.indexOf("## Endpoint registry");
77
+ if (start < 0) return [];
78
+ const section = masterText.slice(start);
79
+ const end = section.indexOf("\n## ", 4);
80
+ const body = end >= 0 ? section.slice(0, end) : section;
81
+ const rows = [];
82
+ for (const line of body.split("\n")) {
83
+ if (!line.startsWith("|") || line.includes("---") || line.toLowerCase().includes("method")) {
84
+ continue;
85
+ }
86
+ const cols = line
87
+ .split("|")
88
+ .map((c) => c.trim())
89
+ .filter(Boolean);
90
+ if (cols.length >= 4) {
91
+ rows.push({
92
+ method: cols[0].toUpperCase(),
93
+ fullPath: cols[1].replace(/^`/, "").replace(/`$/, ""),
94
+ module: cols[2],
95
+ description: cols[3]
96
+ });
97
+ }
98
+ }
99
+ return rows;
100
+ }
101
+
102
+ function registryHasRoute(registryRows, method, fullPath) {
103
+ return registryRows.some(
104
+ (r) => r.method === method && normalizePath(r.fullPath) === normalizePath(fullPath)
105
+ );
106
+ }
107
+
108
+ function collectModules() {
109
+ return readdirSync(modulesDir).filter((name) => {
110
+ if (SKIP_MODULES.has(name)) return false;
111
+ const full = join(modulesDir, name);
112
+ return statSync(full).isDirectory() && existsSync(join(full, "index.js"));
113
+ });
114
+ }
115
+
116
+ function main() {
117
+ const errors = [];
118
+ const masterText = readText(masterApiPath);
119
+ const registryRows = parseRegistryRows(masterText);
120
+
121
+ if (!masterText.includes("## Endpoint registry")) {
122
+ errors.push("docs/API.md is missing ## Endpoint registry section");
123
+ }
124
+
125
+ for (const moduleName of collectModules()) {
126
+ const moduleDir = join(modulesDir, moduleName);
127
+ const basePath = extractBasePath(moduleDir);
128
+ if (!basePath) {
129
+ errors.push(`${moduleName}: could not read app.use base path from index.js`);
130
+ continue;
131
+ }
132
+
133
+ const docPath = moduleDocPath(moduleName);
134
+ if (!existsSync(docPath)) {
135
+ errors.push(`${moduleName}: missing ${docPath.replace(repoRoot + "/", "")}`);
136
+ continue;
137
+ }
138
+
139
+ const docText = readText(docPath);
140
+ const routes = [];
141
+ for (const file of listRouteFiles(moduleDir)) {
142
+ routes.push(...extractRoutesFromFile(file));
143
+ }
144
+
145
+ for (const { method, path: routePath } of routes) {
146
+ const fullPath = normalizePath(`${basePath}${routePath}`);
147
+
148
+ if (!pathDocumentedInModuleDoc(docText, method, routePath)) {
149
+ errors.push(
150
+ `${moduleName}: ${method} ${routePath} not documented in docs/${moduleName}/API.md`
151
+ );
152
+ }
153
+
154
+ if (!registryHasRoute(registryRows, method, fullPath)) {
155
+ errors.push(
156
+ `${moduleName}: ${method} ${fullPath} missing from docs/API.md Endpoint registry`
157
+ );
158
+ } else {
159
+ const row = registryRows.find(
160
+ (r) => r.method === method && normalizePath(r.fullPath) === fullPath
161
+ );
162
+ if (row && row.description.length < 8) {
163
+ errors.push(
164
+ `${moduleName}: ${fullPath} registry description too short (min ~8 chars)`
165
+ );
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ if (errors.length) {
172
+ console.error("API documentation check failed:\n");
173
+ for (const err of errors) {
174
+ console.error(` - ${err}`);
175
+ }
176
+ console.error("\nSee docs/architecture/API_DOCUMENTATION_CONTRACT.md");
177
+ process.exit(1);
178
+ }
179
+
180
+ console.log(`API documentation OK (${registryRows.length} registry rows)`);
181
+ }
182
+
183
+ main();
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Walk the repository and write consolidated-file-structure.json
4
+ * (excludes node_modules, .git, dist, build — same as tree -I).
5
+ *
6
+ * Usage: node scripts/condense-file-structure.mjs
7
+ */
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { writeConsolidatedArtifact } from "./consolidated-output.mjs";
11
+ import { buildRepoTree, TREE_IGNORE_DIRS, TREE_IGNORE_FILES } from "./lib/repo-tree.mjs";
12
+
13
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ async function main() {
16
+ const { rootName, tree, treeText, stats, flatPaths } = await buildRepoTree(repoRoot);
17
+
18
+ const doc = {
19
+ meta: {
20
+ generatedAt: new Date().toISOString(),
21
+ repositoryRoot: repoRoot,
22
+ condensedBy: "condense-file-structure",
23
+ description:
24
+ "Repository file tree for legal-prmpt-eng (excludes node_modules, .git, dist, build).",
25
+ excludeDirs: TREE_IGNORE_DIRS,
26
+ excludeFiles: TREE_IGNORE_FILES,
27
+ treeIgnoreFlag: 'tree -I "node_modules|.git|dist|build"'
28
+ },
29
+ stats,
30
+ tree,
31
+ treeText,
32
+ flatPaths
33
+ };
34
+
35
+ const { exportPath, modelsPath } = await writeConsolidatedArtifact("fileStructure", doc);
36
+ console.log(
37
+ `Consolidated ${stats.fileCount} files, ${stats.directoryCount} dirs → ${exportPath} (+ ${modelsPath})`
38
+ );
39
+ }
40
+
41
+ main().catch((err) => {
42
+ console.error(err);
43
+ process.exit(1);
44
+ });