bazis 0.127.0 → 0.128.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 (33) hide show
  1. package/cli/generateModule.ts +5 -5
  2. package/cli/generateProcess.ts +56 -0
  3. package/cli/main.ts +35 -1
  4. package/cli/moduleRegistration.ts +23 -10
  5. package/cli/parseCli.ts +45 -3
  6. package/cli/templates/process.ts +28 -0
  7. package/cli/workflow.ts +66 -0
  8. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  9. package/core/workflow/contracts/WorkflowClock.ts +21 -0
  10. package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +14 -6
  11. package/core/workflow/definitions_module/code/CodeProcessCatalog.ts +19 -0
  12. package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +44 -0
  13. package/core/workflow/definitions_module/code/codeProcesses.ts +85 -0
  14. package/core/workflow/definitions_module/code/defineProcess.ts +40 -0
  15. package/core/workflow/definitions_module/code/inspectProcesses.ts +74 -0
  16. package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +11 -2
  17. package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +106 -45
  18. package/core/workflow/index.ts +14 -0
  19. package/core/workflow/runtime_module/WorkflowRuntime.module.ts +2 -0
  20. package/core/workflow/runtime_module/background/WorkflowRetention.ts +4 -2
  21. package/core/workflow/runtime_module/background/WorkflowWorker.ts +11 -88
  22. package/core/workflow/runtime_module/internal/Schedules.ts +4 -2
  23. package/core/workflow/runtime_module/internal/ToolStep.ts +3 -3
  24. package/core/workflow/runtime_module/internal/WorkflowEngine.ts +18 -11
  25. package/core/workflow/runtime_module/internal/WorkflowJobs.ts +100 -0
  26. package/core/workflow/tasks_module/WorkflowTasks.module.ts +4 -2
  27. package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +4 -2
  28. package/core/workflow/testing/index.ts +162 -0
  29. package/library/terminal/messages.ts +28 -0
  30. package/library/terminal/screen.ts +1 -0
  31. package/library/workflow/index.ts +1 -0
  32. package/library/workflow/mermaid.ts +87 -0
  33. package/package.json +2 -1
@@ -32,6 +32,8 @@ const NOUNS_EN = {
32
32
  change: ["change", "changes", "changes"],
33
33
  part: ["part", "parts", "parts"],
34
34
  context: ["database context", "database contexts", "database contexts"],
35
+ process: ["process", "processes", "processes"],
36
+ node: ["node", "nodes", "nodes"],
35
37
  } as const satisfies Record<string, Forms>;
36
38
 
37
39
  export type Noun = keyof typeof NOUNS_EN;
@@ -60,6 +62,8 @@ const NOUNS_RU: Record<Noun, Forms> = {
60
62
  change: ["изменение", "изменения", "изменений"],
61
63
  part: ["часть", "части", "частей"],
62
64
  context: ["контекст базы данных", "контекста базы данных", "контекстов базы данных"],
65
+ process: ["процесс", "процесса", "процессов"],
66
+ node: ["узел", "узла", "узлов"],
63
67
  };
64
68
 
65
69
  /** Genitive after «из N»: «из 1 подключения», «из 3 подключений», «из 21 подключения». */
@@ -239,6 +243,18 @@ const en = {
239
243
  needsCache: "this module caches: pass runApp(AppModule, { cache: memory() })",
240
244
  keptOnFailure: "scaffold files were kept — fix the error and run bazis codegen",
241
245
  },
