@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,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Walk the repository and write consolidated-file-structure.json
4
+ * (excludes node_modules, .git, dist, build — same as tree -I).
5
+ *
6
+ * Usage: node scripts/condense-file-structure.mjs
7
+ */
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { writeConsolidatedArtifact } from "./consolidated-output.mjs";
11
+ import { buildRepoTree, TREE_IGNORE_DIRS, TREE_IGNORE_FILES } from "./lib/repo-tree.mjs";
12
+
13
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
14
+
15
+ async function main() {
16
+ const { rootName, tree, treeText, stats, flatPaths } = await buildRepoTree(repoRoot);
17
+
18
+ const doc = {
19
+ meta: {
20
+ generatedAt: new Date().toISOString(),
21
+ repositoryRoot: repoRoot,
22
+ condensedBy: "condense-file-structure",
23
+ description:
24
+ "Repository file tree for legal-prmpt-eng (excludes node_modules, .git, dist, build).",
25
+ excludeDirs: TREE_IGNORE_DIRS,
26
+ excludeFiles: TREE_IGNORE_FILES,
27
+ treeIgnoreFlag: 'tree -I "node_modules|.git|dist|build"'
28
+ },
29
+ stats,
30
+ tree,
31
+ treeText,
32
+ flatPaths
33
+ };
34
+
35
+ const { exportPath, modelsPath } = await writeConsolidatedArtifact("fileStructure", doc);
36
+ console.log(
37
+ `Consolidated ${stats.fileCount} files, ${stats.directoryCount} dirs → ${exportPath} (+ ${modelsPath})`
38
+ );
39
+ }
40
+
41
+ main().catch((err) => {
42
+ console.error(err);
43
+ process.exit(1);
44
+ });
@@ -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 } from "fs/promises";
7
+ import { join, relative, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { writeConsolidatedArtifact } from "./consolidated-output.mjs";
10
+
11
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+
13
+ const SCAN_ROOTS = [
14
+ "backend/src/modules",
15
+ "work-log/handoffs/001_2026-05-23_starter_case-filing-ai-updated/prompts"
16
+ ];
17
+
18
+ const PROMPT_EXTENSIONS = [".prompt.md", ".prompt.js"];
19
+
20
+ function extractVariables(text) {
21
+ const vars = new Set();
22
+ const re = /\{\{(\w+)\}\}/g;
23
+ let m;
24
+ while ((m = re.exec(text)) !== null) {
25
+ vars.add(m[1]);
26
+ }
27
+ return [...vars].sort();
28
+ }
29
+
30
+ async function walk(dir, acc = []) {
31
+ let entries;
32
+ try {
33
+ entries = await readdir(dir, { withFileTypes: true });
34
+ } catch {
35
+ return acc;
36
+ }
37
+ for (const ent of entries) {
38
+ const full = join(dir, ent.name);
39
+ if (ent.isDirectory()) {
40
+ if (ent.name === "node_modules" || ent.name.startsWith(".")) continue;
41
+ await walk(full, acc);
42
+ } else if (PROMPT_EXTENSIONS.some((ext) => ent.name.endsWith(ext))) {
43
+ acc.push(full);
44
+ }
45
+ }
46
+ return acc;
47
+ }
48
+
49
+ async function findManifests() {
50
+ const manifests = {};
51
+ async function scanManifests(dir) {
52
+ let entries;
53
+ try {
54
+ entries = await readdir(dir, { withFileTypes: true });
55
+ } catch {
56
+ return;
57
+ }
58
+ for (const ent of entries) {
59
+ const full = join(dir, ent.name);
60
+ if (ent.isDirectory()) {
61
+ if (ent.name === "node_modules" || ent.name.startsWith(".")) continue;
62
+ await scanManifests(full);
63
+ } else if (ent.name === "manifest.json" && full.includes("/prompts/")) {
64
+ const rel = relative(repoRoot, full);
65
+ try {
66
+ manifests[rel] = JSON.parse(await readFile(full, "utf8"));
67
+ } catch {
68
+ /* skip */
69
+ }
70
+ }
71
+ }
72
+ }
73
+ await scanManifests(join(repoRoot, "backend/src/modules"));
74
+ return manifests;
75
+ }
76
+
77
+ async function loadCaseFilingVersions() {
78
+ const path = join(
79
+ repoRoot,
80
+ "backend/src/modules/case-filing-ai/prompts/promptVersions.js"
81
+ );
82
+ const raw = await readFile(path, "utf8");
83
+ const versions = {};
84
+ const blockRe = /(\w+):\s*\{[^}]*id:\s*"([^"]+)"[^}]*masterCaseFiling:\s*"([^"]+)"[^}]*description:\s*"([^"]+)"/gs;
85
+ let m;
86
+ while ((m = blockRe.exec(raw)) !== null) {
87
+ versions[m[2]] = {
88
+ key: m[1],
89
+ id: m[2],
90
+ masterCaseFiling: m[3],
91
+ description: m[4]
92
+ };
93
+ }
94
+ return { sourcePath: relative(repoRoot, path), versions };
95
+ }
96
+
97
+ async function main() {
98
+ const promptFiles = [];
99
+ for (const root of SCAN_ROOTS) {
100
+ const found = await walk(join(repoRoot, root));
101
+ promptFiles.push(...found.filter((f) => !f.endsWith("manifest.json")));
102
+ }
103
+
104
+ promptFiles.sort();
105
+
106
+ const inventory = [];
107
+ const prompts = {};
108
+
109
+ for (const abs of promptFiles) {
110
+ const rel = relative(repoRoot, abs);
111
+ const content = await readFile(abs, "utf8");
112
+ const st = await stat(abs);
113
+ const moduleMatch = rel.match(/^backend\/src\/modules\/([^/]+)/);
114
+ const module = moduleMatch ? moduleMatch[1] : "starter-handoff";
115
+ const id = rel
116
+ .replace(/^backend\/src\/modules\/[^/]+\/prompts\//, "")
117
+ .replace(/^work-log\/handoffs\/[^/]+\/prompts\//, "starter/")
118
+ .replace(/[/.]/g, "_");
119
+
120
+ const entry = {
121
+ id,
122
+ path: rel,
123
+ module,
124
+ fileName: abs.split("/").pop(),
125
+ variables: extractVariables(content),
126
+ byteLength: Buffer.byteLength(content, "utf8"),
127
+ modifiedAt: st.mtime.toISOString()
128
+ };
129
+ inventory.push(entry);
130
+ prompts[rel] = {
131
+ ...entry,
132
+ content
133
+ };
134
+ }
135
+
136
+ const caseFilingVersions = await loadCaseFilingVersions();
137
+ const moduleManifests = await findManifests();
138
+
139
+ const doc = {
140
+ meta: {
141
+ generatedAt: new Date().toISOString(),
142
+ repositoryRoot: repoRoot,
143
+ condensedBy: "condense-prompts",
144
+ description:
145
+ "Consolidated prompt templates for legal-prmpt-eng (active + starter reference).",
146
+ promptCount: inventory.length
147
+ },
148
+ caseFilingPromptVersions: caseFilingVersions,
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,189 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ const SKIP_MODULES = new Set(["_reference"]);
5
+ const ROUTE_RE = /router\.(get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]/gi;
6
+ const BASE_PATH_RE = /app\.use\(\s*["'`](\/api\/[^"'`]+)["'`]/;
7
+
8
+ function readText(path) {
9
+ return readFileSync(path, "utf8");
10
+ }
11
+
12
+ function parseRegistryRows(masterText) {
13
+ const start = masterText.indexOf("## Endpoint registry");
14
+ if (start < 0) return [];
15
+ const section = masterText.slice(start);
16
+ const end = section.indexOf("\n## ", 4);
17
+ const body = end >= 0 ? section.slice(0, end) : section;
18
+ const rows = [];
19
+ for (const line of body.split("\n")) {
20
+ if (!line.startsWith("|") || line.includes("---") || line.toLowerCase().includes("method")) {
21
+ continue;
22
+ }
23
+ const cols = line
24
+ .split("|")
25
+ .map((c) => c.trim())
26
+ .filter(Boolean);
27
+ if (cols.length >= 4) {
28
+ rows.push({
29
+ method: cols[0].toUpperCase(),
30
+ path: cols[1].replace(/^`/, "").replace(/`$/, ""),
31
+ module: cols[2],
32
+ description: cols[3]
33
+ });
34
+ }
35
+ }
36
+ return rows;
37
+ }
38
+
39
+ function parseModuleIndex(masterText) {
40
+ const start = masterText.indexOf("## Module index");
41
+ if (start < 0) return [];
42
+ const section = masterText.slice(start);
43
+ const end = section.indexOf("\n## ", 4);
44
+ const body = end >= 0 ? section.slice(0, end) : section;
45
+ const rows = [];
46
+ for (const line of body.split("\n")) {
47
+ if (!line.startsWith("|") || line.includes("---") || line.toLowerCase().includes("module")) {
48
+ continue;
49
+ }
50
+ const cols = line
51
+ .split("|")
52
+ .map((c) => c.trim())
53
+ .filter(Boolean);
54
+ if (cols.length >= 3) {
55
+ rows.push({
56
+ module: cols[0],
57
+ basePath: cols[1].replace(/^`/, "").replace(/`$/, ""),
58
+ status: cols[3] || cols[2]
59
+ });
60
+ }
61
+ }
62
+ return rows;
63
+ }
64
+
65
+ function classifyRoute(row) {
66
+ const desc = row.description.toLowerCase();
67
+ if (desc.includes("deprecated")) return "deprecated";
68
+ if (desc.includes("stub") || desc.includes("health only")) return "stub";
69
+ return "active";
70
+ }
71
+
72
+ /**
73
+ * @param {string} repoRoot
74
+ */
75
+ export async function collectApiInventory(repoRoot) {
76
+ const masterApiPath = join(repoRoot, "docs/API.md");
77
+ const masterText = existsSync(masterApiPath) ? readText(masterApiPath) : "";
78
+ const registry = parseRegistryRows(masterText);
79
+ const moduleIndex = parseModuleIndex(masterText);
80
+
81
+ const http = { active: [], stub: [], deprecated: [] };
82
+ for (const row of registry) {
83
+ const bucket = classifyRoute(row);
84
+ http[bucket].push({
85
+ method: row.method,
86
+ path: row.path,
87
+ module: row.module,
88
+ description: row.description
89
+ });
90
+ }
91
+
92
+ const promptVersions = {
93
+ defaultEnv: "n/a",
94
+ envVar: "MASTER_PROMPT_VERSION",
95
+ allowed: [],
96
+ specs: {},
97
+ notes: ["Add promptVersions.js in your domain module when you introduce LLM workflows"]
98
+ };
99
+
100
+ const pkg = JSON.parse(readText(join(repoRoot, "package.json")));
101
+ const pipelineVersions = {
102
+ app: pkg.version ?? "2.0.0",
103
+ note: "Add pipelineVersions.contract.js in domain modules for batch/runtime versioning"
104
+ };
105
+
106
+ const versioned = {
107
+ pipeline: pipelineVersions,
108
+ prompts: promptVersions,
109
+ storage: {},
110
+ app: { packageJson: pkg.version }
111
+ };
112
+
113
+ const deprecated = { http: http.deprecated, cli: [], prompts: [], notes: [] };
114
+ const exportDeprecated = join(repoRoot, "scripts/export-consolidated-models.mjs");
115
+ if (existsSync(exportDeprecated)) {
116
+ const t = readText(exportDeprecated);
117
+ if (/@deprecated/i.test(t)) {
118
+ deprecated.cli.push({
119
+ command: "scripts/export-consolidated-models.mjs",
120
+ replacement: "npm run condense-models or POST /api/model-condenser/condense"
121
+ });
122
+ }
123
+ }
124
+
125
+ const cli = [
126
+ { command: "npm run dev-log:pre-push", purpose: "Paired human + agent dev logs" },
127
+ { command: "npm run condense:all", purpose: "Snapshots → file-exchange/exports/" },
128
+ { command: "npm run import:file-exchange", purpose: "Inbound → file-exchange/imports/{stamp}/" },
129
+ { command: "npm run lint:contracts", purpose: "Verify contract manifest paths" },
130
+ { command: "npm --prefix backend run condense-models", purpose: "Regenerate consolidated-models.json" }
131
+ ];
132
+
133
+ return {
134
+ capturedAt: new Date().toISOString(),
135
+ sourceDocs: ["docs/API.md", "backend/src/modules/*/routes/"],
136
+ http,
137
+ moduleStatus: moduleIndex.map((m) => ({
138
+ module: m.module,
139
+ basePath: m.basePath,
140
+ status: m.status
141
+ })),
142
+ versioned,
143
+ deprecated,
144
+ cli
145
+ };
146
+ }
147
+
148
+ /**
149
+ * @param {Awaited<ReturnType<typeof collectApiInventory>>} apis
150
+ */
151
+ export function formatApisMarkdown(apis) {
152
+ const lines = [
153
+ "### HTTP — active",
154
+ "",
155
+ "| Method | Path | Module | Description |",
156
+ "|--------|------|--------|-------------|"
157
+ ];
158
+ for (const r of apis.http.active) {
159
+ lines.push(`| ${r.method} | \`${r.path}\` | ${r.module} | ${r.description} |`);
160
+ }
161
+ lines.push("", "### HTTP — stub (health only)", "");
162
+ if (apis.http.stub.length) {
163
+ lines.push("| Method | Path | Module | Description |", "|--------|------|--------|-------------|");
164
+ for (const r of apis.http.stub) {
165
+ lines.push(`| ${r.method} | \`${r.path}\` | ${r.module} | ${r.description} |`);
166
+ }
167
+ } else {
168
+ lines.push("_none_");
169
+ }
170
+ lines.push("", "### HTTP — deprecated", "");
171
+ if (apis.http.deprecated.length) {
172
+ lines.push("| Method | Path | Module | Description |", "|--------|------|--------|-------------|");
173
+ for (const r of apis.http.deprecated) {
174
+ lines.push(`| ${r.method} | \`${r.path}\` | ${r.module} | ${r.description} |`);
175
+ }
176
+ } else {
177
+ lines.push("_none registered in docs/API.md_");
178
+ }
179
+ lines.push("", "### Versioned contracts (platform)", "", "```json");
180
+ lines.push(JSON.stringify(apis.versioned.pipeline, null, 2));
181
+ lines.push("```");
182
+ if (apis.deprecated.cli.length) {
183
+ lines.push("", "### Deprecated CLI", "");
184
+ for (const d of apis.deprecated.cli) {
185
+ lines.push(`- \`${d.command}\` → ${d.replacement}`);
186
+ }
187
+ }
188
+ return lines.join("\n");
189
+ }