@tailor-platform/erp-kit 0.0.1 → 0.1.1

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 (231) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -28
  4. package/dist/cli.js +914 -0
  5. package/package.json +67 -8
  6. package/schemas/app-compose/actors.yml +34 -0
  7. package/schemas/app-compose/business-flow.yml +50 -0
  8. package/schemas/app-compose/requirements.yml +33 -0
  9. package/schemas/app-compose/resolver.yml +47 -0
  10. package/schemas/app-compose/screen.yml +81 -0
  11. package/schemas/app-compose/story.yml +67 -0
  12. package/schemas/module/command.yml +52 -0
  13. package/schemas/module/feature.yml +58 -0
  14. package/schemas/module/model.yml +70 -0
  15. package/schemas/module/module.yml +50 -0
  16. package/skills/1-module-docs/SKILL.md +111 -0
  17. package/skills/1-module-docs/references/structure.md +22 -0
  18. package/skills/2-module-feature-breakdown/SKILL.md +72 -0
  19. package/skills/2-module-feature-breakdown/references/commands.md +48 -0
  20. package/skills/2-module-feature-breakdown/references/models.md +29 -0
  21. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  22. package/skills/3-module-doc-review/SKILL.md +236 -0
  23. package/skills/3-module-doc-review/references/commands.md +54 -0
  24. package/skills/3-module-doc-review/references/models.md +29 -0
  25. package/skills/3-module-doc-review/references/testing.md +37 -0
  26. package/skills/4-module-tdd-implementation/SKILL.md +74 -0
  27. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  28. package/skills/4-module-tdd-implementation/references/db-relations.md +69 -0
  29. package/skills/4-module-tdd-implementation/references/errors.md +7 -0
  30. package/skills/4-module-tdd-implementation/references/exports.md +8 -0
  31. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  32. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  33. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  34. package/skills/5-module-implementation-review/SKILL.md +408 -0
  35. package/skills/5-module-implementation-review/references/commands.md +45 -0
  36. package/skills/5-module-implementation-review/references/errors.md +7 -0
  37. package/skills/5-module-implementation-review/references/exports.md +8 -0
  38. package/skills/5-module-implementation-review/references/models.md +30 -0
  39. package/skills/5-module-implementation-review/references/testing.md +29 -0
  40. package/skills/app-compose-1-requirement-analysis/SKILL.md +89 -0
  41. package/skills/app-compose-1-requirement-analysis/references/structure.md +27 -0
  42. package/skills/app-compose-2-requirements-breakdown/SKILL.md +95 -0
  43. package/skills/app-compose-2-requirements-breakdown/references/screen-detailview.md +106 -0
  44. package/skills/app-compose-2-requirements-breakdown/references/screen-form.md +139 -0
  45. package/skills/app-compose-2-requirements-breakdown/references/screen-listview.md +153 -0
  46. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  47. package/skills/app-compose-3-doc-review/SKILL.md +116 -0
  48. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  49. package/skills/app-compose-4-design-mock/SKILL.md +256 -0
  50. package/skills/app-compose-4-design-mock/references/component.md +50 -0
  51. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  52. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  53. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  54. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  55. package/skills/app-compose-5-design-mock-review/SKILL.md +290 -0
  56. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  57. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  58. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  59. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  60. package/skills/app-compose-6-implementation-spec/SKILL.md +127 -0
  61. package/skills/app-compose-6-implementation-spec/references/auth.md +72 -0
  62. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  63. package/skills/mock-scenario/SKILL.md +118 -0
  64. package/src/app.ts +1 -0
  65. package/src/cli.ts +120 -0
  66. package/src/commands/check.test.ts +30 -0
  67. package/src/commands/check.ts +66 -0
  68. package/src/commands/init.test.ts +88 -0
  69. package/src/commands/init.ts +120 -0
  70. package/src/commands/mock/index.ts +53 -0
  71. package/src/commands/mock/start.ts +179 -0
  72. package/src/commands/mock/validate.test.ts +185 -0
  73. package/src/commands/mock/validate.ts +198 -0
  74. package/src/commands/scaffold.test.ts +76 -0
  75. package/src/commands/scaffold.ts +119 -0
  76. package/src/commands/sync-check.test.ts +125 -0
  77. package/src/commands/sync-check.ts +182 -0
  78. package/src/integration.test.ts +63 -0
  79. package/src/mdschema.ts +48 -0
  80. package/src/mockServer.ts +55 -0
  81. package/src/module.ts +86 -0
  82. package/src/modules/accounting/.gitkeep +0 -0
  83. package/src/modules/coa-management/.gitkeep +0 -0
  84. package/src/modules/inventory/.gitkeep +0 -0
  85. package/src/modules/manufacturing/.gitkeep +0 -0
  86. package/src/modules/primitives/README.md +39 -0
  87. package/src/modules/primitives/command/activateCategory.test.ts +75 -0
  88. package/src/modules/primitives/command/activateCategory.ts +50 -0
  89. package/src/modules/primitives/command/activateCurrency.test.ts +70 -0
  90. package/src/modules/primitives/command/activateCurrency.ts +50 -0
  91. package/src/modules/primitives/command/activateUnit.test.ts +53 -0
  92. package/src/modules/primitives/command/activateUnit.ts +50 -0
  93. package/src/modules/primitives/command/convertAmount.test.ts +275 -0
  94. package/src/modules/primitives/command/convertAmount.ts +126 -0
  95. package/src/modules/primitives/command/convertQuantity.test.ts +219 -0
  96. package/src/modules/primitives/command/convertQuantity.ts +73 -0
  97. package/src/modules/primitives/command/createCategory.test.ts +126 -0
  98. package/src/modules/primitives/command/createCategory.ts +89 -0
  99. package/src/modules/primitives/command/createCurrency.test.ts +191 -0
  100. package/src/modules/primitives/command/createCurrency.ts +77 -0
  101. package/src/modules/primitives/command/createExchangeRate.test.ts +216 -0
  102. package/src/modules/primitives/command/createExchangeRate.ts +91 -0
  103. package/src/modules/primitives/command/createUnit.test.ts +214 -0
  104. package/src/modules/primitives/command/createUnit.ts +88 -0
  105. package/src/modules/primitives/command/deactivateCategory.test.ts +97 -0
  106. package/src/modules/primitives/command/deactivateCategory.ts +62 -0
  107. package/src/modules/primitives/command/deactivateCurrency.test.ts +85 -0
  108. package/src/modules/primitives/command/deactivateCurrency.ts +55 -0
  109. package/src/modules/primitives/command/deactivateUnit.test.ts +78 -0
  110. package/src/modules/primitives/command/deactivateUnit.ts +62 -0
  111. package/src/modules/primitives/command/setBaseCurrency.test.ts +98 -0
  112. package/src/modules/primitives/command/setBaseCurrency.ts +74 -0
  113. package/src/modules/primitives/command/setReferenceUnit.test.ts +108 -0
  114. package/src/modules/primitives/command/setReferenceUnit.ts +84 -0
  115. package/src/modules/primitives/db/currency.ts +30 -0
  116. package/src/modules/primitives/db/exchangeRate.ts +28 -0
  117. package/src/modules/primitives/db/unit.ts +32 -0
  118. package/src/modules/primitives/db/uomCategory.ts +32 -0
  119. package/src/modules/primitives/docs/commands/ActivateCategory.md +34 -0
  120. package/src/modules/primitives/docs/commands/ActivateCurrency.md +33 -0
  121. package/src/modules/primitives/docs/commands/ActivateUnit.md +34 -0
  122. package/src/modules/primitives/docs/commands/ConvertAmount.md +50 -0
  123. package/src/modules/primitives/docs/commands/ConvertQuantity.md +43 -0
  124. package/src/modules/primitives/docs/commands/CreateCategory.md +44 -0
  125. package/src/modules/primitives/docs/commands/CreateCurrency.md +47 -0
  126. package/src/modules/primitives/docs/commands/CreateExchangeRate.md +48 -0
  127. package/src/modules/primitives/docs/commands/CreateUnit.md +48 -0
  128. package/src/modules/primitives/docs/commands/DeactivateCategory.md +38 -0
  129. package/src/modules/primitives/docs/commands/DeactivateCurrency.md +38 -0
  130. package/src/modules/primitives/docs/commands/DeactivateUnit.md +38 -0
  131. package/src/modules/primitives/docs/commands/SetBaseCurrency.md +39 -0
  132. package/src/modules/primitives/docs/commands/SetReferenceUnit.md +43 -0
  133. package/src/modules/primitives/docs/features/currency-definitions.md +55 -0
  134. package/src/modules/primitives/docs/features/exchange-rates.md +61 -0
  135. package/src/modules/primitives/docs/features/unit-conversion.md +66 -0
  136. package/src/modules/primitives/docs/features/uom-categories.md +52 -0
  137. package/src/modules/primitives/docs/models/Currency.md +45 -0
  138. package/src/modules/primitives/docs/models/ExchangeRate.md +33 -0
  139. package/src/modules/primitives/docs/models/Unit.md +46 -0
  140. package/src/modules/primitives/docs/models/UoMCategory.md +44 -0
  141. package/src/modules/primitives/generated/kysely-tailordb.ts +95 -0
  142. package/src/modules/primitives/index.ts +40 -0
  143. package/src/modules/primitives/lib/errors.ts +138 -0
  144. package/src/modules/primitives/lib/types.ts +20 -0
  145. package/src/modules/primitives/module.ts +66 -0
  146. package/src/modules/primitives/permissions.ts +18 -0
  147. package/src/modules/primitives/tailor.config.ts +11 -0
  148. package/src/modules/primitives/testing/fixtures.ts +161 -0
  149. package/src/modules/product-management/.gitkeep +0 -0
  150. package/src/modules/purchase/.gitkeep +0 -0
  151. package/src/modules/sales/.gitkeep +0 -0
  152. package/src/modules/shared/createContext.test.ts +39 -0
  153. package/src/modules/shared/createContext.ts +15 -0
  154. package/src/modules/shared/defineCommand.test.ts +42 -0
  155. package/src/modules/shared/defineCommand.ts +19 -0
  156. package/src/modules/shared/definePermissions.test.ts +146 -0
  157. package/src/modules/shared/definePermissions.ts +94 -0
  158. package/src/modules/shared/entityTypes.ts +15 -0
  159. package/src/modules/shared/errors.ts +22 -0
  160. package/src/modules/shared/index.ts +1 -0
  161. package/src/modules/shared/internal.ts +13 -0
  162. package/src/modules/shared/requirePermission.test.ts +47 -0
  163. package/src/modules/shared/requirePermission.ts +8 -0
  164. package/src/modules/shared/types.ts +4 -0
  165. package/src/modules/supplier-management/.gitkeep +0 -0
  166. package/src/modules/supplier-portal/.gitkeep +0 -0
  167. package/src/modules/testing/index.ts +120 -0
  168. package/src/modules/user-management/README.md +38 -0
  169. package/src/modules/user-management/command/activateUser.test.ts +112 -0
  170. package/src/modules/user-management/command/activateUser.ts +67 -0
  171. package/src/modules/user-management/command/assignPermissionToRole.test.ts +119 -0
  172. package/src/modules/user-management/command/assignPermissionToRole.ts +87 -0
  173. package/src/modules/user-management/command/assignRoleToUser.test.ts +162 -0
  174. package/src/modules/user-management/command/assignRoleToUser.ts +93 -0
  175. package/src/modules/user-management/command/createPermission.test.ts +143 -0
  176. package/src/modules/user-management/command/createPermission.ts +66 -0
  177. package/src/modules/user-management/command/createRole.test.ts +115 -0
  178. package/src/modules/user-management/command/createRole.ts +52 -0
  179. package/src/modules/user-management/command/createUser.test.ts +198 -0
  180. package/src/modules/user-management/command/createUser.ts +85 -0
  181. package/src/modules/user-management/command/deactivateUser.test.ts +112 -0
  182. package/src/modules/user-management/command/deactivateUser.ts +67 -0
  183. package/src/modules/user-management/command/logAuditEvent.test.ts +179 -0
  184. package/src/modules/user-management/command/logAuditEvent.ts +59 -0
  185. package/src/modules/user-management/command/reactivateUser.test.ts +115 -0
  186. package/src/modules/user-management/command/reactivateUser.ts +67 -0
  187. package/src/modules/user-management/command/revokePermissionFromRole.test.ts +112 -0
  188. package/src/modules/user-management/command/revokePermissionFromRole.ts +81 -0
  189. package/src/modules/user-management/command/revokeRoleFromUser.test.ts +112 -0
  190. package/src/modules/user-management/command/revokeRoleFromUser.ts +81 -0
  191. package/src/modules/user-management/db/auditEvent.ts +47 -0
  192. package/src/modules/user-management/db/permission.ts +31 -0
  193. package/src/modules/user-management/db/role.ts +28 -0
  194. package/src/modules/user-management/db/rolePermission.ts +44 -0
  195. package/src/modules/user-management/db/user.ts +38 -0
  196. package/src/modules/user-management/db/userRole.ts +44 -0
  197. package/src/modules/user-management/docs/commands/ActivateUser.md +36 -0
  198. package/src/modules/user-management/docs/commands/AssignPermissionToRole.md +39 -0
  199. package/src/modules/user-management/docs/commands/AssignRoleToUser.md +43 -0
  200. package/src/modules/user-management/docs/commands/CreatePermission.md +35 -0
  201. package/src/modules/user-management/docs/commands/CreateRole.md +35 -0
  202. package/src/modules/user-management/docs/commands/CreateUser.md +41 -0
  203. package/src/modules/user-management/docs/commands/DeactivateUser.md +38 -0
  204. package/src/modules/user-management/docs/commands/LogAuditEvent.md +37 -0
  205. package/src/modules/user-management/docs/commands/ReactivateUser.md +37 -0
  206. package/src/modules/user-management/docs/commands/RevokePermissionFromRole.md +40 -0
  207. package/src/modules/user-management/docs/commands/RevokeRoleFromUser.md +40 -0
  208. package/src/modules/user-management/docs/features/audit-trail.md +80 -0
  209. package/src/modules/user-management/docs/features/role-based-access-control.md +76 -0
  210. package/src/modules/user-management/docs/features/user-account-management.md +64 -0
  211. package/src/modules/user-management/docs/models/AuditEvent.md +34 -0
  212. package/src/modules/user-management/docs/models/Permission.md +31 -0
  213. package/src/modules/user-management/docs/models/Role.md +31 -0
  214. package/src/modules/user-management/docs/models/RolePermission.md +33 -0
  215. package/src/modules/user-management/docs/models/User.md +47 -0
  216. package/src/modules/user-management/docs/models/UserRole.md +34 -0
  217. package/src/modules/user-management/docs/plans/2026-01-30-flattened-permissions-design.md +52 -0
  218. package/src/modules/user-management/executor/recomputeOnRolePermissionChange.ts +61 -0
  219. package/src/modules/user-management/generated/enums.ts +24 -0
  220. package/src/modules/user-management/generated/kysely-tailordb.ts +112 -0
  221. package/src/modules/user-management/index.ts +32 -0
  222. package/src/modules/user-management/lib/errors.ts +81 -0
  223. package/src/modules/user-management/lib/recomputeUserPermissions.ts +53 -0
  224. package/src/modules/user-management/lib/types.ts +31 -0
  225. package/src/modules/user-management/module.ts +77 -0
  226. package/src/modules/user-management/permissions.ts +15 -0
  227. package/src/modules/user-management/tailor.config.ts +11 -0
  228. package/src/modules/user-management/testing/fixtures.ts +98 -0
  229. package/src/schemas.ts +25 -0
  230. package/src/testing.ts +10 -0
  231. package/src/util.ts +3 -0
