@pylonts/dsl 1.1.20 → 1.1.22

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 (54) hide show
  1. package/README.md +8 -5
  2. package/dist/action.d.ts +76 -33
  3. package/dist/action.js +19 -17
  4. package/dist/aggregate.d.ts +3 -13
  5. package/dist/aggregate.js +19 -22
  6. package/dist/component.d.ts +3 -3
  7. package/dist/curd.d.ts +2 -2
  8. package/dist/dto.d.ts +18 -2
  9. package/dist/dto.js +75 -9
  10. package/dist/flow-script.js +73 -7
  11. package/dist/flow.d.ts +20 -3
  12. package/dist/flow.js +82 -11
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.js +3 -0
  15. package/dist/journey.d.ts +23 -0
  16. package/dist/journey.js +26 -0
  17. package/dist/mermaid-driver.js +6 -1
  18. package/dist/navigation.d.ts +2 -2
  19. package/dist/page-action.d.ts +39 -0
  20. package/dist/page-action.js +17 -0
  21. package/dist/page-def.d.ts +9 -9
  22. package/dist/page-flow.d.ts +4 -4
  23. package/dist/popup.d.ts +3 -3
  24. package/dist/repository.d.ts +2 -2
  25. package/dist/task.d.ts +20 -0
  26. package/dist/task.js +21 -0
  27. package/dist/utils.d.ts +7 -0
  28. package/dist/utils.js +12 -4
  29. package/docs/aggregate-implementation.md +174 -0
  30. package/docs/aggregate.md +147 -110
  31. package/docs/concepts.md +109 -0
  32. package/docs/dto.md +130 -106
  33. package/docs/table.md +41 -0
  34. package/docs/task.md +81 -0
  35. package/docs/utils.md +19 -12
  36. package/package.json +1 -1
  37. package/src/action.ts +87 -52
  38. package/src/aggregate.ts +94 -103
  39. package/src/component.ts +3 -3
  40. package/src/curd.ts +2 -2
  41. package/src/dto.ts +87 -8
  42. package/src/flow-script.ts +68 -6
  43. package/src/flow.ts +99 -17
  44. package/src/index.ts +3 -0
  45. package/src/journey.ts +61 -0
  46. package/src/mermaid-driver.ts +5 -1
  47. package/src/navigation.ts +2 -2
  48. package/src/page-action.ts +59 -0
  49. package/src/page-def.ts +9 -9
  50. package/src/page-flow.ts +4 -4
  51. package/src/popup.ts +3 -3
  52. package/src/repository.ts +35 -35
  53. package/src/task.ts +52 -0
  54. package/src/utils.ts +18 -4
