@tailor-platform/erp-kit 0.1.1 → 0.1.2

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 (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +77 -50
  3. package/dist/cli.js +691 -571
  4. package/package.json +1 -1
  5. package/schemas/module/command.yml +1 -0
  6. package/schemas/module/model.yml +9 -0
  7. package/schemas/module/query.yml +53 -0
  8. package/src/cli.ts +6 -88
  9. package/src/commands/app/index.ts +74 -0
  10. package/src/commands/check.test.ts +2 -1
  11. package/src/commands/check.ts +1 -0
  12. package/src/commands/module/index.ts +85 -0
  13. package/src/commands/module/list.test.ts +62 -0
  14. package/src/commands/module/list.ts +64 -0
  15. package/src/commands/scaffold.test.ts +5 -0
  16. package/src/commands/scaffold.ts +2 -1
  17. package/src/commands/sync-check.test.ts +28 -0
  18. package/src/commands/sync-check.ts +6 -0
  19. package/src/integration.test.ts +6 -8
  20. package/src/module.ts +4 -3
  21. package/src/modules/primitives/docs/models/Currency.md +4 -0
  22. package/src/modules/primitives/docs/models/ExchangeRate.md +4 -1
  23. package/src/modules/primitives/docs/models/Unit.md +4 -1
  24. package/src/modules/primitives/docs/models/UoMCategory.md +2 -0
  25. package/src/modules/primitives/index.ts +2 -2
  26. package/src/modules/primitives/module.ts +5 -3
  27. package/src/modules/primitives/permissions.ts +0 -2
  28. package/src/modules/primitives/{command → query}/convertAmount.test.ts +2 -19
  29. package/src/modules/primitives/query/convertAmount.ts +122 -0
  30. package/src/modules/primitives/{command → query}/convertQuantity.test.ts +2 -13
  31. package/src/modules/primitives/{command → query}/convertQuantity.ts +4 -6
  32. package/src/modules/shared/defineQuery.test.ts +28 -0
  33. package/src/modules/shared/defineQuery.ts +16 -0
  34. package/src/modules/shared/internal.ts +2 -1
  35. package/src/modules/shared/types.ts +8 -0
  36. package/src/modules/user-management/docs/models/AuditEvent.md +2 -0
  37. package/src/modules/user-management/docs/models/Permission.md +2 -0
  38. package/src/modules/user-management/docs/models/Role.md +2 -0
  39. package/src/modules/user-management/docs/models/RolePermission.md +2 -0
  40. package/src/modules/user-management/docs/models/User.md +2 -0
  41. package/src/modules/user-management/docs/models/UserRole.md +2 -0
  42. package/src/schemas.ts +1 -0
  43. package/src/modules/primitives/command/convertAmount.ts +0 -126
  44. /package/src/modules/primitives/docs/{commands → queries}/ConvertAmount.md +0 -0
  45. /package/src/modules/primitives/docs/{commands → queries}/ConvertQuantity.md +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/erp-kit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Opinionated ERP toolkit for building business applications on Tailor Platform",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -35,6 +35,7 @@ structure:
35
35
  # Error handling - REQUIRED
36
36
  # How errors are handled
37
37
  - heading: "## Error Scenarios"
38
+ description: "Use UPPER_SNAKE_CASE error codes to identify each scenario.\ne.g. `- **ITEM_NOT_FOUND**: Specified item ID does not exist`"
38
39
  lists:
39
40
  - min: 0
40
41
  type: unordered
@@ -37,6 +37,15 @@ structure:
37
37
  type: unordered
38
38
  min_items: 0
39
39
 
40
+ - heading: "### Query Definitions"
41
+ description: |
42
+ Definitions of queries this domain model supports.
43
+ Should match with the queries defined in modules/**/docs/queries/*.md and link to them.
44
+ lists:
45
+ - min: 0
46
+ type: unordered
47
+ min_items: 0
48
+
40
49
  - heading: "### Models"
41
50
  description: |
42
51
  Actual database models necessary for the domain model.
@@ -0,0 +1,53 @@
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/jackchuka/mdschema/main/schema.json
2
+
3
+ structure:
4
+ - heading:
5
+ expr: "filename == heading"
6
+ children:
7
+ # What this query does - REQUIRED
8
+ - heading: "## Overview"
9
+
10
+ # Business rules - REQUIRED
11
+ # Rules, constraints, and validation logic
12
+ - heading: "## Business Rules"
13
+ allow_additional: true
14
+ lists:
15
+ - min: 0
16
+ type: unordered
17
+ min_items: 1
18
+
19
+ # How it works - REQUIRED
20
+ # Step-by-step flow or workflow
21
+ - heading: "## Process Flow"
22
+ code_blocks:
23
+ - lang: mermaid
24
+ min: 1
25
+
26
+ # External dependencies - OPTIONAL
27
+ # Queries from other modules that this query depends on
28
+ - heading: "## External Dependencies"
29
+ description: "e.g. `- [inventory::getStockLevel](../../inventory/queries/getStockLevel.md) - Get current stock level`"
30
+ lists:
31
+ - min: 0
32
+ type: unordered
33
+ min_items: 1
34
+
35
+ # Error handling - REQUIRED
36
+ # How errors are handled
37
+ - heading: "## Error Scenarios"
38
+ description: "Use UPPER_SNAKE_CASE error codes to identify each scenario.\ne.g. `- **ITEM_NOT_FOUND**: Specified item ID does not exist`"
39
+ lists:
40
+ - min: 0
41
+ type: unordered
42
+ min_items: 1
43
+
44
+ # Link validation
45
+ links:
46
+ validate_internal: true
47
+ validate_files: true
48
+
49
+ # Heading rules
50
+ heading_rules:
51
+ no_skip_levels: true
52
+ unique: true
53
+ max_depth: 4
package/src/cli.ts CHANGED
@@ -2,94 +2,13 @@
2
2
 
3
3
  import { z } from "zod";
4
4
  import { defineCommand, runMain, arg } from "politty";
5
- import { runCheck } from "./commands/check.js";
6
- import { runSyncCheck, formatSyncCheckReport } from "./commands/sync-check.js";
7
- import { runScaffold, ALL_TYPES, isModuleType, type ScaffoldType } from "./commands/scaffold.js";
8
5
  import { runInit } from "./commands/init.js";
9
6
  import { mockCommand } from "./commands/mock/index.js";
7
+ import { moduleCommand } from "./commands/module/index.js";
8
+ import { appCommand } from "./commands/app/index.js";
10
9
 
11
10
  const cwd = process.cwd();
12
11
 
13
- const rootArgs = z.object({
14
- modulesRoot: arg(z.string().optional(), {
15
- alias: "m",
16
- description: "Path to modules directory",
17
- }),
18
- appRoot: arg(z.string().optional(), {
19
- alias: "a",
20
- description: "Path to app-compose directory (apps/ or examples/)",
21
- }),
22
- });
23
-
24
- function requireRoot(args: { modulesRoot?: string; appRoot?: string }) {
25
- const paths = { modulesRoot: args.modulesRoot, appRoot: args.appRoot };
26
- if (!paths.modulesRoot && !paths.appRoot) {
27
- console.error("At least one of --modules-root or --app-root is required.");
28
- process.exit(2);
29
- }
30
- return paths;
31
- }
32
-
33
- const checkCommand = defineCommand({
34
- name: "check",
35
- description: "Validate docs against schemas",
36
- args: rootArgs,
37
- run: async (args) => {
38
- const paths = requireRoot(args);
39
- const exitCode = await runCheck(paths, cwd);
40
- process.exit(exitCode);
41
- },
42
- });
43
-
44
- const syncCheckCommand = defineCommand({
45
- name: "sync-check",
46
- description: "Validate source <-> doc correspondence",
47
- args: rootArgs,
48
- run: async (args) => {
49
- const paths = requireRoot(args);
50
- const result = await runSyncCheck(paths, cwd);
51
- console.log(formatSyncCheckReport(result));
52
- process.exit(result.exitCode);
53
- },
54
- });
55
-
56
- const scaffoldCommand = defineCommand({
57
- name: "scaffold",
58
- description: "Generate doc file from schema template",
59
- args: rootArgs.extend({
60
- type: arg(z.enum(ALL_TYPES as unknown as [string, ...string[]]), {
61
- positional: true,
62
- description: `Scaffold type (${ALL_TYPES.join(", ")})`,
63
- }),
64
- parent: arg(z.string(), {
65
- positional: true,
66
- description: "Parent name (module or app name)",
67
- }),
68
- name: arg(z.string().optional(), {
69
- positional: true,
70
- description: "Item name (required for most types)",
71
- }),
72
- }),
73
- run: async (args) => {
74
- const paths = requireRoot(args);
75
- const root = isModuleType(args.type) ? paths.modulesRoot : paths.appRoot;
76
- if (!root) {
77
- console.error(
78
- `--${isModuleType(args.type) ? "modules-root" : "app-root"} is required for scaffold type "${args.type}".`,
79
- );
80
- process.exit(2);
81
- }
82
- const exitCode = await runScaffold(
83
- args.type as ScaffoldType,
84
- args.parent,
85
- args.name,
86
- root,
87
- cwd,
88
- );
89
- process.exit(exitCode);
90
- },
91
- });
92
-
93
12
  const initCommand = defineCommand({
94
13
  name: "init",
95
14
  description: "Set up consumer repo with framework skills",
@@ -107,13 +26,12 @@ const initCommand = defineCommand({
107
26
 
108
27
  const main = defineCommand({
109
28
  name: "erp-kit",
110
- description: "Documentation validation and scaffolding tool",
29
+ description: "ERP module framework CLI",
111
30
  subCommands: {
112
- check: checkCommand,
113
- "sync-check": syncCheckCommand,
114
- scaffold: scaffoldCommand,
115
- init: initCommand,
31
+ module: moduleCommand,
32
+ app: appCommand,
116
33
  mock: mockCommand,
34
+ init: initCommand,
117
35
  },
118
36
  });
119
37
 
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { defineCommand, arg } from "politty";
3
+ import { runCheck } from "../check.js";
4
+ import { runSyncCheck, formatSyncCheckReport } from "../sync-check.js";
5
+ import { runScaffold, APP_TYPES, type ScaffoldType } from "../scaffold.js";
6
+
7
+ const cwd = process.cwd();
8
+
9
+ const rootArgs = z.object({
10
+ root: arg(z.string(), {
11
+ alias: "r",
12
+ description: "Path to app-compose directory",
13
+ }),
14
+ });
15
+
16
+ const checkCommand = defineCommand({
17
+ name: "check",
18
+ description: "Validate app docs against schemas",
19
+ args: rootArgs,
20
+ run: async (args) => {
21
+ const exitCode = await runCheck({ appRoot: args.root }, cwd);
22
+ process.exit(exitCode);
23
+ },
24
+ });
25
+
26
+ const syncCheckCommand = defineCommand({
27
+ name: "sync-check",
28
+ description: "Validate source <-> doc correspondence",
29
+ args: rootArgs,
30
+ run: async (args) => {
31
+ const result = await runSyncCheck({ appRoot: args.root }, cwd);
32
+ console.log(formatSyncCheckReport(result));
33
+ process.exit(result.exitCode);
34
+ },
35
+ });
36
+
37
+ const scaffoldCommand = defineCommand({
38
+ name: "scaffold",
39
+ description: "Generate app doc from schema template",
40
+ args: rootArgs.extend({
41
+ type: arg(z.enum(APP_TYPES as unknown as [string, ...string[]]), {
42
+ positional: true,
43
+ description: `Scaffold type (${APP_TYPES.join(", ")})`,
44
+ }),
45
+ parent: arg(z.string(), {
46
+ positional: true,
47
+ description: "App name",
48
+ }),
49
+ name: arg(z.string().optional(), {
50
+ positional: true,
51
+ description: "Item name (required for most types)",
52
+ }),
53
+ }),
54
+ run: async (args) => {
55
+ const exitCode = await runScaffold(
56
+ args.type as ScaffoldType,
57
+ args.parent,
58
+ args.name,
59
+ args.root,
60
+ cwd,
61
+ );
62
+ process.exit(exitCode);
63
+ },
64
+ });
65
+
66
+ export const appCommand = defineCommand({
67
+ name: "app",
68
+ description: "App-compose management",
69
+ subCommands: {
70
+ check: checkCommand,
71
+ "sync-check": syncCheckCommand,
72
+ scaffold: scaffoldCommand,
73
+ },
74
+ });
@@ -8,6 +8,7 @@ describe("buildCheckTargets", () => {
8
8
  { glob: "modules/[a-zA-Z]*/docs/features/*.md", schemaKey: "feature" },
9
9
  { glob: "modules/[a-zA-Z]*/docs/commands/*.md", schemaKey: "command" },
10
10
  { glob: "modules/[a-zA-Z]*/docs/models/*.md", schemaKey: "model" },
11
+ { glob: "modules/[a-zA-Z]*/docs/queries/*.md", schemaKey: "query" },
11
12
  { glob: "modules/[a-zA-Z]*/README.md", schemaKey: "module" },
12
13
  ]);
