@pukujan/create-modular-monolith 2.0.0 → 2.1.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 (93) hide show
  1. package/README.md +91 -22
  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/AGENTS.md +41 -0
  9. package/template/README.md +18 -57
  10. package/template/backend/.env.example +38 -0
  11. package/template/backend/package.json +14 -4
  12. package/template/backend/src/modules/model-condenser/README.md +7 -0
  13. package/template/backend/src/modules/model-condenser/config/index.js +20 -0
  14. package/template/backend/src/modules/model-condenser/events/index.js +1 -0
  15. package/template/backend/src/modules/model-condenser/index.js +12 -0
  16. package/template/backend/src/modules/model-condenser/routes/health.routes.js +10 -0
  17. package/template/backend/src/modules/model-condenser/routes/index.js +10 -0
  18. package/template/backend/src/modules/model-condenser/routes/modelCondenser.routes.js +44 -0
  19. package/template/backend/src/modules/model-condenser/services/health.service.js +8 -0
  20. package/template/backend/src/modules/model-condenser/services/modelCondenser.facade.js +58 -0
  21. package/template/backend/src/modules/model-condenser/services/modelCondenser.service.js +513 -0
  22. package/template/backend/src/modules/model-condenser/tests/integration/modelCondenser.routes.test.js +40 -0
  23. package/template/backend/src/modules/model-condenser/tests/unit/modelCondenser.service.test.js +31 -0
  24. package/template/backend/src/modules/model-condenser/utils/index.js +1 -0
  25. package/template/backend/src/shared/contracts/consolidatedExports.contract.js +19 -0
  26. package/template/backend/src/shared/contracts/prePushDevLog.contract.js +28 -0
  27. package/template/backend/src/shared/domain/case-filing/core-models.js +117 -0
  28. package/template/backend/src/shared/http/errors.js +8 -0
  29. package/template/backend/src/shared/utils/consolidatedExport.js +30 -0
  30. package/template/backend/src/shared/utils/formatExchangeTimestamp.js +47 -0
  31. package/template/backend/src/shared/utils/formatExchangeTimestamp.test.js +30 -0
  32. package/template/docs/API.md +42 -0
  33. package/template/docs/PUBLISHING.md +13 -1
  34. package/template/docs/README.md +4 -0
  35. package/template/docs/STARTER_PACK.md +4 -0
  36. package/template/docs/architecture/API_DOCUMENTATION_CONTRACT.md +112 -0
  37. package/template/docs/architecture/CONTRACTS_OVERVIEW.md +168 -0
  38. package/template/docs/architecture/MODULE_INTERNAL_CONTRACT.md +2 -0
  39. package/template/docs/architecture/PLATFORM_ARCHITECTURE.md +221 -0
  40. package/template/docs/architecture/REPO_ARTIFACT_LAYOUT.md +76 -0
  41. package/template/docs/architecture/contracts/apiDocumentationRegistry.contract.md +40 -0
  42. package/template/docs/architecture/contracts/changelog.jsonl +12 -0
  43. package/template/docs/architecture/contracts/consolidatedExports.contract.md +58 -0
  44. package/template/docs/architecture/contracts/fileExchange.contract.md +47 -0
  45. package/template/docs/architecture/contracts/manifest.json +56 -0
  46. package/template/docs/architecture/contracts/prePushDevLog.contract.md +69 -0
  47. package/template/docs/model-condenser/API.md +102 -0
  48. package/template/file-exchange/README.md +41 -0
  49. package/template/file-exchange/exports/.gitkeep +0 -0
  50. package/template/file-exchange/imports/.gitkeep +0 -0
  51. package/template/frontend/.env.example +2 -0
  52. package/template/frontend/package.json +1 -1
  53. package/template/frontend/src/index.css +311 -0
  54. package/template/frontend/src/modules/_reference/services/health-api.js +1 -1
  55. package/template/frontend/src/shared/api/client.js +67 -5
  56. package/template/models/.gitkeep +0 -0
  57. package/template/package.json +11 -4
  58. package/template/scripts/check-api-docs.mjs +183 -0
  59. package/template/scripts/condense-file-structure.mjs +44 -0
  60. package/template/scripts/condense-models.mjs +70 -0
  61. package/template/scripts/condense-prompts.mjs +161 -0
  62. package/template/scripts/consolidated-output.mjs +49 -0
  63. package/template/scripts/export-consolidated-models.mjs +11 -0
  64. package/template/scripts/git-hooks/pre-push.sample +15 -0
  65. package/template/scripts/import-to-file-exchange.mjs +43 -0
  66. package/template/scripts/lib/api-inventory.mjs +189 -0
  67. package/template/scripts/lib/dev-log-human-format.mjs +360 -0
  68. package/template/scripts/lib/git-snapshot.mjs +46 -0
  69. package/template/scripts/lib/module-scaffold.mjs +37 -1
  70. package/template/scripts/lib/repo-tree.mjs +127 -0
  71. package/template/scripts/lib/run-tests.mjs +60 -0
  72. package/template/scripts/lint-contracts.mjs +57 -0
  73. package/template/scripts/lint-repo-artifacts.mjs +37 -0
  74. package/template/scripts/new-module.mjs +7 -0
  75. package/template/scripts/resolve-import-stamp.mjs +50 -0
  76. package/template/scripts/verify-dev-log.mjs +50 -0
  77. package/template/scripts/write-pre-push-dev-log.mjs +220 -0
  78. package/template/work-log/INDEX.md +3 -0
  79. package/template/work-log/README.md +40 -0
  80. package/template/work-log/dev-logs/README.md +97 -0
  81. package/template/work-log/dev-logs/schemas/dev-log-agent.v1.schema.json +119 -0
  82. package/template/work-log/dev-logs/templates/dev-log-human.template.md +10 -0
  83. package/template/work-log/handoffs/README.md +36 -0
  84. package/template/work-log/study-docs/README.md +13 -0
  85. package/bin/create-modular-monolith.js +0 -132
  86. package/template/backend/package-lock.json +0 -882
  87. package/template/backend/src/modules/_reference/evals/README.md +0 -6
  88. package/template/backend/src/modules/_reference/evals/datasets/example.cases.json +0 -12
  89. package/template/backend/src/modules/_reference/evals/runners/example.eval.mjs +0 -25
  90. package/template/frontend/package-lock.json +0 -1724
  91. package/template/scripts/run-module-evals.mjs +0 -43
  92. package/template/scripts/sync-cli-template.mjs +0 -44
  93. /package/template/{frontend/src/modules → backend/db/migrations}/.gitkeep +0 -0