package/README.md CHANGED
@@ -7,21 +7,24 @@ Schema 定义与产物生成分离的 DSL 系统:先写 DSL 元数据(表 /
7
7
  从概要设计到详细设计的自顶向下链路:
8
8
 
9
9
  ```
10
- project(地图:应用与 API 拓扑)
11
- dictionary(词典:命名与数据类型标准)
12
- prototype(概要设计:页面与字段)
13
- table / dto(详细设计)
14
- driver 生成产物(SQL / 枚举 / TypeBox)
10
+ concepts(概念:名词 + 名词解释——先于一切存在的名字层)
11
+ project(地图:应用与 API 拓扑)
12
+ dictionary(词典:词根与字段命名标准,概念派生)
13
+ prototype(概要设计:页面与字段)
14
+ table / dto(详细设计)
15
+ → driver 生成产物(SQL / 枚举 / TypeBox)
15
16
  ```
16
17
 
17
18
  ## 文档
18
19
 
20
+ - [concepts.md](./docs/concepts.md) — 概念层(名词 + 名词解释,蓝图的名字层;设计草案)
19
21
  - [project.md](./docs/project.md) — 项目拓扑(地图)
20
22
  - [dictionary.md](./docs/dictionary.md) — 短语词典(命名标准)
21
23
  - [prototype.md](./docs/prototype.md) — 页面原型(概要设计)
22
24
  - [table.md](./docs/table.md) — 定义表(详细设计)
23
25
  - [dto.md](./docs/dto.md) — 定义 DTO
24
26
  - [curd.md](./docs/curd.md) — 管理端 CRUD 页面标准(CurdSchema)
27
+ - [task.md](./docs/task.md) — 系统任务(TaskSchema:scheduled 定时 / async 异步,action 闭包成员)
25
28
  - [utils.md](./docs/utils.md) — 工具模块与领域规则(UtilsSchema:防御 guard / 判断 / 计算)
26
29
  - [aggregate.md](./docs/aggregate.md) — 聚合/仓储 DSL 扩展规划(声明层已落地:DomainAggregate/RepositorySchema + 存储校验,生成器待选型)
27
30
  - [domain-event.md](./docs/domain-event.md) — 领域事件 DSL 扩展规划(声明层已落地:DomainEventSchema + event_schema 存储校验,发布/订阅待实现)
package/dist/action.d.ts CHANGED
@@ -1,39 +1,82 @@
1
1
  import { SchemaBase } from './dsl.js';
2
- import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
3
- import type { RefSchema } from './ref.js';
4
2
  import type { ControllerMethodSchema } from './controller.js';
5
- /** An action a user can perform on a page (e.g. submit, approve, reject).
6
- * Subclasses use `type` as the discriminator. */
3
+ import type { ThirdServiceMethodSchema, ThirdCallbackSchema } from './third-service.js';
4
+ import type { TaskSchema } from './task.js';
5
+ import type { TableSchema } from './db.js';
6
+ import type { PageSchema } from './page.js';
7
+ /** A beat of a journey: type + properties + input data. */
7
8
  export interface ActionSchema extends SchemaBase {
8
- type: string;
9
+ type: 'page' | 'controller' | 'third' | 'db' | 'task';
10
+ /** Input data of this beat (blueprint: names first, refined to refs later). */
11
+ data?: Record<string, unknown>;
9
12
  }
10
- export declare function defineAction(name: string, description?: string): ActionSchema;
11
- /** Parameter data source for a call argument. */
12
- export type DataRef = {
13
- type: 'route';
14
- key: string;
15
- } | {
16
- type: 'data';
17
- key: string;
18
- } | {
19
- type: 'value';
20
- value: unknown;
21
- };
22
- /** Create a route-parameter reference. */
23
- export declare function route(key: string): DataRef;
24
- /** Create a page-data reference. */
25
- export declare function data(key: string): DataRef;
26
- export interface CallAction extends ActionSchema {
27
- type: 'call';
28
- func: ControllerMethodSchema;
29
- args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
13
+ /** Visit a page — `data` present means fill/submit a form, absent means pure view. */
14
+ export interface PageAction extends ActionSchema {
15
+ type: 'page';
16
+ /** Target page (PageFlow node). */
17
+ page: PageSchema;
18
+ /** Page url/path (e.g. '/bd/apply'). */
19
+ url: string;
20
+ }
21
+ /** Call a backend controller method. */
22
+ export interface ControllerAction extends ActionSchema {
23
+ type: 'controller';
24
+ /** The controller method invoked (shared instance). */
25
+ method: ControllerMethodSchema;
26
+ }
27
+ /** Invoke a third-party service method or receive its callback. */
28
+ export interface ThirdAction extends ActionSchema {
29
+ type: 'third';
30
+ /** The third-party method invoked (outbound). */
31
+ method?: ThirdServiceMethodSchema;
32
+ /** The third-party callback received (inbound) mutually exclusive with method. */
33
+ callback?: ThirdCallbackSchema;
34
+ /** Wait for the async callback (inbound) before the journey continues. */
35
+ async?: boolean;
30
36
  }
31
- export declare function call(func: ControllerMethodSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction;
32
- /** Assign a call's result to a page data field.
33
- * React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
34
- export interface SetDataAction extends ActionSchema {
35
- type: 'setData';
36
- call: CallAction;
37
- field: DtoField;
37
+ /** Write to a database table. */
38
+ export interface DbAction extends ActionSchema {
39
+ type: 'db';
40
+ /** The table written (shared instance). */
41
+ table: TableSchema;
42
+ /** Write operation: insert | update | delete. */
43
+ op: 'insert' | 'update' | 'delete';
38
44
  }
39
- export declare function setData(call: CallAction, field: DtoField): SetDataAction;
45
+ /** Trigger a system task (scheduled or async). */
46
+ export interface TaskAction extends ActionSchema {
47
+ type: 'task';
48
+ /** The task triggered (shared instance). */
49
+ task: TaskSchema;
50
+ }
51
+ /** Builder for a journey action. */
52
+ export declare const action: {
53
+ page(options: {
54
+ page: PageSchema;
55
+ url: string;
56
+ data?: Record<string, unknown>;
57
+ description?: string;
58
+ }): PageAction;
59
+ controller(options: {
60
+ method: ControllerMethodSchema;
61
+ data?: Record<string, unknown>;
62
+ description?: string;
63
+ }): ControllerAction;
64
+ third(options: {
65
+ method?: ThirdServiceMethodSchema;
66
+ callback?: ThirdCallbackSchema;
67
+ data?: Record<string, unknown>;
68
+ async?: boolean;
69
+ description?: string;
70
+ }): ThirdAction;
71
+ db(options: {
72
+ table: TableSchema;
73
+ op: 'insert' | 'update' | 'delete';
74
+ data?: Record<string, unknown>;
75
+ description?: string;
76
+ }): DbAction;
77
+ task(options: {
78
+ task: TaskSchema;
79
+ data?: Record<string, unknown>;
80
+ description?: string;
81
+ }): TaskAction;
82
+ };
package/dist/action.js CHANGED
@@ -1,17 +1,19 @@
1
- export function defineAction(name, description) {
2
- return { name, description, type: 'gesture' };
3
- }
4
- /** Create a route-parameter reference. */
5
- export function route(key) {
6
- return { type: 'route', key };
7
- }
8
- /** Create a page-data reference. */
9
- export function data(key) {
10
- return { type: 'data', key };
11
- }
12
- export function call(func, args) {
13
- return { name: func.name, type: 'call', func, args };
14
- }
15
- export function setData(call, field) {
16
- return { name: 'setData', type: 'setData', call, field };
17
- }
1
+ /** Builder for a journey action. */
2
+ export const action = {
3
+ page(options) {
4
+ return { name: options.page.name, type: 'page', page: options.page, url: options.url, data: options.data, description: options.description };
5
+ },
6
+ controller(options) {
7
+ return { name: options.method.name, type: 'controller', method: options.method, data: options.data, description: options.description };
8
+ },
9
+ third(options) {
10
+ const name = options.method?.name ?? options.callback?.name ?? 'third';
11
+ return { name, type: 'third', method: options.method, callback: options.callback, data: options.data, async: options.async, description: options.description };
12
+ },
13
+ db(options) {
14
+ return { name: options.table.name, type: 'db', table: options.table, op: options.op, data: options.data, description: options.description };
15
+ },
16
+ task(options) {
17
+ return { name: options.task.name, type: 'task', task: options.task, data: options.data, description: options.description };
18
+ },
19
+ };
@@ -1,16 +1,7 @@
1
1
  import type { SchemaBase } from './dsl.js';
2
- import type { TableSchema, ForeignKey } from './db.js';
3
- /** How a member table attaches to the aggregate root. */
4
- export interface AggregateMember {
5
- /** The member table (shared instance from schema/*.table.ts). */
6
- table: TableSchema;
7
- /** The foreign key on the member table pointing to the root table.
8
- * Must exist in table.foreignKeys and its references must be the root's PK
9
- * columns. Defaults to the (unique) FK referencing the root table. */
10
- via?: ForeignKey;
11
- /** 1:1 member (unique constraint on via.columns) vs 1:N (default). */
12
- one?: boolean;
13
- }
2
+ import type { TableSchema } from './db.js';
3
+ /** Member table(s) keyed by role name. Array = 1:N; non-array = 1:1 extension. */
4
+ export type AggregateMember = TableSchema | TableSchema[];
14
5
  /** A cross-member invariant, checked by generated repository code. */
15
6
  export interface AggregateInvariant {
16
7
  name: string;
@@ -29,7 +20,6 @@ export interface DomainAggregate extends SchemaBase {
29
20
  references?: Record<string, string>;
30
21
  }
31
22
  export declare function defineAggregate(options: {
32
- name: string;
33
23
  root: TableSchema;
34
24
  members?: Record<string, AggregateMember>;
35
25
  invariants?: AggregateInvariant[];
package/dist/aggregate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export function defineAggregate(options) {
2
2
  const schema = {
3
3
  type: 'aggregate',
4
- name: options.name,
4
+ name: options.root.name,
5
5
  description: options.description,
6
6
  root: options.root,
7
7
  members: options.members ?? {},
@@ -10,36 +10,33 @@ export function defineAggregate(options) {
10
10
  };
11
11
  // Root must have a primary key (aggregate identity).
12
12
  if (options.root.primaryKey === undefined) {
13
- throw new Error(`aggregate '${options.name}': root table '${options.root.name}' must have a primary key`);
13
+ throw new Error(`aggregate '${schema.name}': root table '${options.root.name}' must have a primary key`);
14
14
  }
15
- // Each member must attach to the root via an existing FK referencing the root.
16
15
  const rootPkRefs = Array.isArray(options.root.primaryKey)
17
16
  ? options.root.primaryKey
18
17
  : [options.root.primaryKey];
18
+ // Each member must attach to the root. Array members are normal 1:N tables
19
+ // and must have exactly one FK referencing the root. Non-array (1:1 extension)
20
+ // members are designed but not implemented yet.
19
21
  for (const [role, member] of Object.entries(schema.members)) {
20
- const fks = Object.values(member.table.foreignKeys ?? {}).filter((fk) => {
21
- const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
22
- return refs.every((r) => rootPkRefs.includes(r)) && refs.length === rootPkRefs.length;
23
- });
24
- if (fks.length === 0) {
25
- throw new Error(`aggregate '${options.name}': member '${role}' table '${member.table.name}' has no foreign key referencing root '${options.root.name}' — declare one in the table's foreignKeys`);
26
- }
27
- if (member.via !== undefined) {
28
- const viaKeys = Object.values(member.table.foreignKeys ?? {});
29
- if (!viaKeys.includes(member.via)) {
30
- throw new Error(`aggregate '${options.name}': member '${role}' via must be one of table '${member.table.name}' foreignKeys — got a non-FK object`);
22
+ if (Array.isArray(member)) {
23
+ if (member.length !== 1) {
24
+ throw new Error(`aggregate '${schema.name}': member '${role}' array must contain exactly one table schema`);
31
25
  }
32
- const refs = Array.isArray(member.via.references) ? member.via.references : [member.via.references];
33
- if (!(refs.length === rootPkRefs.length && refs.every((r) => rootPkRefs.includes(r)))) {
34
- throw new Error(`aggregate '${options.name}': member '${role}' via must reference root '${options.root.name}' primary key columns`);
26
+ const table = member[0];
27
+ const fks = Object.values(table.foreignKeys ?? {}).filter((fk) => {
28
+ const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
29
+ return refs.length === rootPkRefs.length && refs.every((r) => rootPkRefs.includes(r));
30
+ });
31
+ if (fks.length === 0) {
32
+ throw new Error(`aggregate '${schema.name}': member '${role}' table '${table.name}' has no foreign key referencing root '${options.root.name}' — declare one in the table's foreignKeys`);
35
33
  }
36
- }
37
- else {
38
- // Default: the (single) FK referencing the root. More than one → must declare via.
39
34
  if (fks.length > 1) {
40
- throw new Error(`aggregate '${options.name}': member '${role}' table '${member.table.name}' has ${fks.length} foreign keys referencing root '${options.root.name}' — declare via explicitly`);
35
+ throw new Error(`aggregate '${schema.name}': member '${role}' table '${table.name}' has ${fks.length} foreign keys referencing root '${options.root.name}' — reduce to one FK for minimal aggregate declarations`);
41
36
  }
42
- member.via = fks[0];
37
+ }
38
+ else {
39
+ throw new Error(`aggregate '${schema.name}': member '${role}' is a non-array table — 1:1 extension tables are designed but not implemented yet; use an array for 1:N members`);
43
40
  }
44
41
  }
45
42
  return schema;
@@ -1,13 +1,13 @@
1
1
  import type { SchemaBase } from './dsl.js';
2
2
  import type { RefSchema } from './ref.js';
3
- import type { ActionSchema } from './action.js';
3
+ import type { PageActionSchema } from './page-action.js';
4
4
  import type { EventDataSchema } from './event.js';
5
5
  /** A component event trigger declaration. */
6
6
  export interface TriggerSchema extends SchemaBase {
7
7
  /** Data the event carries (e.g. e.detail). */
8
8
  eventData?: EventDataSchema;
9
- /** Actions that fire when the event occurs. */
10
- actions?: ActionSchema[];
9
+ /** Page actions that fire when the event occurs. */
10
+ actions?: PageActionSchema[];
11
11
  }
12
12
  /** A UI component declaration — a virtual schema that describes props and
13
13
  * event triggers, not a real renderable component.
package/dist/curd.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SchemaBase, Field } from './dsl.js';
2
2
  import { TableSchema } from './db.js';
3
3
  import { FrontAppSchema } from './project.js';
4
- import { ActionSchema } from './action.js';
4
+ import { PageActionSchema } from './page-action.js';
5
5
  import type { FilterSchema } from './filter.js';
6
6
  /** Mode of an action page: modal dialog or standalone route. */
7
7
  export type ActionPageMode = 'modal' | 'route';
@@ -43,7 +43,7 @@ export interface CurdSchema extends SchemaBase {
43
43
  /** Sidebar menu section (group) this CRUD page belongs to. */
44
44
  section: string;
45
45
  /** Extra user actions on this page (beyond the standard CRUD). */
46
- actions?: ActionSchema[];
46
+ actions?: PageActionSchema[];
47
47
  /** Add/update/detail action pages. */
48
48
  actionPages?: {
49
49
  add?: ActionPage;
package/dist/dto.d.ts CHANGED
@@ -106,8 +106,15 @@ export declare function isDtoField(v: unknown): v is DtoField;
106
106
  /** Resolve a ref chain to its terminal DtoField (the one without .ref).
107
107
  * Cycles are a DSL definition error — fail loudly at render time. */
108
108
  export declare function resolveDtoRefChain(f: DtoField): DtoField;
109
- /** TS type of a DtoField in generated code — unwraps the wrapped field
110
- * (enum its JS name, date/datetime string, containers jsType). */
109
+ /** TS type of a DtoField in generated code.
110
+ * A field shared by reference (its schema is the owning DTO utils args
111
+ * like `args: { items: OrderSubmitRequest.fields.items }`) renders as an
112
+ * indexed access on the DTO's generated type (the DTO owns the structure).
113
+ * Array elements render by name (`ItemDto[]` — named DTO) or by recursion
114
+ * (`Array<string>` — scalar). Plain wire objects (objectField) render
115
+ * their property shape (`{ key: type }`); DtoField-class containers are
116
+ * rejected at build time (DTOs must not nest inline structures).
117
+ * Enum → its JS name, date/datetime → string. */
111
118
  export declare function dtoFieldJsType(df: DtoField): string;
112
119
  /** Enum JS names referenced by a DtoField, recursing into inline array/object
113
120
  * wrappers; DtoMessage item references stop the walk. First-occurrence order. */
@@ -118,6 +125,15 @@ export declare function dtoArrayField(def: {
118
125
  export declare function dtoObjectField(def: {
119
126
  properties: Record<string, DtoField>;
120
127
  } & Omit<BaseField, 'name'>): DtoObjectField;
128
+ /** DTOs must not nest DtoField-class containers inline: dtoObjectField /
129
+ * dtoArrayField instances (and dtoField(dtoObjectField(...))-style wraps)
130
+ * have no reusable name — extract a named DTO and reference it as an array
131
+ * element (dtoArrayField({ items: namedDto })), and array items must be a
132
+ * named DTO or a scalar field. Plain Field containers (objectField /
133
+ * arrayField — wire-format nesting) stay legal and render inline.
134
+ * DtoField-class wrappers (dtoField(dtoArrayField(...))) carry the def
135
+ * inside the instance's .field, so both layers are unwrapped. */
136
+ export declare function assertNoInlineContainers(dtoName: string, fields: Record<string, DtoField>): void;
121
137
  export declare function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
122
138
  export declare function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
123
139
  export declare function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
package/dist/dto.js CHANGED
@@ -133,15 +133,50 @@ export function resolveDtoRefChain(f) {
133
133
  }
134
134
  return cur;
135
135
  }
136
- /** TS type of a DtoField in generated code — unwraps the wrapped field
137
- * (enum its JS name, date/datetime string, containers jsType). */
136
+ /** TS type of a DtoField in generated code.
137
+ * A field shared by reference (its schema is the owning DTO utils args
138
+ * like `args: { items: OrderSubmitRequest.fields.items }`) renders as an
139
+ * indexed access on the DTO's generated type (the DTO owns the structure).
140
+ * Array elements render by name (`ItemDto[]` — named DTO) or by recursion
141
+ * (`Array<string>` — scalar). Plain wire objects (objectField) render
142
+ * their property shape (`{ key: type }`); DtoField-class containers are
143
+ * rejected at build time (DTOs must not nest inline structures).
144
+ * Enum → its JS name, date/datetime → string. */
138
145
  export function dtoFieldJsType(df) {
139
- const f = df.field;
140
- if (f.type === 'enum')
141
- return f.enum.jsName;
142
- if (f.type === 'date' || f.type === 'datetime')
146
+ const owner = df.schema;
147
+ if (owner?.type === 'dto' && owner.name !== undefined && owner.name !== '' && df.name !== '') {
148
+ return `${owner.name}['${df.name}']`;
149
+ }
150
+ return dtoFieldJsTypeInner(df.field);
151
+ }
152
+ /** Type of a raw field object — unwraps DtoField-class wrappers
153
+ * (dtoField(dtoArrayField(...)) stores the def inside the instance's
154
+ * .field) and recurses: named-DTO elements (Name[]), scalar elements
155
+ * (Array<T>), plain wire objects ({ key: type }), enums, and scalars. */
156
+ function dtoFieldJsTypeInner(field) {
157
+ const f = field.field ?? field;
158
+ const inner = f;
159
+ if (inner.type === 'enum')
160
+ return inner.enum.jsName;
161
+ if (inner.type === 'date' || inner.type === 'datetime')
143
162
  return 'string';
144
- return f.jsType;
163
+ if (inner.type === 'array') {
164
+ const items = inner.items;
165
+ return isDtoMessage(items) ? `${items.name}[]` : `Array<${dtoFieldJsType(items)}>`;
166
+ }
167
+ if (inner.type === 'object') {
168
+ // Plain objectField properties are bare Fields; DtoObjectFieldDef
169
+ // properties are DtoFields. Recurse through both.
170
+ const props = Object.entries(inner.properties ?? {})
171
+ .map(([k, v]) => {
172
+ const optional = isDtoField(v) ? v.isOptional() : v.optional ?? false;
173
+ const type = isDtoField(v) ? dtoFieldJsType(v) : dtoFieldJsTypeInner(v);
174
+ return `${k}${optional ? '?' : ''}: ${type}`;
175
+ })
176
+ .join('; ');
177
+ return `{ ${props} }`;
178
+ }
179
+ return inner.jsType ?? '';
145
180
  }
146
181
  /** Enum JS names referenced by a DtoField, recursing into inline array/object
147
182
  * wrappers; DtoMessage item references stop the walk. First-occurrence order. */
@@ -162,14 +197,45 @@ export function dtoCollectEnumRefs(df, out = []) {
162
197
  return out;
163
198
  }
164
199
  export function dtoArrayField(def) {
165
- // Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
166
- // referenced by name (the driver renders Type.Array(<DtoName>)).
200
+ // Items stay as-is: a DtoMessage is referenced by name (the driver renders
201
+ // Type.Array(<DtoName>)); a scalar DtoField element renders its primitive
202
+ // type. Inline container elements are rejected by buildMessage/defineUtils
203
+ // (DTOs must not nest inline structures — every object needs a name).
167
204
  return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
168
205
  }
169
206
  export function dtoObjectField(def) {
170
207
  return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
171
208
  }
209
+ /** DTOs must not nest DtoField-class containers inline: dtoObjectField /
210
+ * dtoArrayField instances (and dtoField(dtoObjectField(...))-style wraps)
211
+ * have no reusable name — extract a named DTO and reference it as an array
212
+ * element (dtoArrayField({ items: namedDto })), and array items must be a
213
+ * named DTO or a scalar field. Plain Field containers (objectField /
214
+ * arrayField — wire-format nesting) stay legal and render inline.
215
+ * DtoField-class wrappers (dtoField(dtoArrayField(...))) carry the def
216
+ * inside the instance's .field, so both layers are unwrapped. */
217
+ export function assertNoInlineContainers(dtoName, fields) {
218
+ for (const [key, df] of Object.entries(fields)) {
219
+ const f = df.field.field ?? df.field;
220
+ const field = f;
221
+ // Only DtoField-class containers are banned (they need a name). Plain
222
+ // Field objects (objectField — wire-format nesting) are legal and render
223
+ // inline: dtoField(objectField({...})) stays allowed.
224
+ const isDtoClassContainer = isDtoField(df.field) || typeof df.properties === 'function';
225
+ if (isDtoClassContainer && field.type === 'object') {
226
+ throw new Error(`[dto] "${dtoName}" field "${key}": inline object is not allowed — dtoObjectField containers must be named: extract a named DTO and reference it (dtoArrayField({ items: itemDto })) or use a plain objectField for wire-format nesting`);
227
+ }
228
+ if (field.type === 'array' && isDtoField(field.items)) {
229
+ const items = field.items.field.field ?? field.items.field;
230
+ const item = items;
231
+ if (item.type === 'object' || item.type === 'array') {
232
+ throw new Error(`[dto] "${dtoName}" field "${key}": inline container elements are not allowed — array items must be a named DTO or a scalar field`);
233
+ }
234
+ }
235
+ }
236
+ }
172
237
  function buildMessage(name, direction, fields, description) {
238
+ assertNoInlineContainers(name, fields);
173
239
  const message = new DtoMessage(name, direction, fields, description);
174
240
  // Write back the DTO field name from the map key (safe: DtoField instances
175
241
  // are created per DTO, never shared).
@@ -9,7 +9,7 @@
9
9
  // The IR stays the single executable model (mermaid, must-analysis, throws
10
10
  // coverage, service contracts all consume it); this layer is a lowering, not
11
11
  // a parallel model.
12
- import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isEnd, isFlowNode, isFlowSlot, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
12
+ import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isConditionGroup, isEnd, isFlowNode, isFlowSlot, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
13
13
  /** Call a method as a statement or (in IF position) as a utils predicate. */
14
14
  export function invoke(method, args, result) {
15
15
  return { kind: 'invoke', method, args, result };
@@ -132,6 +132,11 @@ function addUsed(ctx, slot) {
132
132
  ctx.usedNames.add(slot.name);
133
133
  }
134
134
  function addConditionUsed(ctx, c) {
135
+ if (isConditionGroup(c)) {
136
+ for (const s of c.conds)
137
+ addConditionUsed(ctx, s);
138
+ return;
139
+ }
135
140
  if (!isCall(c)) {
136
141
  addUsed(ctx, isFlowSlot(c.field) ? c.field : c.field.slot);
137
142
  return;
@@ -144,6 +149,8 @@ function addConditionUsed(ctx, c) {
144
149
  function isInvokeStep(c) {
145
150
  return c.kind === 'invoke';
146
151
  }
152
+ /** Lower an IF condition: a top-level invoke is a predicate call, and
153
+ * composites convert invoke steps nested inside them recursively. */
147
154
  function toCondition(c) {
148
155
  if (isInvokeStep(c)) {
149
156
  if (c.result !== undefined) {
@@ -151,6 +158,9 @@ function toCondition(c) {
151
158
  }
152
159
  return { method: c.method, args: c.args === undefined ? [] : Array.isArray(c.args) ? c.args : [c.args] };
153
160
  }
161
+ if (isConditionGroup(c)) {
162
+ return { kind: c.kind, conds: c.conds.map((sub) => toCondition(sub)) };
163
+ }
154
164
  return c;
155
165
  }
156
166
  /** Display form mirrors the mermaid driver: owner.name / schema.name. */
@@ -172,11 +182,36 @@ function methodThrows(m) {
172
182
  return 'throws' in m && m.throws !== undefined ? m.throws : [];
173
183
  }
174
184
  /** Readable condition text used as node/branch labels (and as the throw
175
- * label when THROW carries no message). */
185
+ * label when THROW carries no message). Composites render parenthesized
186
+ * sub-conditions: !(a), (a && b), (a || b). */
176
187
  function renderCondition(c) {
188
+ if (isConditionGroup(c)) {
189
+ const inner = c.conds.map(renderCondition).join(c.kind === 'and' ? ' && ' : c.kind === 'or' ? ' || ' : '');
190
+ return c.kind === 'not' ? `!(${inner})` : `(${inner})`;
191
+ }
177
192
  if (!isCall(c)) {
178
193
  if (isFlowSlot(c.field)) {
179
- return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`;
194
+ const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
195
+ if (nullOp)
196
+ return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`;
197
+ // scalar slot comparison: total > 100
198
+ const ref = c.field.name;
199
+ switch (c.op) {
200
+ case 'lt':
201
+ return `${ref} < ${renderValue(c.value)}`;
202
+ case 'le':
203
+ return `${ref} <= ${renderValue(c.value)}`;
204
+ case 'gt':
205
+ return `${ref} > ${renderValue(c.value)}`;
206
+ case 'ge':
207
+ return `${ref} >= ${renderValue(c.value)}`;
208
+ case 'eq':
209
+ return `${ref} = ${renderValue(c.value)}`;
210
+ case 'ne':
211
+ return `${ref} ≠ ${renderValue(c.value)}`;
212
+ default:
213
+ throw new Error(`unsupported comparison op '${c.op}'`);
214
+ }
180
215
  }
181
216
  const field = c.field.field;
182
217
  const ref = `${c.field.slot.name}.${field.name}`;
@@ -363,15 +398,21 @@ function compileTry(step, ctx, cont, inherit) {
363
398
  const ordinal = ctx.tryTotal - ++ctx.tryCount + 1;
364
399
  const suffix = ordinal === 1 ? '' : `${ordinal}`;
365
400
  const body = compileFlowBody(`${ctx.name}.tryBody${suffix}`, undefined, step.body, ctx, inherit);
401
+ // Catch handlers and finally may read what the body produced before the
402
+ // failure point (Java semantics: try { row = dao.get() } catch { use(row) }).
403
+ // The body's productions seed their entry availability alongside the
404
+ // enclosing flow's inherited slots.
405
+ const bodyProduced = flowProducedSlots(body);
406
+ const catchInherit = [...inherit, ...bodyProduced];
366
407
  const catches = step.catches.map(([ex, steps]) => ({
367
408
  exception: ex,
368
- handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, inherit),
409
+ handler: compileFlowBody(`${ctx.name}.catch${ex.name}${suffix}`, undefined, steps, ctx, catchInherit),
369
410
  }));
370
411
  const t = tryNode(step.name ?? 'try', {
371
412
  body,
372
413
  catches,
373
414
  finally: step.finally
374
- ? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, inherit)
415
+ ? compileFlowBody(`${ctx.name}.finally${suffix}`, undefined, step.finally, ctx, catchInherit)
375
416
  : undefined,
376
417
  });
377
418
  ctx.seen.add(t);
@@ -401,6 +442,25 @@ function compileSub(step, ctx, cont, inherit) {
401
442
  ctx.edges.push(edge(n, cont));
402
443
  return n;
403
444
  }
445
+ /** Slots a flow's own nodes produce (call results and writes) — the try
446
+ * body's productions become visible to its catch handlers and finally. */
447
+ function flowProducedSlots(f) {
448
+ const out = new Set();
449
+ for (const n of f.nodes) {
450
+ if (isEnd(n))
451
+ continue;
452
+ if (isGuard(n) || isFlowNode(n)) {
453
+ for (const m of n.methods ?? []) {
454
+ if (isCall(m) && m.result !== undefined)
455
+ out.add(m.result);
456
+ }
457
+ if (isFlowNode(n))
458
+ for (const w of n.writes ?? [])
459
+ out.add(w);
460
+ }
461
+ }
462
+ return [...out];
463
+ }
404
464
  /** Compile one flow (top-level or sub-flow): its own slots registry (args
405
465
  * plus the named slots actually used inside), its own exception ends, and
406
466
  * its own return end (linked through DANGLE). */
@@ -432,8 +492,11 @@ function buildFlow(name, description, ctx, start) {
432
492
  const nodes = [...ctx.seen];
433
493
  const registry = defineSlots(buildSlots(ctx));
434
494
  // Entry inheritance only for slots the flow actually consumes; unused
435
- // productions of the enclosing flow are not this flow's concern.
436
- const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => ctx.usedSlots.has(s)));
495
+ // productions of the enclosing flow are not this flow's concern. Name-based
496
+ // matching: inherited instances may come from another flow's registry (the
497
+ // try body's re-bound productions), so identity comparison would drop them.
498
+ const usedNames = new Set([...ctx.usedSlots].map((s) => s.name));
499
+ const entrySlots = rewriteSlots(nodes, ctx.edges, registry, ctx.entrySlots.filter((s) => usedNames.has(s.name)));
437
500
  return defineFlow(name, {
438
501
  start,
439
502
  description,
@@ -475,6 +538,9 @@ function rewriteSlots(nodes, edges, slots, entrySlots) {
475
538
  return { method: m.method, args: m.args?.map(map), result: m.result ? map(m.result) : undefined };
476
539
  };
477
540
  const cond = (c) => {
541
+ if (isConditionGroup(c)) {
542
+ return { kind: c.kind, conds: c.conds.map(cond) };
543
+ }
478
544
  if (!isCall(c)) {
479
545
  if (isFlowSlot(c.field)) {
480
546
  return { kind: 'comparison', op: c.op, field: map(c.field), value: c.value };