@@ -0,0 +1,198 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join, resolve, relative, dirname, basename } from "node:path";
3
+ import chalk from "chalk";
4
+
5
+ interface ValidationContext {
6
+ failures: number;
7
+ }
8
+
9
+ function pass(msg: string) {
10
+ console.log(chalk.green(`\u2713 ${msg}`));
11
+ }
12
+
13
+ function fail(ctx: ValidationContext, msg: string) {
14
+ console.log(chalk.red(`\u2717 ${msg}`));
15
+ ctx.failures++;
16
+ }
17
+
18
+ async function subdirs(dir: string): Promise<string[]> {
19
+ const entries = await readdir(dir);
20
+ const dirs: string[] = [];
21
+ for (const entry of entries) {
22
+ const full = join(dir, entry);
23
+ const s = await stat(full);
24
+ if (s.isDirectory()) dirs.push(entry);
25
+ }
26
+ return dirs.sort();
27
+ }
28
+
29
+ function checkStructure(ctx: ValidationContext, entries: string[], label: string): boolean {
30
+ if (entries.includes("README.md")) {
31
+ pass(`${label}: has README.md`);
32
+ } else {
33
+ fail(ctx, `${label}: missing README.md`);
34
+ }
35
+
36
+ if (entries.includes("mock.json")) {
37
+ pass(`${label}: has mock.json`);
38
+ return true;
39
+ }
40
+ fail(ctx, `${label}: missing mock.json`);
41
+ return false;
42
+ }
43
+
44
+ async function checkSchema(ctx: ValidationContext, data: MockoonData, label: string) {
45
+ const commons = await import("@mockoon/commons");
46
+ // EnvironmentSchemaNoFix is a Joi schema typed as `any` in @mockoon/commons
47
+ const schema = commons.EnvironmentSchemaNoFix as {
48
+ validate: (
49
+ value: unknown,
50
+ options?: { abortEarly?: boolean },
51
+ ) => { error?: { details: { message: string }[] } };
52
+ };
53
+ const result = schema.validate(data, { abortEarly: false });
54
+ if (!result.error) {
55
+ pass(`${label}: valid Mockoon schema`);
56
+ } else {
57
+ for (const detail of result.error.details) {
58
+ fail(ctx, `${label}: schema — ${detail.message}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ interface MockoonRoute {
64
+ uuid: string;
65
+ responses?: MockoonResponse[];
66
+ }
67
+
68
+ interface MockoonResponse {
69
+ uuid: string;
70
+ label?: string;
71
+ headers?: { key: string; value: string }[];
72
+ bodyType?: string;
73
+ databucketID?: string;
74
+ }
75
+
76
+ interface MockoonData {
77
+ uuid?: string;
78
+ name?: string;
79
+ port?: number;
80
+ routes?: MockoonRoute[];
81
+ data?: { id: string }[];
82
+ }
83
+
84
+ function checkResponseQuality(ctx: ValidationContext, data: MockoonData, label: string) {
85
+ const routes = data.routes ?? [];
86
+ for (const route of routes) {
87
+ for (const resp of route.responses ?? []) {
88
+ const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
89
+ const headers = resp.headers ?? [];
90
+ const hasContentType = headers.some((h) => h.key.toLowerCase() === "content-type");
91
+ if (hasContentType) {
92
+ pass(`${respLabel}: has Content-Type`);
93
+ } else {
94
+ fail(ctx, `${respLabel}: missing Content-Type header`);
95
+ }
96
+ if (resp.label && resp.label.trim().length > 0) {
97
+ pass(`${respLabel}: has label "${resp.label}"`);
98
+ } else {
99
+ fail(ctx, `${respLabel}: missing or empty label`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ function checkDatabucketRefs(ctx: ValidationContext, data: MockoonData, label: string) {
106
+ const bucketIds = new Set((data.data ?? []).map((d) => d.id));
107
+ for (const route of data.routes ?? []) {
108
+ for (const resp of route.responses ?? []) {
109
+ if (resp.bodyType === "DATABUCKET") {
110
+ const respLabel = `${label} \u2192 ${route.uuid}/${resp.uuid}`;
111
+ if (resp.databucketID && bucketIds.has(resp.databucketID)) {
112
+ pass(`${respLabel}: databucket "${resp.databucketID}" exists`);
113
+ } else {
114
+ fail(ctx, `${respLabel}: databucketID "${resp.databucketID}" not found in data array`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ async function discoverAllScenarios(mocksDir: string): Promise<string[]> {
122
+ const scenarios: string[] = [];
123
+ const providers = await subdirs(mocksDir);
124
+ for (const provider of providers) {
125
+ const providerDir = join(mocksDir, provider);
126
+ for (const scenario of await subdirs(providerDir)) {
127
+ scenarios.push(`${provider}/${scenario}`);
128
+ }
129
+ }
130
+ return scenarios;
131
+ }
132
+
133
+ function resolveScenarioDir(arg: string): string {
134
+ const abs = resolve(arg);
135
+ if (basename(abs) === "mock.json") return dirname(abs);
136
+ return abs;
137
+ }
138
+
139
+ async function validateScenario(ctx: ValidationContext, scenarioDir: string, mocksDir: string) {
140
+ const label = relative(mocksDir, scenarioDir);
141
+ console.log(chalk.bold(`\n\u2500\u2500 ${label} \u2500\u2500`));
142
+
143
+ let entries: string[];
144
+ try {
145
+ entries = await readdir(scenarioDir);
146
+ } catch {
147
+ fail(ctx, `${label}: directory not found`);
148
+ return;
149
+ }
150
+
151
+ const hasMock = checkStructure(ctx, entries, label);
152
+ if (!hasMock) return;
153
+
154
+ const mockPath = join(scenarioDir, "mock.json");
155
+
156
+ let data: MockoonData;
157
+ try {
158
+ data = JSON.parse(await readFile(mockPath, "utf-8")) as MockoonData;
159
+ } catch (err: unknown) {
160
+ const errMsg = err instanceof Error ? err.message : String(err);
161
+ fail(ctx, `${label}: invalid JSON \u2014 ${errMsg}`);
162
+ return;
163
+ }
164
+
165
+ await checkSchema(ctx, data, label);
166
+ checkResponseQuality(ctx, data, label);
167
+ checkDatabucketRefs(ctx, data, label);
168
+ }
169
+
170
+ export async function runMockValidate(mocksRoot: string, paths: string[]): Promise<number> {
171
+ const ctx: ValidationContext = { failures: 0 };
172
+ const mocksDir = resolve(mocksRoot);
173
+
174
+ const targets =
175
+ paths.length > 0
176
+ ? paths.map(resolveScenarioDir)
177
+ : (await discoverAllScenarios(mocksDir)).map((s) => join(mocksDir, s));
178
+
179
+ if (targets.length === 0) {
180
+ fail(ctx, "No scenarios found under mocks/");
181
+ return 1;
182
+ }
183
+
184
+ console.log(chalk.bold("\nValidating mock configs...\n"));
185
+
186
+ for (const target of targets) {
187
+ await validateScenario(ctx, target, mocksDir);
188
+ }
189
+
190
+ console.log(chalk.bold("\n\u2500\u2500 summary \u2500\u2500"));
191
+ if (ctx.failures === 0) {
192
+ console.log(chalk.green("\u2713 All checks passed"));
193
+ } else {
194
+ console.log(chalk.red(`\u2717 ${ctx.failures} check(s) failed`));
195
+ }
196
+
197
+ return ctx.failures === 0 ? 0 : 1;
198
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveScaffoldPath } from "./scaffold.js";
3
+
4
+ describe("resolveScaffoldPath", () => {
5
+ // Module types
6
+ it("resolves module scaffold path", () => {
7
+ const result = resolveScaffoldPath("module", "inventory", undefined, "modules");
8
+ expect(result).toBe("modules/inventory/README.md");
9
+ });
10
+
11
+ it("resolves command scaffold path", () => {
12
+ const result = resolveScaffoldPath("command", "inventory", "CreateOrder", "modules");
13
+ expect(result).toBe("modules/inventory/docs/commands/CreateOrder.md");
14
+ });
15
+
16
+ it("resolves feature scaffold path", () => {
17
+ const result = resolveScaffoldPath("feature", "inventory", "stock-tracking", "modules");
18
+ expect(result).toBe("modules/inventory/docs/features/stock-tracking.md");
19
+ });
20
+
21
+ it("resolves model scaffold path", () => {
22
+ const result = resolveScaffoldPath("model", "inventory", "StockItem", "modules");
23
+ expect(result).toBe("modules/inventory/docs/models/StockItem.md");
24
+ });
25
+
26
+ // App-compose types
27
+ it("resolves requirements scaffold path", () => {
28
+ const result = resolveScaffoldPath("requirements", "my-app", undefined, "examples");
29
+ expect(result).toBe("examples/my-app/README.md");
30
+ });
31
+
32
+ it("resolves actors scaffold path", () => {
33
+ const result = resolveScaffoldPath("actors", "my-app", "admin", "examples");
34
+ expect(result).toBe("examples/my-app/docs/actors/admin.md");
35
+ });
36
+
37
+ it("resolves business-flow scaffold path", () => {
38
+ const result = resolveScaffoldPath("business-flow", "my-app", "onboarding", "examples");
39
+ expect(result).toBe("examples/my-app/docs/business-flow/onboarding/README.md");
40
+ });
41
+
42
+ it("resolves story scaffold path with flow/name format", () => {
43
+ const result = resolveScaffoldPath(
44
+ "story",
45
+ "my-app",
46
+ "onboarding/admin--create-user",
47
+ "examples",
48
+ );
49
+ expect(result).toBe(
50
+ "examples/my-app/docs/business-flow/onboarding/story/admin--create-user.md",
51
+ );
52
+ });
53
+
54
+ it("resolves screen scaffold path", () => {
55
+ const result = resolveScaffoldPath("screen", "my-app", "supplier-list", "examples");
56
+ expect(result).toBe("examples/my-app/docs/screen/supplier-list.md");
57
+ });
58
+
59
+ it("resolves resolver scaffold path", () => {
60
+ const result = resolveScaffoldPath("resolver", "my-app", "create-supplier", "examples");
61
+ expect(result).toBe("examples/my-app/docs/resolver/create-supplier.md");
62
+ });
63
+
64
+ // Validation
65
+ it("throws when name is required but missing", () => {
66
+ expect(() => resolveScaffoldPath("command", "inventory", undefined, "modules")).toThrow(
67
+ "Name is required",
68
+ );
69
+ });
70
+
71
+ it("throws when story name is not flow/name format", () => {
72
+ expect(() => resolveScaffoldPath("story", "my-app", "bad-name", "examples")).toThrow(
73
+ "Story name must be",
74
+ );
75
+ });
76
+ });
@@ -0,0 +1,119 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { runMdschema } from "../mdschema.js";
4
+ import { ALL_SCHEMAS } from "../schemas.js";
5
+
6
+ export const MODULE_TYPES = ["module", "feature", "command", "model"] as const;
7
+ export const APP_TYPES = [
8
+ "requirements",
9
+ "actors",
10
+ "business-flow",
11
+ "story",
12
+ "screen",
13
+ "resolver",
14
+ ] as const;
15
+ export const ALL_TYPES = [...MODULE_TYPES, ...APP_TYPES] as const;
16
+
17
+ export type ScaffoldType = (typeof ALL_TYPES)[number];
18
+
19
+ const MODULE_DIR_MAP: Record<string, string> = {
20
+ feature: "docs/features",
21
+ command: "docs/commands",
22
+ model: "docs/models",
23
+ };
24
+
25
+ const APP_DIR_MAP: Record<string, string> = {
26
+ actors: "docs/actors",
27
+ "business-flow": "docs/business-flow",
28
+ screen: "docs/screen",
29
+ resolver: "docs/resolver",
30
+ };
31
+
32
+ export function isModuleType(type: string): boolean {
33
+ return (MODULE_TYPES as readonly string[]).includes(type);
34
+ }
35
+
36
+ export function resolveScaffoldPath(
37
+ type: ScaffoldType,
38
+ parentName: string,
39
+ name: string | undefined,
40
+ root: string,
41
+ ): string {
42
+ // Types that map to README.md (no name needed)
43
+ if (type === "module" || type === "requirements") {
44
+ return path.join(root, parentName, "README.md");
45
+ }
46
+
47
+ if (!name) {
48
+ throw new Error(`Name is required for scaffold type "${type}"`);
49
+ }
50
+
51
+ // business-flow produces a README inside a named directory
52
+ if (type === "business-flow") {
53
+ return path.join(root, parentName, "docs/business-flow", name, "README.md");
54
+ }
55
+
56
+ // story accepts "flow/story-name" to place under the correct flow directory
57
+ if (type === "story") {
58
+ const parts = name.split("/");
59
+ if (parts.length !== 2) {
60
+ throw new Error(
61
+ `Story name must be "<flow>/<story>" (e.g., "onboarding/admin--create-user")`,
62
+ );
63
+ }
64
+ return path.join(root, parentName, "docs/business-flow", parts[0], "story", `${parts[1]}.md`);
65
+ }
66
+
67
+ // Module sub-types
68
+ if (MODULE_DIR_MAP[type]) {
69
+ return path.join(root, parentName, MODULE_DIR_MAP[type], `${name}.md`);
70
+ }
71
+
72
+ // App sub-types
73
+ if (APP_DIR_MAP[type]) {
74
+ return path.join(root, parentName, APP_DIR_MAP[type], `${name}.md`);
75
+ }
76
+
77
+ throw new Error(`Unknown scaffold type: ${type}`);
78
+ }
79
+
80
+ export async function runScaffold(
81
+ type: ScaffoldType,
82
+ parentName: string,
83
+ name: string | undefined,
84
+ root: string,
85
+ cwd: string,
86
+ ): Promise<number> {
87
+ const outputPath = resolveScaffoldPath(type, parentName, name, root);
88
+ const absoluteOutput = path.resolve(cwd, outputPath);
89
+
90
+ if (fs.existsSync(absoluteOutput)) {
91
+ console.error(`File already exists: ${outputPath}`);
92
+ return 1;
93
+ }
94
+
95
+ const schemaPath = ALL_SCHEMAS[type];
96
+ if (!schemaPath) {
97
+ console.error(`No schema found for type: ${type}`);
98
+ return 2;
99
+ }
100
+
101
+ try {
102
+ fs.mkdirSync(path.dirname(absoluteOutput), { recursive: true });
103
+ } catch (err) {
104
+ console.error(
105
+ `Failed to create directory: ${err instanceof Error ? err.message : String(err)}`,
106
+ );
107
+ return 1;
108
+ }
109
+
110
+ const { exitCode, stdout, stderr } = await runMdschema(
111
+ ["generate", "--schema", schemaPath, "--output", absoluteOutput],
112
+ cwd,
113
+ );
114
+
115
+ if (stdout.trim()) console.log(stdout);
116
+ if (stderr.trim()) console.error(stderr);
117
+
118
+ return exitCode;
119
+ }
@@ -0,0 +1,125 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import fs from "node:fs";
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import { runSyncCheck } from "./sync-check.js";
6
+
7
+ describe("runSyncCheck", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sync-check-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ it("returns 0 for an empty directory with no sources or docs", async () => {
19
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
20
+ expect(result.exitCode).toBe(0);
21
+ expect(result.errors).toHaveLength(0);
22
+ });
23
+
24
+ it("reports missing doc when source exists without doc", async () => {
25
+ const srcDir = path.join(tmpDir, "modules", "foo", "command");
26
+ fs.mkdirSync(srcDir, { recursive: true });
27
+ fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");
28
+
29
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
30
+ expect(result.exitCode).toBe(1);
31
+ expect(result.errors).toContainEqual(
32
+ expect.objectContaining({ type: "missing-doc", expectedBasename: "createorder" }),
33
+ );
34
+ });
35
+
36
+ it("reports orphaned doc when doc exists without source", async () => {
37
+ const docDir = path.join(tmpDir, "modules", "foo", "docs", "commands");
38
+ fs.mkdirSync(docDir, { recursive: true });
39
+ fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");
40
+
41
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
42
+ expect(result.exitCode).toBe(1);
43
+ expect(result.errors).toContainEqual(
44
+ expect.objectContaining({ type: "orphaned-doc", expectedBasename: "createorder" }),
45
+ );
46
+ });
47
+
48
+ it("returns 0 when source and doc match", async () => {
49
+ const srcDir = path.join(tmpDir, "modules", "foo", "command");
50
+ const docDir = path.join(tmpDir, "modules", "foo", "docs", "commands");
51
+ fs.mkdirSync(srcDir, { recursive: true });
52
+ fs.mkdirSync(docDir, { recursive: true });
53
+ fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");
54
+ fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");
55
+
56
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
57
+ expect(result.exitCode).toBe(0);
58
+ expect(result.errors).toHaveLength(0);
59
+ });
60
+
61
+ it("excludes test files from source matching", async () => {
62
+ const srcDir = path.join(tmpDir, "modules", "foo", "command");
63
+ fs.mkdirSync(srcDir, { recursive: true });
64
+ fs.writeFileSync(path.join(srcDir, "createOrder.test.ts"), "test()");
65
+
66
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
67
+ expect(result.exitCode).toBe(0);
68
+ expect(result.errors).toHaveLength(0);
69
+ });
70
+
71
+ it("uses custom modulesRoot to rewrite glob patterns", async () => {
72
+ const srcDir = path.join(tmpDir, "custom-mods", "foo", "command");
73
+ const docDir = path.join(tmpDir, "custom-mods", "foo", "docs", "commands");
74
+ fs.mkdirSync(srcDir, { recursive: true });
75
+ fs.mkdirSync(docDir, { recursive: true });
76
+ fs.writeFileSync(path.join(srcDir, "createOrder.ts"), "export {}");
77
+ fs.writeFileSync(path.join(docDir, "createOrder.md"), "# CreateOrder");
78
+
79
+ const result = await runSyncCheck({ modulesRoot: "custom-mods" }, tmpDir);
80
+ expect(result.exitCode).toBe(0);
81
+ expect(result.summary.totalSources).toBe(1);
82
+ });
83
+
84
+ it("uses custom appRoot to rewrite glob patterns", async () => {
85
+ const srcDir = path.join(
86
+ tmpDir,
87
+ "apps",
88
+ "app1",
89
+ "backend",
90
+ "src",
91
+ "modules",
92
+ "m1",
93
+ "resolvers",
94
+ );
95
+ const docDir = path.join(tmpDir, "apps", "app1", "docs", "resolver");
96
+ fs.mkdirSync(srcDir, { recursive: true });
97
+ fs.mkdirSync(docDir, { recursive: true });
98
+ fs.writeFileSync(path.join(srcDir, "myResolver.ts"), "export {}");
99
+ fs.writeFileSync(path.join(docDir, "myResolver.md"), "# MyResolver");
100
+
101
+ const result = await runSyncCheck({ appRoot: "apps" }, tmpDir);
102
+ expect(result.exitCode).toBe(0);
103
+ expect(result.summary.totalSources).toBe(1);
104
+ });
105
+
106
+ it("checks only module patterns when appRoot is not set", async () => {
107
+ const resolverSrcDir = path.join(
108
+ tmpDir,
109
+ "examples",
110
+ "app1",
111
+ "backend",
112
+ "src",
113
+ "modules",
114
+ "m1",
115
+ "resolvers",
116
+ );
117
+ fs.mkdirSync(resolverSrcDir, { recursive: true });
118
+ fs.writeFileSync(path.join(resolverSrcDir, "myResolver.ts"), "export {}");
119
+
120
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: undefined }, tmpDir);
121
+ // Without appRoot, app-compose patterns are not checked
122
+ expect(result.exitCode).toBe(0);
123
+ expect(result.errors).toHaveLength(0);
124
+ });
125
+ });
@@ -0,0 +1,182 @@
1
+ import path from "node:path";
2
+ import fg from "fast-glob";
3
+ import chalk from "chalk";
4
+
5
+ interface CategoryConfig {
6
+ name: string;
7
+ sourcePattern: string;
8
+ docPattern: string;
9
+ exclusions: RegExp[];
10
+ }
11
+
12
+ export interface CheckError {
13
+ type: "missing-doc" | "orphaned-doc";
14
+ category: string;
15
+ sourcePath?: string;
16
+ docPath?: string;
17
+ expectedBasename: string;
18
+ }
19
+
20
+ export interface SyncCheckResult {
21
+ exitCode: number;
22
+ errors: CheckError[];
23
+ summary: {
24
+ categoriesChecked: number;
25
+ totalSources: number;
26
+ totalDocs: number;
27
+ };
28
+ }
29
+
30
+ function moduleCategories(root: string): CategoryConfig[] {
31
+ return [
32
+ {
33
+ name: "command",
34
+ sourcePattern: `${root}/*/command/*.ts`,
35
+ docPattern: `${root}/*/docs/commands/*.md`,
36
+ exclusions: [/\.test\.ts$/],
37
+ },
38
+ {
39
+ name: "model",
40
+ sourcePattern: `${root}/*/db/*.ts`,
41
+ docPattern: `${root}/*/docs/models/*.md`,
42
+ exclusions: [/\.test\.ts$/, /^index\.ts$/],
43
+ },
44
+ ];
45
+ }
46
+
47
+ function appComposeCategories(root: string): CategoryConfig[] {
48
+ return [
49
+ {
50
+ name: "resolver",
51
+ sourcePattern: `${root}/*/backend/src/modules/**/resolvers/*.ts`,
52
+ docPattern: `${root}/*/docs/resolver/*.md`,
53
+ exclusions: [/\.test\.ts$/, /^index\.ts$/],
54
+ },
55
+ ];
56
+ }
57
+
58
+ function shouldExclude(fileName: string, exclusions: RegExp[]): boolean {
59
+ return exclusions.some((pattern) => pattern.test(fileName));
60
+ }
61
+
62
+ export async function runSyncCheck(
63
+ config: { modulesRoot?: string; appRoot?: string },
64
+ cwd: string,
65
+ ): Promise<SyncCheckResult> {
66
+ const errors: CheckError[] = [];
67
+ let totalSources = 0;
68
+ let totalDocs = 0;
69
+
70
+ const allCategories: CategoryConfig[] = [];
71
+ if (config.modulesRoot) {
72
+ allCategories.push(...moduleCategories(config.modulesRoot));
73
+ }
74
+ if (config.appRoot) {
75
+ allCategories.push(...appComposeCategories(config.appRoot));
76
+ }
77
+
78
+ for (const category of allCategories) {
79
+ const sources = await fg(category.sourcePattern, { cwd });
80
+ const docs = await fg(category.docPattern, { cwd });
81
+
82
+ const sourceBasenames = new Map<string, string>();
83
+ const docBasenames = new Map<string, string>();
84
+
85
+ for (const sourcePath of sources) {
86
+ const fileName = path.basename(sourcePath);
87
+ if (shouldExclude(fileName, category.exclusions)) continue;
88
+ const basename = path.basename(sourcePath, path.extname(sourcePath));
89
+ sourceBasenames.set(basename.toLowerCase(), sourcePath);
90
+ }
91
+
92
+ for (const docPath of docs) {
93
+ const basename = path.basename(docPath, path.extname(docPath));
94
+ docBasenames.set(basename.toLowerCase(), docPath);
95
+ }
96
+
97
+ for (const [basename, sourcePath] of sourceBasenames) {
98
+ if (!docBasenames.has(basename)) {
99
+ errors.push({
100
+ type: "missing-doc",
101
+ category: category.name,
102
+ sourcePath,
103
+ expectedBasename: basename,
104
+ });
105
+ }
106
+ }
107
+
108
+ for (const [basename, docPath] of docBasenames) {
109
+ if (!sourceBasenames.has(basename)) {
110
+ errors.push({
111
+ type: "orphaned-doc",
112
+ category: category.name,
113
+ docPath,
114
+ expectedBasename: basename,
115
+ });
116
+ }
117
+ }
118
+
119
+ totalSources += sourceBasenames.size;
120
+ totalDocs += docBasenames.size;
121
+ }
122
+
123
+ return {
124
+ exitCode: errors.length > 0 ? 1 : 0,
125
+ errors,
126
+ summary: {
127
+ categoriesChecked: allCategories.length,
128
+ totalSources,
129
+ totalDocs,
130
+ },
131
+ };
132
+ }
133
+
134
+ export function formatSyncCheckReport(result: SyncCheckResult): string {
135
+ const lines: string[] = [];
136
+ lines.push(chalk.bold("docs-sync-check: Checking source-documentation correspondence...\n"));
137
+
138
+ if (result.errors.length > 0) {
139
+ lines.push(chalk.red.bold("Errors:\n"));
140
+ const byCategory = new Map<string, CheckError[]>();
141
+ for (const error of result.errors) {
142
+ const existing = byCategory.get(error.category) ?? [];
143
+ existing.push(error);
144
+ byCategory.set(error.category, existing);
145
+ }
146
+ for (const [category, categoryErrors] of byCategory) {
147
+ lines.push(chalk.cyan(` Category: ${category}\n`));
148
+ for (const error of categoryErrors) {
149
+ if (error.type === "missing-doc") {
150
+ lines.push(` ${chalk.red(error.sourcePath)}`);
151
+ lines.push(` ${chalk.yellow("Missing documentation for:")} ${error.expectedBasename}`);
152
+ } else {
153
+ lines.push(` ${chalk.red(error.docPath)}`);
154
+ lines.push(
155
+ ` ${chalk.yellow("Orphaned documentation:")} no source file for ${error.expectedBasename}`,
156
+ );
157
+ }
158
+ lines.push("");
159
+ }
160
+ }
161
+ } else {
162
+ lines.push(chalk.green("All source files have corresponding documentation.\n"));
163
+ }
164
+
165
+ lines.push(chalk.bold("Summary:"));
166
+ lines.push(` Categories checked: ${result.summary.categoriesChecked}`);
167
+ lines.push(
168
+ ` Source files: ${result.summary.totalSources}, Doc files: ${result.summary.totalDocs}`,
169
+ );
170
+
171
+ if (result.errors.length > 0) {
172
+ lines.push(chalk.red(` Errors: ${result.errors.length}`));
173
+ lines.push("");
174
+ lines.push(chalk.red.bold(`docs-sync-check failed with ${result.errors.length} error(s).`));
175
+ } else {
176
+ lines.push(chalk.green(" Errors: 0"));
177
+ lines.push("");
178
+ lines.push(chalk.green.bold("docs-sync-check passed."));
179
+ }
180
+
181
+ return lines.join("\n");
182
+ }