246
+ workflow: {
247
+ app: "app",
248
+ section: "PROCESSES IN CODE",
249
+ none: "no module declares processes — add one with bazis g process <Name> --module <Name>",
250
+ passed: (count: number) => `${count} ${formEn(count, NOUNS_EN.process)} checked`,
251
+ failed: (count: number) => `process check failed · ${count} ${formEn(count, NOUNS_EN.error)}`,
252
+ fix: "fix the documents or the Tools: the application refuses to start with these errors",
253
+ processKind: "process",
254
+ processCreated: (name: string) => `process ${name} is generated`,
255
+ processPlanned: (name: string) => `process ${name} is planned`,
256
+ processRegistered: (host: string) => `listed in processes of ${host}`,
257
+ },
242
258
  errors: {
243
259
  unknown: "Use bazis --help to list commands and options.",
244
260
  bunTooOld: (required: string, actual: string) => `bazis needs Bun ${required.startsWith(">=") ? `${required.slice(2).trim()} or newer` : required}; this is Bun ${actual}.`,
@@ -404,6 +420,18 @@ const ru: Messages = {
404
420
  needsCache: "модуль кэширует: передайте runApp(AppModule, { cache: memory() })",
405
421
  keptOnFailure: "файлы модуля оставлены — исправьте ошибку и запустите bazis codegen",
406
422
  },
423
+ workflow: {
424
+ app: "приложение",
425
+ section: "ПРОЦЕССЫ В КОДЕ",
426
+ none: "ни один модуль не объявляет процессы — добавьте процесс: bazis g process <Name> --module <Name>",
427
+ passed: (count) => `процессы в порядке · ${count} ${formRu(count, NOUNS_RU.process)}`,
428
+ failed: (count) => `проверка процессов не прошла · ${count} ${formRu(count, NOUNS_RU.error)}`,
429
+ fix: "исправьте документы или Tools: с этими ошибками приложение не запустится",
430
+ processKind: "процесс",
431
+ processCreated: (name) => `процесс ${name} создан`,
432
+ processPlanned: (name) => `процесс ${name} запланирован`,
433
+ processRegistered: (host) => `добавлен в processes модуля ${host}`,
434
+ },
407
435
  errors: {
408
436
  unknown: "Список команд и опций: bazis --help.",
409
437
  bunTooOld: (required, actual) => `bazis нужен Bun ${required.startsWith(">=") ? `${required.slice(2).trim()} или новее` : required}, а запущен Bun ${actual}.`,
@@ -58,6 +58,7 @@ export const MODE = Object.freeze({
58
58
  new: ["#ff2ee6", "#7b61ff"] as readonly string[],
59
59
  gen: ["#c77dff", "#ff2ee6"] as readonly string[],
60
60
  codegen: ["#00f0ff", "#c77dff"] as readonly string[],
61
+ workflow: ["#3dff7a", "#ffd60a"] as readonly string[],
61
62
  });
62
63
 
63
64
  /**
@@ -22,4 +22,5 @@ export { DATA_PATH, isPath, readPath, resolveInput, type InstanceData } from "./
22
22
  export { evaluateCondition } from "./conditions";
23
23
  export { durationMs } from "./duration";
24
24
  export { parseCron, type CronSchedule } from "./cron";
25
+ export { processToMermaid } from "./mermaid";
25
26
  export { checkFormSpec, formFieldCodes, validateForm, type FormField, type FormFieldType, type FormIssue, type FormSpec } from "./formFields";
@@ -0,0 +1,87 @@
1
+ import type { Condition, NodeKind, ProcessDocument } from "./ProcessDocument";
2
+
3
+ /** Words Mermaid reads as syntax: such node ids are renamed. */
4
+ const RESERVED = new Set(["end", "graph", "flowchart", "subgraph", "style", "class", "classdef", "click", "default", "linkstyle", "direction"]);
5
+
6
+ const SHAPES: Record<NodeKind, readonly [open: string, close: string]> = {
7
+ manualStart: ["([", "])"],
8
+ scheduleStart: ["([", "])"],
9
+ eventStart: ["([", "])"],
10
+ end: ["((", "))"],
11
+ userTask: ["[", "]"],
12
+ serviceTask: ["[[", "]]"],
13
+ timerCatch: ["{{", "}}"],
14
+ eventWait: ["{{", "}}"],
15
+ exclusiveGateway: ["{", "}"],
16
+ parallelGateway: ["{", "}"],
17
+ boundaryError: ["((", "))"],
18
+ boundaryTimer: ["((", "))"],
19
+ boundaryEvent: ["((", "))"],
20
+ };
21
+
22
+ /**
23
+ * A process as a Mermaid `flowchart` (WORKFLOW-001 §14): starts are stadiums,
24
+ * Tool steps subroutines, tasks rectangles, waits hexagons, gateways rhombi,
25
+ * ends and boundary events circles. A boundary event hangs on its step by a
26
+ * dotted link. Labels show the node kind's detail: the Tool, the cron, the
27
+ * duration, the event key; flows show their title or condition.
28
+ */
29
+ export function processToMermaid(document: ProcessDocument, direction: "LR" | "TB" = "LR"): string {
30
+ const ids = new Map<string, string>();
31
+ const used = new Set<string>();
32
+ const idOf = (nodeId: string): string => {
33
+ const known = ids.get(nodeId);
34
+ if (known !== undefined) return known;
35
+ let id = /^[A-Za-z][A-Za-z0-9_]*$/.test(nodeId) && !RESERVED.has(nodeId.toLowerCase()) ? nodeId : `n_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
36
+ while (used.has(id)) id = `${id}_`;
37
+ used.add(id);
38
+ ids.set(nodeId, id);
39
+ return id;
40
+ };
41
+ const lines = [`flowchart ${direction}`];
42
+ for (const [nodeId, node] of Object.entries(document.nodes)) {
43
+ const [open, close] = SHAPES[node.kind] ?? ["[", "]"];
44
+ const detail = detailOf(node.kind, node.config ?? {});
45
+ lines.push(` ${idOf(nodeId)}${open}"${label(node.title || nodeId)}${detail === undefined ? "" : `<br/><small>${label(detail)}</small>`}"${close}`);
46
+ }
47
+ for (const [nodeId, node] of Object.entries(document.nodes)) {
48
+ if (node.attachedTo !== undefined && document.nodes[node.attachedTo] !== undefined) lines.push(` ${idOf(node.attachedTo)} -.- ${idOf(nodeId)}`);
49
+ }
50
+ for (const flow of Object.values(document.flows)) {
51
+ if (document.nodes[flow.source] === undefined || document.nodes[flow.target] === undefined) continue;
52
+ const text = flow.title ?? (flow.condition !== undefined ? conditionText(flow.condition) : flow.isDefault === true ? "default" : undefined);
53
+ lines.push(` ${idOf(flow.source)} -->${text === undefined ? "" : `|"${label(text)}"|`} ${idOf(flow.target)}`);
54
+ }
55
+ return `${lines.join("\n")}\n`;
56
+ }
57
+
58
+ function detailOf(kind: NodeKind, config: Readonly<Record<string, unknown>>): string | undefined {
59
+ const text = (value: unknown) => (typeof value === "string" && value !== "" ? value : undefined);
60
+ switch (kind) {
61
+ case "serviceTask": return text(config.tool);
62
+ case "scheduleStart": return text(config.cron);
63
+ case "eventStart":
64
+ case "eventWait":
65
+ case "boundaryEvent": return text(config.event);
66
+ case "timerCatch":
67
+ case "boundaryTimer": return text(config.duration) ?? (text(config.at) === undefined ? undefined : `at ${config.at}`);
68
+ case "boundaryError": return text(config.errorCode);
69
+ case "userTask": {
70
+ const assignee = config.assignee as { role?: unknown; user?: unknown } | undefined;
71
+ return assignee === undefined ? undefined : text(assignee.role) !== undefined ? `role ${assignee.role}` : text(assignee.user) === undefined ? undefined : `user ${assignee.user}`;
72
+ }
73
+ default: return undefined;
74
+ }
75
+ }
76
+
77
+ function conditionText(condition: Condition): string {
78
+ if ("all" in condition) return condition.all.map(conditionText).join(" and ");
79
+ if ("any" in condition) return condition.any.map(conditionText).join(" or ");
80
+ if ("not" in condition) return `not (${conditionText(condition.not)})`;
81
+ return condition.op === "empty" || condition.op === "notEmpty" ? `${condition.path} ${condition.op}` : `${condition.path} ${condition.op} ${JSON.stringify(condition.value)}`;
82
+ }
83
+
84
+ /** Text inside a quoted Mermaid label: quotes and markup are entities. */
85
+ function label(value: string): string {
86
+ return value.replace(/&/g, "&amp;").replace(/"/g, "#quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.127.0",
3
+ "version": "0.128.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",
@@ -44,6 +44,7 @@
44
44
  "./core/testing": "./core/testing/index.ts",
45
45
  "./core/websocket": "./core/websocket/index.ts",
46
46
  "./core/workflow": "./core/workflow/index.ts",
47
+ "./core/workflow/testing": "./core/workflow/testing/index.ts",
47
48
  "./library/boundary": "./library/boundary/index.ts",
48
49
  "./library/http-client": "./library/http-client/index.ts",
49
50
  "./library/jsonapi": "./library/jsonapi/index.ts",