@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.
- package/README.md +91 -22
- package/index.js +47 -0
- package/package.json +16 -19
- package/template/.cursor/commands/planning-study-log.md +25 -0
- package/template/.cursor/commands/pre-push-dev-log.md +52 -0
- package/template/.cursor/rules/api-documentation.mdc +21 -0
- package/template/.cursor/rules/file-exchange-inbox.mdc +29 -0
- package/template/AGENTS.md +41 -0
- package/template/README.md +18 -57
- package/template/backend/.env.example +38 -0
- package/template/backend/package.json +14 -4
- package/template/backend/src/modules/model-condenser/README.md +7 -0
- package/template/backend/src/modules/model-condenser/config/index.js +20 -0
- package/template/backend/src/modules/model-condenser/events/index.js +1 -0
- package/template/backend/src/modules/model-condenser/index.js +12 -0
- package/template/backend/src/modules/model-condenser/routes/health.routes.js +10 -0
- package/template/backend/src/modules/model-condenser/routes/index.js +10 -0
- package/template/backend/src/modules/model-condenser/routes/modelCondenser.routes.js +44 -0
- package/template/backend/src/modules/model-condenser/services/health.service.js +8 -0
- package/template/backend/src/modules/model-condenser/services/modelCondenser.facade.js +58 -0
- package/template/backend/src/modules/model-condenser/services/modelCondenser.service.js +513 -0
- package/template/backend/src/modules/model-condenser/tests/integration/modelCondenser.routes.test.js +40 -0
- package/template/backend/src/modules/model-condenser/tests/unit/modelCondenser.service.test.js +31 -0
- package/template/backend/src/modules/model-condenser/utils/index.js +1 -0
- package/template/backend/src/shared/contracts/consolidatedExports.contract.js +19 -0
- package/template/backend/src/shared/contracts/prePushDevLog.contract.js +28 -0
- package/template/backend/src/shared/domain/case-filing/core-models.js +117 -0
- package/template/backend/src/shared/http/errors.js +8 -0
- package/template/backend/src/shared/utils/consolidatedExport.js +30 -0
- package/template/backend/src/shared/utils/formatExchangeTimestamp.js +47 -0
- package/template/backend/src/shared/utils/formatExchangeTimestamp.test.js +30 -0
- package/template/docs/API.md +42 -0
- package/template/docs/PUBLISHING.md +13 -1
- package/template/docs/README.md +4 -0
- package/template/docs/STARTER_PACK.md +4 -0
- package/template/docs/architecture/API_DOCUMENTATION_CONTRACT.md +112 -0
- package/template/docs/architecture/CONTRACTS_OVERVIEW.md +168 -0
- package/template/docs/architecture/MODULE_INTERNAL_CONTRACT.md +2 -0
- package/template/docs/architecture/PLATFORM_ARCHITECTURE.md +221 -0
- package/template/docs/architecture/REPO_ARTIFACT_LAYOUT.md +76 -0
- package/template/docs/architecture/contracts/apiDocumentationRegistry.contract.md +40 -0
- package/template/docs/architecture/contracts/changelog.jsonl +12 -0
- package/template/docs/architecture/contracts/consolidatedExports.contract.md +58 -0
- package/template/docs/architecture/contracts/fileExchange.contract.md +47 -0
- package/template/docs/architecture/contracts/manifest.json +56 -0
- package/template/docs/architecture/contracts/prePushDevLog.contract.md +69 -0
- package/template/docs/model-condenser/API.md +102 -0
- package/template/file-exchange/README.md +41 -0
- package/template/file-exchange/exports/.gitkeep +0 -0
- package/template/file-exchange/imports/.gitkeep +0 -0
- package/template/frontend/.env.example +2 -0
- package/template/frontend/package.json +1 -1
- package/template/frontend/src/index.css +311 -0
- package/template/frontend/src/modules/_reference/services/health-api.js +1 -1
- package/template/frontend/src/shared/api/client.js +67 -5
- package/template/models/.gitkeep +0 -0
- package/template/package.json +11 -4
- package/template/scripts/check-api-docs.mjs +183 -0
- package/template/scripts/condense-file-structure.mjs +44 -0
- package/template/scripts/condense-models.mjs +70 -0
- package/template/scripts/condense-prompts.mjs +161 -0
- package/template/scripts/consolidated-output.mjs +49 -0
- package/template/scripts/export-consolidated-models.mjs +11 -0
- package/template/scripts/git-hooks/pre-push.sample +15 -0
- package/template/scripts/import-to-file-exchange.mjs +43 -0
- package/template/scripts/lib/api-inventory.mjs +189 -0
- package/template/scripts/lib/dev-log-human-format.mjs +360 -0
- package/template/scripts/lib/git-snapshot.mjs +46 -0
- package/template/scripts/lib/module-scaffold.mjs +37 -1
- package/template/scripts/lib/repo-tree.mjs +127 -0
- package/template/scripts/lib/run-tests.mjs +60 -0
- package/template/scripts/lint-contracts.mjs +57 -0
- package/template/scripts/lint-repo-artifacts.mjs +37 -0
- package/template/scripts/new-module.mjs +7 -0
- package/template/scripts/resolve-import-stamp.mjs +50 -0
- package/template/scripts/verify-dev-log.mjs +50 -0
- package/template/scripts/write-pre-push-dev-log.mjs +220 -0
- package/template/work-log/INDEX.md +3 -0
- package/template/work-log/README.md +40 -0
- package/template/work-log/dev-logs/README.md +97 -0
- package/template/work-log/dev-logs/schemas/dev-log-agent.v1.schema.json +119 -0
- package/template/work-log/dev-logs/templates/dev-log-human.template.md +10 -0
- package/template/work-log/handoffs/README.md +36 -0
- package/template/work-log/study-docs/README.md +13 -0
- package/bin/create-modular-monolith.js +0 -132
- package/template/backend/package-lock.json +0 -882
- package/template/backend/src/modules/_reference/evals/README.md +0 -6
- package/template/backend/src/modules/_reference/evals/datasets/example.cases.json +0 -12
- package/template/backend/src/modules/_reference/evals/runners/example.eval.mjs +0 -25
- package/template/frontend/package-lock.json +0 -1724
- package/template/scripts/run-module-evals.mjs +0 -43
- package/template/scripts/sync-cli-template.mjs +0 -44
- /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 "
|
|
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
|
+
}
|