@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,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Model condenser CLI — calls POST /api/model-condenser/condense (or --local direct service).
4
+ * Always copies consolidated-models.json → file-exchange/exports/.
5
+ *
6
+ * Usage:
7
+ * npm run condense-models
8
+ * node scripts/condense-models.mjs --local
9
+ */
10
+ import { readFile } from "fs/promises";
11
+ import { join, dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+
14
+ const args = new Set(process.argv.slice(2));
15
+ const useLocal = args.has("--local");
16
+ const includePayload = args.has("--include-payload");
17
+ const baseUrl = process.env.API_BASE_URL || "http://localhost:3001";
18
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
19
+
20
+ async function condenseViaApi() {
21
+ const res = await fetch(`${baseUrl}/api/model-condenser/condense`, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ includePayload })
25
+ });
26
+
27
+ const body = await res.json().catch(() => ({}));
28
+ if (!res.ok) {
29
+ throw new Error(body.error || body.message || `HTTP ${res.status}`);
30
+ }
31
+ return body;
32
+ }
33
+
34
+ async function condenseLocal() {
35
+ const { getModuleConfig } = await import(
36
+ "../backend/src/modules/model-condenser/config/index.js"
37
+ );
38
+ const { condenseModels } = await import(
39
+ "../backend/src/modules/model-condenser/services/modelCondenser.service.js"
40
+ );
41
+ const config = getModuleConfig();
42
+ return condenseModels({
43
+ repoRoot: config.repoRoot,
44
+ modelsDir: config.modelsDir,
45
+ consolidatedFileName: config.consolidatedFileName,
46
+ includePayload
47
+ });
48
+ }
49
+
50
+ try {
51
+ const result = useLocal ? await condenseLocal() : await condenseViaApi();
52
+ const { CONSOLIDATED_EXPORT_DIR, CONSOLIDATED_FILENAMES, writeConsolidatedExport } =
53
+ await import("../backend/src/shared/utils/consolidatedExport.js");
54
+ const modelsPath = join(repoRoot, "models", CONSOLIDATED_FILENAMES.models);
55
+ const json = await readFile(modelsPath, "utf8");
56
+ await writeConsolidatedExport(repoRoot, CONSOLIDATED_FILENAMES.models, json);
57
+ const exportRel = `${CONSOLIDATED_EXPORT_DIR}/${CONSOLIDATED_FILENAMES.models}`;
58
+
59
+ console.log(`Model condenser: ${result.modelCount} models`);
60
+ console.log(` → ${exportRel}`);
61
+ console.log(` → models/${CONSOLIDATED_FILENAMES.models} (API mirror)`);
62
+ console.log(`Generated at: ${result.generatedAt}`);
63
+ } catch (error) {
64
+ if (!useLocal) {
65
+ console.error(`API condense failed (${error.message}). Retry with --local or start the backend.`);
66
+ } else {
67
+ console.error(error.message);
68
+ }
69
+ process.exit(1);
70
+ }
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Collect all versioned prompt templates into models/consolidated-prompts.json
4
+ * Usage: node scripts/condense-prompts.mjs
5
+ */
6
+ import { readFile, readdir, stat, access } from "fs/promises";
7
+ import { existsSync } from "fs";
8
+ import { join, relative, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { writeConsolidatedArtifact } from "./consolidated-output.mjs";
11
+
12
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
13
+
14
+ const SCAN_ROOTS = ["backend/src/modules"];
15
+
16
+ const PROMPT_EXTENSIONS = [".prompt.md", ".prompt.js"];
17
+
18
+ function extractVariables(text) {
19
+ const vars = new Set();
20
+ const re = /\{\{(\w+)\}\}/g;
21
+ let m;
22
+ while ((m = re.exec(text)) !== null) {
23
+ vars.add(m[1]);
24
+ }
25
+ return [...vars].sort();
26
+ }
27
+
28
+ async function walk(dir, acc = []) {
29
+ let entries;
30
+ try {
31
+ entries = await readdir(dir, { withFileTypes: true });
32
+ } catch {
33
+ return acc;
34
+ }
35
+ for (const ent of entries) {
36
+ const full = join(dir, ent.name);
37
+ if (ent.isDirectory()) {
38
+ if (ent.name === "node_modules" || ent.name.startsWith(".")) continue;
39
+ await walk(full, acc);
40
+ } else if (PROMPT_EXTENSIONS.some((ext) => ent.name.endsWith(ext))) {
41
+ acc.push(full);
42
+ }
43
+ }
44
+ return acc;
45
+ }
46
+
47
+ async function findManifests() {
48
+ const manifests = {};
49
+ async function scanManifests(dir) {
50
+ let entries;
51
+ try {
52
+ entries = await readdir(dir, { withFileTypes: true });
53
+ } catch {
54
+ return;
55
+ }
56
+ for (const ent of entries) {
57
+ const full = join(dir, ent.name);
58
+ if (ent.isDirectory()) {
59
+ if (ent.name === "node_modules" || ent.name.startsWith(".")) continue;
60
+ await scanManifests(full);
61
+ } else if (ent.name === "manifest.json" && full.includes("/prompts/")) {
62
+ const rel = relative(repoRoot, full);
63
+ try {
64
+ manifests[rel] = JSON.parse(await readFile(full, "utf8"));
65
+ } catch {
66
+ /* skip */
67
+ }
68
+ }
69
+ }
70
+ }
71
+ await scanManifests(join(repoRoot, "backend/src/modules"));
72
+ return manifests;
73
+ }
74
+
75
+ async function loadDomainPromptVersions() {
76
+ const path = join(
77
+ repoRoot,
78
+ "backend/src/modules/case-filing-ai/prompts/promptVersions.js"
79
+ );
80
+ if (!existsSync(path)) {
81
+ return { sourcePath: null, versions: {} };
82
+ }
83
+ const raw = await readFile(path, "utf8");
84
+ const versions = {};
85
+ const blockRe = /(\w+):\s*\{[^}]*id:\s*"([^"]+)"[^}]*masterCaseFiling:\s*"([^"]+)"[^}]*description:\s*"([^"]+)"/gs;
86
+ let m;
87
+ while ((m = blockRe.exec(raw)) !== null) {
88
+ versions[m[2]] = {
89
+ key: m[1],
90
+ id: m[2],
91
+ masterCaseFiling: m[3],
92
+ description: m[4]
93
+ };
94
+ }
95
+ return { sourcePath: relative(repoRoot, path), versions };
96
+ }
97
+
98
+ async function main() {
99
+ const promptFiles = [];
100
+ for (const root of SCAN_ROOTS) {
101
+ const found = await walk(join(repoRoot, root));
102
+ promptFiles.push(...found.filter((f) => !f.endsWith("manifest.json")));
103
+ }
104
+
105
+ promptFiles.sort();
106
+
107
+ const inventory = [];
108
+ const prompts = {};
109
+
110
+ for (const abs of promptFiles) {
111
+ const rel = relative(repoRoot, abs);
112
+ const content = await readFile(abs, "utf8");
113
+ const st = await stat(abs);
114
+ const moduleMatch = rel.match(/^backend\/src\/modules\/([^/]+)/);
115
+ const module = moduleMatch ? moduleMatch[1] : "starter-handoff";
116
+ const id = rel
117
+ .replace(/^backend\/src\/modules\/[^/]+\/prompts\//, "")
118
+ .replace(/^work-log\/handoffs\/[^/]+\/prompts\//, "starter/")
119
+ .replace(/[/.]/g, "_");
120
+
121
+ const entry = {
122
+ id,
123
+ path: rel,
124
+ module,
125
+ fileName: abs.split("/").pop(),
126
+ variables: extractVariables(content),
127
+ byteLength: Buffer.byteLength(content, "utf8"),
128
+ modifiedAt: st.mtime.toISOString()
129
+ };
130
+ inventory.push(entry);
131
+ prompts[rel] = {
132
+ ...entry,
133
+ content
134
+ };
135
+ }
136
+
137
+ const domainPromptVersions = await loadDomainPromptVersions();
138
+ const moduleManifests = await findManifests();
139
+
140
+ const doc = {
141
+ meta: {
142
+ generatedAt: new Date().toISOString(),
143
+ repositoryRoot: repoRoot,
144
+ condensedBy: "condense-prompts",
145
+ description: "Consolidated prompt templates across backend modules.",
146
+ promptCount: inventory.length
147
+ },
148
+ domainPromptVersions,
149
+ moduleManifests,
150
+ inventory,
151
+ prompts
152
+ };
153
+
154
+ const { exportPath, modelsPath } = await writeConsolidatedArtifact("prompts", doc);
155
+ console.log(`Consolidated ${inventory.length} prompts → ${exportPath} (+ ${modelsPath})`);
156
+ }
157
+
158
+ main().catch((err) => {
159
+ console.error(err);
160
+ process.exit(1);
161
+ });
@@ -0,0 +1,49 @@
1
+ import { mkdir, writeFile } from "fs/promises";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import {
5
+ CONSOLIDATED_EXPORT_DIR,
6
+ CONSOLIDATED_FILENAMES,
7
+ writeConsolidatedExport
8
+ } from "../backend/src/shared/utils/consolidatedExport.js";
9
+
10
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+
12
+ export { CONSOLIDATED_EXPORT_DIR, CONSOLIDATED_FILENAMES };
13
+
14
+ /** Repo-relative paths: exports/ is primary; models/ mirrors for model-condenser API. */
15
+ export const CONSOLIDATED_ARTIFACTS = {
16
+ models: {
17
+ filename: CONSOLIDATED_FILENAMES.models,
18
+ exportPath: `${CONSOLIDATED_EXPORT_DIR}/${CONSOLIDATED_FILENAMES.models}`,
19
+ modelsPath: `models/${CONSOLIDATED_FILENAMES.models}`
20
+ },
21
+ prompts: {
22
+ filename: CONSOLIDATED_FILENAMES.prompts,
23
+ exportPath: `${CONSOLIDATED_EXPORT_DIR}/${CONSOLIDATED_FILENAMES.prompts}`,
24
+ modelsPath: `models/${CONSOLIDATED_FILENAMES.prompts}`
25
+ },
26
+ fileStructure: {
27
+ filename: CONSOLIDATED_FILENAMES.fileStructure,
28
+ exportPath: `${CONSOLIDATED_EXPORT_DIR}/${CONSOLIDATED_FILENAMES.fileStructure}`,
29
+ modelsPath: `models/${CONSOLIDATED_FILENAMES.fileStructure}`
30
+ }
31
+ };
32
+
33
+ /**
34
+ * Write JSON to file-exchange/exports/ (primary) and models/ (API mirror).
35
+ * @param {"models"|"prompts"|"fileStructure"} kind
36
+ * @param {object} doc
37
+ */
38
+ export async function writeConsolidatedArtifact(kind, doc) {
39
+ const spec = CONSOLIDATED_ARTIFACTS[kind];
40
+ const json = JSON.stringify(doc, null, 2);
41
+ await writeConsolidatedExport(repoRoot, spec.filename, json);
42
+ const modelsAbs = join(repoRoot, spec.modelsPath);
43
+ await mkdir(dirname(modelsAbs), { recursive: true });
44
+ await writeFile(modelsAbs, json);
45
+ return {
46
+ exportPath: spec.exportPath,
47
+ modelsPath: spec.modelsPath
48
+ };
49
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /** @deprecated Use `npm run condense-models` (model condenser API) instead. */
3
+ import { spawn } from "child_process";
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, join } from "path";
6
+
7
+ const script = join(dirname(fileURLToPath(import.meta.url)), "condense-models.mjs");
8
+ const child = spawn(process.execPath, [script, ...process.argv.slice(2)], {
9
+ stdio: "inherit"
10
+ });
11
+ child.on("exit", (code) => process.exit(code ?? 0));
@@ -0,0 +1,15 @@
1
+ #!/bin/sh
2
+ # Optional pre-push reminder — install:
3
+ # cp scripts/git-hooks/pre-push.sample .git/hooks/pre-push && chmod +x .git/hooks/pre-push
4
+ #
5
+ # Enforce agent log for HEAD (strict):
6
+ # DEVLOG_STRICT=1 npm run dev-log:pre-push -- --check
7
+
8
+ if [ "$DEVLOG_STRICT" = "1" ]; then
9
+ npm run dev-log:pre-push -- --check || exit 1
10
+ else
11
+ echo "pre-push: consider npm run dev-log:pre-push -- --slug <topic> (human + agent audit)"
12
+ echo "pre-push: strict check: DEVLOG_STRICT=1 (set in hook or env)"
13
+ fi
14
+
15
+ exit 0
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy an inbound bundle into file-exchange/imports/{UTC-stamp}/
4
+ * Usage: node scripts/import-to-file-exchange.mjs /path/to/bundle [optional-stamp]
5
+ */
6
+ import { cp, mkdir, access } from "fs/promises";
7
+ import { join, basename, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { formatExchangeTimestamp } from "../backend/src/shared/utils/formatExchangeTimestamp.js";
10
+
11
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+
13
+ async function main() {
14
+ const source = process.argv[2];
15
+ if (!source) {
16
+ console.error("Usage: node scripts/import-to-file-exchange.mjs <sourceDirOrZipParent> [UTC-stamp]");
17
+ process.exit(1);
18
+ }
19
+
20
+ const stamp = process.argv[3] || formatExchangeTimestamp();
21
+ const dest = join(repoRoot, "file-exchange/imports", stamp);
22
+ const absSource = join(process.cwd(), source);
23
+
24
+ try {
25
+ await access(absSource);
26
+ } catch {
27
+ console.error("Source not found:", absSource);
28
+ process.exit(1);
29
+ }
30
+
31
+ await mkdir(dest, { recursive: true });
32
+ const folderName = basename(absSource);
33
+ const target = join(dest, folderName);
34
+ await cp(absSource, target, { recursive: true });
35
+
36
+ console.log(`Imported → file-exchange/imports/${stamp}/${folderName}`);
37
+ console.log(`Use this stamp for ingest scripts or pass paths under imports/${stamp}/`);
38
+ }
39
+
40
+ main().catch((err) => {
41
+ console.error(err);
42
+ process.exit(1);
43
+ });
@@ -0,0 +1,182 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ function readText(path) {
5
+ return readFileSync(path, "utf8");
6
+ }
7
+
8
+ function parseRegistryRows(masterText) {
9
+ const start = masterText.indexOf("## Endpoint registry");
10
+ if (start < 0) return [];
11
+ const section = masterText.slice(start);
12
+ const end = section.indexOf("\n## ", 4);
13
+ const body = end >= 0 ? section.slice(0, end) : section;
14
+ const rows = [];
15
+ for (const line of body.split("\n")) {
16
+ if (!line.startsWith("|") || line.includes("---") || line.toLowerCase().includes("method")) {
17
+ continue;
18
+ }
19
+ const cols = line
20
+ .split("|")
21
+ .map((c) => c.trim())
22
+ .filter(Boolean);
23
+ if (cols.length >= 4) {
24
+ rows.push({
25
+ method: cols[0].toUpperCase(),
26
+ path: cols[1].replace(/^`/, "").replace(/`$/, ""),
27
+ module: cols[2],
28
+ description: cols[3]
29
+ });
30
+ }
31
+ }
32
+ return rows;
33
+ }
34
+
35
+ function parseModuleIndex(masterText) {
36
+ const start = masterText.indexOf("## Module index");
37
+ if (start < 0) return [];
38
+ const section = masterText.slice(start);
39
+ const end = section.indexOf("\n## ", 4);
40
+ const body = end >= 0 ? section.slice(0, end) : section;
41
+ const rows = [];
42
+ for (const line of body.split("\n")) {
43
+ if (!line.startsWith("|") || line.includes("---") || line.toLowerCase().includes("module")) {
44
+ continue;
45
+ }
46
+ const cols = line
47
+ .split("|")
48
+ .map((c) => c.trim())
49
+ .filter(Boolean);
50
+ if (cols.length >= 3) {
51
+ rows.push({
52
+ module: cols[0],
53
+ basePath: cols[1].replace(/^`/, "").replace(/`$/, ""),
54
+ status: cols[3] || cols[2]
55
+ });
56
+ }
57
+ }
58
+ return rows;
59
+ }
60
+
61
+ function classifyRoute(row) {
62
+ const desc = row.description.toLowerCase();
63
+ if (desc.includes("deprecated")) return "deprecated";
64
+ if (desc.includes("stub") || desc.includes("health only")) return "stub";
65
+ return "active";
66
+ }
67
+
68
+ /**
69
+ * @param {string} repoRoot
70
+ */
71
+ export async function collectApiInventory(repoRoot) {
72
+ const masterApiPath = join(repoRoot, "docs/API.md");
73
+ const masterText = existsSync(masterApiPath) ? readText(masterApiPath) : "";
74
+ const registry = parseRegistryRows(masterText);
75
+ const moduleIndex = parseModuleIndex(masterText);
76
+
77
+ const http = { active: [], stub: [], deprecated: [] };
78
+ for (const row of registry) {
79
+ http[classifyRoute(row)].push({
80
+ method: row.method,
81
+ path: row.path,
82
+ module: row.module,
83
+ description: row.description
84
+ });
85
+ }
86
+
87
+ const pkg = JSON.parse(readText(join(repoRoot, "package.json")));
88
+
89
+ const versioned = {
90
+ pipeline: {
91
+ app: pkg.version ?? "2.0.0",
92
+ note: "Add pipelineVersions.contract.js in domain modules when you introduce batch workflows"
93
+ },
94
+ prompts: {
95
+ defaultEnv: "n/a",
96
+ envVar: "MASTER_PROMPT_VERSION",
97
+ allowed: [],
98
+ specs: {},
99
+ notes: ["Add promptVersions.js in your domain module when you introduce LLM workflows"]
100
+ },
101
+ storage: {},
102
+ app: { packageJson: pkg.version }
103
+ };
104
+
105
+ const deprecated = { http: http.deprecated, cli: [], prompts: [], notes: [] };
106
+ const exportDeprecated = join(repoRoot, "scripts/export-consolidated-models.mjs");
107
+ if (existsSync(exportDeprecated)) {
108
+ const t = readText(exportDeprecated);
109
+ if (/@deprecated/i.test(t)) {
110
+ deprecated.cli.push({
111
+ command: "scripts/export-consolidated-models.mjs",
112
+ replacement: "npm run condense-models or POST /api/model-condenser/condense"
113
+ });
114
+ }
115
+ }
116
+
117
+ const cli = [
118
+ { command: "npm run test:ci", purpose: "All CI gates (lint + test + evals)" },
119
+ { command: "npm run dev-log:pre-push", purpose: "Paired human + agent dev logs" },
120
+ { command: "npm run condense:all", purpose: "Snapshots → file-exchange/exports/" },
121
+ { command: "npm run import:file-exchange", purpose: "Inbound → file-exchange/imports/{stamp}/" },
122
+ { command: "npm run lint:contracts", purpose: "Verify contract manifest paths" },
123
+ { command: "npm --prefix backend run condense-models", purpose: "Regenerate consolidated-models.json" }
124
+ ];
125
+
126
+ return {
127
+ capturedAt: new Date().toISOString(),
128
+ sourceDocs: ["docs/API.md", "backend/src/modules/*/routes/"],
129
+ http,
130
+ moduleStatus: moduleIndex.map((m) => ({
131
+ module: m.module,
132
+ basePath: m.basePath,
133
+ status: m.status
134
+ })),
135
+ versioned,
136
+ deprecated,
137
+ cli
138
+ };
139
+ }
140
+
141
+ /**
142
+ * @param {Awaited<ReturnType<typeof collectApiInventory>>} apis
143
+ */
144
+ export function formatApisMarkdown(apis) {
145
+ const lines = [
146
+ "### HTTP — active",
147
+ "",
148
+ "| Method | Path | Module | Description |",
149
+ "|--------|------|--------|-------------|"
150
+ ];
151
+ for (const r of apis.http.active) {
152
+ lines.push(`| ${r.method} | \`${r.path}\` | ${r.module} | ${r.description} |`);
153
+ }
154
+ lines.push("", "### HTTP — stub (health only)", "");
155
+ if (apis.http.stub.length) {
156
+ lines.push("| Method | Path | Module | Description |", "|--------|------|--------|-------------|");
157
+ for (const r of apis.http.stub) {
158
+ lines.push(`| ${r.method} | \`${r.path}\` | ${r.module} | ${r.description} |`);
159
+ }
160
+ } else {
161
+ lines.push("_none_");
162
+ }
163
+ lines.push("", "### HTTP — deprecated", "");
164
+ if (apis.http.deprecated.length) {
165
+ lines.push("| Method | Path | Module | Description |", "|--------|------|--------|-------------|");
166
+ for (const r of apis.http.deprecated) {
167
+ lines.push(`| ${r.method} | \`${r.path}\` | ${r.module} | ${r.description} |`);
168
+ }
169
+ } else {
170
+ lines.push("_none registered in docs/API.md_");
171
+ }
172
+ lines.push("", "### Versioned contracts (platform)", "", "```json");
173
+ lines.push(JSON.stringify(apis.versioned.pipeline, null, 2));
174
+ lines.push("```");
175
+ if (apis.deprecated.cli.length) {
176
+ lines.push("", "### Deprecated CLI", "");
177
+ for (const d of apis.deprecated.cli) {
178
+ lines.push(`- \`${d.command}\` → ${d.replacement}`);
179
+ }
180
+ }
181
+ return lines.join("\n");
182
+ }