@@ -0,0 +1,41 @@
1
+ # File exchange
2
+
3
+ Dated folders for human ↔ agent file handoff. **No sensitive filing text in git** — use synthetic fixtures only.
4
+
5
+ ## Layout
6
+
7
+ ```text
8
+ file-exchange/
9
+ imports/{2026-05-23_15-59-43Z}/ ← inbound bundles
10
+ exports/{2026-05-23_15-59-43Z_...}/ ← session deliverables (batch runs, curl logs)
11
+ exports/consolidated-models.json ← repo snapshots (regenerate with condense:all)
12
+ exports/consolidated-prompts.json
13
+ exports/consolidated-file-structure.json
14
+ ```
15
+
16
+ **Stamp format:** `YYYY-MM-DD_HH-MM-SSZ` via `formatExchangeTimestamp()` in `backend/src/shared/utils/formatExchangeTimestamp.js`.
17
+
18
+ ## Consolidated exports (in `exports/`)
19
+
20
+ ```bash
21
+ npm run condense:all
22
+ ```
23
+
24
+ | File in `exports/` | Mirror (API) |
25
+ |--------------------|--------------|
26
+ | `consolidated-models.json` | `models/consolidated-models.json` |
27
+ | `consolidated-prompts.json` | `models/consolidated-prompts.json` |
28
+ | `consolidated-file-structure.json` | `models/consolidated-file-structure.json` |
29
+
30
+ **Open `file-exchange/exports/`** — all three consolidated files sit next to dated batch export folders.
31
+
32
+ ## Workflow
33
+
34
+ 1. Triage loose files into `imports/{stamp}/` (`npm run import:file-exchange -- <path>`).
35
+ 2. Process via case-filing APIs using files **under that stamp** only.
36
+ 3. Copy batch bundles / reports to `exports/{stamp}/` when done.
37
+ 4. Refresh consolidated snapshots: `npm run condense:all`.
38
+
39
+ **Cursor agents:** mandatory — see [AGENTS.md](../AGENTS.md) and `.cursor/rules/file-exchange-inbox.mdc`.
40
+
41
+ See [docs/architecture/REPO_ARTIFACT_LAYOUT.md](../docs/architecture/REPO_ARTIFACT_LAYOUT.md).
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ # Frontend — copy to frontend/.env.local (optional)
2
+ VITE_API_BASE_URL=http://localhost:3001
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "modular-monolith-starter-frontend",
2
+ "name": "legal-prmpt-eng-frontend",
3
3
  "private": true,
4
4
  "version": "1.0.0",
5
5
  "type": "module",
@@ -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,23 @@
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
+ "new:module": "node scripts/new-module.mjs",
16
+ "condense-prompts": "node scripts/condense-prompts.mjs",
17
+ "condense-file-structure": "node scripts/condense-file-structure.mjs",
18
+ "condense:all": "npm run condense-prompts && npm run condense-file-structure && npm --prefix backend run condense-models -- --local",
19
+ "dev-log:pre-push": "node scripts/write-pre-push-dev-log.mjs",
20
+ "dev-log:verify": "node scripts/verify-dev-log.mjs",
21
+ "import:file-exchange": "node scripts/import-to-file-exchange.mjs"
15
22
  }
16
23
  }
@@ -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();