@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
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Build two-part human dev log: Part I Summary (TOC, mermaid, audit tables) + Part II Detailed.
3
+ */
4
+
5
+ function anchor(slug) {
6
+ return slug.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7
+ }
8
+
9
+ function condensedTree(treeText, maxLines = 48) {
10
+ const lines = treeText.split("\n");
11
+ if (lines.length <= maxLines) return treeText;
12
+ return `${lines.slice(0, maxLines).join("\n")}\n│ └── … (${lines.length - maxLines} more lines — [full tree](#${anchor("repository-tree-full")}))`;
13
+ }
14
+
15
+ function buildMermaidModuleMap(apis) {
16
+ const modules = new Map();
17
+ for (const r of [...apis.http.active, ...apis.http.stub]) {
18
+ const id = r.module.replace(/\s+/g, "");
19
+ if (!modules.has(id)) {
20
+ modules.set(id, r.module);
21
+ }
22
+ }
23
+ const lines = ["flowchart LR", " client[Client / Frontend]"];
24
+ let i = 0;
25
+ for (const [, label] of modules) {
26
+ const node = `m${i}[${label}]`;
27
+ lines.push(` client --> ${node}`);
28
+ i += 1;
29
+ }
30
+ return lines.join("\n");
31
+ }
32
+
33
+ function buildMermaidPipeline(apis) {
34
+ const p = apis.versioned.pipeline;
35
+ return `flowchart TB
36
+ upload[Upload PDFs] --> parse[Parse cache ${p.parsedArtifacts}]
37
+ parse --> master[Master prompt ${p.masterPrompt}]
38
+ master --> out[Batch outputs]
39
+ rules[Rule fixtures ${p.ruleSet}] -.-> master
40
+ golden[Golden ${p.goldenDataset}] -.-> evals[Eval runner]`;
41
+ }
42
+
43
+ function buildMermaidPrePush() {
44
+ return `flowchart LR
45
+ code[Code changes] --> devlog[npm run dev-log:pre-push]
46
+ devlog --> human[human/*.md]
47
+ devlog --> agent[agent/*.json]
48
+ human --> push[git push]
49
+ agent --> push`;
50
+ }
51
+
52
+ function formatApiSummaryTable(apis) {
53
+ const rows = [
54
+ "| Kind | Count | Notes |",
55
+ "|------|------:|-------|",
56
+ `| Active HTTP routes | ${apis.http.active.length} | Case-filing-ai + condenser + pipeline |`,
57
+ `| Stub modules (health only) | ${apis.http.stub.length} | Workflow, court-rules, vault, review, docketing |`,
58
+ `| Deprecated HTTP | ${apis.http.deprecated.length} | From docs/API.md descriptions |`,
59
+ `| Deprecated CLI | ${apis.deprecated.cli.length} | See version audit |`
60
+ ];
61
+ const key = apis.http.active
62
+ .filter((r) => /process-batch|parsed-documents|condense/.test(r.path))
63
+ .slice(0, 6);
64
+ if (key.length) {
65
+ rows.push("", "**Key routes this program:**", "");
66
+ rows.push("| Method | Path |", "|--------|------|");
67
+ for (const r of key) {
68
+ rows.push(`| ${r.method} | \`${r.path}\` |`);
69
+ }
70
+ }
71
+ return rows.join("\n");
72
+ }
73
+
74
+ function formatVersionAuditTable(apis) {
75
+ const p = apis.versioned.pipeline;
76
+ const rows = [
77
+ "| Contract | Version | Status |",
78
+ "|----------|---------|--------|",
79
+ `| App (package.json) | ${p.app} | current |`,
80
+ `| Storage layout | ${p.storageLayout} | current |`,
81
+ `| Parsed artifacts | ${p.parsedArtifacts} | current |`,
82
+ `| Master prompt (default) | ${p.masterPrompt} | env \`${apis.versioned.prompts.envVar}\` |`,
83
+ `| Rule prompt | ${p.rulePrompt} | current |`,
84
+ `| Golden dataset | ${p.goldenDataset} | current |`,
85
+ `| Parser | ${p.parser} | current |`,
86
+ `| OCR | ${p.ocr} | current |`
87
+ ];
88
+ rows.push("", "**Master prompt keys:**", "");
89
+ rows.push("| Key | Template | Notes |", "|-----|----------|-------|");
90
+ for (const [key, spec] of Object.entries(apis.versioned.prompts.specs)) {
91
+ const status = key === "v2" ? "alias → compact" : key === "v001" ? "opt-in" : key === apis.versioned.prompts.defaultEnv ? "default" : "available";
92
+ rows.push(`| ${key} | \`${spec.masterCaseFiling}\` | ${status} |`);
93
+ }
94
+ if (apis.deprecated.cli.length) {
95
+ rows.push("", "**Deprecated surfaces:**", "");
96
+ for (const d of apis.deprecated.cli) {
97
+ rows.push(`- \`${d.command}\` → ${d.replacement}`);
98
+ }
99
+ }
100
+ return rows.join("\n");
101
+ }
102
+
103
+ function formatTestAuditTable(tests) {
104
+ if (!tests.ran) {
105
+ return "| Status | Value |\n|--------|-------|\n| Tests | _not run_ (`--no-tests` or fill after run) |";
106
+ }
107
+ return [
108
+ "| Status | Value |",
109
+ "|--------|-------|",
110
+ `| Ran | yes |`,
111
+ `| Exit code | ${tests.exitCode} |`,
112
+ `| Summary | ${tests.summary} |`,
113
+ `| Passed (sample) | ${tests.passed.length} lines captured |`,
114
+ `| Failed (sample) | ${tests.failed.length} lines captured |`
115
+ ].join("\n");
116
+ }
117
+
118
+ function formatGitAuditTable(git) {
119
+ const changed = git.changedFiles?.length ?? 0;
120
+ return [
121
+ "| Field | Value |",
122
+ "|-------|-------|",
123
+ `| Branch | \`${git.branch}\` |`,
124
+ `| Commit | \`${git.shortSha}\` (\`${git.sha}\`) |`,
125
+ `| Changed paths (porcelain) | ${changed} |`,
126
+ `| Recent commits | ${git.recentCommits?.length ?? 0} listed below |`
127
+ ].join("\n");
128
+ }
129
+
130
+ function formatRepoShapeTable(tree) {
131
+ return [
132
+ "| Metric | Value |",
133
+ "|--------|------:|",
134
+ `| Files | ${tree.stats.fileCount} |`,
135
+ `| Directories | ${tree.stats.directoryCount} |`,
136
+ `| Tree ignores | node_modules, .git, dist, build |`,
137
+ `| Top extensions | ${Object.entries(tree.stats.byExtension || {})
138
+ .slice(0, 5)
139
+ .map(([k, v]) => `${k} (${v})`)
140
+ .join(", ")} |`
141
+ ].join("\n");
142
+ }
143
+
144
+ /**
145
+ * @param {object} opts
146
+ */
147
+ export function buildHumanDevLog(opts) {
148
+ const {
149
+ title,
150
+ entryId,
151
+ date,
152
+ time,
153
+ humanFilename,
154
+ agentFilename,
155
+ git,
156
+ tests,
157
+ apis,
158
+ apisDetailedMarkdown,
159
+ tree,
160
+ treeIgnoreList
161
+ } = opts;
162
+
163
+ const p1 = "part-i-summary";
164
+ const p2 = "part-ii-detailed";
165
+
166
+ const toc = [
167
+ "## Table of contents",
168
+ "",
169
+ "### [Part I — Summary](#" + anchor(p1) + ") _(read first)_",
170
+ "- [I.1 At a glance](#" + anchor("i1-at-a-glance") + ")",
171
+ "- [I.2 Diagrams](#" + anchor("i2-diagrams") + ")",
172
+ "- [I.3 API surface (summary)](#" + anchor("i3-api-surface-summary") + ")",
173
+ "- [I.4 Version & prompt audit](#" + anchor("i4-version-prompt-audit") + ")",
174
+ "- [I.5 Test audit](#" + anchor("i5-test-audit") + ")",
175
+ "- [I.6 Git audit](#" + anchor("i6-git-audit") + ")",
176
+ "- [I.7 Repository shape](#" + anchor("i7-repository-shape") + ")",
177
+ "",
178
+ "### [Part II — Detailed](#" + anchor(p2) + ") _(full audit trail)_",
179
+ "- [II.1 Goals and scope](#" + anchor("ii1-goals-and-scope") + ")",
180
+ "- [II.2 Decisions](#" + anchor("ii2-decisions") + ")",
181
+ "- [II.3 Changes by area](#" + anchor("ii3-changes-by-area") + ")",
182
+ "- [II.4 Iterations](#" + anchor("ii4-iterations") + ")",
183
+ "- [II.5 Tests (detail)](#" + anchor("ii5-tests-detail") + ")",
184
+ "- [II.6 What got better / trade-offs / risks](#" + anchor("ii6-outcomes") + ")",
185
+ "- [II.7 Follow-ups](#" + anchor("ii7-follow-ups") + ")",
186
+ "- [II.8 APIs (full registry)](#" + anchor("ii8-apis-full-registry") + ")",
187
+ "- [II.9 Git snapshot (full)](#" + anchor("ii9-git-snapshot-full") + ")",
188
+ "- [II.10 Repository tree (full)](#" + anchor("repository-tree-full") + ")"
189
+ ].join("\n");
190
+
191
+ const summary = [
192
+ `# Dev log (human): ${title}`,
193
+ "",
194
+ "| Field | Value |",
195
+ "|-------|--------|",
196
+ `| **Entry** | ${entryId} |`,
197
+ `| **Date** | ${date} |`,
198
+ `| **Time** | ${time} |`,
199
+ `| **Filename** | \`${humanFilename}\` |`,
200
+ `| **Agent audit** | [${agentFilename}](../agent/${agentFilename}) |`,
201
+ `| **Git** | \`${git.branch}\` @ \`${git.shortSha}\` |`,
202
+ "",
203
+ toc,
204
+ "",
205
+ "---",
206
+ "",
207
+ `## Part I — Summary {#${anchor(p1)}}`,
208
+ "",
209
+ `> **Purpose:** One-screen picture for reviewers — APIs, versions, tests, git, repo shape. `,
210
+ `> **Detail:** [Part II](#${anchor(p2)}) below.`,
211
+ "",
212
+ `### I.1 At a glance {#${anchor("i1-at-a-glance")}}`,
213
+ "",
214
+ "_FILL: 2–4 sentences — what shipped, why it matters, current blockers._",
215
+ "",
216
+ `### I.2 Diagrams {#${anchor("i2-diagrams")}}`,
217
+ "",
218
+ "**HTTP modules (active + stub)**",
219
+ "",
220
+ "```mermaid",
221
+ buildMermaidModuleMap(apis),
222
+ "```",
223
+ "",
224
+ "**Pipeline versions (defaults at push)**",
225
+ "",
226
+ "```mermaid",
227
+ buildMermaidPipeline(apis),
228
+ "```",
229
+ "",
230
+ "**Pre-push dev log flow**",
231
+ "",
232
+ "```mermaid",
233
+ buildMermaidPrePush(),
234
+ "```",
235
+ "",
236
+ `### I.3 API surface (summary) {#${anchor("i3-api-surface-summary")}}`,
237
+ "",
238
+ formatApiSummaryTable(apis),
239
+ "",
240
+ `_Session API changes not in docs/API.md — FILL in [II.8](#${anchor("ii8-apis-full-registry")})._`,
241
+ "",
242
+ `### I.4 Version & prompt audit {#${anchor("i4-version-prompt-audit")}}`,
243
+ "",
244
+ formatVersionAuditTable(apis),
245
+ "",
246
+ `### I.5 Test audit {#${anchor("i5-test-audit")}}`,
247
+ "",
248
+ formatTestAuditTable(tests),
249
+ "",
250
+ `### I.6 Git audit {#${anchor("i6-git-audit")}}`,
251
+ "",
252
+ formatGitAuditTable(git),
253
+ "",
254
+ `### I.7 Repository shape {#${anchor("i7-repository-shape")}}`,
255
+ "",
256
+ formatRepoShapeTable(tree),
257
+ "",
258
+ "_Condensed tree (full tree in [II.10](#repository-tree-full)):_",
259
+ "",
260
+ "```text",
261
+ condensedTree(tree.treeText),
262
+ "```",
263
+ "",
264
+ "---",
265
+ "",
266
+ `## Part II — Detailed {#${anchor(p2)}}`,
267
+ "",
268
+ `> **Purpose:** Decisions, iterations, narrative, and full machine-captured snapshots.`,
269
+ "",
270
+ `### II.1 Goals and scope {#${anchor("ii1-goals-and-scope")}}`,
271
+ "",
272
+ "- **In scope:** _FILL_",
273
+ "- **Out of scope:** _FILL_",
274
+ "",
275
+ `### II.2 Decisions {#${anchor("ii2-decisions")}}`,
276
+ "",
277
+ "| ID | Decision | Rationale | Alternatives rejected |",
278
+ "|----|----------|-----------|------------------------|",
279
+ "| D1 | _FILL_ | _FILL_ | _FILL_ |",
280
+ "",
281
+ `### II.3 Changes by area {#${anchor("ii3-changes-by-area")}}`,
282
+ "",
283
+ "#### Backend / API",
284
+ "- _FILL_",
285
+ "",
286
+ "#### Frontend",
287
+ "- _FILL_",
288
+ "",
289
+ "#### Data / contracts / prompts",
290
+ "- _FILL_",
291
+ "",
292
+ "#### Tooling / CI / docs",
293
+ "- _FILL_",
294
+ "",
295
+ `### II.4 Iterations {#${anchor("ii4-iterations")}}`,
296
+ "",
297
+ "1. **Attempt 1** — _FILL_ → _outcome_",
298
+ "",
299
+ `### II.5 Tests (detail) {#${anchor("ii5-tests-detail")}}`,
300
+ "",
301
+ "#### Passed",
302
+ "- _FILL_",
303
+ "",
304
+ "#### Failed",
305
+ "- _FILL or none_",
306
+ "",
307
+ tests.ran && tests.rawTail
308
+ ? `#### Raw tail (auto)\n\n\`\`\`\n${tests.rawTail.slice(-2000)}\n\`\`\``
309
+ : "",
310
+ "",
311
+ `### II.6 What got better / trade-offs / risks {#${anchor("ii6-outcomes")}}`,
312
+ "",
313
+ "**Better**",
314
+ "- _FILL_",
315
+ "",
316
+ "**Trade-offs**",
317
+ "- _FILL_",
318
+ "",
319
+ "**Regressions / risks**",
320
+ "- _FILL_",
321
+ "",
322
+ `### II.7 Follow-ups {#${anchor("ii7-follow-ups")}}`,
323
+ "",
324
+ "- [ ] _FILL_",
325
+ "",
326
+ `### II.8 APIs (full registry) {#${anchor("ii8-apis-full-registry")}}`,
327
+ "",
328
+ apisDetailedMarkdown,
329
+ "",
330
+ `### II.9 Git snapshot (full) {#${anchor("ii9-git-snapshot-full")}}`,
331
+ "",
332
+ "**Changed files (porcelain)**",
333
+ "",
334
+ "```",
335
+ git.statusPorcelain || "(clean)",
336
+ "```",
337
+ "",
338
+ "**Diff stat vs HEAD**",
339
+ "",
340
+ "```",
341
+ git.diffStatAgainstHead || "(no diff)",
342
+ "```",
343
+ "",
344
+ "**Recent commits**",
345
+ "",
346
+ "```",
347
+ (git.recentCommits || []).join("\n"),
348
+ "```",
349
+ "",
350
+ `### II.10 Repository tree (full) {#${anchor("repository-tree-full")}}`,
351
+ "",
352
+ `_Ignores: \`${treeIgnoreList}\` — equivalent to \`tree -I "node_modules|.git|dist|build"\`._`,
353
+ "",
354
+ "```text",
355
+ tree.treeText,
356
+ "```"
357
+ ];
358
+
359
+ return summary.join("\n");
360
+ }
@@ -0,0 +1,46 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+
4
+ const exec = promisify(execFile);
5
+
6
+ async function git(args, cwd) {
7
+ try {
8
+ const { stdout } = await exec("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
9
+ return stdout.trim();
10
+ } catch (err) {
11
+ return err.stdout?.trim() || "";
12
+ }
13
+ }
14
+
15
+ /**
16
+ * @param {string} repoRoot
17
+ */
18
+ export async function collectGitSnapshot(repoRoot) {
19
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
20
+ const sha = await git(["rev-parse", "HEAD"], repoRoot);
21
+ const shortSha = await git(["rev-parse", "--short", "HEAD"], repoRoot);
22
+ const statusPorcelain = await git(["status", "--porcelain"], repoRoot);
23
+ const diffStat = await git(["diff", "--stat", "HEAD"], repoRoot);
24
+ const diffCachedStat = await git(["diff", "--cached", "--stat"], repoRoot);
25
+ const logOneline = await git(["log", "-5", "--oneline"], repoRoot);
26
+
27
+ const changedFiles = statusPorcelain
28
+ .split("\n")
29
+ .filter(Boolean)
30
+ .map((line) => {
31
+ const code = line.slice(0, 2);
32
+ const path = line.slice(3);
33
+ return { code, path };
34
+ });
35
+
36
+ return {
37
+ branch: branch || "unknown",
38
+ sha: sha || "unknown",
39
+ shortSha: shortSha || "unknown",
40
+ statusPorcelain,
41
+ changedFiles,
42
+ diffStatAgainstHead: diffStat,
43
+ diffCachedStat,
44
+ recentCommits: logOneline.split("\n").filter(Boolean)
45
+ };
46
+ }
@@ -257,6 +257,8 @@ test("GET /api/${moduleName}/health", async () => {
257
257
 
258
258
  See [Module internal contract](../../../docs/architecture/MODULE_INTERNAL_CONTRACT.md).
259
259
 
260
+ **HTTP API:** [docs/${moduleName}/API.md](../../../docs/${moduleName}/API.md) · [All modules](../../../docs/API.md)
261
+
260
262
  ## Layout
261
263
 
262
264
  \`routes\` → \`services\` → \`repositories\` / \`domain\` / \`adapters\`
@@ -355,7 +357,7 @@ export function useModuleHealth() {
355
357
 
356
358
  add(
357
359
  "services/health-api.js",
358
- `import { apiGet } from "../../shared/api/client.js";
360
+ `import { apiGet } from "../../../shared/api/client.js";
359
361
 
360
362
  export function fetchModuleHealth() {
361
363
  return apiGet("/api/${moduleName}/health");
@@ -407,3 +409,37 @@ See [Module internal contract](../../../docs/architecture/MODULE_INTERNAL_CONTRA
407
409
 
408
410
  return files;
409
411
  }
412
+
413
+ /** @param {string} moduleName kebab-case */
414
+ export function getModuleApiDocContent(moduleName, label = toTitleCase(moduleName)) {
415
+ return `# ${label} — HTTP API
416
+
417
+ **Base path:** \`/api/${moduleName}\`
418
+
419
+ **Routes:** [\`backend/src/modules/${moduleName}/routes/\`](../../backend/src/modules/${moduleName}/routes/)
420
+
421
+ **Contract:** [API documentation contract](../architecture/API_DOCUMENTATION_CONTRACT.md)
422
+
423
+ ---
424
+
425
+ ## Endpoint quick reference
426
+
427
+ | Method | Path | Description |
428
+ |--------|------|-------------|
429
+ | GET | \`/health\` | Module health |
430
+
431
+ ---
432
+
433
+ ## Health
434
+
435
+ | Method | Path | Description |
436
+ |--------|------|-------------|
437
+ | GET | \`/health\` | Module health and config summary |
438
+
439
+ ---
440
+
441
+ ## Master index
442
+
443
+ [docs/API.md](../API.md) — add new rows to **Endpoint registry** when you add routes.
444
+ `;
445
+ }
@@ -0,0 +1,127 @@
1
+ import { readdir, stat } from "fs/promises";
2
+ import { join, extname } from "path";
3
+
4
+ /** Same ignores as `tree -I "node_modules|.git|dist|build"` */
5
+ export const TREE_IGNORE_DIRS = ["node_modules", ".git", "dist", "build", ".DS_Store"];
6
+ export const TREE_IGNORE_FILES = [".DS_Store"];
7
+
8
+ const EXCLUDE_DIRS = new Set(TREE_IGNORE_DIRS);
9
+ const EXCLUDE_FILES = new Set(TREE_IGNORE_FILES);
10
+
11
+ function renderTreeText(nodes, prefix = "") {
12
+ const lines = [];
13
+ for (let i = 0; i < nodes.length; i += 1) {
14
+ const n = nodes[i];
15
+ const last = i === nodes.length - 1;
16
+ const branch = last ? "└── " : "├── ";
17
+ const childPrefix = prefix + (last ? " " : "│ ");
18
+ const label = n.type === "directory" ? `${n.name}/` : n.name;
19
+ lines.push(`${prefix}${branch}${label}`);
20
+ if (n.type === "directory" && n.children?.length) {
21
+ lines.push(...renderTreeText(n.children, childPrefix));
22
+ }
23
+ }
24
+ return lines;
25
+ }
26
+
27
+ async function walkDir(absDir, relBase = "") {
28
+ const entries = await readdir(absDir, { withFileTypes: true });
29
+ const dirs = [];
30
+ const files = [];
31
+
32
+ for (const ent of entries) {
33
+ if (ent.isDirectory()) {
34
+ if (EXCLUDE_DIRS.has(ent.name)) continue;
35
+ dirs.push(ent.name);
36
+ } else if (ent.isFile()) {
37
+ if (EXCLUDE_FILES.has(ent.name)) continue;
38
+ files.push(ent.name);
39
+ }
40
+ }
41
+
42
+ dirs.sort((a, b) => a.localeCompare(b));
43
+ files.sort((a, b) => a.localeCompare(b));
44
+
45
+ const children = [];
46
+ const flatPaths = [];
47
+
48
+ for (const name of files) {
49
+ const rel = relBase ? `${relBase}/${name}` : name;
50
+ const st = await stat(join(absDir, name));
51
+ flatPaths.push(rel);
52
+ children.push({
53
+ name,
54
+ type: "file",
55
+ path: rel,
56
+ byteLength: st.size,
57
+ modifiedAt: st.mtime.toISOString()
58
+ });
59
+ }
60
+
61
+ for (const name of dirs) {
62
+ const rel = relBase ? `${relBase}/${name}` : name;
63
+ const sub = await walkDir(join(absDir, name), rel);
64
+ flatPaths.push(...sub.flatPaths);
65
+ children.push({
66
+ name,
67
+ type: "directory",
68
+ path: rel,
69
+ childCount: sub.childCount,
70
+ children: sub.children
71
+ });
72
+ }
73
+
74
+ return { children, flatPaths, childCount: children.length };
75
+ }
76
+
77
+ function countStats(flatPaths, treeChildren) {
78
+ const byExtension = {};
79
+ let fileCount = 0;
80
+ let dirCount = 0;
81
+
82
+ function walk(nodes) {
83
+ for (const n of nodes) {
84
+ if (n.type === "file") {
85
+ fileCount += 1;
86
+ const ext = extname(n.name).toLowerCase() || "(no extension)";
87
+ byExtension[ext] = (byExtension[ext] ?? 0) + 1;
88
+ } else {
89
+ dirCount += 1;
90
+ if (n.children) walk(n.children);
91
+ }
92
+ }
93
+ }
94
+ walk(treeChildren);
95
+
96
+ return {
97
+ fileCount,
98
+ directoryCount: dirCount,
99
+ pathCount: flatPaths.length,
100
+ byExtension: Object.fromEntries(
101
+ Object.entries(byExtension).sort((a, b) => b[1] - a[1])
102
+ )
103
+ };
104
+ }
105
+
106
+ /**
107
+ * @param {string} repoRoot
108
+ * @returns {Promise<{ rootName: string, tree: object, treeText: string, stats: object, flatPaths: string[] }>}
109
+ */
110
+ export async function buildRepoTree(repoRoot) {
111
+ const rootName = repoRoot.split("/").pop() || "repo";
112
+ const walked = await walkDir(repoRoot);
113
+ const stats = countStats(walked.flatPaths, walked.children);
114
+ const treeText = [rootName + "/", ...renderTreeText(walked.children)].join("\n");
115
+ return {
116
+ rootName,
117
+ tree: {
118
+ name: rootName,
119
+ type: "directory",
120
+ path: "",
121
+ children: walked.children
122
+ },
123
+ treeText,
124
+ stats,
125
+ flatPaths: walked.flatPaths.sort((a, b) => a.localeCompare(b))
126
+ };
127
+ }
@@ -0,0 +1,60 @@
1
+ import { spawn } from "child_process";
2
+ import { join } from "path";
3
+
4
+ /**
5
+ * @param {string} repoRoot
6
+ * @param {{ run?: boolean }} options
7
+ */
8
+ export function runTestSuite(repoRoot, { run = true } = {}) {
9
+ if (!run) {
10
+ return Promise.resolve({
11
+ ran: false,
12
+ exitCode: null,
13
+ passed: [],
14
+ failed: [],
15
+ summary: "skipped (--no-tests)"
16
+ });
17
+ }
18
+
19
+ return new Promise((resolve) => {
20
+ const child = spawn("npm", ["test"], {
21
+ cwd: repoRoot,
22
+ shell: true,
23
+ env: { ...process.env, FORCE_COLOR: "0" }
24
+ });
25
+
26
+ let stdout = "";
27
+ let stderr = "";
28
+ child.stdout.on("data", (d) => {
29
+ stdout += d.toString();
30
+ });
31
+ child.stderr.on("data", (d) => {
32
+ stderr += d.toString();
33
+ });
34
+
35
+ child.on("close", (code) => {
36
+ const combined = `${stdout}\n${stderr}`;
37
+ const passed = [];
38
+ const failed = [];
39
+
40
+ for (const line of combined.split("\n")) {
41
+ if (/✔|✓|pass/i.test(line) && line.length < 200) passed.push(line.trim());
42
+ if (/✖|✗|fail/i.test(line) && line.length < 200) failed.push(line.trim());
43
+ }
44
+
45
+ const suitesMatch = combined.match(/ℹ tests (\d+)[\s\S]*?ℹ pass (\d+)[\s\S]*?ℹ fail (\d+)/);
46
+ const summary = suitesMatch
47
+ ? `tests=${suitesMatch[1]} pass=${suitesMatch[2]} fail=${suitesMatch[3]} exit=${code}`
48
+ : `exit=${code}`;
49
+
50
+ resolve({
51
+ ran: true,
52
+ exitCode: code ?? 1,
53
+ passed: [...new Set(passed)].slice(0, 80),
54
+ failed: [...new Set(failed)].slice(0, 80),
55
+ summary,
56
+ rawTail: combined.slice(-8000)
57
+ });
58
+ });
59
+ });
60
+ }