13
14
  });
@@ -20,7 +21,7 @@ describe("buildCheckTargets", () => {
20
21
 
21
22
  it("generates both when both roots are set", () => {
22
23
  const targets = buildCheckTargets({ modulesRoot: "modules", appRoot: "examples" });
23
- expect(targets).toHaveLength(10);
24
+ expect(targets).toHaveLength(11);
24
25
  });
25
26
 
26
27
  it("returns empty when neither root is set", () => {
@@ -17,6 +17,7 @@ export function buildCheckTargets(config: {
17
17
  { glob: `${m}/[a-zA-Z]*/docs/features/*.md`, schemaKey: "feature" },
18
18
  { glob: `${m}/[a-zA-Z]*/docs/commands/*.md`, schemaKey: "command" },
19
19
  { glob: `${m}/[a-zA-Z]*/docs/models/*.md`, schemaKey: "model" },
20
+ { glob: `${m}/[a-zA-Z]*/docs/queries/*.md`, schemaKey: "query" },
20
21
  { glob: `${m}/[a-zA-Z]*/README.md`, schemaKey: "module" },
21
22
  );
22
23
  }
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ import { defineCommand, arg } from "politty";
3
+ import { runCheck } from "../check.js";
4
+ import { runSyncCheck, formatSyncCheckReport } from "../sync-check.js";
5
+ import { runScaffold, MODULE_TYPES, type ScaffoldType } from "../scaffold.js";
6
+ import { runModuleList } from "./list.js";
7
+
8
+ const cwd = process.cwd();
9
+
10
+ const rootArgs = z.object({
11
+ root: arg(z.string(), {
12
+ alias: "r",
13
+ description: "Path to modules directory",
14
+ }),
15
+ });
16
+
17
+ const listCommand = defineCommand({
18
+ name: "list",
19
+ description: "List available modules",
20
+ run: () => {
21
+ const exitCode = runModuleList();
22
+ process.exit(exitCode);
23
+ },
24
+ });
25
+
26
+ const checkCommand = defineCommand({
27
+ name: "check",
28
+ description: "Validate module docs against schemas",
29
+ args: rootArgs,
30
+ run: async (args) => {
31
+ const exitCode = await runCheck({ modulesRoot: args.root }, cwd);
32
+ process.exit(exitCode);
33
+ },
34
+ });
35
+
36
+ const syncCheckCommand = defineCommand({
37
+ name: "sync-check",
38
+ description: "Validate source <-> doc correspondence",
39
+ args: rootArgs,
40
+ run: async (args) => {
41
+ const result = await runSyncCheck({ modulesRoot: args.root }, cwd);
42
+ console.log(formatSyncCheckReport(result));
43
+ process.exit(result.exitCode);
44
+ },
45
+ });
46
+
47
+ const scaffoldCommand = defineCommand({
48
+ name: "scaffold",
49
+ description: "Generate module doc from schema template",
50
+ args: rootArgs.extend({
51
+ type: arg(z.enum(MODULE_TYPES as unknown as [string, ...string[]]), {
52
+ positional: true,
53
+ description: `Scaffold type (${MODULE_TYPES.join(", ")})`,
54
+ }),
55
+ parent: arg(z.string(), {
56
+ positional: true,
57
+ description: "Module name",
58
+ }),
59
+ name: arg(z.string().optional(), {
60
+ positional: true,
61
+ description: "Item name (required for feature, command, model)",
62
+ }),
63
+ }),
64
+ run: async (args) => {
65
+ const exitCode = await runScaffold(
66
+ args.type as ScaffoldType,
67
+ args.parent,
68
+ args.name,
69
+ args.root,
70
+ cwd,
71
+ );
72
+ process.exit(exitCode);
73
+ },
74
+ });
75
+
76
+ export const moduleCommand = defineCommand({
77
+ name: "module",
78
+ description: "Module management",
79
+ subCommands: {
80
+ list: listCommand,
81
+ check: checkCommand,
82
+ "sync-check": syncCheckCommand,
83
+ scaffold: scaffoldCommand,
84
+ },
85
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("listModules", () => {
4
+ it("returns the built-in modules with counts", async () => {
5
+ const { listModules } = await import("./list.js");
6
+ const modules = listModules();
7
+ expect(modules.length).toBeGreaterThan(0);
8
+
9
+ const primitives = modules.find((m) => m.name === "primitives");
10
+ expect(primitives).toBeDefined();
11
+ expect(primitives!.commands).toBeGreaterThan(0);
12
+ expect(primitives!.models).toBeGreaterThan(0);
13
+ expect(primitives!.features).toBeGreaterThan(0);
14
+ });
15
+
16
+ it("returns zero counts for stub modules", async () => {
17
+ const { listModules } = await import("./list.js");
18
+ const modules = listModules();
19
+ const inventory = modules.find((m) => m.name === "inventory");
20
+ expect(inventory).toBeDefined();
21
+ expect(inventory!.commands).toBe(0);
22
+ expect(inventory!.models).toBe(0);
23
+ expect(inventory!.features).toBe(0);
24
+ });
25
+
26
+ it("excludes shared and testing directories", async () => {
27
+ const { listModules } = await import("./list.js");
28
+ const modules = listModules();
29
+ const names = modules.map((m) => m.name);
30
+ expect(names).not.toContain("shared");
31
+ expect(names).not.toContain("testing");
32
+ });
33
+
34
+ it("returns modules in alphabetical order", async () => {
35
+ const { listModules } = await import("./list.js");
36
+ const modules = listModules();
37
+ const names = modules.map((m) => m.name);
38
+ const sorted = [...names].sort();
39
+ expect(names).toEqual(sorted);
40
+ });
41
+ });
42
+
43
+ describe("formatModuleList", () => {
44
+ it("formats modules with counts", async () => {
45
+ const { formatModuleList } = await import("./list.js");
46
+ const output = formatModuleList([
47
+ { name: "primitives", commands: 14, models: 4, features: 4 },
48
+ { name: "inventory", commands: 0, models: 0, features: 0 },
49
+ ]);
50
+ expect(output).toContain("primitives");
51
+ expect(output).toContain("14 commands");
52
+ expect(output).toContain("4 models");
53
+ expect(output).toContain("4 features");
54
+ expect(output).toContain("inventory");
55
+ expect(output).toContain("2 modules");
56
+ });
57
+
58
+ it("handles empty list", async () => {
59
+ const { formatModuleList } = await import("./list.js");
60
+ expect(formatModuleList([])).toBe("No modules found.");
61
+ });
62
+ });
@@ -0,0 +1,64 @@
1
+ import { readdirSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import chalk from "chalk";
4
+ import { PACKAGE_ROOT } from "../../util.js";
5
+
6
+ const MODULES_DIR = join(PACKAGE_ROOT, "src", "modules");
7
+ const EXCLUDED_DIRS = new Set(["shared", "testing"]);
8
+
9
+ export interface ModuleInfo {
10
+ name: string;
11
+ commands: number;
12
+ models: number;
13
+ features: number;
14
+ }
15
+
16
+ function countFiles(dir: string, pattern: RegExp, exclusions: RegExp[]): number {
17
+ if (!existsSync(dir)) return 0;
18
+ return readdirSync(dir).filter((f) => pattern.test(f) && !exclusions.some((p) => p.test(f)))
19
+ .length;
20
+ }
21
+
22
+ export function listModules(): ModuleInfo[] {
23
+ if (!existsSync(MODULES_DIR)) return [];
24
+ return readdirSync(MODULES_DIR, { withFileTypes: true })
25
+ .filter((d) => d.isDirectory() && !EXCLUDED_DIRS.has(d.name))
26
+ .map((d) => {
27
+ const modDir = join(MODULES_DIR, d.name);
28
+ return {
29
+ name: d.name,
30
+ commands: countFiles(join(modDir, "command"), /\.ts$/, [/\.test\.ts$/]),
31
+ models: countFiles(join(modDir, "db"), /\.ts$/, [/\.test\.ts$/, /^index\.ts$/]),
32
+ features: countFiles(join(modDir, "docs", "features"), /\.md$/, []),
33
+ };
34
+ })
35
+ .sort((a, b) => a.name.localeCompare(b.name));
36
+ }
37
+
38
+ export function formatModuleList(modules: ModuleInfo[]): string {
39
+ if (modules.length === 0) return "No modules found.";
40
+
41
+ const lines: string[] = [];
42
+ lines.push(chalk.bold("Modules:\n"));
43
+
44
+ const nameWidth = Math.max(...modules.map((m) => m.name.length), 4);
45
+
46
+ for (const mod of modules) {
47
+ const counts = [
48
+ `${mod.commands} commands`,
49
+ `${mod.models} models`,
50
+ `${mod.features} features`,
51
+ ].join(", ");
52
+ lines.push(` ${mod.name.padEnd(nameWidth)} ${counts}`);
53
+ }
54
+
55
+ lines.push("");
56
+ lines.push(`${modules.length} modules`);
57
+ return lines.join("\n");
58
+ }
59
+
60
+ export function runModuleList(): number {
61
+ const modules = listModules();
62
+ console.log(formatModuleList(modules));
63
+ return 0;
64
+ }
@@ -61,6 +61,11 @@ describe("resolveScaffoldPath", () => {
61
61
  expect(result).toBe("examples/my-app/docs/resolver/create-supplier.md");
62
62
  });
63
63
 
64
+ it("resolves query scaffold path", () => {
65
+ const result = resolveScaffoldPath("query", "inventory", "ConvertQuantity", "modules");
66
+ expect(result).toBe("modules/inventory/docs/queries/ConvertQuantity.md");
67
+ });
68
+
64
69
  // Validation
65
70
  it("throws when name is required but missing", () => {
66
71
  expect(() => resolveScaffoldPath("command", "inventory", undefined, "modules")).toThrow(
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import { runMdschema } from "../mdschema.js";
4
4
  import { ALL_SCHEMAS } from "../schemas.js";
5
5
 
6
- export const MODULE_TYPES = ["module", "feature", "command", "model"] as const;
6
+ export const MODULE_TYPES = ["module", "feature", "command", "model", "query"] as const;
7
7
  export const APP_TYPES = [
8
8
  "requirements",
9
9
  "actors",
@@ -20,6 +20,7 @@ const MODULE_DIR_MAP: Record<string, string> = {
20
20
  feature: "docs/features",
21
21
  command: "docs/commands",
22
22
  model: "docs/models",
23
+ query: "docs/queries",
23
24
  };
24
25
 
25
26
  const APP_DIR_MAP: Record<string, string> = {
@@ -103,6 +103,34 @@ describe("runSyncCheck", () => {
103
103
  expect(result.summary.totalSources).toBe(1);
104
104
  });
105
105
 
106
+ it("reports missing doc for query source without doc", async () => {
107
+ const srcDir = path.join(tmpDir, "modules", "foo", "query");
108
+ fs.mkdirSync(srcDir, { recursive: true });
109
+ fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
110
+
111
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
112
+ expect(result.exitCode).toBe(1);
113
+ expect(result.errors).toContainEqual(
114
+ expect.objectContaining({
115
+ type: "missing-doc",
116
+ category: "query",
117
+ expectedBasename: "convertamount",
118
+ }),
119
+ );
120
+ });
121
+
122
+ it("returns 0 when query source and doc match", async () => {
123
+ const srcDir = path.join(tmpDir, "modules", "foo", "query");
124
+ const docDir = path.join(tmpDir, "modules", "foo", "docs", "queries");
125
+ fs.mkdirSync(srcDir, { recursive: true });
126
+ fs.mkdirSync(docDir, { recursive: true });
127
+ fs.writeFileSync(path.join(srcDir, "convertAmount.ts"), "export {}");
128
+ fs.writeFileSync(path.join(docDir, "convertAmount.md"), "# ConvertAmount");
129
+
130
+ const result = await runSyncCheck({ modulesRoot: "modules", appRoot: "examples" }, tmpDir);
131
+ expect(result.exitCode).toBe(0);
132
+ });
133
+
106
134
  it("checks only module patterns when appRoot is not set", async () => {
107
135
  const resolverSrcDir = path.join(
108
136
  tmpDir,
@@ -41,6 +41,12 @@ function moduleCategories(root: string): CategoryConfig[] {
41
41
  docPattern: `${root}/*/docs/models/*.md`,
42
42
  exclusions: [/\.test\.ts$/, /^index\.ts$/],
43
43
  },
44
+ {
45
+ name: "query",
46
+ sourcePattern: `${root}/*/query/*.ts`,
47
+ docPattern: `${root}/*/docs/queries/*.md`,
48
+ exclusions: [/\.test\.ts$/],
49
+ },
44
50
  ];
45
51
  }
46
52
 
@@ -11,13 +11,11 @@ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
11
11
  const skipReason = fs.existsSync(CLI_PATH) ? undefined : "Run `pnpm build` first";
12
12
 
13
13
  describe.skipIf(skipReason)("integration", () => {
14
- it("check command runs against sdk-plugins modules", () => {
15
- // mdschema may report validation errors in real docs, so we allow non-zero exit codes
16
- // but verify the CLI itself doesn't crash with an unexpected error
14
+ it("module check command runs against sdk-plugins modules", () => {
17
15
  try {
18
16
  const result = execFileSync(
19
17
  "node",
20
- [CLI_PATH, "check", "--modules-root", "packages/erp-kit/src/modules"],
18
+ [CLI_PATH, "module", "check", "--root", "packages/erp-kit/src/modules"],
21
19
  {
22
20
  cwd: REPO_ROOT,
23
21
  encoding: "utf-8",
@@ -33,11 +31,11 @@ describe.skipIf(skipReason)("integration", () => {
33
31
  }
34
32
  });
35
33
 
36
- it("sync-check command runs against sdk-plugins modules", () => {
34
+ it("module sync-check command runs against sdk-plugins modules", () => {
37
35
  try {
38
36
  const result = execFileSync(
39
37
  "node",
40
- [CLI_PATH, "sync-check", "--modules-root", "packages/erp-kit/src/modules"],
38
+ [CLI_PATH, "module", "sync-check", "--root", "packages/erp-kit/src/modules"],
41
39
  {
42
40
  cwd: REPO_ROOT,
43
41
  encoding: "utf-8",
@@ -57,7 +55,7 @@ describe.skipIf(skipReason)("integration", () => {
57
55
  encoding: "utf-8",
58
56
  });
59
57
  expect(result).toContain("erp-kit");
60
- expect(result).toContain("check");
61
- expect(result).toContain("scaffold");
58
+ expect(result).toContain("module");
59
+ expect(result).toContain("app");
62
60
  });
63
61
  });
package/src/module.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  // shared/internal
2
2
  export { defineCommand, type Command } from "./modules/shared/defineCommand";
3
+ export { defineQuery, type Query } from "./modules/shared/defineQuery";
3
4
  export { definePermissions } from "./modules/shared/definePermissions";
4
5
  export { requirePermission } from "./modules/shared/requirePermission";
5
6
  export { createDomainError, InsufficientPermissionError } from "./modules/shared/errors";
6
- export type { CommandContext } from "./modules/shared/types";
7
+ export type { CommandContext, QueryContext, ReadonlyDB } from "./modules/shared/types";
7
8
  export type {
8
9
  InferSchema,
9
10
  Selectable,
@@ -43,13 +44,13 @@ export {
43
44
  SameCurrencyPairError,
44
45
  InvalidExchangeRateError,
45
46
  } from "./modules/primitives/lib/errors";
46
- export { type ConvertQuantityInput } from "./modules/primitives/command/convertQuantity";
47
+ export { type ConvertQuantityInput } from "./modules/primitives/query/convertQuantity";
47
48
  export { type ActivateCategoryInput } from "./modules/primitives/command/activateCategory";
48
49
  export { type DeactivateCategoryInput } from "./modules/primitives/command/deactivateCategory";
49
50
  export { type SetReferenceUnitInput } from "./modules/primitives/command/setReferenceUnit";
50
51
  export { type ActivateUnitInput } from "./modules/primitives/command/activateUnit";
51
52
  export { type DeactivateUnitInput } from "./modules/primitives/command/deactivateUnit";
52
- export { type ConvertAmountInput } from "./modules/primitives/command/convertAmount";
53
+ export { type ConvertAmountInput } from "./modules/primitives/query/convertAmount";
53
54
  export { type ActivateCurrencyInput } from "./modules/primitives/command/activateCurrency";
54
55
  export { type DeactivateCurrencyInput } from "./modules/primitives/command/deactivateCurrency";
55
56
  export { type SetBaseCurrencyInput } from "./modules/primitives/command/setBaseCurrency";
@@ -29,6 +29,10 @@ stateDiagram-v2
29
29
  - [deactivateCurrency](../commands/DeactivateCurrency.md)
30
30
  - [setBaseCurrency](../commands/SetBaseCurrency.md)
31
31
 
32
+ ### Query Definitions
33
+
34
+ - [convertAmount](../queries/ConvertAmount.md)
35
+
32
36
  ### Models
33
37
 
34
38
  - Currency